solverforge-solver 0.19.13

Solver engine for SolverForge
Documentation
#[test]
fn time_limit_after_first_fit_selection_commits_before_termination() {
    let gate = BlockingEvaluationGate::delaying(Duration::from_millis(100));
    let director = ConstructionPauseDirector::new(ConstructionPauseSolution::new(None));
    let mut solver_scope = SolverScope::new(director);
    solver_scope.start_solving();
    solver_scope.set_time_limit(Duration::from_millis(20));

    let placer =
        ScoredConstructionPlacer::new((0..66).collect(), false).with_eval_gate(Some(gate));
    let mut phase = ConstructionHeuristicPhase::new(placer, FirstFitForager::new());
    phase.solve(&mut solver_scope);

    assert_eq!(solver_scope.working_solution().entities[0].value, Some(0));
    assert_eq!(solver_scope.stats().moves_generated, 1);
    assert_eq!(solver_scope.stats().moves_evaluated, 1);
    assert_eq!(solver_scope.stats().moves_accepted, 1);
    assert_eq!(solver_scope.stats().moves_applied, 1);
    assert_eq!(
        solver_scope.terminal_reason(),
        SolverTerminalReason::TerminatedByConfig
    );
}

#[test]
fn time_limit_after_first_feasible_selection_commits_before_termination() {
    let gate = BlockingEvaluationGate::delaying(Duration::from_millis(100));
    let director = ConstructionPauseDirector::with_score_mode(
        ConstructionPauseSolution::new(None),
        ConstructionPauseScoreMode::AssignedSum {
            unassigned_score: -2,
        },
    );
    let mut solver_scope = SolverScope::new(director);
    solver_scope.start_solving();
    solver_scope.set_time_limit(Duration::from_millis(20));

    let placer =
        ScoredConstructionPlacer::new((0..66).collect(), true).with_eval_gate(Some(gate));
    let mut phase = ConstructionHeuristicPhase::new(placer, FirstFeasibleForager::new());
    phase.solve(&mut solver_scope);

    assert_eq!(solver_scope.working_solution().entities[0].value, Some(0));
    assert_eq!(solver_scope.stats().moves_generated, 1);
    assert_eq!(solver_scope.stats().moves_evaluated, 1);
    assert_eq!(solver_scope.stats().moves_accepted, 1);
    assert_eq!(solver_scope.stats().moves_applied, 1);
    assert_eq!(
        solver_scope.terminal_reason(),
        SolverTerminalReason::TerminatedByConfig
    );
}

