solverforge-solver 0.9.0

Solver engine for SolverForge
Documentation

#[test]
fn builds_solution_count_scalar_selectors_without_descriptor_bindings() {
    let director = create_director(Schedule {
        workers: vec![0, 1, 2],
        shifts: vec![
            Shift {
                worker: Some(0),
                allowed_workers: vec![0, 2],
            },
            Shift {
                worker: Some(1),
                allowed_workers: vec![1],
            },
        ],
        score: None,
    });

    let scalar_variables = vec![ScalarVariableContext::new(
        0,
        0,
        "Shift",
        shift_count,
        "worker",
        get_worker,
        set_worker,
        ValueSource::SolutionCount {
            count_fn: worker_count,
            provider_index: 0,
        },
        true,
    )];

    let selector = build_scalar_move_selector::<Schedule>(None, &scalar_variables, None);
    let moves: Vec<_> = selector.iter_moves(&director).collect();

    assert_eq!(selector.size(&director), 9);
    assert_eq!(moves.len(), 9);
    assert_eq!(
        moves.iter()
            .filter(|mov| matches!(mov, crate::heuristic::r#move::ScalarMoveUnion::Change(change) if change.to_value().is_none()))
            .count(),
        2
    );
}

#[test]
fn filters_change_moves_against_entity_slice_candidates() {
    let director = create_director(Schedule {
        workers: vec![0, 1, 2],
        shifts: vec![
            Shift {
                worker: Some(0),
                allowed_workers: vec![0, 2],
            },
            Shift {
                worker: Some(1),
                allowed_workers: vec![1],
            },
        ],
        score: None,
    });

    let scalar_variables = vec![ScalarVariableContext::new(
        0,
        0,
        "Shift",
        shift_count,
        "worker",
        get_worker,
        set_worker,
        ValueSource::EntitySlice {
            values_for_entity: allowed_workers,
        },
        true,
    )];

    let config = MoveSelectorConfig::ChangeMoveSelector(ChangeMoveConfig {
        target: VariableTargetConfig {
            entity_class: Some("Shift".to_string()),
            variable_name: Some("worker".to_string()),
        },
    });

    let selector = build_scalar_move_selector(Some(&config), &scalar_variables, None);
    let moves: Vec<_> = selector.iter_moves(&director).collect();

    assert_eq!(selector.size(&director), 5);
    assert_eq!(moves.len(), 5);
    assert_eq!(
        moves.iter()
            .filter(|mov| matches!(mov, crate::heuristic::r#move::ScalarMoveUnion::Change(change) if change.to_value().is_none()))
            .count(),
        2
    );
}

#[test]
fn filters_swap_moves_against_entity_slice_candidates_before_evaluation() {
    let director = create_director(Schedule {
        workers: vec![0, 1, 2],
        shifts: vec![
            Shift {
                worker: Some(0),
                allowed_workers: vec![0, 1],
            },
            Shift {
                worker: Some(1),
                allowed_workers: vec![0, 1],
            },
            Shift {
                worker: Some(2),
                allowed_workers: vec![2],
            },
        ],
        score: None,
    });

    let scalar_variables = vec![ScalarVariableContext::new(
        0,
        0,
        "Shift",
        shift_count,
        "worker",
        get_worker,
        set_worker,
        ValueSource::EntitySlice {
            values_for_entity: allowed_workers,
        },
        true,
    )];
    let config = MoveSelectorConfig::SwapMoveSelector(SwapMoveConfig {
        target: VariableTargetConfig {
            entity_class: Some("Shift".to_string()),
            variable_name: Some("worker".to_string()),
        },
    });

    let selector = build_scalar_move_selector(Some(&config), &scalar_variables, None);
    let moves: Vec<_> = selector.iter_moves(&director).collect();
    let swap_pairs: Vec<_> = moves
        .iter()
        .map(|mov| match mov {
            crate::heuristic::r#move::ScalarMoveUnion::Swap(swap) => {
                (swap.left_entity_index(), swap.right_entity_index())
            }
            other => panic!("expected swap move, got {other:?}"),
        })
        .collect();

    assert_eq!(swap_pairs, vec![(0, 1)]);
    assert!(moves.iter().all(|mov| mov.is_doable(&director)));
}

#[test]
fn swap_selector_emits_complete_assignment_swaps_without_domain() {
    let director = create_director(Schedule {
        workers: vec![],
        shifts: vec![
            Shift {
                worker: Some(0),
                allowed_workers: vec![],
            },
            Shift {
                worker: Some(1),
                allowed_workers: vec![],
            },
        ],
        score: None,
    });

    let scalar_variables = vec![ScalarVariableContext::new(
        0,
        0,
        "Shift",
        shift_count,
        "worker",
        get_worker,
        set_worker,
        ValueSource::Empty,
        false,
    )];
    let config = MoveSelectorConfig::SwapMoveSelector(SwapMoveConfig {
        target: VariableTargetConfig {
            entity_class: Some("Shift".to_string()),
            variable_name: Some("worker".to_string()),
        },
    });

    let selector = build_scalar_move_selector(Some(&config), &scalar_variables, None);
    let moves: Vec<_> = selector.iter_moves(&director).collect();

    assert_eq!(selector.size(&director), 1);
    assert_eq!(moves.len(), 1);
    assert!(matches!(
        &moves[0],
        crate::heuristic::r#move::ScalarMoveUnion::Swap(swap)
            if (swap.left_entity_index(), swap.right_entity_index()) == (0, 1)
    ));
    assert!(moves[0].is_doable(&director));
}

#[test]
fn swap_selector_rejects_explicit_empty_entity_slice_domain() {
    let director = create_director(Schedule {
        workers: vec![],
        shifts: vec![
            Shift {
                worker: Some(0),
                allowed_workers: vec![],
            },
            Shift {
                worker: Some(1),
                allowed_workers: vec![],
            },
        ],
        score: None,
    });

    let scalar_variables = vec![ScalarVariableContext::new(
        0,
        0,
        "Shift",
        shift_count,
        "worker",
        get_worker,
        set_worker,
        ValueSource::EntitySlice {
            values_for_entity: allowed_workers,
        },
        false,
    )];
    let config = MoveSelectorConfig::SwapMoveSelector(SwapMoveConfig {
        target: VariableTargetConfig {
            entity_class: Some("Shift".to_string()),
            variable_name: Some("worker".to_string()),
        },
    });

    let selector = build_scalar_move_selector(Some(&config), &scalar_variables, None);
    let moves: Vec<_> = selector.iter_moves(&director).collect();

    assert_eq!(selector.size(&director), 0);
    assert!(moves.is_empty());
}