solverforge_solver/termination/
score_calculation_count.rs

1//! Score calculation count termination.
2
3use std::fmt::Debug;
4
5use solverforge_core::domain::PlanningSolution;
6
7use super::Termination;
8use crate::scope::SolverScope;
9
10/// Terminates when a maximum number of score calculations is reached.
11///
12/// This termination condition requires a `StatisticsCollector` to be attached
13/// to the `SolverScope`. If no collector is attached, it will never terminate.
14///
15/// # Example
16///
17/// ```
18/// use solverforge_solver::termination::ScoreCalculationCountTermination;
19/// use solverforge_core::score::SimpleScore;
20/// use solverforge_core::domain::PlanningSolution;
21///
22/// #[derive(Clone)]
23/// struct MySolution;
24/// impl PlanningSolution for MySolution {
25///     type Score = SimpleScore;
26///     fn score(&self) -> Option<Self::Score> { None }
27///     fn set_score(&mut self, _: Option<Self::Score>) {}
28/// }
29///
30/// // Terminate after 10,000 score calculations
31/// let termination = ScoreCalculationCountTermination::<MySolution>::new(10_000);
32/// ```
33#[derive(Clone)]
34pub struct ScoreCalculationCountTermination<S: PlanningSolution> {
35    /// Maximum number of score calculations before termination.
36    score_calculation_count_limit: u64,
37    _phantom: std::marker::PhantomData<fn() -> S>,
38}
39
40impl<S: PlanningSolution> Debug for ScoreCalculationCountTermination<S> {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        f.debug_struct("ScoreCalculationCountTermination")
43            .field(
44                "score_calculation_count_limit",
45                &self.score_calculation_count_limit,
46            )
47            .finish()
48    }
49}
50
51impl<S: PlanningSolution> ScoreCalculationCountTermination<S> {
52    /// Creates a new score calculation count termination.
53    ///
54    /// # Arguments
55    /// * `score_calculation_count_limit` - Maximum score calculations before terminating
56    pub fn new(score_calculation_count_limit: u64) -> Self {
57        Self {
58            score_calculation_count_limit,
59            _phantom: std::marker::PhantomData,
60        }
61    }
62}
63
64impl<S: PlanningSolution> Termination<S> for ScoreCalculationCountTermination<S> {
65    fn is_terminated(&self, solver_scope: &SolverScope<S>) -> bool {
66        if let Some(stats) = solver_scope.statistics() {
67            stats.current_score_calculations() >= self.score_calculation_count_limit
68        } else {
69            false // No statistics collector, never terminate based on this
70        }
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crate::statistics::StatisticsCollector;
78    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
79    use solverforge_core::score::SimpleScore;
80    use solverforge_scoring::SimpleScoreDirector;
81    use std::any::TypeId;
82    use std::sync::Arc;
83
84    #[derive(Clone, Debug)]
85    struct Entity {}
86
87    #[derive(Clone, Debug)]
88    struct TestSolution {
89        entities: Vec<Entity>,
90        score: Option<SimpleScore>,
91    }
92
93    impl PlanningSolution for TestSolution {
94        type Score = SimpleScore;
95        fn score(&self) -> Option<Self::Score> {
96            self.score
97        }
98        fn set_score(&mut self, score: Option<Self::Score>) {
99            self.score = score;
100        }
101    }
102
103    fn get_entities(s: &TestSolution) -> &Vec<Entity> {
104        &s.entities
105    }
106    fn get_entities_mut(s: &mut TestSolution) -> &mut Vec<Entity> {
107        &mut s.entities
108    }
109
110    fn create_scope_with_stats() -> SolverScope<TestSolution> {
111        let solution = TestSolution {
112            entities: vec![],
113            score: None,
114        };
115        let extractor = Box::new(TypedEntityExtractor::new(
116            "Entity",
117            "entities",
118            get_entities,
119            get_entities_mut,
120        ));
121        let entity_desc = EntityDescriptor::new("Entity", TypeId::of::<Entity>(), "entities")
122            .with_extractor(extractor);
123        let descriptor = SolutionDescriptor::new("TestSolution", TypeId::of::<TestSolution>())
124            .with_entity(entity_desc);
125        let director =
126            SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0));
127        let collector = Arc::new(StatisticsCollector::<SimpleScore>::new());
128        SolverScope::new(Box::new(director)).with_statistics(collector)
129    }
130
131    #[test]
132    fn test_not_terminated_initially() {
133        let scope = create_scope_with_stats();
134        let termination = ScoreCalculationCountTermination::<TestSolution>::new(100);
135        assert!(!termination.is_terminated(&scope));
136    }
137
138    #[test]
139    fn test_terminates_at_limit() {
140        let scope = create_scope_with_stats();
141        let stats = scope.statistics().unwrap().clone();
142
143        // Record 99 calculations - not yet terminated
144        for _ in 0..99 {
145            stats.record_score_calculation();
146        }
147        let termination = ScoreCalculationCountTermination::<TestSolution>::new(100);
148        assert!(!termination.is_terminated(&scope));
149
150        // Record one more - now terminated
151        stats.record_score_calculation();
152        assert!(termination.is_terminated(&scope));
153    }
154
155    #[test]
156    fn test_no_stats_never_terminates() {
157        let solution = TestSolution {
158            entities: vec![],
159            score: None,
160        };
161        let extractor = Box::new(TypedEntityExtractor::new(
162            "Entity",
163            "entities",
164            get_entities,
165            get_entities_mut,
166        ));
167        let entity_desc = EntityDescriptor::new("Entity", TypeId::of::<Entity>(), "entities")
168            .with_extractor(extractor);
169        let descriptor = SolutionDescriptor::new("TestSolution", TypeId::of::<TestSolution>())
170            .with_entity(entity_desc);
171        let director =
172            SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0));
173        let scope = SolverScope::new(Box::new(director));
174
175        let termination = ScoreCalculationCountTermination::<TestSolution>::new(1);
176        // Without statistics collector, never terminates
177        assert!(!termination.is_terminated(&scope));
178    }
179}