fn assert_direct_limit_commits_and_terminates(
    configure: impl FnOnce(&mut SolverScope<'static, ConstructionPauseSolution, ConstructionPauseDirector>),
    keep_current_legal: bool,
) {
    let director = ConstructionPauseDirector::with_score_mode(
        ConstructionPauseSolution::new(None),
        ConstructionPauseScoreMode::AssignedSum {
            unassigned_score: -2,
        },
    );
    let mut solver_scope = SolverScope::new(director);
    solver_scope.start_solving();
    configure(&mut solver_scope);

    let placer = ScoredConstructionPlacer::new(vec![0, 1], keep_current_legal);
    let mut phase = ConstructionHeuristicPhase::new(placer, FirstFitForager::new());
    phase.solve(&mut solver_scope);

    assert_eq!(solver_scope.working_solution().entities[0].value, Some(0));
    assert_eq!(solver_scope.stats().moves_generated, 1);
    assert_eq!(solver_scope.stats().moves_evaluated, 1);
    assert_eq!(solver_scope.stats().moves_accepted, 1);
    assert_eq!(solver_scope.stats().moves_applied, 1);
    assert_eq!(
        solver_scope.terminal_reason(),
        SolverTerminalReason::TerminatedByConfig
    );
}

#[test]
fn direct_step_limit_terminates_after_committing_selection() {
    assert_direct_limit_commits_and_terminates(
        |scope| scope.inphase_step_count_limit = Some(1),
        false,
    );
}

#[test]
fn direct_move_limit_terminates_after_committing_selection() {
    assert_direct_limit_commits_and_terminates(
        |scope| scope.inphase_move_count_limit = Some(1),
        false,
    );
}

#[test]
fn direct_score_calculation_limit_terminates_after_committing_selection() {
    assert_direct_limit_commits_and_terminates(
        |scope| scope.inphase_score_calc_count_limit = Some(2),
        true,
    );
}

#[test]
fn shared_move_budget_commits_selection_before_child_termination() {
    let parent_director = ConstructionPauseDirector::new(ConstructionPauseSolution::new(None));
    let mut parent_scope = SolverScope::new(parent_director);
    parent_scope.inphase_move_count_limit = Some(1);
    let phase_budget = parent_scope.child_phase_budget();
    let child_config = parent_scope.child_config(Some(&phase_budget));

    let child_director = ConstructionPauseDirector::new(ConstructionPauseSolution::new(None));
    let mut child_scope = child_config.build_scope(child_director, 0);
    child_scope.start_solving();
    let placer = ScoredConstructionPlacer::new((0..66).collect(), false);
    let mut phase = ConstructionHeuristicPhase::new(placer, FirstFitForager::new());
    phase.solve(&mut child_scope);

    assert_eq!(child_scope.working_solution().entities[0].value, Some(0));
    assert_eq!(child_scope.stats().moves_generated, 1);
    assert_eq!(child_scope.stats().moves_evaluated, 1);
    assert_eq!(child_scope.stats().moves_accepted, 1);
    assert_eq!(child_scope.stats().moves_applied, 1);
    assert_eq!(
        child_scope.terminal_reason(),
        SolverTerminalReason::TerminatedByConfig
    );
}

#[test]
fn cancellation_after_decisive_evaluation_keeps_the_committed_selection() {
    let gate = BlockingEvaluationGate::new(1);
    let terminate = AtomicBool::new(false);

    rayon::scope(|scope| {
        scope.spawn(|_| {
            let director = ConstructionPauseDirector::new(ConstructionPauseSolution::new(None));
            let mut solver_scope = SolverScope::new(director).with_terminate(Some(&terminate));
            solver_scope.start_solving();
            let placer = ScoredConstructionPlacer::new((0..66).collect(), false)
                .with_eval_gate(Some(gate.clone()));
            let mut phase = ConstructionHeuristicPhase::new(placer, FirstFitForager::new());
            phase.solve(&mut solver_scope);

            assert_eq!(solver_scope.working_solution().entities[0].value, Some(0));
            assert_eq!(solver_scope.stats().moves_generated, 1);
            assert_eq!(solver_scope.stats().moves_applied, 1);
            assert_eq!(solver_scope.terminal_reason(), SolverTerminalReason::Cancelled);
        });

        gate.wait_until_blocked();
        terminate.store(true, Ordering::SeqCst);
        gate.release();
    });
}

#[test]
fn pause_after_decisive_evaluation_snapshots_the_committed_selection() {
    static MANAGER: SolverManager<ConstructionPauseSolution> = SolverManager::new();

    let gate = BlockingEvaluationGate::new(1);
    let (job_id, mut receiver) = MANAGER
        .solve(ConstructionPauseSolution::decisive(gate.clone()))
        .expect("decisive construction job should start");
    gate.wait_until_blocked();
    MANAGER.pause(job_id).expect("pause should be accepted");

    assert!(matches!(
        receiver.blocking_recv().expect("pause requested event"),
        SolverEvent::PauseRequested { .. }
    ));
    gate.release();

    let revision = match receiver.blocking_recv().expect("paused event") {
        SolverEvent::Paused { metadata } => metadata
            .snapshot_revision
            .expect("paused snapshot revision"),
        other => panic!("unexpected event: {other:?}"),
    };
    let snapshot = MANAGER
        .get_snapshot(job_id, Some(revision))
        .expect("paused snapshot");
    assert_eq!(snapshot.solution.entities[0].value, Some(0));
    assert_eq!(snapshot.telemetry.moves_generated, 1);
    assert_eq!(snapshot.telemetry.moves_applied, 1);

    MANAGER.resume(job_id).expect("resume should be accepted");
    assert!(matches!(
        receiver.blocking_recv().expect("resumed event"),
        SolverEvent::Resumed { .. }
    ));
    match receiver.blocking_recv().expect("completed event") {
        SolverEvent::Completed { solution, .. } => {
            assert_eq!(solution.entities[0].value, Some(0));
        }
        other => panic!("unexpected event: {other:?}"),
    }
    MANAGER.delete(job_id).expect("delete decisive job");
}

impl ConstructionPauseSolution {
    fn best_fit_interrupted_pull(gate: BlockingEvaluationGate) -> Self {
        Self {
            eval_gate: Some(gate),
            solvable_mode: ConstructionPauseSolvableMode::BestFitInterruptedPull,
            ..Self::new(None)
        }
    }
}

fn solve_best_fit_interrupted_pull(
    solver_scope: &mut SolverScope<'_, ConstructionPauseSolution, ConstructionPauseDirector>,
    gate: Option<BlockingEvaluationGate>,
) {
    let placer = InterruptAfterRetainedCandidatePlacer {
        gate: gate.expect("interrupted-pull solve requires a generation gate"),
    };
    let mut phase = ConstructionHeuristicPhase::new(placer, BestFitForager::new());
    phase.solve(solver_scope);
}

struct InterruptAfterRetainedCandidateCursor {
    candidate: Option<ConstructionPauseMove>,
    pulled: bool,
    gate: BlockingEvaluationGate,
}

impl InterruptAfterRetainedCandidateCursor {
    fn new(gate: BlockingEvaluationGate) -> Self {
        Self {
            candidate: Some(ConstructionPauseMove::new(0, 7, true, None)),
            pulled: false,
            gate,
        }
    }
}

impl MoveCursor<ConstructionPauseSolution, ConstructionPauseMove>
    for InterruptAfterRetainedCandidateCursor
{
    fn next_candidate(&mut self) -> Option<CandidateId> {
        if self.pulled {
            None
        } else {
            self.pulled = true;
            Some(CandidateId::new(0))
        }
    }

    fn next_candidate_with_control<ShouldStop>(
        &mut self,
        should_stop: &mut ShouldStop,
    ) -> Option<CandidateId>
    where
        ShouldStop: FnMut() -> bool,
    {
        if !self.pulled {
            return self.next_candidate();
        }
        self.gate.on_evaluation();
        let _ = should_stop();
        None
    }

    fn candidate(
        &self,
        id: CandidateId,
    ) -> Option<MoveCandidateRef<'_, ConstructionPauseSolution, ConstructionPauseMove>> {
        if id.index() != 0 {
            return None;
        }
        self.candidate.as_ref().map(MoveCandidateRef::Borrowed)
    }

    fn take_candidate(&mut self, id: CandidateId) -> ConstructionPauseMove {
        assert_eq!(id.index(), 0);
        self.candidate
            .take()
            .expect("retained construction candidate must remain live")
    }
}

