1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[cfg(all(feature = "checkpoint", not(target_arch = "wasm32")))]
10use std::fs::File;
11#[cfg(all(feature = "checkpoint", not(target_arch = "wasm32")))]
12use std::io::{BufReader, BufWriter};
13#[cfg(all(feature = "checkpoint", not(target_arch = "wasm32")))]
14use std::path::Path;
15
16use super::aggregation::FitnessAggregator;
17use super::evaluator::{Candidate, CandidateId, EvaluationRequest};
18use super::uncertainty::FitnessEstimate;
19use crate::error::CheckpointError;
20use crate::genome::traits::EvolutionaryGenome;
21
22pub const SESSION_VERSION: u32 = 1;
24
25#[derive(Clone, Debug, Default, Serialize, Deserialize)]
27pub struct CoverageStats {
28 pub coverage: f64,
30 pub avg_evaluations: f64,
32 pub min_evaluations: usize,
34 pub max_evaluations: usize,
36 pub unevaluated_count: usize,
38 pub population_size: usize,
40}
41
42impl CoverageStats {
43 pub fn meets_threshold(&self, min_coverage: f64) -> bool {
45 self.coverage >= min_coverage
46 }
47}
48
49#[derive(Clone, Debug, Serialize, Deserialize)]
55#[serde(bound = "G: Serialize + for<'a> Deserialize<'a>")]
56pub struct InteractiveSession<G>
57where
58 G: EvolutionaryGenome,
59{
60 pub version: u32,
62 pub population: Vec<Candidate<G>>,
64 pub generation: usize,
66 pub evaluations_requested: usize,
68 pub responses_received: usize,
70 pub skipped: usize,
72 pub aggregator: FitnessAggregator,
74 pub request_history: Vec<SerializedRequest>,
76 pub metadata: HashMap<String, String>,
78 pub next_candidate_id: usize,
80}
81
82#[derive(Clone, Debug, Serialize, Deserialize)]
84pub struct SerializedRequest {
85 pub request_type: String,
87 pub candidate_ids: Vec<CandidateId>,
89 pub generation: usize,
91 pub was_skipped: bool,
93}
94
95impl<G> InteractiveSession<G>
96where
97 G: EvolutionaryGenome,
98{
99 pub fn new(aggregator: FitnessAggregator) -> Self {
101 Self {
102 version: SESSION_VERSION,
103 population: Vec::new(),
104 generation: 0,
105 evaluations_requested: 0,
106 responses_received: 0,
107 skipped: 0,
108 aggregator,
109 request_history: Vec::new(),
110 metadata: HashMap::new(),
111 next_candidate_id: 0,
112 }
113 }
114
115 pub fn with_population(population: Vec<Candidate<G>>, aggregator: FitnessAggregator) -> Self {
117 let next_id = population.iter().map(|c| c.id.0).max().unwrap_or(0) + 1;
118 Self {
119 version: SESSION_VERSION,
120 population,
121 generation: 0,
122 evaluations_requested: 0,
123 responses_received: 0,
124 skipped: 0,
125 aggregator,
126 request_history: Vec::new(),
127 metadata: HashMap::new(),
128 next_candidate_id: next_id,
129 }
130 }
131
132 pub fn next_id(&mut self) -> CandidateId {
134 let id = CandidateId(self.next_candidate_id);
135 self.next_candidate_id += 1;
136 id
137 }
138
139 pub fn add_candidate(&mut self, genome: G) -> CandidateId {
141 let id = self.next_id();
142 let candidate = Candidate::with_generation(id, genome, self.generation);
143 self.population.push(candidate);
144 id
145 }
146
147 pub fn get_candidate(&self, id: CandidateId) -> Option<&Candidate<G>> {
149 self.population.iter().find(|c| c.id == id)
150 }
151
152 pub fn get_candidate_mut(&mut self, id: CandidateId) -> Option<&mut Candidate<G>> {
154 self.population.iter_mut().find(|c| c.id == id)
155 }
156
157 pub fn unevaluated_candidates(&self) -> Vec<&Candidate<G>> {
159 self.population
160 .iter()
161 .filter(|c| !c.is_evaluated())
162 .collect()
163 }
164
165 pub fn ranked_candidates(&self) -> Vec<&Candidate<G>> {
167 let mut candidates: Vec<_> = self.population.iter().collect();
168 candidates.sort_by(|a, b| {
169 let fa = a.fitness_estimate.unwrap_or(f64::NEG_INFINITY);
170 let fb = b.fitness_estimate.unwrap_or(f64::NEG_INFINITY);
171 fb.partial_cmp(&fa).unwrap_or(std::cmp::Ordering::Equal)
172 });
173 candidates
174 }
175
176 pub fn best_candidate(&self) -> Option<&Candidate<G>> {
178 self.population
179 .iter()
180 .filter(|c| c.fitness_estimate.is_some())
181 .max_by(|a, b| {
182 let fa = a.fitness_estimate.unwrap();
183 let fb = b.fitness_estimate.unwrap();
184 fa.partial_cmp(&fb).unwrap_or(std::cmp::Ordering::Equal)
185 })
186 }
187
188 pub fn coverage_stats(&self) -> CoverageStats {
190 if self.population.is_empty() {
191 return CoverageStats::default();
192 }
193
194 let eval_counts: Vec<usize> = self.population.iter().map(|c| c.evaluation_count).collect();
195
196 let evaluated = eval_counts.iter().filter(|&&c| c > 0).count();
197 let total_evals: usize = eval_counts.iter().sum();
198
199 CoverageStats {
200 coverage: evaluated as f64 / self.population.len() as f64,
201 avg_evaluations: total_evals as f64 / self.population.len() as f64,
202 min_evaluations: eval_counts.iter().copied().min().unwrap_or(0),
203 max_evaluations: eval_counts.iter().copied().max().unwrap_or(0),
204 unevaluated_count: self.population.len() - evaluated,
205 population_size: self.population.len(),
206 }
207 }
208
209 pub fn record_request<GG: EvolutionaryGenome>(&mut self, request: &EvaluationRequest<GG>) {
211 self.evaluations_requested += 1;
212
213 let serialized = SerializedRequest {
214 request_type: match request {
215 EvaluationRequest::RateCandidates { .. } => "rating".to_string(),
216 EvaluationRequest::PairwiseComparison { .. } => "pairwise".to_string(),
217 EvaluationRequest::BatchSelection { .. } => "batch".to_string(),
218 },
219 candidate_ids: request.candidate_ids(),
220 generation: self.generation,
221 was_skipped: false,
222 };
223
224 const MAX_HISTORY: usize = 1000;
226 if self.request_history.len() >= MAX_HISTORY {
227 self.request_history.remove(0);
228 }
229 self.request_history.push(serialized);
230 }
231
232 pub fn record_response(&mut self, was_skipped: bool) {
234 if was_skipped {
235 self.skipped += 1;
236 if let Some(last) = self.request_history.last_mut() {
237 last.was_skipped = true;
238 }
239 } else {
240 self.responses_received += 1;
241 }
242 }
243
244 pub fn advance_generation(&mut self) {
246 self.generation += 1;
247 self.aggregator.set_generation(self.generation);
248 }
249
250 pub fn update_fitness(&mut self, id: CandidateId, fitness: f64) {
259 if let Some(candidate) = self.get_candidate_mut(id) {
260 candidate.set_fitness(fitness);
261 }
262 }
263
264 pub fn update_fitness_with_uncertainty(&mut self, id: CandidateId, estimate: FitnessEstimate) {
269 if let Some(candidate) = self.get_candidate_mut(id) {
270 candidate.set_fitness_with_uncertainty(estimate);
271 }
272 }
273
274 pub fn sync_fitness_estimates(&mut self) {
279 for candidate in &mut self.population {
280 if let Some(estimate) = self.aggregator.get_fitness_estimate(&candidate.id) {
281 candidate.fitness_estimate = Some(estimate.mean);
282 candidate.fitness_with_uncertainty = Some(estimate);
283 }
284 }
285 }
286
287 pub fn all_fitness_estimates(&self) -> Vec<(CandidateId, FitnessEstimate)> {
291 self.population
292 .iter()
293 .filter_map(|c| {
294 self.aggregator
295 .get_fitness_estimate(&c.id)
296 .map(|e| (c.id, e))
297 })
298 .collect()
299 }
300
301 pub fn candidates_by_uncertainty(&self) -> Vec<&Candidate<G>> {
305 let mut candidates: Vec<_> = self.population.iter().collect();
306 candidates.sort_by(|a, b| {
307 let var_a = self
308 .aggregator
309 .get_fitness_estimate(&a.id)
310 .map(|e| e.variance)
311 .unwrap_or(f64::INFINITY);
312 let var_b = self
313 .aggregator
314 .get_fitness_estimate(&b.id)
315 .map(|e| e.variance)
316 .unwrap_or(f64::INFINITY);
317 var_b
319 .partial_cmp(&var_a)
320 .unwrap_or(std::cmp::Ordering::Equal)
321 });
322 candidates
323 }
324
325 pub fn average_uncertainty(&self) -> f64 {
327 let estimates: Vec<_> = self
328 .population
329 .iter()
330 .filter_map(|c| self.aggregator.get_fitness_estimate(&c.id))
331 .collect();
332
333 if estimates.is_empty() {
334 return f64::INFINITY;
335 }
336
337 let total_variance: f64 = estimates
338 .iter()
339 .map(|e| {
340 if e.variance.is_finite() {
341 e.variance
342 } else {
343 1e6 }
345 })
346 .sum();
347
348 total_variance / estimates.len() as f64
349 }
350
351 pub fn replace_population(&mut self, new_population: Vec<Candidate<G>>) {
353 let max_id = new_population.iter().map(|c| c.id.0).max().unwrap_or(0);
354 self.next_candidate_id = max_id + 1;
355 self.population = new_population;
356 }
357
358 pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
360 self.metadata.insert(key.into(), value.into());
361 }
362
363 pub fn get_metadata(&self, key: &str) -> Option<&String> {
365 self.metadata.get(key)
366 }
367
368 pub fn response_rate(&self) -> f64 {
370 if self.evaluations_requested > 0 {
371 self.responses_received as f64 / self.evaluations_requested as f64
372 } else {
373 0.0
374 }
375 }
376
377 pub fn skip_rate(&self) -> f64 {
379 if self.evaluations_requested > 0 {
380 self.skipped as f64 / self.evaluations_requested as f64
381 } else {
382 0.0
383 }
384 }
385}
386
387#[cfg(all(feature = "checkpoint", not(target_arch = "wasm32")))]
391impl<G> InteractiveSession<G>
392where
393 G: EvolutionaryGenome + Serialize + for<'de> Deserialize<'de>,
394{
395 pub fn save(&self, path: &Path) -> Result<(), CheckpointError> {
397 let file = File::create(path)?;
398 let writer = BufWriter::new(file);
399 serde_json::to_writer_pretty(writer, self).map_err(|e| {
400 CheckpointError::Serialization(format!("Failed to serialize session: {}", e))
401 })?;
402 Ok(())
403 }
404
405 pub fn load(path: &Path) -> Result<Self, CheckpointError> {
407 let file = File::open(path)?;
408 let reader = BufReader::new(file);
409 let session: Self = serde_json::from_reader(reader).map_err(|e| {
410 CheckpointError::Deserialization(format!("Failed to deserialize session: {}", e))
411 })?;
412
413 if session.version > SESSION_VERSION {
415 return Err(CheckpointError::VersionTooNew(session.version));
416 }
417
418 Ok(session)
419 }
420}
421
422impl<G> InteractiveSession<G>
423where
424 G: EvolutionaryGenome + Serialize + for<'de> Deserialize<'de>,
425{
426 pub fn to_json(&self) -> Result<String, CheckpointError> {
428 serde_json::to_string_pretty(self).map_err(|e| {
429 CheckpointError::Serialization(format!("Failed to serialize session: {}", e))
430 })
431 }
432
433 pub fn from_json(json: &str) -> Result<Self, CheckpointError> {
435 let session: Self = serde_json::from_str(json).map_err(|e| {
436 CheckpointError::Deserialization(format!("Failed to deserialize session: {}", e))
437 })?;
438
439 if session.version > SESSION_VERSION {
441 return Err(CheckpointError::VersionTooNew(session.version));
442 }
443
444 Ok(session)
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451 use crate::genome::real_vector::RealVector;
452 use crate::interactive::aggregation::AggregationModel;
453
454 #[test]
455 fn test_session_creation() {
456 let aggregator = FitnessAggregator::new(AggregationModel::default());
457 let session: InteractiveSession<RealVector> = InteractiveSession::new(aggregator);
458
459 assert_eq!(session.generation, 0);
460 assert!(session.population.is_empty());
461 assert_eq!(session.evaluations_requested, 0);
462 }
463
464 #[test]
465 fn test_add_candidate() {
466 let aggregator = FitnessAggregator::new(AggregationModel::default());
467 let mut session: InteractiveSession<RealVector> = InteractiveSession::new(aggregator);
468
469 let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
470 let id = session.add_candidate(genome);
471
472 assert_eq!(id, CandidateId(0));
473 assert_eq!(session.population.len(), 1);
474 assert_eq!(session.get_candidate(id).unwrap().birth_generation, 0);
475 }
476
477 #[test]
478 fn test_coverage_stats() {
479 let aggregator = FitnessAggregator::new(AggregationModel::default());
480 let mut session: InteractiveSession<RealVector> = InteractiveSession::new(aggregator);
481
482 for i in 0..4 {
484 session.add_candidate(RealVector::new(vec![i as f64]));
485 }
486
487 session.population[0].record_evaluation();
489 session.population[1].record_evaluation();
490 session.population[1].record_evaluation(); let stats = session.coverage_stats();
493
494 assert_eq!(stats.population_size, 4);
495 assert_eq!(stats.coverage, 0.5);
496 assert_eq!(stats.unevaluated_count, 2);
497 assert_eq!(stats.min_evaluations, 0);
498 assert_eq!(stats.max_evaluations, 2);
499 }
500
501 #[test]
502 fn test_ranked_candidates() {
503 let aggregator = FitnessAggregator::new(AggregationModel::default());
504 let mut session: InteractiveSession<RealVector> = InteractiveSession::new(aggregator);
505
506 for i in 0..3 {
507 let id = session.add_candidate(RealVector::new(vec![i as f64]));
508 session.update_fitness(id, i as f64 * 10.0);
509 }
510
511 let ranked = session.ranked_candidates();
512 assert_eq!(ranked[0].fitness_estimate, Some(20.0)); assert_eq!(ranked[2].fitness_estimate, Some(0.0)); }
515
516 #[test]
517 fn test_advance_generation() {
518 let aggregator = FitnessAggregator::new(AggregationModel::default());
519 let mut session: InteractiveSession<RealVector> = InteractiveSession::new(aggregator);
520
521 session.advance_generation();
522 assert_eq!(session.generation, 1);
523
524 let id = session.add_candidate(RealVector::new(vec![1.0]));
525 assert_eq!(session.get_candidate(id).unwrap().birth_generation, 1);
526 }
527
528 #[test]
529 fn test_response_tracking() {
530 let aggregator = FitnessAggregator::new(AggregationModel::default());
531 let mut session: InteractiveSession<RealVector> = InteractiveSession::new(aggregator);
532
533 let c1: Candidate<RealVector> = Candidate::new(CandidateId(0), RealVector::new(vec![1.0]));
534 let request = EvaluationRequest::rate(vec![c1]);
535 session.record_request(&request);
536 session.record_response(false);
537
538 session.record_request(&request);
539 session.record_response(true); assert_eq!(session.evaluations_requested, 2);
542 assert_eq!(session.responses_received, 1);
543 assert_eq!(session.skipped, 1);
544 assert_eq!(session.response_rate(), 0.5);
545 assert_eq!(session.skip_rate(), 0.5);
546 }
547
548 #[test]
549 fn test_metadata() {
550 let aggregator = FitnessAggregator::new(AggregationModel::default());
551 let mut session: InteractiveSession<RealVector> = InteractiveSession::new(aggregator);
552
553 session.set_metadata("experiment", "test_run");
554 session.set_metadata("user", "alice");
555
556 assert_eq!(
557 session.get_metadata("experiment"),
558 Some(&"test_run".to_string())
559 );
560 assert_eq!(session.get_metadata("user"), Some(&"alice".to_string()));
561 assert_eq!(session.get_metadata("missing"), None);
562 }
563
564 #[test]
565 fn test_session_serialization() {
566 let aggregator = FitnessAggregator::new(AggregationModel::DirectRating {
567 default_rating: 5.0,
568 });
569 let mut session: InteractiveSession<RealVector> = InteractiveSession::new(aggregator);
570
571 session.add_candidate(RealVector::new(vec![1.0, 2.0]));
572 session.add_candidate(RealVector::new(vec![3.0, 4.0]));
573 session.set_metadata("test", "value");
574
575 let json = serde_json::to_string(&session).expect("Failed to serialize");
577
578 let loaded: InteractiveSession<RealVector> =
580 serde_json::from_str(&json).expect("Failed to deserialize");
581
582 assert_eq!(loaded.population.len(), 2);
583 assert_eq!(loaded.get_metadata("test"), Some(&"value".to_string()));
584 }
585}