solverforge-solver 0.18.0

Solver engine for SolverForge
Documentation
use std::collections::HashSet;

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

use super::assignment_candidate::ScalarAssignmentMoveOptions;
use super::assignment_stream::ScalarAssignmentMoveCursor;
use super::move_build::compound_move_for_prevalidated_group_candidate;
use super::placement::{assignment_group_slot, assignment_move_target, principal_assignment_edit};
use crate::builder::context::{ScalarAssignmentBinding, ScalarCandidate, ScalarGroupBinding};
use crate::heuristic::r#move::CompoundScalarMove;
use crate::heuristic::selector::move_selector::{
    CandidateId, CandidateStore, MoveCandidateRef, MoveCursor,
};
use crate::heuristic::selector::EntityReference;
use crate::phase::construction::{
    ConstructionGroupSlotId, ConstructionSlotId, ConstructionTarget, Placement,
};

pub(super) struct CandidatePlacementSeed<S>
where
    S: PlanningSolution + 'static,
{
    pub(super) sequence: usize,
    pub(super) entity_ref: EntityReference,
    pub(super) candidates: Vec<ScalarCandidate<S>>,
    pub(super) group_slot: ConstructionGroupSlotId,
    pub(super) scalar_slots: Vec<ConstructionSlotId>,
    pub(super) keep_current_legal: bool,
    pub(super) entity_order_key: Option<i64>,
}

pub(crate) struct CandidatePlacementGenerator<S>
where
    S: PlanningSolution + 'static,
{
    pub(super) placements: std::vec::IntoIter<CandidatePlacementSeed<S>>,
    pub(super) group: ScalarGroupBinding<S>,
}

pub(crate) struct AssignmentPlacementGenerator<S>
where
    S: PlanningSolution + 'static,
{
    pub(super) group_index: usize,
    pub(super) assignment: ScalarAssignmentBinding<S>,
    pub(super) cursor: ScalarAssignmentMoveCursor<S>,
    pub(super) pending: Option<CompoundScalarMove<S>>,
    pub(super) options: ScalarAssignmentMoveOptions,
    pub(super) accepted: usize,
}

#[allow(clippy::large_enum_variant)]
enum ScalarGroupCandidateSource<S>
where
    S: PlanningSolution + 'static,
{
    Empty,
    Candidates {
        group: ScalarGroupBinding<S>,
        candidates: std::vec::IntoIter<ScalarCandidate<S>>,
    },
    Assignment {
        generator: AssignmentPlacementGenerator<S>,
        active_entity: usize,
        completed_slots: HashSet<ConstructionSlotId>,
        first: Option<(CompoundScalarMove<S>, ConstructionTarget)>,
    },
}

pub(crate) struct ScalarGroupCandidateCursor<S>
where
    S: PlanningSolution + 'static,
{
    store: CandidateStore<S, CompoundScalarMove<S>>,
    targets: Vec<ConstructionTarget>,
    source: ScalarGroupCandidateSource<S>,
}

impl<S> ScalarGroupCandidateCursor<S>
where
    S: PlanningSolution + 'static,
{
    fn empty() -> Self {
        Self {
            store: CandidateStore::new(),
            targets: Vec::new(),
            source: ScalarGroupCandidateSource::Empty,
        }
    }

    fn candidates(group: ScalarGroupBinding<S>, candidates: Vec<ScalarCandidate<S>>) -> Self {
        Self {
            store: CandidateStore::with_capacity(candidates.len()),
            targets: Vec::new(),
            source: ScalarGroupCandidateSource::Candidates {
                group,
                candidates: candidates.into_iter(),
            },
        }
    }

    fn assignment(
        generator: AssignmentPlacementGenerator<S>,
        active_entity: usize,
        completed_slots: HashSet<ConstructionSlotId>,
        first: (CompoundScalarMove<S>, ConstructionTarget),
    ) -> Self {
        Self {
            store: CandidateStore::new(),
            targets: Vec::new(),
            source: ScalarGroupCandidateSource::Assignment {
                generator,
                active_entity,
                completed_slots,
                first: Some(first),
            },
        }
    }

    pub(super) fn construction_target(
        &self,
        candidate_id: CandidateId,
    ) -> Option<&ConstructionTarget> {
        self.targets.get(candidate_id.index())
    }
}