#[derive(Clone, Debug)]
struct InterruptAfterRetainedCandidatePlacer {
    gate: BlockingEvaluationGate,
}

impl EntityPlacerCursor<ConstructionPauseSolution, ConstructionPauseMove>
    for InterruptAfterRetainedCandidatePlacer
{
    type CandidateCursor = InterruptAfterRetainedCandidateCursor;

    fn next_placement<D, IsCompleted, ShouldStop>(
        &mut self,
        _score_director: &D,
        mut is_completed: IsCompleted,
        mut should_stop: ShouldStop,
    ) -> Option<Placement<ConstructionPauseSolution, ConstructionPauseMove, Self::CandidateCursor>>
    where
        D: Director<ConstructionPauseSolution>,
        IsCompleted: FnMut(
            &Placement<ConstructionPauseSolution, ConstructionPauseMove, Self::CandidateCursor>,
        ) -> bool,
        ShouldStop: FnMut() -> bool,
    {
        if should_stop() {
            return None;
        }
        let placement = Placement::new(
            EntityReference::new(0, 0),
            InterruptAfterRetainedCandidateCursor::new(self.gate.clone()),
        )
        .with_slot_id(crate::phase::construction::ConstructionSlotId::new(0, 0));
        (!is_completed(&placement)).then_some(placement)
    }
}

impl EntityPlacer<ConstructionPauseSolution, ConstructionPauseMove>
    for InterruptAfterRetainedCandidatePlacer
{
    type Cursor<'a>
        = Self
    where
        Self: 'a;

    fn open_cursor<'a, D: Director<ConstructionPauseSolution>>(
        &'a self,
        _score_director: &D,
    ) -> Self::Cursor<'a> {
        Self {
            gate: self.gate.clone(),
        }
    }
}

#[test]
fn paused_candidate_pull_restarts_without_committing_or_completing_retained_selection() {
    static MANAGER: SolverManager<ConstructionPauseSolution> = SolverManager::new();

    let gate = BlockingEvaluationGate::new(1);
    let (job_id, mut receiver) = MANAGER
        .solve(ConstructionPauseSolution::best_fit_interrupted_pull(
            gate.clone(),
        ))
        .expect("interrupted-pull construction job should start");
    gate.wait_until_blocked();
    MANAGER.pause(job_id).expect("pause should be accepted");
    assert!(matches!(
        receiver.blocking_recv().expect("pause requested event"),
        SolverEvent::PauseRequested { .. }
    ));
    gate.release();

    let paused_revision = match receiver.blocking_recv().expect("paused event") {
        SolverEvent::Paused { metadata } => metadata
            .snapshot_revision
            .expect("paused snapshot revision"),
        other => panic!("unexpected event: {other:?}"),
    };
    let paused_snapshot = MANAGER
        .get_snapshot(job_id, Some(paused_revision))
        .expect("paused snapshot");

    MANAGER.resume(job_id).expect("resume should be accepted");
    assert!(matches!(
        receiver.blocking_recv().expect("resumed event"),
        SolverEvent::Resumed { .. }
    ));
    let completed_solution = match receiver.blocking_recv().expect("completed event") {
        SolverEvent::Completed { solution, .. } => solution,
        other => panic!("unexpected event: {other:?}"),
    };
    MANAGER.delete(job_id).expect("delete resumed job");

    assert_eq!(paused_snapshot.solution.entities[0].value, None);
    assert_eq!(paused_snapshot.telemetry.moves_generated, 1);
    assert_eq!(paused_snapshot.telemetry.moves_evaluated, 1);
    assert_eq!(paused_snapshot.telemetry.moves_accepted, 0);
    assert_eq!(paused_snapshot.telemetry.moves_applied, 0);
    assert_eq!(completed_solution.entities[0].value, Some(7));
}