solverforge-solver 0.18.0

Solver engine for SolverForge
Documentation
use std::fmt::{self, Debug};
use std::marker::PhantomData;

use solverforge_config::{ConstructionHeuristicType, ConstructionObligation};
use solverforge_core::domain::PlanningSolution;
use solverforge_scoring::Director;

use super::assignment_candidate::{remaining_required_count, ScalarAssignmentMoveOptions};
use super::assignment_stream::ScalarAssignmentMoveCursor;
use super::placement::{group_slot_id, scalar_slots_for_candidate};
use super::placer_stream::{
    assignment_placement_move_limit, next_assignment_placement, next_candidate_placement,
    AssignmentPlacementGenerator, CandidatePlacementGenerator, CandidatePlacementSeed,
    ScalarGroupCandidateCursor,
};
use crate::builder::context::{
    ScalarAssignmentBinding, ScalarGroupBinding, ScalarGroupBindingKind, ScalarGroupLimits,
};
use crate::descriptor::ResolvedVariableBinding;
use crate::heuristic::r#move::CompoundScalarMove;
use crate::heuristic::selector::EntityReference;
use crate::phase::construction::{EntityPlacer, EntityPlacerCursor, Placement};

pub(super) type ScalarGroupPlacement<S> =
    Placement<S, CompoundScalarMove<S>, ScalarGroupCandidateCursor<S>>;

fn grouped_heuristic_requires_entity_order(heuristic: ConstructionHeuristicType) -> bool {
    matches!(
        heuristic,
        ConstructionHeuristicType::FirstFitDecreasing
            | ConstructionHeuristicType::WeakestFitDecreasing
            | ConstructionHeuristicType::StrongestFitDecreasing
    )
}

fn grouped_heuristic_requires_value_order(heuristic: ConstructionHeuristicType) -> bool {
    matches!(
        heuristic,
        ConstructionHeuristicType::WeakestFit
            | ConstructionHeuristicType::WeakestFitDecreasing
            | ConstructionHeuristicType::StrongestFit
            | ConstructionHeuristicType::StrongestFitDecreasing
    )
}

pub(crate) struct ScalarGroupPlacer<S> {
    group_index: usize,
    group: ScalarGroupBinding<S>,
    scalar_bindings: Vec<ResolvedVariableBinding<S>>,
    limits: ScalarGroupLimits,
    heuristic: ConstructionHeuristicType,
    construction_obligation: ConstructionObligation,
    required_only: bool,
    _phantom: PhantomData<fn() -> S>,
}

impl<S> Clone for ScalarGroupPlacer<S> {
    fn clone(&self) -> Self {
        Self {
            group_index: self.group_index,
            group: self.group.clone(),
            scalar_bindings: self.scalar_bindings.clone(),
            limits: self.limits,
            heuristic: self.heuristic,
            construction_obligation: self.construction_obligation,
            required_only: self.required_only,
            _phantom: PhantomData,
        }
    }
}

impl<S> ScalarGroupPlacer<S> {
    pub(crate) fn new(
        group_index: usize,
        group: ScalarGroupBinding<S>,
        scalar_bindings: Vec<ResolvedVariableBinding<S>>,
        limits: ScalarGroupLimits,
        heuristic: ConstructionHeuristicType,
        construction_obligation: ConstructionObligation,
        required_only: bool,
    ) -> Self {
        Self {
            group_index,
            group,
            scalar_bindings,
            limits,
            heuristic,
            construction_obligation,
            required_only,
            _phantom: PhantomData,
        }
    }
}

impl<S> Debug for ScalarGroupPlacer<S> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("ScalarGroupPlacer")
            .field("group_index", &self.group_index)
            .field("group_name", &self.group.group_name)
            .field("limits", &self.limits)
            .field("heuristic", &self.heuristic)
            .field("construction_obligation", &self.construction_obligation)
            .field("required_only", &self.required_only)
            .finish()
    }
}

pub(crate) struct ScalarGroupPlacerCursor<'a, S>
where
    S: PlanningSolution + 'static,
{
    placer: &'a ScalarGroupPlacer<S>,
}

impl<S> EntityPlacerCursor<S, CompoundScalarMove<S>> for ScalarGroupPlacerCursor<'_, S>
where
    S: PlanningSolution + 'static,
{
    type CandidateCursor = ScalarGroupCandidateCursor<S>;

    fn next_placement<D, IsCompleted, ShouldStop>(
        &mut self,
        score_director: &D,
        mut is_completed: IsCompleted,
        mut should_stop: ShouldStop,
    ) -> Option<ScalarGroupPlacement<S>>
    where
        D: Director<S>,
        IsCompleted: FnMut(&ScalarGroupPlacement<S>) -> bool,
        ShouldStop: FnMut() -> bool,
    {
        match &self.placer.group.kind {
            ScalarGroupBindingKind::Candidates { candidate_provider } => {
                let mut generator = self
                    .placer
                    .candidate_placement_generator(score_director, *candidate_provider);
                next_candidate_placement(&mut generator, &mut is_completed, &mut should_stop)
            }
            ScalarGroupBindingKind::Assignment(assignment) => next_assignment_placement(
                self.placer
                    .assignment_placement_generator(score_director, assignment)?,
                &mut is_completed,
                &mut should_stop,
            ),
        }
    }
}