impl<S> MoveCursor<S, CompoundScalarMove<S>> for ScalarGroupCandidateCursor<S>
where
    S: PlanningSolution + 'static,
{
    fn next_candidate(&mut self) -> Option<CandidateId> {
        let (mov, target) = match &mut self.source {
            ScalarGroupCandidateSource::Empty => return None,
            ScalarGroupCandidateSource::Candidates { group, candidates } => {
                let candidate = candidates.next()?;
                let order_key = candidate.construction_value_order_key();
                let mov = compound_move_for_prevalidated_group_candidate(group, &candidate)
                    .with_construction_value_order_key(order_key);
                (mov, None)
            }
            ScalarGroupCandidateSource::Assignment {
                generator,
                active_entity,
                completed_slots,
                first,
            } => {
                let (mov, target) = if let Some(first) = first.take() {
                    first
                } else {
                    let (entity_index, mov, target) =
                        next_assignment_candidate(generator, completed_slots)?;
                    if entity_index != *active_entity {
                        generator.pending = Some(mov);
                        return None;
                    }
                    (mov, target)
                };
                generator.accepted += 1;
                (mov, Some(target))
            }
        };

        let candidate_id = self.store.push(mov);
        if let Some(target) = target {
            debug_assert_eq!(self.targets.len(), candidate_id.index());
            self.targets.push(target);
        }
        Some(candidate_id)
    }

    fn candidate(&self, id: CandidateId) -> Option<MoveCandidateRef<'_, S, CompoundScalarMove<S>>> {
        self.store.candidate(id)
    }

    fn take_candidate(&mut self, id: CandidateId) -> CompoundScalarMove<S> {
        self.store.take_candidate(id)
    }

    fn release_candidate(&mut self, id: CandidateId) -> bool {
        self.store.release_candidate(id)
    }
}

pub(super) fn next_candidate_placement<S, IsCompleted, ShouldStop>(
    generator: &mut CandidatePlacementGenerator<S>,
    is_completed: &mut IsCompleted,
    should_stop: &mut ShouldStop,
) -> Option<Placement<S, CompoundScalarMove<S>, ScalarGroupCandidateCursor<S>>>
where
    S: PlanningSolution + 'static,
    IsCompleted: FnMut(&Placement<S, CompoundScalarMove<S>, ScalarGroupCandidateCursor<S>>) -> bool,
    ShouldStop: FnMut() -> bool,
{
    while !should_stop() {
        let seed = generator.placements.next()?;
        let placement = Placement::new(
            seed.entity_ref,
            ScalarGroupCandidateCursor::candidates(generator.group.clone(), seed.candidates),
        )
        .with_group_slot(seed.group_slot)
        .with_scalar_slots(seed.scalar_slots)
        .with_keep_current_legal(seed.keep_current_legal);
        if !is_completed(&placement) {
            return Some(placement);
        }
    }
    None
}

pub(super) fn next_assignment_placement<S, IsCompleted, ShouldStop>(
    mut generator: AssignmentPlacementGenerator<S>,
    mut is_completed: IsCompleted,
    mut should_stop: ShouldStop,
) -> Option<Placement<S, CompoundScalarMove<S>, ScalarGroupCandidateCursor<S>>>
where
    S: PlanningSolution + 'static,
    IsCompleted: FnMut(&Placement<S, CompoundScalarMove<S>, ScalarGroupCandidateCursor<S>>) -> bool,
    ShouldStop: FnMut() -> bool,
{
    let completed_slots = completed_assignment_slots(&generator, &mut is_completed);
    if generator.accepted < generator.options.max_moves && !should_stop() {
        let (entity_index, mov, target) =
            next_assignment_candidate(&mut generator, &completed_slots)?;
        let group_slot = assignment_group_slot(generator.group_index, entity_index);
        let scalar_slots = target.scalar_slots().to_vec();
        let allows_unassigned = generator.assignment.target.allows_unassigned;
        let placement = Placement::new(
            EntityReference::new(generator.assignment.target.descriptor_index, entity_index),
            ScalarGroupCandidateCursor::assignment(
                generator,
                entity_index,
                completed_slots,
                (mov, target),
            ),
        )
        .with_group_slot(group_slot)
        .with_scalar_slots(scalar_slots)
        .with_keep_current_legal(allows_unassigned)
        .with_candidate_target(ScalarGroupCandidateCursor::construction_target);
        if !is_completed(&placement) {
            return Some(placement);
        }
    }
    None
}

