solverforge-bridge 0.16.0

Public dynamic model bridge contracts for SolverForge host-language bindings
Documentation
use solverforge_core::domain::PlanningSolution;

use crate::{
    DynamicListVariableSlot, DynamicModelBackend, DynamicScalarVariableSlot, DynamicScore,
    EntityClassId, VariableId,
};

#[derive(Clone)]
struct DynamicRows {
    task_values: Vec<Option<usize>>,
    vehicle_routes: Vec<Vec<usize>>,
    candidates: Vec<usize>,
    score: Option<DynamicScore>,
}

impl PlanningSolution for DynamicRows {
    type Score = DynamicScore;

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

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

impl DynamicModelBackend for DynamicRows {
    type Score = DynamicScore;

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

    fn get_scalar(&self, entity: EntityClassId, row: usize, variable: VariableId) -> Option<usize> {
        match (entity.0, variable.0) {
            (0, 0) => self.task_values[row],
            _ => None,
        }
    }

    fn set_scalar(
        &mut self,
        entity: EntityClassId,
        row: usize,
        variable: VariableId,
        value: Option<usize>,
    ) {
        if (entity.0, variable.0) == (0, 0) {
            self.task_values[row] = value;
        }
    }

    fn list_len(&self, entity: EntityClassId, row: usize, variable: VariableId) -> usize {
        match (entity.0, variable.0) {
            (1, 1) => self.vehicle_routes[row].len(),
            _ => 0,
        }
    }

    fn list_get(
        &self,
        entity: EntityClassId,
        row: usize,
        variable: VariableId,
        pos: usize,
    ) -> Option<usize> {
        match (entity.0, variable.0) {
            (1, 1) => self.vehicle_routes[row].get(pos).copied(),
            _ => None,
        }
    }

    fn list_insert(
        &mut self,
        entity: EntityClassId,
        row: usize,
        variable: VariableId,
        pos: usize,
        value: usize,
    ) {
        if (entity.0, variable.0) == (1, 1) {
            self.vehicle_routes[row].insert(pos, value);
        }
    }

    fn list_remove(
        &mut self,
        entity: EntityClassId,
        row: usize,
        variable: VariableId,
        pos: usize,
    ) -> Option<usize> {
        if (entity.0, variable.0) == (1, 1) {
            return Some(self.vehicle_routes[row].remove(pos));
        }
        None
    }

    fn candidate_values(
        &self,
        entity: EntityClassId,
        _row: usize,
        variable: VariableId,
    ) -> &[usize] {
        match (entity.0, variable.0) {
            (0, 0) => &self.candidates,
            _ => &[],
        }
    }

    fn list_element_count(&self, entity: EntityClassId, variable: VariableId) -> usize {
        match (entity.0, variable.0) {
            (1, 1) => 4,
            _ => 0,
        }
    }

    fn list_assigned_elements(&self, entity: EntityClassId, variable: VariableId) -> Vec<usize> {
        match (entity.0, variable.0) {
            (1, 1) => self
                .vehicle_routes
                .iter()
                .flat_map(|route| route.iter().copied())
                .collect(),
            _ => Vec::new(),
        }
    }
}

#[test]
fn one_rust_state_type_can_host_multiple_logical_entity_classes() {
    let mut model = DynamicRows {
        task_values: vec![None, Some(1)],
        vehicle_routes: vec![vec![0, 2], vec![1]],
        candidates: vec![0, 1, 2],
        score: None,
    };

    let task = EntityClassId(0);
    let vehicle = EntityClassId(1);
    let task_assignment = VariableId(0);
    let visits = VariableId(1);

    assert_eq!(model.entity_count(task), 2);
    assert_eq!(model.entity_count(vehicle), 2);
    assert_eq!(model.get_scalar(task, 0, task_assignment), None);
    assert_eq!(model.candidate_values(task, 0, task_assignment), &[0, 1, 2]);

    model.set_scalar(task, 0, task_assignment, Some(2));
    assert_eq!(model.get_scalar(task, 0, task_assignment), Some(2));

    assert_eq!(model.list_get(vehicle, 0, visits, 1), Some(2));
    model.list_insert(vehicle, 1, visits, 1, 3);
    assert_eq!(model.list_len(vehicle, 1, visits), 2);
    assert_eq!(model.list_remove(vehicle, 1, visits, 0), Some(1));
    assert_eq!(model.list_get(vehicle, 1, visits, 0), Some(3));
}

#[test]
fn dynamic_scalar_slot_carries_logical_identity() {
    let mut model = DynamicRows {
        task_values: vec![None],
        vehicle_routes: Vec::new(),
        candidates: vec![2, 4, 6],
        score: None,
    };
    let slot =
        DynamicScalarVariableSlot::new(EntityClassId(0), VariableId(0), "Task", "worker", true);

    assert_eq!(slot.entity_count(&model), 1);
    assert_eq!(slot.current_value(&model, 0), None);
    assert!(slot.value_is_legal(&model, 0, None));
    assert!(slot.value_is_legal(&model, 0, Some(4)));
    assert!(!slot.value_is_legal(&model, 0, Some(5)));

    slot.set_value(&mut model, 0, Some(6));
    assert_eq!(slot.current_value(&model, 0), Some(6));
}

#[test]
fn dynamic_list_slot_carries_logical_identity() {
    let mut model = DynamicRows {
        task_values: Vec::new(),
        vehicle_routes: vec![vec![1], vec![2, 3]],
        candidates: Vec::new(),
        score: None,
    };
    let slot = DynamicListVariableSlot::new(EntityClassId(1), VariableId(1), "Vehicle", "visits");

    assert_eq!(slot.entity_count(&model), 2);
    assert_eq!(slot.element_count(&model), 4);
    assert_eq!(slot.assigned_elements(&model), vec![1, 2, 3]);
    assert_eq!(slot.list_len(&model, 1), 2);
    assert_eq!(slot.list_get(&model, 1, 0), Some(2));

    slot.list_insert(&mut model, 0, 1, 0);
    assert_eq!(slot.list_get(&model, 0, 1), Some(0));
    assert_eq!(slot.list_remove(&mut model, 1, 1), Some(3));
    assert_eq!(slot.assigned_elements(&model), vec![1, 0, 2]);
}