solverforge-solver 0.12.0

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

use solverforge_core::domain::PlanningSolution;
use solverforge_scoring::Director;

use crate::builder::context::{ScalarCandidate, ScalarGroupBinding, ScalarGroupLimits};
use crate::descriptor::ResolvedVariableBinding;
use crate::heuristic::r#move::{CompoundScalarMove, Move};
use crate::scope::{PhaseScope, ProgressCallback};

use super::move_build::compound_move_for_group_candidate;
use crate::phase::construction::{
    ConstructionGroupSlotId, ConstructionGroupSlotKey, ConstructionSlotId,
};

pub(super) struct NormalizedGroupedCandidate<S>
where
    S: PlanningSolution,
{
    pub(super) sequence: usize,
    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(super) value_order_key: Option<i64>,
    pub(super) mov: CompoundScalarMove<S>,
}

pub(super) fn normalize_grouped_candidates<S, D, ProgressCb>(
    phase_scope: &PhaseScope<'_, '_, S, D, ProgressCb>,
    group_index: usize,
    group: &ScalarGroupBinding<S>,
    scalar_bindings: &[ResolvedVariableBinding<S>],
    limits: ScalarGroupLimits,
    group_candidate_limit: Option<usize>,
) -> Vec<NormalizedGroupedCandidate<S>>
where
    S: PlanningSolution + 'static,
    D: Director<S>,
    ProgressCb: ProgressCallback<S>,
{
    let solution = phase_scope.score_director().working_solution();
    let raw_candidates = (group.candidate_provider)(solution, limits);
    let total_limit = group_candidate_limit.unwrap_or(usize::MAX);
    if total_limit == 0 {
        return Vec::new();
    }
    let mut seen = HashSet::new();
    let mut normalized = Vec::new();

    for (sequence, candidate) in raw_candidates.into_iter().enumerate() {
        if candidate.edits().is_empty()
            || !seen.insert((candidate.reason(), candidate.edits().to_vec()))
        {
            continue;
        }
        let Some((scalar_slots, keep_current_legal, has_unfinished_unassigned_slot)) =
            scalar_slots_for_candidate(phase_scope, group, scalar_bindings, solution, &candidate)
        else {
            continue;
        };
        if !has_unfinished_unassigned_slot {
            continue;
        }

        let group_slot = group_slot_id(group_index, &candidate, &scalar_slots);
        if phase_scope
            .solver_scope()
            .is_group_slot_completed(group_slot.clone())
        {
            continue;
        }

        let Some(mov) = compound_move_for_group_candidate(group, solution, &candidate) else {
            continue;
        };
        if !mov.is_doable(phase_scope.score_director()) {
            continue;
        }

        normalized.push(NormalizedGroupedCandidate {
            sequence,
            group_slot,
            scalar_slots,
            keep_current_legal,
            entity_order_key: candidate.construction_entity_order_key(),
            value_order_key: candidate.construction_value_order_key(),
            mov,
        });
        if normalized.len() >= total_limit {
            break;
        }
    }

    normalized
}

fn scalar_slots_for_candidate<S, D, ProgressCb>(
    phase_scope: &PhaseScope<'_, '_, S, D, ProgressCb>,
    group: &ScalarGroupBinding<S>,
    scalar_bindings: &[ResolvedVariableBinding<S>],
    solution: &S,
    candidate: &ScalarCandidate<S>,
) -> Option<(Vec<ConstructionSlotId>, bool, bool)>
where
    S: PlanningSolution,
    D: Director<S>,
    ProgressCb: ProgressCallback<S>,
{
    let mut targets = HashSet::new();
    let mut scalar_slots = Vec::with_capacity(candidate.edits().len());
    let mut keep_current_legal = true;
    let mut has_unfinished_unassigned_slot = false;

    for edit in candidate.edits() {
        if !targets.insert((
            edit.descriptor_index(),
            edit.entity_index(),
            edit.variable_name(),
        )) {
            return None;
        }
        let member = group.member_for_edit(edit)?;
        if !member.value_is_legal(solution, edit.entity_index(), edit.to_value()) {
            return None;
        }
        let binding = scalar_bindings.iter().find(|binding| {
            binding.descriptor_index == member.descriptor_index
                && binding.variable_index == member.variable_index
        })?;
        let slot = binding.slot_id(edit.entity_index());
        if phase_scope.solver_scope().is_scalar_slot_completed(slot) {
            return None;
        }
        if member
            .current_value(solution, edit.entity_index())
            .is_none()
        {
            has_unfinished_unassigned_slot = true;
        }
        keep_current_legal &= member.allows_unassigned;
        scalar_slots.push(slot);
    }

    scalar_slots.sort_unstable();
    scalar_slots.dedup();
    Some((
        scalar_slots,
        keep_current_legal,
        has_unfinished_unassigned_slot,
    ))
}

fn group_slot_id<S>(
    group_index: usize,
    candidate: &ScalarCandidate<S>,
    scalar_slots: &[ConstructionSlotId],
) -> ConstructionGroupSlotId {
    let key = candidate
        .construction_slot_key()
        .map(ConstructionGroupSlotKey::Explicit)
        .unwrap_or_else(|| ConstructionGroupSlotKey::Targets(scalar_slots.to_vec()));
    ConstructionGroupSlotId::new(group_index, key)
}