fn next_assignment_candidate<S>(
    generator: &mut AssignmentPlacementGenerator<S>,
    completed_slots: &HashSet<ConstructionSlotId>,
) -> Option<(usize, CompoundScalarMove<S>, ConstructionTarget)>
where
    S: PlanningSolution + 'static,
{
    loop {
        if generator.accepted >= generator.options.max_moves {
            return None;
        }
        let mov = generator
            .pending
            .take()
            .or_else(|| generator.cursor.next_move())?;
        if mov.edits().iter().any(|edit| {
            completed_slots.contains(&ConstructionSlotId::new(
                generator.assignment.target().construction_binding_index(),
                edit.entity_index,
            ))
        }) {
            continue;
        }
        let snapshot = generator.cursor.construction_snapshot();
        if mov.edits().is_empty()
            || !mov
                .edits()
                .iter()
                .any(|edit| edit.current_value(snapshot) != edit.to_value)
        {
            continue;
        }
        let anchor = mov
            .edits()
            .iter()
            .find(|edit| edit.to_value.is_some())
            .or_else(|| mov.edits().first())?;
        let entity_index = anchor.entity_index;
        let order_key = principal_assignment_edit(&mov, entity_index)
            .and_then(|principal| principal.to_value)
            .and_then(|value| {
                generator
                    .assignment
                    .value_order_key(snapshot, entity_index, value)
            });
        let mov = mov.with_construction_value_order_key(order_key);
        let group_slot = assignment_group_slot(generator.group_index, entity_index);
        let target = assignment_move_target(generator.assignment.target(), &group_slot, &mov);
        return Some((entity_index, mov, target));
    }
}

fn completed_assignment_slots<S, IsCompleted>(
    generator: &AssignmentPlacementGenerator<S>,
    is_completed: &mut IsCompleted,
) -> HashSet<ConstructionSlotId>
where
    S: PlanningSolution + 'static,
    IsCompleted: FnMut(&Placement<S, CompoundScalarMove<S>, ScalarGroupCandidateCursor<S>>) -> bool,
{
    (0..generator
        .assignment
        .entity_count(generator.cursor.construction_snapshot()))
        .filter_map(|entity_index| {
            let slot_id = ConstructionSlotId::new(
                generator.assignment.target().construction_binding_index(),
                entity_index,
            );
            let placement = Placement::new(
                EntityReference::new(generator.assignment.target.descriptor_index, entity_index),
                ScalarGroupCandidateCursor::empty(),
            )
            .with_scalar_slots(vec![slot_id]);
            is_completed(&placement).then_some(slot_id)
        })
        .collect()
}

pub(super) fn assignment_placement_move_limit(
    heuristic: ConstructionHeuristicType,
    construction_obligation: ConstructionObligation,
    required_only: bool,
    entity_count: usize,
    options: ScalarAssignmentMoveOptions,
) -> usize {
    if matches!(
        heuristic,
        ConstructionHeuristicType::FirstFit | ConstructionHeuristicType::FirstFitDecreasing
    ) && matches!(
        construction_obligation,
        ConstructionObligation::AssignWhenCandidateExists
    ) {
        if required_only && matches!(heuristic, ConstructionHeuristicType::FirstFit) {
            return options.max_moves.min(1);
        }
        if options.max_moves != usize::MAX {
            return options.max_moves;
        }
        return entity_count
            .saturating_mul(options.max_rematch_size)
            .clamp(256, 4096);
    }
    options.max_moves
}