Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion crates/solverforge-bridge/WIREFRAME.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,6 @@ pub fn try_run_dynamic_solver_with_config_parts<S, C, V, DM, IDM>(
entity_count_by_descriptor: fn(&S, usize) -> usize,
runtime: SolverRuntime<S>,
config: SolverConfig,
default_time_limit_secs: u64,
log_scale: fn(&S),
qualified_candidate_trace_provenance: Option<QualifiedCandidateTraceRunProvenance>,
model: RuntimeModel<S, V, DM, IDM>,
Expand Down
2 changes: 0 additions & 2 deletions crates/solverforge-bridge/src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ pub fn try_run_dynamic_solver_with_config_parts<S, C, V, DM, IDM>(
entity_count_by_descriptor: fn(&S, usize) -> usize,
runtime: SolverRuntime<S>,
config: SolverConfig,
default_time_limit_secs: u64,
log_scale: fn(&S),
qualified_candidate_trace_provenance: Option<QualifiedCandidateTraceRunProvenance>,
model: RuntimeModel<S, V, DM, IDM>,
Expand All @@ -156,7 +155,6 @@ where
entity_count_by_descriptor,
runtime,
config,
default_time_limit_secs,
log_scale,
qualified_candidate_trace_provenance,
move |config, descriptor| dynamic_search_declaration(config, descriptor, model),
Expand Down
7 changes: 6 additions & 1 deletion crates/solverforge-config/WIREFRAME.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ Constructor: `const fn new(max_entries: NonZeroUsize) -> Self`.

Derives: `Debug, Clone, Default, Deserialize, Serialize`.

Every configured criterion applies, combined with OR, at solver and phase
level alike: the solve or phase stops at whichever is reached first. Nothing is
added that is not configured; a score or work criterion without a time limit
runs without a time limit.

| Field | Type | Note |
|-------|------|------|
| `seconds_spent_limit` | `Option<u64>` | Max seconds |
Expand All @@ -112,7 +117,7 @@ Derives: `Debug, Clone, Default, Deserialize, Serialize`.

| Method | Signature | Note |
|--------|-----------|------|
| `time_limit` | `fn(&self) -> Option<Duration>` | Combines seconds + minutes × 60 |
| `time_limit` | `fn(&self) -> Option<Duration>` | Seconds + minutes × 60, saturating at `Duration::MAX`; `None` only when neither is set, so `0` is a zero limit |
| `unimproved_time_limit` | `fn(&self) -> Option<Duration>` | Maps unimproved seconds to Duration |

### `DirectorConfig`
Expand Down
25 changes: 14 additions & 11 deletions crates/solverforge-config/src/termination.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ use std::time::Duration;

use serde::{Deserialize, Serialize};

/// Termination configuration.
/// Termination configuration. Every configured criterion applies, combined
/// with OR: the solve (or phase) stops at whichever is reached first. Nothing
/// is added that is not configured; without a time limit there is no time
/// limit.
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct TerminationConfig {
/// Maximum seconds to spend solving.
/// Maximum seconds to spend solving; added to `minutes_spent_limit`.
pub seconds_spent_limit: Option<u64>,

/// Maximum minutes to spend solving.
/// Maximum minutes to spend solving; added to `seconds_spent_limit`.
pub minutes_spent_limit: Option<u64>,

/// Target best score to achieve (as string, e.g., "0hard/0soft").
Expand All @@ -26,16 +29,16 @@ pub struct TerminationConfig {
}

impl TerminationConfig {
/// Sum of `seconds_spent_limit` and `minutes_spent_limit`; `None` when the
/// sum is zero.
/// Sum of `seconds_spent_limit` and `minutes_spent_limit`; `None` when
/// neither is set. Zero is a limit of zero; a sum past `Duration::MAX`
/// saturates there.
pub fn time_limit(&self) -> Option<Duration> {
let seconds =
self.seconds_spent_limit.unwrap_or(0) + self.minutes_spent_limit.unwrap_or(0) * 60;
if seconds > 0 {
Some(Duration::from_secs(seconds))
} else {
None
if self.seconds_spent_limit.is_none() && self.minutes_spent_limit.is_none() {
return None;
}
let seconds = Duration::from_secs(self.seconds_spent_limit.unwrap_or(0));
let minutes = Duration::from_secs(self.minutes_spent_limit.unwrap_or(0)).saturating_mul(60);
Some(seconds.saturating_add(minutes))
}

/// `unimproved_seconds_spent_limit` as a `Duration`.
Expand Down
23 changes: 23 additions & 0 deletions crates/solverforge-config/src/tests/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,26 @@ fn score_director_defaults_to_the_constraints_function_with_analysis_enabled() {
);
assert!(!set.constraint_match_enabled());
}

#[test]
fn time_limit_is_literal_and_saturating() {
use crate::TerminationConfig;
use std::time::Duration;

let limit = |seconds, minutes| {
TerminationConfig {
seconds_spent_limit: seconds,
minutes_spent_limit: minutes,
..TerminationConfig::default()
}
.time_limit()
};
assert_eq!(limit(None, None), None);
assert_eq!(limit(Some(0), None), Some(Duration::ZERO));
assert_eq!(limit(None, Some(0)), Some(Duration::ZERO));
assert_eq!(limit(Some(30), Some(2)), Some(Duration::from_secs(150)));
assert_eq!(
limit(Some(u64::MAX), Some(u64::MAX)),
Some(Duration::from_secs(u64::MAX).saturating_add(Duration::MAX))
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -241,14 +241,6 @@ pub(super) fn generate_runtime_phase_support(
impl #solution_name {
#scalar_candidate_count_helper

fn __solverforge_default_time_limit_secs() -> u64 {
if Self::__solverforge_has_list_variable() {
60
} else {
30
}
}

#[inline]
fn __solverforge_has_list_variable() -> bool {
false #(|| #has_list_variable_terms)*
Expand Down Expand Up @@ -294,10 +286,6 @@ pub(super) fn generate_runtime_phase_support(
impl #solution_name {
#scalar_candidate_count_helper

const fn __solverforge_default_time_limit_secs() -> u64 {
30
}

fn __solverforge_log_scale(solution: &Self) {
let descriptor = Self::descriptor();
::solverforge::__internal::log_solve_start(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ pub(super) fn generate_runtime_solve_internal(
Self::entity_count,
runtime,
config,
Self::__solverforge_default_time_limit_secs(),
Self::__solverforge_log_scale,
#provenance,
Self::__solverforge_search_declaration,
Expand Down
1 change: 0 additions & 1 deletion crates/solverforge-solver/WIREFRAME.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,6 @@ src/
│ ├── step_count.rs — StepCountTermination
│ ├── best_score.rs — BestScoreTermination<Sc>
│ ├── unimproved.rs — UnimprovedStepCountTermination<S>, UnimprovedTimeTermination<S>
│ ├── composite.rs — OrTermination<T, S, D> (tuple impls up to 8)
│ └── tests.rs — Tests
│
└── realtime/
Expand Down
2 changes: 1 addition & 1 deletion crates/solverforge-solver/public-api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ fn analyze: pub fn analyze<S>(solution: &S) -> ScoreAnalysis<S::Score> where S:
fn bind_scalar_groups: pub fn bind_scalar_groups<S>( groups: Vec<ScalarGroup<S>>, scalar_slots: &[ScalarVariableSlot<S>], ) -> Vec<ScalarGroupBinding<S>>
fn local_search: pub fn local_search<S, M, MS, A, Fo>( move_selector: MS, acceptor: A, forager: Fo, ) -> LocalSearchPhase<S, M, SelectorCursorSource<MS>, A, Fo> where S: PlanningSolution, M: Move<S> + 'static, MS: MoveSelector<S, M>, A: Acceptor<S>, Fo: LocalSearchForager<S, M>,
fn log_solve_start: pub fn log_solve_start( entity_count: usize, element_count: Option<usize>, candidate_count: Option<usize>, )
fn try_run_solver_with_config_and_search: pub fn try_run_solver_with_config_and_search<S, C, V, DM, IDM, Declaration, BuildSearch>( solution: S, constraints: fn(&S) -> C, descriptor: SolutionDescriptor, entity_count_by_descriptor: fn(&S, usize) -> usize, runtime: SolverRuntime<S>, config: SolverConfig, default_time_limit_secs: u64, log_scale: fn(&S), qualified_candidate_trace_provenance: Option<QualifiedCandidateTraceRunProvenance>, build_search: BuildSearch, ) -> RuntimeBuildResult<S> where S: PlanningSolution + Clone + Send + Sync + 'static, S::Score: Score + Copy + Ord + ParseableScore, C: ConstraintSet<S, S::Score>, V: Clone + Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static, DM: CrossEntityDistanceMeter<S> + Clone + Send + Sync + Debug + 'static, IDM: CrossEntityDistanceMeter<S> + Clone + Send + Sync + Debug + 'static, Declaration: Search<S, V, DM, IDM>, Declaration::Extensions: RuntimeExtensionRegistry<S, V, DM, IDM>, BuildSearch: FnOnce(&SolverConfig, SolutionDescriptor) -> RuntimeBuildResult<Declaration>,
fn try_run_solver_with_config_and_search: pub fn try_run_solver_with_config_and_search<S, C, V, DM, IDM, Declaration, BuildSearch>( solution: S, constraints: fn(&S) -> C, descriptor: SolutionDescriptor, entity_count_by_descriptor: fn(&S, usize) -> usize, runtime: SolverRuntime<S>, config: SolverConfig, log_scale: fn(&S), qualified_candidate_trace_provenance: Option<QualifiedCandidateTraceRunProvenance>, build_search: BuildSearch, ) -> RuntimeBuildResult<S> where S: PlanningSolution + Clone + Send + Sync + 'static, S::Score: Score + Copy + Ord + ParseableScore, C: ConstraintSet<S, S::Score>, V: Clone + Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static, DM: CrossEntityDistanceMeter<S> + Clone + Send + Sync + Debug + 'static, IDM: CrossEntityDistanceMeter<S> + Clone + Send + Sync + Debug + 'static, Declaration: Search<S, V, DM, IDM>, Declaration::Extensions: RuntimeExtensionRegistry<S, V, DM, IDM>, BuildSearch: FnOnce(&SolverConfig, SolutionDescriptor) -> RuntimeBuildResult<Declaration>,
type RepairProvider: pub type RepairProvider<S> = fn(&S, RepairLimits) -> Vec<RepairCandidate<S>>;
type RuntimeBuildResult: pub type RuntimeBuildResult<T> = Result<T, RuntimeBuildError>;
type ScalarAssignmentRule: pub type ScalarAssignmentRule<S> = fn(&S, usize, usize, usize, usize) -> bool;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,6 @@ impl Solvable for ZeroVariableLifecycleSolution {
zero_variable_entity_count,
runtime,
solverforge_config::SolverConfig::default(),
30,
zero_variable_log_scale,
None,
|config, descriptor| {
Expand Down
66 changes: 3 additions & 63 deletions crates/solverforge-solver/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,35 +25,22 @@ use crate::runtime::compiler::executor::{
use crate::runtime::compiler::{compile_runtime_graph, CompiledRuntimeExecutor, RuntimeGraphInput};
use crate::runtime_build_error::{RuntimeBuildError, RuntimeBuildResult};
use crate::scope::{ProgressCallback, SolverProgressKind, SolverProgressRef, SolverScope};
use crate::solver::{NoTermination, Solver};
use crate::solver::Solver;
use crate::stats::{
format_duration, whole_units_per_second, CandidateTraceExecutionPolicy,
QualifiedCandidateTraceRunProvenance,
};
use crate::termination::{
BestScoreTermination, OrTermination, StepCountTermination, Termination, TimeTermination,
BestScoreTermination, StepCountTermination, Termination, TimeTermination,
UnimprovedStepCountTermination, UnimprovedTimeTermination,
};

pub(crate) mod termination;
use termination::build_director_termination;
use termination::{build_solver_termination, ConfiguredSolverTermination};
pub(crate) use termination::{
configured_best_score_limit, configured_execution_policy, parse_configured_termination,
};

/// Monomorphized termination enum for config-driven solver configurations.
///
/// Avoids repeated branching across termination overloads by capturing the
/// selected termination variant upfront.
pub(crate) enum AnyTermination<S: PlanningSolution, D: Director<S>> {
None(NoTermination),
Default(OrTermination<(TimeTermination,), S, D>),
WithBestScore(OrTermination<(TimeTermination, BestScoreTermination<S::Score>), S, D>),
WithStepCount(OrTermination<(TimeTermination, StepCountTermination), S, D>),
WithUnimprovedStep(OrTermination<(TimeTermination, UnimprovedStepCountTermination<S>), S, D>),
WithUnimprovedTime(OrTermination<(TimeTermination, UnimprovedTimeTermination<S>), S, D>),
}

#[derive(Clone)]
pub(crate) struct ChannelProgressCallback<S: PlanningSolution> {
runtime: SolverRuntime<S>,
Expand Down Expand Up @@ -99,47 +86,6 @@ impl<S: PlanningSolution> ProgressCallback<S> for ChannelProgressCallback<S> {
}
}

impl<S: PlanningSolution, D: Director<S>> fmt::Debug for AnyTermination<S, D> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::None(_) => write!(f, "AnyTermination::None"),
Self::Default(_) => write!(f, "AnyTermination::Default"),
Self::WithBestScore(_) => write!(f, "AnyTermination::WithBestScore"),
Self::WithStepCount(_) => write!(f, "AnyTermination::WithStepCount"),
Self::WithUnimprovedStep(_) => write!(f, "AnyTermination::WithUnimprovedStep"),
Self::WithUnimprovedTime(_) => write!(f, "AnyTermination::WithUnimprovedTime"),
}
}
}

impl<S: PlanningSolution, D: Director<S>, ProgressCb: ProgressCallback<S>>
Termination<S, D, ProgressCb> for AnyTermination<S, D>
where
S::Score: Score,
{
fn is_terminated(&mut self, solver_scope: &SolverScope<S, D, ProgressCb>) -> bool {
match self {
Self::None(t) => t.is_terminated(solver_scope),
Self::Default(t) => t.is_terminated(solver_scope),
Self::WithBestScore(t) => t.is_terminated(solver_scope),
Self::WithStepCount(t) => t.is_terminated(solver_scope),
Self::WithUnimprovedStep(t) => t.is_terminated(solver_scope),
Self::WithUnimprovedTime(t) => t.is_terminated(solver_scope),
}
}

fn install_inphase_limits(&self, solver_scope: &mut SolverScope<S, D, ProgressCb>) {
match self {
Self::None(t) => t.install_inphase_limits(solver_scope),
Self::Default(t) => t.install_inphase_limits(solver_scope),
Self::WithBestScore(t) => t.install_inphase_limits(solver_scope),
Self::WithStepCount(t) => t.install_inphase_limits(solver_scope),
Self::WithUnimprovedStep(t) => t.install_inphase_limits(solver_scope),
Self::WithUnimprovedTime(t) => t.install_inphase_limits(solver_scope),
}
}
}

/// Logs the `solve_start` tracing event with the entity count and exactly
/// one solve scale: `element_count` for list models or `candidate_count` for
/// scalar models.
Expand Down Expand Up @@ -202,7 +148,6 @@ pub fn try_run_solver_with_config_and_search<S, C, V, DM, IDM, Declaration, Buil
entity_count_by_descriptor: fn(&S, usize) -> usize,
runtime: SolverRuntime<S>,
config: SolverConfig,
default_time_limit_secs: u64,
log_scale: fn(&S),
qualified_candidate_trace_provenance: Option<QualifiedCandidateTraceRunProvenance>,
build_search: BuildSearch,
Expand Down Expand Up @@ -235,7 +180,6 @@ where
entity_count_by_descriptor,
runtime,
config,
default_time_limit_secs,
log_scale,
qualified_candidate_trace_provenance,
build_search,
Expand All @@ -250,7 +194,6 @@ fn try_run_solver_with_config_and_search_request<S, C, M, V, DM, IDM, Declaratio
entity_count_by_descriptor: fn(&S, usize) -> usize,
runtime: SolverRuntime<S>,
config: SolverConfig,
default_time_limit_secs: u64,
log_scale: fn(&S),
qualified_candidate_trace_provenance: Option<QualifiedCandidateTraceRunProvenance>,
build_search: BuildSearch,
Expand Down Expand Up @@ -284,7 +227,6 @@ where
entity_count_by_descriptor,
runtime,
config,
default_time_limit_secs,
log_scale,
qualified_candidate_trace_provenance,
move |config, descriptor| {
Expand Down Expand Up @@ -346,7 +288,6 @@ fn try_run_solver_with_candidate_trace_request<S, C, W, Runner, BuildRunner>(
entity_count_by_descriptor: fn(&S, usize) -> usize,
runtime: SolverRuntime<S>,
config: SolverConfig,
default_time_limit_secs: u64,
log_scale: fn(&S),
qualified_candidate_trace_provenance: Option<QualifiedCandidateTraceRunProvenance>,
build_runner: BuildRunner,
Expand Down Expand Up @@ -401,7 +342,6 @@ where
entity_count_by_descriptor,
runtime,
config: &config,
default_time_limit_secs,
qualified_candidate_trace_provenance,
},
runner,
Expand Down
Loading
Loading