solverforge-solver 0.18.0

Solver engine for SolverForge
Documentation
use solverforge_config::SelectionOrder;
use solverforge_core::domain::{PlanningSolution, SolutionDescriptor};

use crate::builder::{RuntimeModel, ScalarAccessCapability};

use super::super::defaults::DefaultScalarBinding;
use super::super::graph::CompiledSelectorNode;
use super::super::selector_tree::compile_selector;
use super::super::types::{CompiledListSlot, RuntimeCompileError, RuntimeCompileErrorKind};
use super::{
    DefaultLocalSearchAcceptorPolicy, DefaultLocalSearchComponents,
    DefaultLocalSearchForagerPolicy, DefaultLocalSearchPlan,
};

mod list;
mod scalar;

const DEFAULT_LOCAL_SEARCH_LATE_ACCEPTANCE_SIZE: usize = 400;
const DEFAULT_LOCAL_SEARCH_ACCEPTED_COUNT: usize = 256;
const DEFAULT_SIMULATED_ANNEALING_DECAY_RATE: f64 = 0.999_985;

pub(crate) fn compile_default_local_search_components<S, V, DM, IDM>(
    model: &RuntimeModel<S, V, DM, IDM>,
    list_slots: &[CompiledListSlot<S, V, DM, IDM>],
    scalar_slots: &[DefaultScalarBinding<S>],
    random_seed: Option<u64>,
) -> DefaultLocalSearchComponents
where
    S: PlanningSolution,
{
    let has_lists = !list_slots.is_empty();
    let has_groups = !model.scalar_groups().is_empty();
    let has_precedence = list_slots.iter().any(list::supports_precedence_moves);
    let has_nearby_scalar = scalar_slots.iter().any(|binding| {
        !binding.assignment_owned
            && (binding
                .slot
                .has_capability(ScalarAccessCapability::NearbyValue)
                || binding
                    .slot
                    .has_capability(ScalarAccessCapability::NearbyEntity))
    });
    let registry = model.runtime_provider_registry();
    let has_conflict_repairs = !model.conflict_repairs().is_empty()
        || !registry.repairs().is_empty()
        || !registry.static_repairs().is_empty();

    let acceptor = if has_lists {
        DefaultLocalSearchAcceptorPolicy::LateAcceptance {
            history_size: DEFAULT_LOCAL_SEARCH_LATE_ACCEPTANCE_SIZE,
        }
    } else if has_groups {
        DefaultLocalSearchAcceptorPolicy::DiversifiedLateAcceptance {
            history_size: DEFAULT_LOCAL_SEARCH_LATE_ACCEPTANCE_SIZE,
        }
    } else {
        DefaultLocalSearchAcceptorPolicy::SimulatedAnnealing {
            decay_rate_bits: DEFAULT_SIMULATED_ANNEALING_DECAY_RATE.to_bits(),
            random_seed,
        }
    };

    let forager = if has_groups && !has_lists {
        DefaultLocalSearchForagerPolicy::FirstLastStepScoreImproving {
            accepted_count_limit: None,
        }
    } else if has_precedence {
        DefaultLocalSearchForagerPolicy::FirstLastStepScoreImproving {
            accepted_count_limit: Some(DEFAULT_LOCAL_SEARCH_ACCEPTED_COUNT),
        }
    } else {
        DefaultLocalSearchForagerPolicy::AcceptedCount {
            limit: if has_lists || has_nearby_scalar || has_conflict_repairs {
                DEFAULT_LOCAL_SEARCH_ACCEPTED_COUNT
            } else {
                1
            },
        }
    };

    DefaultLocalSearchComponents { acceptor, forager }
}

pub(crate) fn compile_default_local_search_plan<S, V, DM, IDM>(
    descriptor: &SolutionDescriptor,
    model: &RuntimeModel<S, V, DM, IDM>,
    list_slots: &[CompiledListSlot<S, V, DM, IDM>],
    scalar_slots: &[DefaultScalarBinding<S>],
    components: DefaultLocalSearchComponents,
) -> Result<
    Option<(
        DefaultLocalSearchPlan,
        Vec<CompiledSelectorNode<S, V, DM, IDM>>,
    )>,
    RuntimeCompileError,
>
where
    S: PlanningSolution + 'static,
    V: Clone,
    DM: Clone,
    IDM: Clone,
{
    let mut declarations = Vec::new();

    list::append_list_policy(list_slots, &mut declarations);
    scalar::append_nearby_scalar_policy(scalar_slots, &mut declarations);
    scalar::append_group_policy(model, &mut declarations)?;
    scalar::append_conflict_repair_policy(model, &mut declarations)?;
    scalar::append_ordinary_scalar_policy(scalar_slots, &mut declarations);

    if declarations.is_empty() {
        return Ok(None);
    }
    let selection_order = if declarations.len() == 1 {
        solverforge_config::UnionSelectionOrder::Sequential
    } else {
        solverforge_config::UnionSelectionOrder::StratifiedRandom
    };
    let nodes = declarations
        .iter()
        .enumerate()
        .map(|(index, declaration)| {
            compile_selector(
                &declaration.config,
                SelectionOrder::Random,
                &format!("default_runtime.local_search.selectors[{index}]"),
                descriptor,
                model,
            )
        })
        .collect::<Result<Vec<_>, _>>()?;
    Ok(Some((
        DefaultLocalSearchPlan {
            components,
            selection_order,
            selectors: declarations,
        },
        nodes,
    )))
}

pub(super) fn default_selector_error(message: impl Into<String>) -> RuntimeCompileError {
    RuntimeCompileError {
        path: "default_runtime.local_search".to_string(),
        kind: RuntimeCompileErrorKind::LocalSearchShape {
            message: message.into(),
        },
    }
}