impl<S> EntityPlacer<S, CompoundScalarMove<S>> for ScalarGroupPlacer<S>
where
    S: PlanningSolution + 'static,
{
    type Cursor<'a>
        = ScalarGroupPlacerCursor<'a, S>
    where
        Self: 'a;

    fn open_cursor<'a, D: Director<S>>(&'a self, score_director: &D) -> Self::Cursor<'a> {
        let _ = score_director;
        ScalarGroupPlacerCursor { placer: self }
    }
}

impl<S> ScalarGroupPlacer<S>
where
    S: PlanningSolution + 'static,
{
    fn candidate_placement_generator<D>(
        &self,
        score_director: &D,
        candidate_provider: crate::builder::context::ScalarCandidateProvider<S>,
    ) -> CandidatePlacementGenerator<S>
    where
        D: Director<S>,
    {
        let solution = score_director.working_solution();
        let raw_candidates = candidate_provider(solution, self.limits);
        let total_limit = self.limits.group_candidate_limit.unwrap_or(usize::MAX);
        if total_limit == 0 {
            return CandidatePlacementGenerator {
                placements: Vec::new().into_iter(),
                group: self.group.clone(),
            };
        }

        let mut seen_candidates = Vec::new();
        let mut placements = Vec::new();
        let mut accepted = 0usize;
        for (sequence, candidate) in raw_candidates.into_iter().enumerate() {
            if accepted >= total_limit {
                break;
            }
            if candidate.edits().is_empty()
                || seen_candidates
                    .iter()
                    .any(|seen_candidate| seen_candidate == &candidate)
            {
                continue;
            }
            let Some((scalar_slots, keep_current_legal, has_unfinished_unassigned_slot)) =
                scalar_slots_for_candidate(
                    &self.group,
                    &self.scalar_bindings,
                    solution,
                    &candidate,
                )
            else {
                continue;
            };
            if !has_unfinished_unassigned_slot {
                continue;
            }
            if grouped_heuristic_requires_entity_order(self.heuristic)
                && candidate.construction_entity_order_key().is_none()
            {
                panic!(
                    "grouped scalar construction heuristic {:?} requires construction entity order metadata",
                    self.heuristic
                );
            }
            if grouped_heuristic_requires_value_order(self.heuristic)
                && candidate.construction_value_order_key().is_none()
            {
                panic!(
                    "grouped scalar construction heuristic {:?} requires construction value order metadata",
                    self.heuristic
                );
            }

            if !candidate.edits().iter().any(|edit| {
                let member = self
                    .group
                    .member_for_edit(edit)
                    .expect("validated grouped scalar candidate member must exist");
                member.current_value(solution, edit.entity_index()) != edit.to_value()
            }) {
                continue;
            }
            let group_slot = group_slot_id(self.group_index, &candidate, &scalar_slots);
            if let Some(existing) =
                placements
                    .iter_mut()
                    .find(|existing: &&mut CandidatePlacementSeed<S>| {
                        existing.group_slot == group_slot && existing.scalar_slots == scalar_slots
                    })
            {
                existing.candidates.push(candidate.clone());
            } else {
                let entity_ref = candidate
                    .edits()
                    .first()
                    .map(|edit| EntityReference::new(edit.descriptor_index(), edit.entity_index()))
                    .unwrap_or_else(|| EntityReference::new(0, sequence));
                placements.push(CandidatePlacementSeed {
                    sequence,
                    entity_ref,
                    candidates: vec![candidate.clone()],
                    group_slot,
                    scalar_slots,
                    keep_current_legal,
                    entity_order_key: candidate.construction_entity_order_key(),
                });
            }
            seen_candidates.push(candidate);
            accepted += 1;
        }
        if grouped_heuristic_requires_entity_order(self.heuristic) {
            placements.sort_by(|left, right| {
                right
                    .entity_order_key
                    .cmp(&left.entity_order_key)
                    .then_with(|| left.sequence.cmp(&right.sequence))
            });
        }
        CandidatePlacementGenerator {
            placements: placements.into_iter(),
            group: self.group.clone(),
        }
    }

    fn assignment_placement_generator<D>(
        &self,
        score_director: &D,
        assignment: &ScalarAssignmentBinding<S>,
    ) -> Option<AssignmentPlacementGenerator<S>>
    where
        D: Director<S>,
    {
        let solution = score_director.working_solution();
        let _ = assignment.target().construction_binding_index();
        let options = ScalarAssignmentMoveOptions::for_construction(self.limits);
        if options.max_moves == 0 {
            return None;
        }

        let required_remaining = remaining_required_count(assignment, solution);
        if self.required_only && required_remaining == 0 {
            return None;
        }
        let target_required = self.required_only || required_remaining > 0;
        let max_moves = assignment_placement_move_limit(
            self.heuristic,
            self.construction_obligation,
            self.required_only,
            assignment.entity_count(solution),
            options,
        );
        let options = options.with_max_moves(max_moves);
        let cursor = if self.required_only {
            ScalarAssignmentMoveCursor::required_construction(
                assignment.clone(),
                solution.clone(),
                options,
            )
        } else if target_required {
            ScalarAssignmentMoveCursor::required(assignment.clone(), solution.clone(), options)
        } else {
            ScalarAssignmentMoveCursor::optional_construction(
                assignment.clone(),
                solution.clone(),
                options,
            )
        };

        Some(AssignmentPlacementGenerator {
            group_index: self.group_index,
            assignment: assignment.clone(),
            cursor,
            pending: None,
            options,
            accepted: 0,
        })
    }
}