solverforge-scoring 0.13.0

Incremental constraint scoring for SolverForge
Documentation
use super::support::*;

#[test]
fn projected_self_join_score_director_cached_score_matches_fresh_after_updates() {
    let mut director = ScoreDirector::new(
        projected_asymmetric_self_join_plan(),
        (projected_asymmetric_self_join_constraint(),),
    );
    assert_projected_director_matches_fresh(&mut director);

    for (entity_index, bucket, demand, enabled) in [
        (2, 0, 40, true),
        (1, 0, 20, false),
        (1, 0, 5, true),
        (0, 2, 2, true),
        (2, 0, 30, false),
        (2, 0, 30, true),
    ] {
        director.before_variable_changed(0, entity_index);
        {
            let work = &mut director.working_solution_mut().work[entity_index];
            work.bucket = bucket;
            work.demand = demand;
            work.enabled = enabled;
        }
        director.after_variable_changed(0, entity_index);
        assert_projected_director_matches_fresh(&mut director);
    }
}

#[test]
fn projected_self_join_nested_typed_undo_restores_cached_score() {
    let initial_plan = projected_asymmetric_self_join_plan();
    let mut director = ScoreDirector::new(
        initial_plan.clone(),
        (projected_asymmetric_self_join_constraint(),),
    );
    assert_projected_director_matches_fresh(&mut director);

    let outer_score_state = director.snapshot_score_state();
    let old_outer_work = director.working_solution().work[1].clone();
    director.before_variable_changed(0, 1);
    {
        let work = &mut director.working_solution_mut().work[1];
        work.bucket = 0;
        work.demand = 5;
        work.enabled = true;
    }
    director.after_variable_changed(0, 1);
    assert_eq!(
        director.calculate_score(),
        fresh_projected_asymmetric_self_join_score(director.working_solution())
    );

    let nested_score_state = director.snapshot_score_state();
    let old_nested_work = director.working_solution().work[2].clone();
    director.before_variable_changed(0, 2);
    {
        let work = &mut director.working_solution_mut().work[2];
        work.bucket = 0;
        work.demand = 30;
        work.enabled = false;
    }
    director.after_variable_changed(0, 2);
    assert_eq!(
        director.calculate_score(),
        fresh_projected_asymmetric_self_join_score(director.working_solution())
    );

    director.before_variable_changed(0, 2);
    director.working_solution_mut().work[2] = old_nested_work;
    director.after_variable_changed(0, 2);
    director.restore_score_state(nested_score_state);
    assert_eq!(
        director.calculate_score(),
        fresh_projected_asymmetric_self_join_score(director.working_solution())
    );

    director.before_variable_changed(0, 1);
    director.working_solution_mut().work[1] = old_outer_work;
    director.after_variable_changed(0, 1);
    director.restore_score_state(outer_score_state);
    assert_eq!(director.working_solution().work, initial_plan.work);
    assert_projected_director_matches_fresh(&mut director);
}

#[test]
fn projected_merged_descriptor_sources_update_only_owning_slot() {
    let mut constraint = ConstraintFactory::<Plan, SoftScore>::new()
        .for_each(source(
            work as fn(&Plan) -> &[Work],
            ChangeSource::Descriptor(0),
        ))
        .project(WorkEntryProjection)
        .merge(
            ConstraintFactory::<Plan, SoftScore>::new()
                .for_each(source(
                    capacity as fn(&Plan) -> &[Capacity],
                    ChangeSource::Descriptor(1),
                ))
                .project(CapacityEntryProjection),
        )
        .group_by(
            |entry: &Entry| entry.bucket,
            sum(|entry: &Entry| entry.delta),
        )
        .penalize(|_bucket: &usize, delta: &i64| SoftScore::of((*delta).max(0)))
        .named("capacity shortage");

    let mut plan = Plan {
        work: vec![Work {
            bucket: 0,
            demand: 5,
            enabled: true,
        }],
        capacity: vec![Capacity {
            bucket: 0,
            capacity: 3,
        }],
    };

    let mut total = constraint.initialize(&plan);
    assert_eq!(total, SoftScore::of(-2));

    total = total + constraint.on_retract(&plan, 0, 1);
    plan.capacity[0].capacity = 8;
    total = total + constraint.on_insert(&plan, 0, 1);

    assert_eq!(total, SoftScore::of(0));
    assert_eq!(total, constraint.evaluate(&plan));
}

#[test]
fn projected_merged_descriptor_sources_keep_same_entity_index_slots_distinct() {
    let mut constraint = ConstraintFactory::<Plan, SoftScore>::new()
        .for_each(source(
            work as fn(&Plan) -> &[Work],
            ChangeSource::Descriptor(0),
        ))
        .project(WorkEntryProjection)
        .merge(
            ConstraintFactory::<Plan, SoftScore>::new()
                .for_each(source(
                    capacity as fn(&Plan) -> &[Capacity],
                    ChangeSource::Descriptor(1),
                ))
                .project(CapacityEntryProjection),
        )
        .penalize(|entry: &Entry| SoftScore::of(entry.delta))
        .named("merged projected rows");

    let mut plan = Plan {
        work: vec![Work {
            bucket: 0,
            demand: 5,
            enabled: true,
        }],
        capacity: vec![Capacity {
            bucket: 0,
            capacity: 3,
        }],
    };

    let mut total = constraint.initialize(&plan);
    assert_eq!(total, SoftScore::of(-2));

    total = total + constraint.on_retract(&plan, 0, 0);
    plan.work[0].demand = 8;
    total = total + constraint.on_insert(&plan, 0, 0);

    assert_eq!(total, SoftScore::of(-5));
    assert_eq!(total, constraint.evaluate(&plan));
}

#[test]
#[should_panic(expected = "cannot localize entity indexes")]
fn projected_unknown_source_panics_on_localized_callback() {
    let mut constraint = ConstraintFactory::<Plan, SoftScore>::new()
        .for_each(work as fn(&Plan) -> &[Work])
        .project(WorkEntryProjection)
        .penalize(|entry: &Entry| SoftScore::of(entry.delta))
        .named("unknown projected");

    let plan = Plan {
        work: vec![Work {
            bucket: 0,
            demand: 5,
            enabled: true,
        }],
        capacity: Vec::new(),
    };

    constraint.initialize(&plan);
    constraint.on_retract(&plan, 0, 0);
}