solverforge-solver 0.9.0

Solver engine for SolverForge
Documentation

#[test]
fn pillar_change_uses_public_pillar_semantics() {
    let director = create_director(Schedule {
        workers: vec![0, 1, 2],
        shifts: vec![
            Shift {
                worker: Some(0),
                allowed_workers: vec![0, 1, 2],
            },
            Shift {
                worker: Some(0),
                allowed_workers: vec![0, 1, 2],
            },
            Shift {
                worker: Some(1),
                allowed_workers: vec![0, 1, 2],
            },
        ],
        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 config = MoveSelectorConfig::PillarChangeMoveSelector(PillarChangeMoveConfig {
        minimum_sub_pillar_size: 0,
        maximum_sub_pillar_size: 0,
        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!(moves.len(), 2);
    assert!(moves.iter().all(|mov| matches!(
        mov,
        crate::heuristic::r#move::ScalarMoveUnion::PillarChange(_)
    )));
}

#[test]
fn pillar_change_intersects_entity_slice_domains() {
    let director = create_director(Schedule {
        workers: vec![0, 1, 2],
        shifts: vec![
            Shift {
                worker: Some(0),
                allowed_workers: vec![0, 1, 2],
            },
            Shift {
                worker: Some(0),
                allowed_workers: vec![0, 2],
            },
            Shift {
                worker: Some(1),
                allowed_workers: vec![0, 1, 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::PillarChangeMoveSelector(PillarChangeMoveConfig {
        minimum_sub_pillar_size: 0,
        maximum_sub_pillar_size: 0,
        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!(moves.len(), 1);
    assert!(moves.iter().all(|mov| mov.is_doable(&director)));
    assert!(matches!(
        &moves[0],
        crate::heuristic::r#move::ScalarMoveUnion::PillarChange(change)
            if change.to_value() == Some(&2)
    ));
}

#[test]
fn pillar_swap_prunes_illegal_entity_slice_partners() {
    let director = create_director(Schedule {
        workers: vec![0, 1, 2],
        shifts: vec![
            Shift {
                worker: Some(0),
                allowed_workers: vec![0, 2],
            },
            Shift {
                worker: Some(0),
                allowed_workers: vec![0, 2],
            },
            Shift {
                worker: Some(1),
                allowed_workers: vec![1, 2],
            },
            Shift {
                worker: Some(1),
                allowed_workers: vec![1, 2],
            },
            Shift {
                worker: Some(2),
                allowed_workers: vec![0, 1, 2],
            },
            Shift {
                worker: Some(2),
                allowed_workers: vec![0, 1, 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::PillarSwapMoveSelector(PillarSwapMoveConfig {
        minimum_sub_pillar_size: 0,
        maximum_sub_pillar_size: 0,
        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 mut swap_pairs = Vec::new();
    for mov in &moves {
        assert!(mov.is_doable(&director));
        if let crate::heuristic::r#move::ScalarMoveUnion::PillarSwap(swap) = mov {
            let left_value =
                get_worker(director.working_solution(), swap.left_indices()[0], 0).unwrap();
            let right_value =
                get_worker(director.working_solution(), swap.right_indices()[0], 0).unwrap();
            swap_pairs.push((left_value, right_value));
        }
    }
    swap_pairs.sort_unstable();

    assert_eq!(swap_pairs, vec![(0, 2), (1, 2)]);
}

fn keep_all_cartesian_scalar_candidates(
    candidate: MoveCandidateRef<
        '_,
        Schedule,
        crate::heuristic::r#move::ScalarMoveUnion<Schedule, usize>,
    >,
) -> bool {
    matches!(candidate, MoveCandidateRef::Sequential(_))
}

#[test]
fn scalar_builder_cartesian_selector_survives_filtering_wrapper() {
    let director = create_director(Schedule {
        workers: vec![0, 1, 2],
        shifts: vec![
            Shift {
                worker: Some(0),
                allowed_workers: vec![0, 1, 2],
            },
            Shift {
                worker: Some(1),
                allowed_workers: vec![0, 1, 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::CartesianProductMoveSelector(CartesianProductConfig {
        selectors: vec![
            MoveSelectorConfig::ChangeMoveSelector(ChangeMoveConfig {
                target: VariableTargetConfig::default(),
            }),
            MoveSelectorConfig::SwapMoveSelector(SwapMoveConfig {
                target: VariableTargetConfig::default(),
            }),
        ],
    });

    let selector = build_scalar_move_selector(Some(&config), &scalar_variables, None);
    let filtered = FilteringMoveSelector::new(selector, keep_all_cartesian_scalar_candidates);
    let mut cursor = filtered.open_cursor(&director);
    let indices = collect_cursor_indices::<
        Schedule,
        crate::heuristic::r#move::ScalarMoveUnion<Schedule, usize>,
        _,
    >(&mut cursor);

    assert!(!indices.is_empty());
    assert!(indices.iter().all(|&index| matches!(
        cursor.candidate(index),
        Some(MoveCandidateRef::Sequential(_))
    )));
    assert!(cursor.take_candidate(indices[0]).is_doable(&director));
}