solverforge-solver 0.17.0

Solver engine for SolverForge
Documentation
use solverforge_core::domain::{
    DynamicListVariableSlot, DynamicModelBackend, DynamicScalarVariableSlot, EntityClassId,
    VariableId,
};

#[derive(Clone, Debug)]
struct DynamicRow;

#[derive(Clone, Debug)]
struct DynamicPlan {
    score: Option<SoftScore>,
    scalar_values: Vec<Option<usize>>,
    scalar_candidates: Vec<Vec<usize>>,
    lists: Vec<Vec<usize>>,
    list_element_count: usize,
}

impl PlanningSolution for DynamicPlan {
    type Score = SoftScore;

    fn score(&self) -> Option<Self::Score> {
        self.score
    }

    fn set_score(&mut self, score: Option<Self::Score>) {
        self.score = score;
    }

    fn is_initialized(&self) -> bool {
        self.scalar_values.iter().all(Option::is_some)
            && self
                .lists
                .iter()
                .map(Vec::len)
                .sum::<usize>()
                == self.list_element_count
    }
}

impl DynamicModelBackend for DynamicPlan {
    type Score = SoftScore;

    fn entity_count(&self, entity: EntityClassId) -> usize {
        match entity.0 {
            0 => self.scalar_values.len(),
            1 => self.lists.len(),
            _ => 0,
        }
    }

    fn get_scalar(
        &self,
        _entity: EntityClassId,
        row: usize,
        _variable: VariableId,
    ) -> Option<usize> {
        self.scalar_values.get(row).copied().flatten()
    }

    fn set_scalar(
        &mut self,
        _entity: EntityClassId,
        row: usize,
        _variable: VariableId,
        value: Option<usize>,
    ) {
        if let Some(slot) = self.scalar_values.get_mut(row) {
            *slot = value;
        }
    }

    fn list_len(&self, _entity: EntityClassId, row: usize, _variable: VariableId) -> usize {
        self.lists.get(row).map(Vec::len).unwrap_or(0)
    }

    fn list_get(
        &self,
        _entity: EntityClassId,
        row: usize,
        _variable: VariableId,
        pos: usize,
    ) -> Option<usize> {
        self.lists.get(row)?.get(pos).copied()
    }

    fn list_insert(
        &mut self,
        _entity: EntityClassId,
        row: usize,
        _variable: VariableId,
        pos: usize,
        value: usize,
    ) {
        let Some(list) = self.lists.get_mut(row) else {
            return;
        };
        list.insert(pos.min(list.len()), value);
    }

    fn list_remove(
        &mut self,
        _entity: EntityClassId,
        row: usize,
        _variable: VariableId,
        pos: usize,
    ) -> Option<usize> {
        let list = self.lists.get_mut(row)?;
        (pos < list.len()).then(|| list.remove(pos))
    }

    fn candidate_values(
        &self,
        _entity: EntityClassId,
        row: usize,
        _variable: VariableId,
    ) -> &[usize] {
        self.scalar_candidates
            .get(row)
            .map(Vec::as_slice)
            .unwrap_or(&[])
    }

    fn list_element_count(&self, _entity: EntityClassId, _variable: VariableId) -> usize {
        self.list_element_count
    }

    fn list_assigned_elements(&self, _entity: EntityClassId, _variable: VariableId) -> Vec<usize> {
        self.lists
            .iter()
            .flat_map(|list| list.iter().copied())
            .collect()
    }
}

fn dynamic_descriptor() -> SolutionDescriptor {
    let task = EntityDescriptor::new("Task", TypeId::of::<DynamicRow>(), "tasks")
        .with_logical_id(EntityClassId(0))
        .with_variable(
            VariableDescriptor::genuine("worker")
                .with_logical_id(VariableId(0))
                .with_allows_unassigned(false),
        );
    let vehicle = EntityDescriptor::new("Vehicle", TypeId::of::<DynamicRow>(), "vehicles")
        .with_logical_id(EntityClassId(1))
        .with_variable(VariableDescriptor::list("visits").with_logical_id(VariableId(0)));
    SolutionDescriptor::new("DynamicPlan", TypeId::of::<DynamicPlan>())
        .with_entity(task)
        .with_entity(vehicle)
}

fn reversed_dynamic_descriptor() -> SolutionDescriptor {
    let task = EntityDescriptor::new("Task", TypeId::of::<DynamicRow>(), "tasks")
        .with_logical_id(EntityClassId(0))
        .with_variable(
            VariableDescriptor::genuine("worker")
                .with_logical_id(VariableId(0))
                .with_allows_unassigned(false),
        );
    let vehicle = EntityDescriptor::new("Vehicle", TypeId::of::<DynamicRow>(), "vehicles")
        .with_logical_id(EntityClassId(1))
        .with_variable(VariableDescriptor::list("visits").with_logical_id(VariableId(0)));
    SolutionDescriptor::new("DynamicPlan", TypeId::of::<DynamicPlan>())
        .with_entity(vehicle)
        .with_entity(task)
}

fn dynamic_director(solution: DynamicPlan) -> ScoreDirector<DynamicPlan, ()> {
    let descriptor = dynamic_descriptor();
    ScoreDirector::simple(solution, descriptor, |solution, _| {
        solution.scalar_values.len() + solution.list_element_count
    })
}

#[derive(Clone, Debug)]
struct PreferWorkerOne {
    constraint_ref: solverforge_core::ConstraintRef,
    descriptor_index: usize,
}

impl Default for PreferWorkerOne {
    fn default() -> Self {
        Self {
            constraint_ref: solverforge_core::ConstraintRef::new("", "Prefer worker one"),
            descriptor_index: 0,
        }
    }
}

