vision_calibration_pipeline/session/
calibsession.rs1use crate::Error;
8use serde::{Deserialize, Serialize};
9
10use super::problem_type::ProblemType;
11use super::types::{ExportRecord, LogEntry, SessionMetadata};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(bound = "P: ProblemType")]
45pub struct CalibrationSession<P: ProblemType> {
46 pub metadata: SessionMetadata,
48
49 pub config: P::Config,
51
52 input: Option<P::Input>,
54
55 pub state: P::State,
57
58 output: Option<P::Output>,
60
61 pub exports: Vec<ExportRecord<P::Export>>,
63
64 pub log: Vec<LogEntry>,
66}
67
68impl<P: ProblemType> CalibrationSession<P> {
69 pub fn new() -> Self {
75 Self {
76 metadata: SessionMetadata::new(P::name(), P::schema_version()),
77 config: P::Config::default(),
78 input: None,
79 state: P::State::default(),
80 output: None,
81 exports: Vec::new(),
82 log: Vec::new(),
83 }
84 }
85
86 pub fn with_description(description: impl Into<String>) -> Self {
88 Self {
89 metadata: SessionMetadata::with_description(
90 P::name(),
91 P::schema_version(),
92 description,
93 ),
94 config: P::Config::default(),
95 input: None,
96 state: P::State::default(),
97 output: None,
98 exports: Vec::new(),
99 log: Vec::new(),
100 }
101 }
102
103 pub fn with_input(input: P::Input) -> Result<Self, Error> {
109 let mut session = Self::new();
110 session.set_input(input)?;
111 Ok(session)
112 }
113
114 pub fn set_input(&mut self, input: P::Input) -> Result<(), Error> {
124 P::validate_input(&input)?;
125
126 let policy = P::on_input_change();
127 if policy.clear_state {
128 self.state = P::State::default();
129 }
130 if policy.clear_output {
131 self.output = None;
132 }
133 if policy.clear_exports {
134 self.exports.clear();
135 }
136
137 self.input = Some(input);
138 self.metadata.touch();
139 Ok(())
140 }
141
142 pub fn input(&self) -> Option<&P::Input> {
144 self.input.as_ref()
145 }
146
147 pub fn input_mut(&mut self) -> Option<&mut P::Input> {
149 self.input.as_mut()
150 }
151
152 pub fn require_input(&self) -> Result<&P::Input, Error> {
158 self.input
159 .as_ref()
160 .ok_or_else(|| Error::not_available("input"))
161 }
162
163 pub fn require_input_mut(&mut self) -> Result<&mut P::Input, Error> {
169 self.input
170 .as_mut()
171 .ok_or_else(|| Error::not_available("input"))
172 }
173
174 pub fn has_input(&self) -> bool {
176 self.input.is_some()
177 }
178
179 pub fn clear_input(&mut self) {
181 let policy = P::on_input_change();
182 if policy.clear_state {
183 self.state = P::State::default();
184 }
185 if policy.clear_output {
186 self.output = None;
187 }
188 if policy.clear_exports {
189 self.exports.clear();
190 }
191 self.input = None;
192 self.metadata.touch();
193 }
194
195 pub fn set_config(&mut self, config: P::Config) -> Result<(), Error> {
205 P::validate_config(&config)?;
206
207 let policy = P::on_config_change();
208 if policy.clear_state {
209 self.state = P::State::default();
210 }
211 if policy.clear_output {
212 self.output = None;
213 }
214 if policy.clear_exports {
215 self.exports.clear();
216 }
217
218 self.config = config;
219 self.metadata.touch();
220 Ok(())
221 }
222
223 pub fn update_config<F>(&mut self, f: F) -> Result<(), Error>
232 where
233 F: FnOnce(&mut P::Config),
234 {
235 let mut new_config = self.config.clone();
236 f(&mut new_config);
237 self.set_config(new_config)
238 }
239
240 pub fn output(&self) -> Option<&P::Output> {
246 self.output.as_ref()
247 }
248
249 pub fn output_mut(&mut self) -> Option<&mut P::Output> {
251 self.output.as_mut()
252 }
253
254 pub fn require_output(&self) -> Result<&P::Output, Error> {
260 self.output
261 .as_ref()
262 .ok_or_else(|| Error::not_available("output"))
263 }
264
265 pub fn set_output(&mut self, output: P::Output) {
267 self.output = Some(output);
268 self.metadata.touch();
269 }
270
271 pub fn has_output(&self) -> bool {
273 self.output.is_some()
274 }
275
276 pub fn clear_output(&mut self) {
278 self.output = None;
279 self.metadata.touch();
280 }
281
282 pub fn export(&mut self) -> Result<P::Export, Error> {
292 let output = self.require_output()?;
293 let export = P::export(output, &self.config)?;
294 self.exports.push(ExportRecord::new(export.clone()));
295 self.metadata.touch();
296 Ok(export)
297 }
298
299 pub fn export_with_notes(&mut self, notes: impl Into<String>) -> Result<P::Export, Error> {
305 let output = self.require_output()?;
306 let export = P::export(output, &self.config)?;
307 self.exports
308 .push(ExportRecord::with_notes(export.clone(), notes));
309 self.metadata.touch();
310 Ok(export)
311 }
312
313 pub fn export_peek(&self) -> Result<P::Export, Error> {
321 let output = self.require_output()?;
322 P::export(output, &self.config)
323 }
324
325 pub fn validate(&self) -> Result<(), Error> {
340 let input = self.require_input()?;
341 P::validate_input(input)?;
342 P::validate_config(&self.config)?;
343 P::validate_input_config(input, &self.config)?;
344 Ok(())
345 }
346
347 pub fn log_success(&mut self, operation: impl Into<String>) {
353 self.log.push(LogEntry::success(operation));
354 self.metadata.touch();
355 }
356
357 pub fn log_success_with_notes(
359 &mut self,
360 operation: impl Into<String>,
361 notes: impl Into<String>,
362 ) {
363 self.log
364 .push(LogEntry::success_with_notes(operation, notes));
365 self.metadata.touch();
366 }
367
368 pub fn log_failure(&mut self, operation: impl Into<String>, error: impl Into<String>) {
370 self.log.push(LogEntry::failure(operation, error));
371 self.metadata.touch();
372 }
373
374 pub fn reset_state(&mut self) {
380 self.state = P::State::default();
381 self.metadata.touch();
382 }
383
384 pub fn reset_output(&mut self) {
386 self.output = None;
387 self.metadata.touch();
388 }
389
390 pub fn reset(&mut self) {
392 self.input = None;
393 self.state = P::State::default();
394 self.output = None;
395 self.exports.clear();
396 self.log.clear();
397 self.metadata.touch();
398 }
399
400 pub fn to_json(&self) -> Result<String, Error> {
410 let mut value = serde_json::to_value(self).map_err(Error::Serde)?;
412 let metadata = value
413 .get_mut("metadata")
414 .and_then(serde_json::Value::as_object_mut)
415 .ok_or_else(|| Error::numerical("session metadata missing during serialization"))?;
416 metadata.insert(
417 "problem_type".to_string(),
418 serde_json::Value::String(P::name().to_string()),
419 );
420 metadata.insert(
421 "schema_version".to_string(),
422 serde_json::Value::Number(P::schema_version().into()),
423 );
424 serde_json::to_string_pretty(&value).map_err(Error::Serde)
425 }
426
427 pub fn from_json(json: &str) -> Result<Self, Error> {
436 let session: Self = serde_json::from_str(json)?;
437 let expected_problem_type = P::name();
438 let expected_schema_version = P::schema_version();
439
440 if session.metadata.problem_type != expected_problem_type {
441 return Err(Error::invalid_input(format!(
442 "session problem_type '{}' does not match expected '{}'",
443 session.metadata.problem_type, expected_problem_type
444 )));
445 }
446
447 if session.metadata.schema_version != expected_schema_version {
448 return Err(Error::invalid_input(format!(
449 "session schema version {} does not match expected version {}",
450 session.metadata.schema_version, expected_schema_version
451 )));
452 }
453
454 Ok(session)
455 }
456}
457
458impl<P: ProblemType> Default for CalibrationSession<P> {
459 fn default() -> Self {
460 Self::new()
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467 use serde::{Deserialize, Serialize};
468
469 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
474 struct MockConfig {
475 scale: f64,
476 max_iters: usize,
477 }
478
479 #[derive(Clone, Debug, Serialize, Deserialize)]
480 struct MockInput {
481 data: Vec<f64>,
482 }
483
484 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
485 struct MockState {
486 computed: Option<f64>,
487 }
488
489 #[derive(Clone, Debug, Serialize, Deserialize)]
490 struct MockOutput {
491 result: f64,
492 }
493
494 #[derive(Clone, Debug, Serialize, Deserialize)]
495 struct MockExport {
496 value: f64,
497 }
498
499 #[derive(Debug)]
500 struct MockProblem;
501
502 impl ProblemType for MockProblem {
503 type Config = MockConfig;
504 type Input = MockInput;
505 type State = MockState;
506 type Output = MockOutput;
507 type Export = MockExport;
508
509 fn name() -> &'static str {
510 "mock_problem"
511 }
512
513 fn schema_version() -> u32 {
514 1
515 }
516
517 fn validate_input(input: &Self::Input) -> Result<(), Error> {
518 if input.data.is_empty() {
519 return Err(Error::invalid_input("input data cannot be empty"));
520 }
521 Ok(())
522 }
523
524 fn validate_config(config: &Self::Config) -> Result<(), Error> {
525 if config.max_iters == 0 {
526 return Err(Error::invalid_input("max_iters must be positive"));
527 }
528 Ok(())
529 }
530
531 fn export(output: &Self::Output, _config: &Self::Config) -> Result<Self::Export, Error> {
532 Ok(MockExport {
533 value: output.result,
534 })
535 }
536 }
537
538 #[test]
543 fn new_session_has_defaults() {
544 let session = CalibrationSession::<MockProblem>::new();
545 assert_eq!(session.metadata.problem_type, "mock_problem");
546 assert_eq!(session.metadata.schema_version, 1);
547 assert!(session.metadata.description.is_none());
548 assert!(session.input().is_none());
549 assert!(session.output().is_none());
550 assert!(session.exports.is_empty());
551 assert!(session.log.is_empty());
552 }
553
554 #[test]
555 fn with_description() {
556 let session =
557 CalibrationSession::<MockProblem>::with_description("Test calibration session");
558 assert_eq!(
559 session.metadata.description,
560 Some("Test calibration session".to_string())
561 );
562 }
563
564 #[test]
565 fn set_input_validates() {
566 let mut session = CalibrationSession::<MockProblem>::new();
567
568 let result = session.set_input(MockInput { data: vec![] });
570 assert!(result.is_err());
571 assert!(result.unwrap_err().to_string().contains("empty"));
572
573 let result = session.set_input(MockInput {
575 data: vec![1.0, 2.0],
576 });
577 assert!(result.is_ok());
578 assert!(session.has_input());
579 }
580
581 #[test]
582 fn set_input_clears_state_and_output() {
583 let mut session = CalibrationSession::<MockProblem>::new();
584 session.set_input(MockInput { data: vec![1.0] }).unwrap();
585 session.state.computed = Some(42.0);
586 session.set_output(MockOutput { result: 100.0 });
587
588 session.set_input(MockInput { data: vec![2.0] }).unwrap();
590
591 assert!(session.state.computed.is_none());
592 assert!(session.output().is_none());
593 }
594
595 #[test]
596 fn set_config_keeps_output() {
597 let mut session = CalibrationSession::<MockProblem>::new();
598 session.set_input(MockInput { data: vec![1.0] }).unwrap();
599 session.set_output(MockOutput { result: 100.0 });
600
601 session
603 .set_config(MockConfig {
604 scale: 2.0,
605 max_iters: 100,
606 })
607 .unwrap();
608
609 assert!(session.has_output());
610 assert_eq!(session.output().unwrap().result, 100.0);
611 }
612
613 #[test]
614 fn set_config_validates() {
615 let mut session = CalibrationSession::<MockProblem>::new();
616
617 let result = session.set_config(MockConfig {
619 scale: 1.0,
620 max_iters: 0,
621 });
622 assert!(result.is_err());
623 assert!(result.unwrap_err().to_string().contains("max_iters"));
624 }
625
626 #[test]
627 fn update_config() {
628 let mut session = CalibrationSession::<MockProblem>::new();
629 session.config.scale = 1.0;
630 session.config.max_iters = 10;
631
632 session.update_config(|c| c.scale = 5.0).unwrap();
633
634 assert_eq!(session.config.scale, 5.0);
635 assert_eq!(session.config.max_iters, 10); }
637
638 #[test]
639 fn require_input_errors_when_none() {
640 let session = CalibrationSession::<MockProblem>::new();
641 let result = session.require_input();
642 assert!(result.is_err());
643 assert!(result.unwrap_err().to_string().contains("input"));
644 }
645
646 #[test]
647 fn require_output_errors_when_none() {
648 let session = CalibrationSession::<MockProblem>::new();
649 let result = session.require_output();
650 assert!(result.is_err());
651 assert!(result.unwrap_err().to_string().contains("output"));
652 }
653
654 #[test]
655 fn export_adds_to_collection() {
656 let mut session = CalibrationSession::<MockProblem>::new();
657 session.set_input(MockInput { data: vec![1.0] }).unwrap();
658 session.set_output(MockOutput { result: 42.0 });
659
660 assert!(session.exports.is_empty());
661
662 let export1 = session.export().unwrap();
663 assert_eq!(export1.value, 42.0);
664 assert_eq!(session.exports.len(), 1);
665
666 let export2 = session.export().unwrap();
667 assert_eq!(export2.value, 42.0);
668 assert_eq!(session.exports.len(), 2);
669 }
670
671 #[test]
672 fn export_peek_does_not_store() {
673 let mut session = CalibrationSession::<MockProblem>::new();
674 session.set_input(MockInput { data: vec![1.0] }).unwrap();
675 session.set_output(MockOutput { result: 42.0 });
676
677 let export = session.export_peek().unwrap();
678 assert_eq!(export.value, 42.0);
679 assert!(session.exports.is_empty()); }
681
682 #[test]
683 fn export_with_notes() {
684 let mut session = CalibrationSession::<MockProblem>::new();
685 session.set_input(MockInput { data: vec![1.0] }).unwrap();
686 session.set_output(MockOutput { result: 42.0 });
687
688 session.export_with_notes("final result").unwrap();
689
690 assert_eq!(session.exports.len(), 1);
691 assert_eq!(session.exports[0].notes, Some("final result".to_string()));
692 }
693
694 #[test]
695 fn validate_checks_all_hooks() {
696 let mut session = CalibrationSession::<MockProblem>::new();
697
698 let result = session.validate();
700 assert!(result.is_err());
701
702 session.set_input(MockInput { data: vec![1.0] }).unwrap();
704 session.config.max_iters = 10;
705 let result = session.validate();
706 assert!(result.is_ok());
707 }
708
709 #[test]
710 fn log_entries_recorded() {
711 let mut session = CalibrationSession::<MockProblem>::new();
712
713 session.log_success("init");
714 session.log_success_with_notes("optimize", "converged");
715 session.log_failure("filter", "too few points");
716
717 assert_eq!(session.log.len(), 3);
718 assert!(session.log[0].success);
719 assert_eq!(session.log[0].operation, "init");
720 assert!(session.log[1].success);
721 assert_eq!(session.log[1].notes, Some("converged".to_string()));
722 assert!(!session.log[2].success);
723 assert_eq!(session.log[2].notes, Some("too few points".to_string()));
724 }
725
726 #[test]
727 fn json_roundtrip_empty() {
728 let session = CalibrationSession::<MockProblem>::new();
729 let json = session.to_json().unwrap();
730 let restored = CalibrationSession::<MockProblem>::from_json(&json).unwrap();
731
732 assert_eq!(restored.metadata.problem_type, "mock_problem");
733 assert!(restored.input().is_none());
734 assert!(restored.output().is_none());
735 }
736
737 #[test]
738 fn json_roundtrip_with_input() {
739 let mut session = CalibrationSession::<MockProblem>::new();
740 session
741 .set_input(MockInput {
742 data: vec![1.0, 2.0, 3.0],
743 })
744 .unwrap();
745
746 let json = session.to_json().unwrap();
747 let restored = CalibrationSession::<MockProblem>::from_json(&json).unwrap();
748
749 let input = restored.input().unwrap();
750 assert_eq!(input.data, vec![1.0, 2.0, 3.0]);
751 }
752
753 #[test]
754 fn json_roundtrip_with_output() {
755 let mut session = CalibrationSession::<MockProblem>::new();
756 session.set_input(MockInput { data: vec![1.0] }).unwrap();
757 session.set_output(MockOutput { result: 42.0 });
758
759 let json = session.to_json().unwrap();
760 let restored = CalibrationSession::<MockProblem>::from_json(&json).unwrap();
761
762 assert_eq!(restored.output().unwrap().result, 42.0);
763 }
764
765 #[test]
766 fn json_roundtrip_with_exports() {
767 let mut session = CalibrationSession::<MockProblem>::new();
768 session.set_input(MockInput { data: vec![1.0] }).unwrap();
769 session.set_output(MockOutput { result: 42.0 });
770 session.export().unwrap();
771 session.export_with_notes("second export").unwrap();
772
773 let json = session.to_json().unwrap();
774 let restored = CalibrationSession::<MockProblem>::from_json(&json).unwrap();
775
776 assert_eq!(restored.exports.len(), 2);
777 assert!(restored.exports[0].notes.is_none());
778 assert_eq!(restored.exports[1].notes, Some("second export".to_string()));
779 }
780
781 #[test]
782 fn schema_version_checked() {
783 let session = CalibrationSession::<MockProblem>::new();
785 let json = session.to_json().unwrap();
786
787 let newer = json.replace("\"schema_version\": 1", "\"schema_version\": 999");
789 let newer_result = CalibrationSession::<MockProblem>::from_json(&newer);
790 assert!(newer_result.is_err());
791 assert!(
792 newer_result
793 .unwrap_err()
794 .to_string()
795 .contains("schema version")
796 );
797
798 let older = json.replace("\"schema_version\": 1", "\"schema_version\": 0");
800 let older_result = CalibrationSession::<MockProblem>::from_json(&older);
801 assert!(older_result.is_err());
802 assert!(
803 older_result
804 .unwrap_err()
805 .to_string()
806 .contains("schema version")
807 );
808 }
809
810 #[test]
811 fn problem_type_checked() {
812 let session = CalibrationSession::<MockProblem>::new();
813 let json = session.to_json().unwrap();
814 let json = json.replace(
815 "\"problem_type\": \"mock_problem\"",
816 "\"problem_type\": \"other\"",
817 );
818
819 let result = CalibrationSession::<MockProblem>::from_json(&json);
820 assert!(result.is_err());
821 assert!(result.unwrap_err().to_string().contains("problem_type"));
822 }
823
824 #[test]
825 fn to_json_pins_problem_type_and_schema_version() {
826 let mut session = CalibrationSession::<MockProblem>::new();
827 session.metadata.problem_type = "tampered".to_string();
828 session.metadata.schema_version = 42;
829
830 let json = session.to_json().unwrap();
831 let restored: CalibrationSession<MockProblem> = serde_json::from_str(&json).unwrap();
832
833 assert_eq!(restored.metadata.problem_type, MockProblem::name());
834 assert_eq!(
835 restored.metadata.schema_version,
836 MockProblem::schema_version()
837 );
838 }
839
840 #[test]
841 fn reset_state() {
842 let mut session = CalibrationSession::<MockProblem>::new();
843 session.set_input(MockInput { data: vec![1.0] }).unwrap();
844 session.state.computed = Some(42.0);
845 session.set_output(MockOutput { result: 100.0 });
846
847 session.reset_state();
848
849 assert!(session.has_input()); assert!(session.state.computed.is_none()); assert!(session.has_output()); }
853
854 #[test]
855 fn reset_output() {
856 let mut session = CalibrationSession::<MockProblem>::new();
857 session.set_input(MockInput { data: vec![1.0] }).unwrap();
858 session.state.computed = Some(42.0);
859 session.set_output(MockOutput { result: 100.0 });
860
861 session.reset_output();
862
863 assert!(session.has_input()); assert!(session.state.computed.is_some()); assert!(!session.has_output()); }
867
868 #[test]
869 fn reset_all() {
870 let mut session = CalibrationSession::<MockProblem>::new();
871 session.set_input(MockInput { data: vec![1.0] }).unwrap();
872 session.state.computed = Some(42.0);
873 session.set_output(MockOutput { result: 100.0 });
874 session.export().unwrap();
875 session.log_success("test");
876
877 session.reset();
878
879 assert!(!session.has_input());
880 assert!(session.state.computed.is_none());
881 assert!(!session.has_output());
882 assert!(session.exports.is_empty());
883 assert!(session.log.is_empty());
884 }
885
886 #[test]
887 fn clear_input() {
888 let mut session = CalibrationSession::<MockProblem>::new();
889 session.set_input(MockInput { data: vec![1.0] }).unwrap();
890 session.state.computed = Some(42.0);
891 session.set_output(MockOutput { result: 100.0 });
892
893 session.clear_input();
894
895 assert!(!session.has_input());
896 assert!(session.state.computed.is_none()); assert!(!session.has_output()); }
899
900 #[test]
901 fn with_input_validates() {
902 let result = CalibrationSession::<MockProblem>::with_input(MockInput { data: vec![] });
904 assert!(result.is_err());
905
906 let result = CalibrationSession::<MockProblem>::with_input(MockInput { data: vec![1.0] });
908 assert!(result.is_ok());
909 assert!(result.unwrap().has_input());
910 }
911
912 #[test]
913 fn metadata_timestamps_update() {
914 let mut session = CalibrationSession::<MockProblem>::new();
915 let created = session.metadata.created_at;
916 let initial_modified = session.metadata.last_modified;
917
918 session.set_input(MockInput { data: vec![1.0] }).unwrap();
920
921 assert_eq!(session.metadata.created_at, created);
922 assert!(session.metadata.last_modified >= initial_modified);
923 }
924}