1use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::Mutex;
8use std::time::{Duration, Instant};
9
10use solverforge_core::score::Score;
11
12#[derive(Debug, Clone)]
14pub struct PhaseStatistics<Sc: Score> {
15 pub phase_index: usize,
17 pub phase_type: String,
19 pub duration: Duration,
21 pub step_count: u64,
23 pub moves_evaluated: u64,
25 pub moves_accepted: u64,
27 pub starting_score: Option<Sc>,
29 pub ending_score: Option<Sc>,
31}
32
33impl<Sc: Score> PhaseStatistics<Sc> {
34 pub fn new(phase_index: usize, phase_type: impl Into<String>) -> Self {
36 Self {
37 phase_index,
38 phase_type: phase_type.into(),
39 duration: Duration::ZERO,
40 step_count: 0,
41 moves_evaluated: 0,
42 moves_accepted: 0,
43 starting_score: None,
44 ending_score: None,
45 }
46 }
47
48 pub fn acceptance_rate(&self) -> f64 {
50 if self.moves_evaluated == 0 {
51 0.0
52 } else {
53 self.moves_accepted as f64 / self.moves_evaluated as f64
54 }
55 }
56
57 pub fn avg_time_per_step(&self) -> Duration {
59 if self.step_count == 0 {
60 Duration::ZERO
61 } else {
62 self.duration / self.step_count as u32
63 }
64 }
65}
66
67#[derive(Debug, Clone)]
69pub struct ScoreImprovement<Sc: Score> {
70 pub time_offset: Duration,
72 pub step_count: u64,
74 pub score: Sc,
76}
77
78#[derive(Debug, Clone)]
80pub struct SolverStatistics<Sc: Score> {
81 pub total_duration: Duration,
83 pub total_step_count: u64,
85 pub total_moves_evaluated: u64,
87 pub total_moves_accepted: u64,
89 pub score_calculation_count: u64,
91 pub phase_statistics: Vec<PhaseStatistics<Sc>>,
93 pub score_history: Vec<ScoreImprovement<Sc>>,
95}
96
97impl<Sc: Score> SolverStatistics<Sc> {
98 pub fn new() -> Self {
100 Self {
101 total_duration: Duration::ZERO,
102 total_step_count: 0,
103 total_moves_evaluated: 0,
104 total_moves_accepted: 0,
105 score_calculation_count: 0,
106 phase_statistics: Vec::new(),
107 score_history: Vec::new(),
108 }
109 }
110
111 pub fn acceptance_rate(&self) -> f64 {
113 if self.total_moves_evaluated == 0 {
114 0.0
115 } else {
116 self.total_moves_accepted as f64 / self.total_moves_evaluated as f64
117 }
118 }
119
120 pub fn phase_count(&self) -> usize {
122 self.phase_statistics.len()
123 }
124
125 pub fn best_score(&self) -> Option<&Sc> {
127 self.score_history.last().map(|s| &s.score)
128 }
129
130 pub fn improvement_count(&self) -> usize {
132 self.score_history.len()
133 }
134}
135
136impl<Sc: Score> Default for SolverStatistics<Sc> {
137 fn default() -> Self {
138 Self::new()
139 }
140}
141
142pub struct StatisticsCollector<Sc: Score> {
147 start_time: Instant,
149 moves_evaluated: AtomicU64,
151 moves_accepted: AtomicU64,
153 step_count: AtomicU64,
155 score_calculations: AtomicU64,
157 phases: Mutex<Vec<PhaseStatistics<Sc>>>,
159 score_history: Mutex<Vec<ScoreImprovement<Sc>>>,
161}
162
163impl<Sc: Score> StatisticsCollector<Sc> {
164 pub fn new() -> Self {
168 Self {
169 start_time: Instant::now(),
170 moves_evaluated: AtomicU64::new(0),
171 moves_accepted: AtomicU64::new(0),
172 step_count: AtomicU64::new(0),
173 score_calculations: AtomicU64::new(0),
174 phases: Mutex::new(Vec::new()),
175 score_history: Mutex::new(Vec::new()),
176 }
177 }
178
179 pub fn record_move_evaluated(&self) {
184 self.moves_evaluated.fetch_add(1, Ordering::Relaxed);
185 }
186
187 pub fn record_move_accepted(&self) {
191 self.moves_accepted.fetch_add(1, Ordering::Relaxed);
192 }
193
194 pub fn record_move(&self, accepted: bool) {
198 self.record_move_evaluated();
199 if accepted {
200 self.record_move_accepted();
201 }
202 }
203
204 pub fn record_step(&self) {
206 self.step_count.fetch_add(1, Ordering::Relaxed);
207 }
208
209 pub fn record_score_calculation(&self) {
211 self.score_calculations.fetch_add(1, Ordering::Relaxed);
212 }
213
214 pub fn record_improvement(&self, score: Sc) {
218 let time_offset = self.start_time.elapsed();
219 let step_count = self.step_count.load(Ordering::Relaxed);
220
221 let improvement = ScoreImprovement {
222 time_offset,
223 step_count,
224 score,
225 };
226
227 if let Ok(mut history) = self.score_history.lock() {
228 history.push(improvement);
229 }
230 }
231
232 pub fn start_phase(&self, phase_type: impl Into<String>) -> usize {
236 let mut phases = self.phases.lock().unwrap();
237 let index = phases.len();
238 phases.push(PhaseStatistics::new(index, phase_type));
239 index
240 }
241
242 #[allow(clippy::too_many_arguments)]
246 pub fn end_phase(
247 &self,
248 phase_index: usize,
249 duration: Duration,
250 step_count: u64,
251 moves_evaluated: u64,
252 moves_accepted: u64,
253 starting_score: Option<Sc>,
254 ending_score: Option<Sc>,
255 ) {
256 if let Ok(mut phases) = self.phases.lock() {
257 if let Some(phase) = phases.get_mut(phase_index) {
258 phase.duration = duration;
259 phase.step_count = step_count;
260 phase.moves_evaluated = moves_evaluated;
261 phase.moves_accepted = moves_accepted;
262 phase.starting_score = starting_score;
263 phase.ending_score = ending_score;
264 }
265 }
266 }
267
268 pub fn elapsed(&self) -> Duration {
270 self.start_time.elapsed()
271 }
272
273 pub fn current_step_count(&self) -> u64 {
275 self.step_count.load(Ordering::Relaxed)
276 }
277
278 pub fn current_moves_evaluated(&self) -> u64 {
280 self.moves_evaluated.load(Ordering::Relaxed)
281 }
282
283 pub fn current_moves_accepted(&self) -> u64 {
285 self.moves_accepted.load(Ordering::Relaxed)
286 }
287
288 pub fn current_score_calculations(&self) -> u64 {
290 self.score_calculations.load(Ordering::Relaxed)
291 }
292
293 pub fn into_statistics(self) -> SolverStatistics<Sc> {
297 SolverStatistics {
298 total_duration: self.start_time.elapsed(),
299 total_step_count: self.step_count.load(Ordering::Relaxed),
300 total_moves_evaluated: self.moves_evaluated.load(Ordering::Relaxed),
301 total_moves_accepted: self.moves_accepted.load(Ordering::Relaxed),
302 score_calculation_count: self.score_calculations.load(Ordering::Relaxed),
303 phase_statistics: self.phases.into_inner().unwrap(),
304 score_history: self.score_history.into_inner().unwrap(),
305 }
306 }
307
308 pub fn snapshot(&self) -> SolverStatistics<Sc> {
310 SolverStatistics {
311 total_duration: self.start_time.elapsed(),
312 total_step_count: self.step_count.load(Ordering::Relaxed),
313 total_moves_evaluated: self.moves_evaluated.load(Ordering::Relaxed),
314 total_moves_accepted: self.moves_accepted.load(Ordering::Relaxed),
315 score_calculation_count: self.score_calculations.load(Ordering::Relaxed),
316 phase_statistics: self.phases.lock().unwrap().clone(),
317 score_history: self.score_history.lock().unwrap().clone(),
318 }
319 }
320}
321
322impl<Sc: Score> Default for StatisticsCollector<Sc> {
323 fn default() -> Self {
324 Self::new()
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use solverforge_core::score::SimpleScore;
332 use std::thread;
333
334 #[test]
335 fn test_phase_statistics_new() {
336 let stats: PhaseStatistics<SimpleScore> = PhaseStatistics::new(0, "ConstructionHeuristic");
337 assert_eq!(stats.phase_index, 0);
338 assert_eq!(stats.phase_type, "ConstructionHeuristic");
339 assert_eq!(stats.step_count, 0);
340 }
341
342 #[test]
343 fn test_phase_statistics_acceptance_rate() {
344 let mut stats: PhaseStatistics<SimpleScore> = PhaseStatistics::new(0, "LocalSearch");
345 stats.moves_evaluated = 100;
346 stats.moves_accepted = 25;
347 assert!((stats.acceptance_rate() - 0.25).abs() < f64::EPSILON);
348 }
349
350 #[test]
351 fn test_phase_statistics_acceptance_rate_zero() {
352 let stats: PhaseStatistics<SimpleScore> = PhaseStatistics::new(0, "Test");
353 assert_eq!(stats.acceptance_rate(), 0.0);
354 }
355
356 #[test]
357 fn test_solver_statistics_new() {
358 let stats: SolverStatistics<SimpleScore> = SolverStatistics::new();
359 assert_eq!(stats.total_step_count, 0);
360 assert_eq!(stats.phase_count(), 0);
361 assert!(stats.best_score().is_none());
362 }
363
364 #[test]
365 fn test_collector_record_move() {
366 let collector: StatisticsCollector<SimpleScore> = StatisticsCollector::new();
367
368 collector.record_move(true);
369 collector.record_move(false);
370 collector.record_move(true);
371
372 assert_eq!(collector.current_moves_evaluated(), 3);
373 assert_eq!(collector.current_moves_accepted(), 2);
374 }
375
376 #[test]
377 fn test_collector_record_step() {
378 let collector: StatisticsCollector<SimpleScore> = StatisticsCollector::new();
379
380 collector.record_step();
381 collector.record_step();
382 collector.record_step();
383
384 assert_eq!(collector.current_step_count(), 3);
385 }
386
387 #[test]
388 fn test_collector_record_improvement() {
389 let collector: StatisticsCollector<SimpleScore> = StatisticsCollector::new();
390
391 collector.record_improvement(SimpleScore::of(-10));
392 collector.record_improvement(SimpleScore::of(-5));
393 collector.record_improvement(SimpleScore::of(0));
394
395 let stats = collector.into_statistics();
396 assert_eq!(stats.improvement_count(), 3);
397 assert_eq!(*stats.best_score().unwrap(), SimpleScore::of(0));
398 }
399
400 #[test]
401 fn test_collector_phases() {
402 let collector: StatisticsCollector<SimpleScore> = StatisticsCollector::new();
403
404 let phase0 = collector.start_phase("Construction");
405 let phase1 = collector.start_phase("LocalSearch");
406
407 assert_eq!(phase0, 0);
408 assert_eq!(phase1, 1);
409
410 collector.end_phase(
411 phase0,
412 Duration::from_millis(100),
413 5,
414 10,
415 5,
416 None,
417 Some(SimpleScore::of(-5)),
418 );
419
420 collector.end_phase(
421 phase1,
422 Duration::from_millis(200),
423 20,
424 100,
425 50,
426 Some(SimpleScore::of(-5)),
427 Some(SimpleScore::of(0)),
428 );
429
430 let stats = collector.into_statistics();
431 assert_eq!(stats.phase_count(), 2);
432
433 let p0 = &stats.phase_statistics[0];
434 assert_eq!(p0.phase_type, "Construction");
435 assert_eq!(p0.step_count, 5);
436
437 let p1 = &stats.phase_statistics[1];
438 assert_eq!(p1.phase_type, "LocalSearch");
439 assert_eq!(p1.moves_evaluated, 100);
440 assert!((p1.acceptance_rate() - 0.5).abs() < f64::EPSILON);
441 }
442
443 #[test]
444 fn test_collector_snapshot() {
445 let collector: StatisticsCollector<SimpleScore> = StatisticsCollector::new();
446
447 collector.record_step();
448 collector.record_step();
449
450 let snapshot = collector.snapshot();
451 assert_eq!(snapshot.total_step_count, 2);
452
453 collector.record_step();
455 assert_eq!(collector.current_step_count(), 3);
456 }
457
458 #[test]
459 fn test_collector_thread_safety() {
460 use std::sync::Arc;
461
462 let collector: Arc<StatisticsCollector<SimpleScore>> = Arc::new(StatisticsCollector::new());
463
464 let handles: Vec<_> = (0..4)
465 .map(|_| {
466 let c = collector.clone();
467 thread::spawn(move || {
468 for _ in 0..1000 {
469 c.record_move_evaluated();
470 c.record_step();
471 }
472 })
473 })
474 .collect();
475
476 for h in handles {
477 h.join().unwrap();
478 }
479
480 assert_eq!(collector.current_moves_evaluated(), 4000);
481 assert_eq!(collector.current_step_count(), 4000);
482 }
483
484 #[test]
485 fn test_solver_statistics_acceptance_rate() {
486 let stats: SolverStatistics<SimpleScore> = SolverStatistics {
487 total_duration: Duration::from_secs(1),
488 total_step_count: 100,
489 total_moves_evaluated: 1000,
490 total_moves_accepted: 100,
491 score_calculation_count: 1000,
492 phase_statistics: Vec::new(),
493 score_history: Vec::new(),
494 };
495
496 assert!((stats.acceptance_rate() - 0.1).abs() < f64::EPSILON);
497 }
498}