impl PreferWorkerOne {
    fn for_descriptor_index(descriptor_index: usize) -> Self {
        Self {
            constraint_ref: solverforge_core::ConstraintRef::new("", "Prefer worker one"),
            descriptor_index,
        }
    }

    fn row_score(&self, solution: &DynamicPlan, row: usize) -> i64 {
        if solution.scalar_values.get(row).copied().flatten() == Some(1) {
            0
        } else {
            -10
        }
    }
}

impl solverforge_scoring::IncrementalConstraintSealed for PreferWorkerOne {}

impl solverforge_scoring::IncrementalConstraint<DynamicPlan, SoftScore> for PreferWorkerOne {
    fn evaluate(&self, solution: &DynamicPlan) -> SoftScore {
        SoftScore::of(
            solution
                .scalar_values
                .iter()
                .enumerate()
                .map(|(row, _)| self.row_score(solution, row))
                .sum(),
        )
    }

    fn match_count(&self, solution: &DynamicPlan) -> usize {
        solution
            .scalar_values
            .iter()
            .filter(|value| **value != Some(1))
            .count()
    }

    fn initialize(&mut self, solution: &DynamicPlan) -> SoftScore {
        self.evaluate(solution)
    }

    fn on_insert(
        &mut self,
        solution: &DynamicPlan,
        entity_index: usize,
        descriptor_index: usize,
    ) -> SoftScore {
        if descriptor_index == self.descriptor_index {
            SoftScore::of(self.row_score(solution, entity_index))
        } else {
            SoftScore::ZERO
        }
    }

    fn on_retract(
        &mut self,
        solution: &DynamicPlan,
        entity_index: usize,
        descriptor_index: usize,
    ) -> SoftScore {
        if descriptor_index == self.descriptor_index {
            SoftScore::of(-self.row_score(solution, entity_index))
        } else {
            SoftScore::ZERO
        }
    }

    fn reset(&mut self) {}

    fn constraint_ref(&self) -> &solverforge_core::ConstraintRef {
        &self.constraint_ref
    }

    fn weight(&self) -> SoftScore {
        SoftScore::of(-10)
    }
}

fn dynamic_preference_director(
    solution: DynamicPlan,
) -> ScoreDirector<DynamicPlan, PreferWorkerOne> {
    let descriptor = dynamic_descriptor();
    ScoreDirector::with_descriptor(
        solution,
        PreferWorkerOne::default(),
        descriptor,
        |solution, descriptor_index| match descriptor_index {
            0 => solution.scalar_values.len(),
            1 => solution.lists.len(),
            _ => 0,
        },
    )
}

#[derive(Clone, Debug)]
struct PreferOrderedVisits {
    constraint_ref: solverforge_core::ConstraintRef,
    descriptor_index: usize,
}

impl Default for PreferOrderedVisits {
    fn default() -> Self {
        Self {
            constraint_ref: solverforge_core::ConstraintRef::new("", "Prefer ordered visits"),
            descriptor_index: 1,
        }
    }
}

impl PreferOrderedVisits {
    fn for_descriptor_index(descriptor_index: usize) -> Self {
        Self {
            constraint_ref: solverforge_core::ConstraintRef::new("", "Prefer ordered visits"),
            descriptor_index,
        }
    }

    fn row_score(&self, solution: &DynamicPlan, row: usize) -> i64 {
        if solution
            .lists
            .get(row)
            .is_some_and(|list| list.as_slice() == [0, 1])
        {
            0
        } else {
            -10
        }
    }
}

impl solverforge_scoring::IncrementalConstraintSealed for PreferOrderedVisits {}

impl solverforge_scoring::IncrementalConstraint<DynamicPlan, SoftScore> for PreferOrderedVisits {
    fn evaluate(&self, solution: &DynamicPlan) -> SoftScore {
        SoftScore::of(
            solution
                .lists
                .iter()
                .enumerate()
                .map(|(row, _)| self.row_score(solution, row))
                .sum(),
        )
    }

    fn match_count(&self, solution: &DynamicPlan) -> usize {
        solution
            .lists
            .iter()
            .filter(|list| list.as_slice() != [0, 1])
            .count()
    }

    fn initialize(&mut self, solution: &DynamicPlan) -> SoftScore {
        self.evaluate(solution)
    }

    fn on_insert(
        &mut self,
        solution: &DynamicPlan,
        entity_index: usize,
        descriptor_index: usize,
    ) -> SoftScore {
        if descriptor_index == self.descriptor_index {
            SoftScore::of(self.row_score(solution, entity_index))
        } else {
            SoftScore::ZERO
        }
    }

    fn on_retract(
        &mut self,
        solution: &DynamicPlan,
        entity_index: usize,
        descriptor_index: usize,
    ) -> SoftScore {
        if descriptor_index == self.descriptor_index {
            SoftScore::of(-self.row_score(solution, entity_index))
        } else {
            SoftScore::ZERO
        }
    }

    fn reset(&mut self) {}

    fn constraint_ref(&self) -> &solverforge_core::ConstraintRef {
        &self.constraint_ref
    }

    fn weight(&self) -> SoftScore {
        SoftScore::of(-10)
    }
}

fn dynamic_ordered_visits_director(
    solution: DynamicPlan,
) -> ScoreDirector<DynamicPlan, PreferOrderedVisits> {
    let descriptor = dynamic_descriptor();
    ScoreDirector::with_descriptor(
        solution,
        PreferOrderedVisits::default(),
        descriptor,
        |solution, descriptor_index| match descriptor_index {
            0 => solution.scalar_values.len(),
            1 => solution.lists.len(),
            _ => 0,
        },
    )
}