solverforge-scoring 0.8.9

Incremental constraint scoring for SolverForge
Documentation
use crate::api::constraint_set::IncrementalConstraint;
use crate::constraint::IncrementalCrossBiConstraint;
use solverforge_core::score::SoftScore;
use solverforge_core::{ConstraintRef, ImpactType};

#[derive(Clone, Debug, PartialEq, Eq)]
struct Employee {
    id: usize,
    unavailable_days: Vec<u32>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
struct Shift {
    employee_id: Option<usize>,
    day: u32,
}

#[derive(Clone)]
struct Schedule {
    shifts: Vec<Shift>,
    employees: Vec<Employee>,
}

fn create_unavailable_employee_constraint() -> impl IncrementalConstraint<Schedule, SoftScore> {
    IncrementalCrossBiConstraint::new(
        ConstraintRef::new("", "Unavailable employee"),
        ImpactType::Penalty,
        (|schedule: &Schedule| schedule.shifts.as_slice()) as fn(&Schedule) -> &[Shift],
        (|schedule: &Schedule| schedule.employees.as_slice()) as fn(&Schedule) -> &[Employee],
        |shift: &Shift| shift.employee_id,
        |employee: &Employee| Some(employee.id),
        |_schedule: &Schedule, shift: &Shift, employee: &Employee| {
            shift.employee_id.is_some() && employee.unavailable_days.contains(&shift.day)
        },
        |_schedule: &Schedule, _shift_idx: usize, _employee_idx: usize| SoftScore::of(1),
        false,
    )
}

fn sample_schedule() -> Schedule {
    Schedule {
        shifts: vec![
            Shift {
                employee_id: Some(0),
                day: 5,
            },
            Shift {
                employee_id: Some(0),
                day: 6,
            },
        ],
        employees: vec![Employee {
            id: 0,
            unavailable_days: vec![5],
        }],
    }
}

#[test]
fn test_cross_bi_evaluate_works_without_initialize() {
    let constraint = create_unavailable_employee_constraint();
    let schedule = sample_schedule();

    assert_eq!(constraint.evaluate(&schedule), SoftScore::of(-1));
}

#[test]
fn test_cross_bi_match_count_works_without_initialize() {
    let constraint = create_unavailable_employee_constraint();
    let schedule = sample_schedule();

    assert_eq!(constraint.match_count(&schedule), 1);
}

#[test]
fn test_cross_bi_get_matches_works_without_initialize() {
    let constraint = create_unavailable_employee_constraint();
    let schedule = sample_schedule();

    let matches = constraint.get_matches(&schedule);

    assert_eq!(matches.len(), 1);
    assert_eq!(matches[0].constraint_ref.name, "Unavailable employee");
    assert_eq!(matches[0].score, SoftScore::of(-1));
    assert_eq!(matches[0].justification.entities.len(), 2);
    assert_eq!(
        matches[0].justification.entities[0]
            .as_entity::<Shift>()
            .unwrap(),
        &Shift {
            employee_id: Some(0),
            day: 5,
        }
    );
    assert_eq!(
        matches[0].justification.entities[1]
            .as_entity::<Employee>()
            .unwrap(),
        &Employee {
            id: 0,
            unavailable_days: vec![5],
        }
    );
}

#[test]
fn test_cross_bi_incremental_updates_still_work() {
    let mut constraint = create_unavailable_employee_constraint();
    let schedule = sample_schedule();

    let initial = constraint.initialize(&schedule);
    assert_eq!(initial, SoftScore::of(-1));

    let delta = constraint.on_retract(&schedule, 0, 0);
    assert_eq!(delta, SoftScore::of(1));

    let delta = constraint.on_insert(&schedule, 0, 0);
    assert_eq!(delta, SoftScore::of(-1));
}