solverforge-solver 0.13.0

Solver engine for SolverForge
Documentation
fn assignment_model_with_rule(
    rule: crate::planning::ScalarAssignmentRule<CoveragePlan>,
) -> RuntimeModel<CoveragePlan, usize, DefaultMeter, DefaultMeter> {
    let scalar_slot = ScalarVariableSlot::new(
        0,
        0,
        "CoverageSlot",
        |solution: &CoveragePlan| solution.slots.len(),
        "worker",
        |solution, entity_index, _variable_index| solution.slots[entity_index].assigned,
        |solution, entity_index, _variable_index, value| {
            solution.slots[entity_index].assigned = value;
        },
        ValueSource::EntitySlice {
            values_for_entity: coverage_values,
        },
        true,
    );
    RuntimeModel::new(vec![VariableSlot::Scalar(scalar_slot)]).with_scalar_groups(
        bind_scalar_groups(
            vec![ScalarGroup::assignment(
                "slot_assignment",
                ScalarTarget::from_descriptor_index(0, "worker"),
            )
            .with_required_entity(coverage_required)
            .with_capacity_key(coverage_capacity_key)
            .with_assignment_rule(rule)
            .with_position_key(coverage_position_key)
            .with_sequence_key(coverage_sequence_key)
            .with_entity_order(coverage_entity_order)
            .with_value_order(coverage_value_order)
            .with_limits(ScalarGroupLimits {
                max_augmenting_depth: Some(3),
                ..ScalarGroupLimits::new()
            })],
            &[scalar_slot],
        ),
    )
}

fn no_adjacent_same_worker(
    solution: &CoveragePlan,
    left_entity: usize,
    left_worker: usize,
    right_entity: usize,
    right_worker: usize,
) -> bool {
    left_worker != right_worker
        || solution.slots[left_entity]
            .day
            .abs_diff(solution.slots[right_entity].day)
            > 1
}

#[test]
fn scalar_assignment_dense_construction_uses_full_required_augmenting_chain() {
    let solver_scope = solve_assignment_with_config_and_model(
        coverage_plan(
            5,
            vec![
                coverage_slot(true, 0, None, &[0]),
                coverage_slot(true, 0, Some(0), &[0, 1]),
                coverage_slot(true, 0, Some(1), &[1, 2]),
                coverage_slot(true, 0, Some(2), &[2, 3]),
                coverage_slot(true, 0, Some(3), &[3, 4]),
            ],
        ),
        assignment_config(),
        assignment_model_with_limits(ScalarGroupLimits {
            max_augmenting_depth: Some(8),
            ..ScalarGroupLimits::new()
        }),
    );

    let slots = &solver_scope.working_solution().slots;
    assert_eq!(slots[0].assigned, Some(0));
    assert_eq!(slots[1].assigned, Some(1));
    assert_eq!(slots[2].assigned, Some(2));
    assert_eq!(slots[3].assigned, Some(3));
    assert_eq!(slots[4].assigned, Some(4));
    assert_eq!(
        solver_scope.current_score().copied(),
        Some(HardSoftScore::of(0, 0))
    );
}

#[test]
fn scalar_assignment_rule_filters_required_construction_candidates() {
    let solver_scope = solve_assignment_with_config_and_model(
        coverage_plan(
            2,
            vec![
                coverage_slot(true, 0, Some(0), &[0]),
                coverage_slot(true, 1, None, &[0, 1]),
            ],
        ),
        assignment_config(),
        assignment_model_with_rule(no_adjacent_same_worker),
    );

    let slots = &solver_scope.working_solution().slots;
    assert_eq!(slots[0].assigned, Some(0));
    assert_eq!(slots[1].assigned, Some(1));
    assert_eq!(
        solver_scope.current_score().copied(),
        Some(HardSoftScore::of(0, 0))
    );
}

#[test]
fn scalar_assignment_selector_rechecks_temporal_neighbors_for_compound_moves() {
    let plan = coverage_plan(
        3,
        vec![
            coverage_slot(true, 0, Some(0), &[0, 1, 2]),
            coverage_slot(true, 1, Some(1), &[0, 1, 2]),
            coverage_slot(true, 2, Some(0), &[0, 1, 2]),
            coverage_slot(true, 3, Some(1), &[0, 1, 2]),
        ],
    );
    let model = assignment_model_with_rule(no_adjacent_same_worker);
    let group = &model.scalar_groups()[0];
    let crate::builder::ScalarGroupBindingKind::Assignment(assignment) = group.kind else {
        panic!("test model should contain an assignment-backed scalar group");
    };
    let options =
        crate::phase::construction::grouped_scalar::ScalarAssignmentMoveOptions::for_selector(
            group.limits,
            None,
            64,
            0,
        );
    let moves = crate::phase::construction::grouped_scalar::selector_assignment_moves(
        &assignment,
        &plan,
        options,
    );

    assert!(!moves.is_empty());
    for mov in moves {
        let mut assigned = plan
            .slots
            .iter()
            .map(|slot| slot.assigned)
            .collect::<Vec<_>>();
        for edit in mov.edits() {
            assigned[edit.entity_index] = edit.to_value;
        }
        for (entity_index, worker) in assigned.iter().copied().enumerate() {
            let Some(worker) = worker else {
                continue;
            };
            let day = plan.slots[entity_index].day;
            assert!(
                plan.slots.iter().enumerate().all(|(other_index, slot)| {
                    other_index == entity_index
                        || assigned[other_index] != Some(worker)
                        || slot.day.abs_diff(day) > 1
                }),
                "assignment move {:?} violates temporal neighbor rule",
                mov
            );
        }
    }
}