Skip to main content

solverforge_solver/stats/
solver.rs

1use std::cmp::Ordering;
2use std::collections::BTreeMap;
3use std::time::{Duration, Instant};
4
5use super::{AppliedMoveTelemetry, MoveTelemetry, SelectorTelemetry, SolverTelemetry, Throughput};
6
7const APPLIED_MOVE_TRACE_LIMIT: usize = 8;
8
9#[derive(Debug, Default)]
10pub struct SolverStats {
11    start_time: Option<Instant>,
12    pause_started_at: Option<Instant>,
13    // Total steps taken across all phases.
14    pub step_count: u64,
15    // Total moves generated across all phases.
16    pub moves_generated: u64,
17    // Total moves evaluated across all phases.
18    pub moves_evaluated: u64,
19    // Total moves accepted across all phases.
20    pub moves_accepted: u64,
21    // Total moves applied across all phases.
22    pub moves_applied: u64,
23    pub moves_not_doable: u64,
24    pub moves_acceptor_rejected: u64,
25    pub moves_forager_ignored: u64,
26    pub moves_hard_improving: u64,
27    pub moves_hard_neutral: u64,
28    pub moves_hard_worse: u64,
29    pub conflict_repair_provider_generated: u64,
30    pub conflict_repair_duplicate_filtered: u64,
31    pub conflict_repair_illegal_filtered: u64,
32    pub conflict_repair_not_doable_filtered: u64,
33    pub conflict_repair_hard_improving: u64,
34    pub conflict_repair_exposed: u64,
35    // Total score calculations performed.
36    pub score_calculations: u64,
37    pub construction_slots_assigned: u64,
38    pub construction_slots_kept: u64,
39    pub construction_slots_no_doable: u64,
40    pub scalar_assignment_required_remaining: u64,
41    scalar_assignment_required_remaining_by_group: BTreeMap<&'static str, u64>,
42    generation_time: Duration,
43    evaluation_time: Duration,
44    selector_stats: Vec<SelectorTelemetry>,
45    move_stats: BTreeMap<&'static str, MoveTelemetry>,
46    applied_move_trace: Vec<AppliedMoveTelemetry>,
47}
48
49impl SolverStats {
50    /// Marks the start of solving.
51    pub fn start(&mut self) {
52        self.start_time = Some(Instant::now());
53        self.pause_started_at = None;
54    }
55
56    pub fn elapsed(&self) -> Duration {
57        match (self.start_time, self.pause_started_at) {
58            (Some(start), Some(paused_at)) => paused_at.duration_since(start),
59            (Some(start), None) => start.elapsed(),
60            _ => Duration::default(),
61        }
62    }
63
64    pub fn pause(&mut self) {
65        if self.start_time.is_some() && self.pause_started_at.is_none() {
66            self.pause_started_at = Some(Instant::now());
67        }
68    }
69
70    pub fn resume(&mut self) {
71        if let (Some(start), Some(paused_at)) = (self.start_time, self.pause_started_at.take()) {
72            self.start_time = Some(start + paused_at.elapsed());
73        }
74    }
75
76    /// Records one or more generated candidate moves and the time spent generating them.
77    pub fn record_generated_batch(&mut self, count: u64, duration: Duration) {
78        self.moves_generated += count;
79        self.generation_time += duration;
80    }
81
82    pub fn record_selector_generated(
83        &mut self,
84        selector_index: usize,
85        count: u64,
86        duration: Duration,
87    ) {
88        self.record_generated_batch(count, duration);
89        let selector = self.selector_stats_entry(selector_index);
90        selector.moves_generated += count;
91        selector.generation_time += duration;
92    }
93
94    /// Records generation time that did not itself yield a counted move.
95    pub fn record_generation_time(&mut self, duration: Duration) {
96        self.generation_time += duration;
97    }
98
99    /// Records a single generated candidate move and the time spent generating it.
100    pub fn record_generated_move(&mut self, duration: Duration) {
101        self.record_generated_batch(1, duration);
102    }
103
104    /// Records a move evaluation and the time spent evaluating it.
105    pub fn record_evaluated_move(&mut self, duration: Duration) {
106        self.moves_evaluated += 1;
107        self.evaluation_time += duration;
108    }
109
110    pub fn record_selector_evaluated(&mut self, selector_index: usize, duration: Duration) {
111        self.record_evaluated_move(duration);
112        let selector = self.selector_stats_entry(selector_index);
113        selector.moves_evaluated += 1;
114        selector.evaluation_time += duration;
115    }
116
117    /// Records an accepted move.
118    pub fn record_move_accepted(&mut self) {
119        self.moves_accepted += 1;
120    }
121
122    pub fn record_selector_accepted(&mut self, selector_index: usize) {
123        self.record_move_accepted();
124        self.selector_stats_entry(selector_index).moves_accepted += 1;
125    }
126
127    pub fn record_move_applied(&mut self) {
128        self.moves_applied += 1;
129    }
130
131    pub fn record_selector_applied(&mut self, selector_index: usize) {
132        self.record_move_applied();
133        self.selector_stats_entry(selector_index).moves_applied += 1;
134    }
135
136    pub fn record_move_not_doable(&mut self) {
137        self.moves_not_doable += 1;
138    }
139
140    pub fn record_selector_not_doable(&mut self, selector_index: usize) {
141        self.record_move_not_doable();
142        self.selector_stats_entry(selector_index).moves_not_doable += 1;
143    }
144
145    pub fn record_move_acceptor_rejected(&mut self) {
146        self.moves_acceptor_rejected += 1;
147    }
148
149    pub fn record_selector_acceptor_rejected(&mut self, selector_index: usize) {
150        self.record_move_acceptor_rejected();
151        self.selector_stats_entry(selector_index)
152            .moves_acceptor_rejected += 1;
153    }
154
155    pub fn record_moves_forager_ignored(&mut self, count: u64) {
156        self.moves_forager_ignored += count;
157    }
158
159    pub fn record_move_hard_improving(&mut self) {
160        self.moves_hard_improving += 1;
161    }
162
163    pub fn record_move_hard_neutral(&mut self) {
164        self.moves_hard_neutral += 1;
165    }
166
167    pub fn record_move_hard_worse(&mut self) {
168        self.moves_hard_worse += 1;
169    }
170
171    pub fn record_conflict_repair_provider_generated(&mut self, count: u64) {
172        self.conflict_repair_provider_generated += count;
173    }
174
175    pub fn record_conflict_repair_duplicate_filtered(&mut self) {
176        self.conflict_repair_duplicate_filtered += 1;
177    }
178
179    pub fn record_conflict_repair_illegal_filtered(&mut self) {
180        self.conflict_repair_illegal_filtered += 1;
181    }
182
183    pub fn record_conflict_repair_not_doable_filtered(&mut self) {
184        self.conflict_repair_not_doable_filtered += 1;
185    }
186
187    pub fn record_conflict_repair_hard_improving(&mut self) {
188        self.conflict_repair_hard_improving += 1;
189    }
190
191    pub fn record_conflict_repair_exposed(&mut self) {
192        self.conflict_repair_exposed += 1;
193    }
194
195    /// Records a step completion.
196    pub fn record_step(&mut self) {
197        self.step_count += 1;
198    }
199
200    /// Records a score calculation.
201    pub fn record_score_calculation(&mut self) {
202        self.score_calculations += 1;
203    }
204
205    pub fn record_construction_slot_assigned(&mut self) {
206        self.construction_slots_assigned += 1;
207    }
208
209    pub fn record_construction_slot_kept(&mut self) {
210        self.construction_slots_kept += 1;
211    }
212
213    pub fn record_construction_slot_no_doable(&mut self) {
214        self.construction_slots_no_doable += 1;
215    }
216
217    pub fn record_scalar_assignment_required_remaining(
218        &mut self,
219        group_name: &'static str,
220        count: u64,
221    ) {
222        self.scalar_assignment_required_remaining_by_group
223            .insert(group_name, count);
224        self.scalar_assignment_required_remaining = self
225            .scalar_assignment_required_remaining_by_group
226            .values()
227            .copied()
228            .sum();
229    }
230
231    pub fn generated_throughput(&self) -> Throughput {
232        Throughput {
233            count: self.moves_generated,
234            elapsed: self.generation_time,
235        }
236    }
237
238    pub fn evaluated_throughput(&self) -> Throughput {
239        Throughput {
240            count: self.moves_evaluated,
241            elapsed: self.evaluation_time,
242        }
243    }
244
245    pub fn acceptance_rate(&self) -> f64 {
246        if self.moves_evaluated == 0 {
247            0.0
248        } else {
249            self.moves_accepted as f64 / self.moves_evaluated as f64
250        }
251    }
252
253    pub fn generation_time(&self) -> Duration {
254        self.generation_time
255    }
256
257    pub fn evaluation_time(&self) -> Duration {
258        self.evaluation_time
259    }
260
261    pub fn snapshot(&self) -> SolverTelemetry {
262        self.snapshot_with_applied_move_trace(true)
263    }
264
265    pub fn snapshot_without_applied_move_trace(&self) -> SolverTelemetry {
266        self.snapshot_with_applied_move_trace(false)
267    }
268
269    fn snapshot_with_applied_move_trace(
270        &self,
271        include_applied_move_trace: bool,
272    ) -> SolverTelemetry {
273        SolverTelemetry {
274            elapsed: self.elapsed(),
275            step_count: self.step_count,
276            moves_generated: self.moves_generated,
277            moves_evaluated: self.moves_evaluated,
278            moves_accepted: self.moves_accepted,
279            moves_applied: self.moves_applied,
280            moves_not_doable: self.moves_not_doable,
281            moves_acceptor_rejected: self.moves_acceptor_rejected,
282            moves_forager_ignored: self.moves_forager_ignored,
283            moves_hard_improving: self.moves_hard_improving,
284            moves_hard_neutral: self.moves_hard_neutral,
285            moves_hard_worse: self.moves_hard_worse,
286            conflict_repair_provider_generated: self.conflict_repair_provider_generated,
287            conflict_repair_duplicate_filtered: self.conflict_repair_duplicate_filtered,
288            conflict_repair_illegal_filtered: self.conflict_repair_illegal_filtered,
289            conflict_repair_not_doable_filtered: self.conflict_repair_not_doable_filtered,
290            conflict_repair_hard_improving: self.conflict_repair_hard_improving,
291            conflict_repair_exposed: self.conflict_repair_exposed,
292            score_calculations: self.score_calculations,
293            construction_slots_assigned: self.construction_slots_assigned,
294            construction_slots_kept: self.construction_slots_kept,
295            construction_slots_no_doable: self.construction_slots_no_doable,
296            scalar_assignment_required_remaining: self.scalar_assignment_required_remaining,
297            generation_time: self.generation_time,
298            evaluation_time: self.evaluation_time,
299            selector_telemetry: self.selector_stats.clone(),
300            move_telemetry: self.move_stats.values().cloned().collect(),
301            applied_move_trace: if include_applied_move_trace {
302                self.applied_move_trace.to_vec()
303            } else {
304                Vec::new()
305            },
306        }
307    }
308
309    pub fn record_move_kind_generated(&mut self, move_label: &'static str) {
310        self.move_stats_entry(move_label).moves_generated += 1;
311    }
312
313    pub fn record_move_kind_evaluated(
314        &mut self,
315        move_label: &'static str,
316        score_ordering: Ordering,
317    ) {
318        let entry = self.move_stats_entry(move_label);
319        entry.moves_evaluated += 1;
320        match score_ordering {
321            Ordering::Greater => entry.moves_score_improving += 1,
322            Ordering::Equal => entry.moves_score_equal += 1,
323            Ordering::Less => entry.moves_score_worse += 1,
324        }
325    }
326
327    pub fn record_move_kind_evaluated_unscored(&mut self, move_label: &'static str) {
328        self.move_stats_entry(move_label).moves_evaluated += 1;
329    }
330
331    pub fn record_move_kind_accepted(&mut self, move_label: &'static str) {
332        self.move_stats_entry(move_label).moves_accepted += 1;
333    }
334
335    pub fn record_move_kind_applied(&mut self, move_label: &'static str, score_improvement: f64) {
336        let entry = self.move_stats_entry(move_label);
337        entry.moves_applied += 1;
338        if score_improvement > 0.0 {
339            entry.applied_score_improvement += score_improvement;
340        }
341    }
342
343    pub fn record_move_kind_not_doable(&mut self, move_label: &'static str) {
344        self.move_stats_entry(move_label).moves_not_doable += 1;
345    }
346
347    pub fn record_move_kind_acceptor_rejected(
348        &mut self,
349        move_label: &'static str,
350        score_ordering: Ordering,
351    ) {
352        let entry = self.move_stats_entry(move_label);
353        entry.moves_acceptor_rejected += 1;
354        if score_ordering == Ordering::Greater {
355            entry.moves_rejected_improving += 1;
356        }
357    }
358
359    pub fn record_move_kind_forager_ignored(&mut self, move_label: &'static str, count: u64) {
360        if count == 0 {
361            return;
362        }
363        self.move_stats_entry(move_label).moves_forager_ignored += count;
364    }
365
366    pub fn record_applied_move_trace(&mut self, applied_move: AppliedMoveTelemetry) {
367        if self.applied_move_trace.len() < APPLIED_MOVE_TRACE_LIMIT {
368            self.applied_move_trace.push(applied_move);
369        }
370    }
371
372    pub fn record_selector_generated_with_label(
373        &mut self,
374        selector_index: usize,
375        selector_label: impl Into<String>,
376        count: u64,
377        duration: Duration,
378    ) {
379        self.record_generated_batch(count, duration);
380        let selector = self.selector_stats_entry_with_label(selector_index, selector_label);
381        selector.moves_generated += count;
382        selector.generation_time += duration;
383    }
384
385    fn selector_stats_entry(&mut self, selector_index: usize) -> &mut SelectorTelemetry {
386        self.selector_stats_entry_with_label(selector_index, format!("selector-{selector_index}"))
387    }
388
389    fn selector_stats_entry_with_label(
390        &mut self,
391        selector_index: usize,
392        selector_label: impl Into<String>,
393    ) -> &mut SelectorTelemetry {
394        let selector_label = selector_label.into();
395        if let Some(position) = self
396            .selector_stats
397            .iter()
398            .position(|entry| entry.selector_index == selector_index)
399        {
400            if self.selector_stats[position]
401                .selector_label
402                .starts_with("selector-")
403                && !selector_label.starts_with("selector-")
404            {
405                self.selector_stats[position].selector_label = selector_label;
406            }
407            return &mut self.selector_stats[position];
408        }
409        self.selector_stats.push(SelectorTelemetry {
410            selector_index,
411            selector_label,
412            ..SelectorTelemetry::default()
413        });
414        self.selector_stats
415            .last_mut()
416            .expect("selector stats entry was just inserted")
417    }
418
419    fn move_stats_entry(&mut self, move_label: &'static str) -> &mut MoveTelemetry {
420        self.move_stats
421            .entry(move_label)
422            .or_insert_with(|| MoveTelemetry {
423                move_label: move_label.to_string(),
424                ..MoveTelemetry::default()
425            })
426    }
427}
428
429/* Phase-level statistics.
430
431Tracks metrics for a single solver phase.
432
433# Example
434
435```
436use solverforge_solver::stats::PhaseStats;
437use std::time::Duration;
438
439let mut stats = PhaseStats::new(0, "LocalSearch");
440stats.record_step();
441stats.record_generated_move(Duration::from_millis(1));
442stats.record_evaluated_move(Duration::from_millis(2));
443stats.record_move_accepted();
444
445assert_eq!(stats.phase_index, 0);
446assert_eq!(stats.phase_type, "LocalSearch");
447assert_eq!(stats.step_count, 1);
448assert_eq!(stats.moves_accepted, 1);
449```
450*/