1use serde::{Deserialize, Serialize};
9use std::path::Path;
10use validator::Validate;
11
12use crate::engine::jidoka::JidokaConfig;
13use crate::error::{SimError, SimResult};
14
15#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
19#[serde(deny_unknown_fields)]
20pub struct SimConfig {
21 #[validate(length(min = 1))]
23 #[serde(default = "default_schema_version")]
24 pub schema_version: String,
25
26 #[validate(nested)]
28 #[serde(default)]
29 pub simulation: SimulationMeta,
30
31 #[validate(nested)]
33 pub reproducibility: ReproducibilityConfig,
34
35 #[validate(nested)]
37 #[serde(default)]
38 pub domains: DomainsConfig,
39
40 #[serde(default)]
42 pub jidoka: JidokaConfig,
43
44 #[validate(nested)]
46 #[serde(default)]
47 pub replay: ReplayConfig,
48
49 #[serde(default)]
51 pub visualization: VisualizationConfig,
52
53 #[serde(default)]
55 pub falsification: FalsificationConfig,
56}
57
58fn default_schema_version() -> String {
59 "1.0".to_string()
60}
61
62impl SimConfig {
63 pub fn load<P: AsRef<Path>>(path: P) -> SimResult<Self> {
72 let content = std::fs::read_to_string(path)?;
73 Self::from_yaml(&content)
74 }
75
76 pub fn from_yaml(yaml: &str) -> SimResult<Self> {
82 let config: Self = serde_yaml::from_str(yaml)?;
83
84 config.validate()?;
86
87 config.validate_semantic()?;
89
90 Ok(config)
91 }
92
93 #[must_use]
95 pub fn builder() -> SimConfigBuilder {
96 SimConfigBuilder::default()
97 }
98
99 fn validate_semantic(&self) -> SimResult<()> {
101 if self.domains.monte_carlo.enabled && self.domains.monte_carlo.samples < 100 {
103 return Err(SimError::config(format!(
104 "Monte Carlo requires at least 100 samples, got {}",
105 self.domains.monte_carlo.samples
106 )));
107 }
108
109 let dt = self.domains.physics.timestep.dt;
111 if dt <= 0.0 {
112 return Err(SimError::config("Timestep must be positive"));
113 }
114 if dt > 1.0 {
115 return Err(SimError::config("Timestep should not exceed 1 second"));
116 }
117
118 Ok(())
119 }
120
121 #[must_use]
123 pub const fn get_timestep(&self) -> f64 {
124 self.domains.physics.timestep.dt
125 }
126}
127
128impl Default for SimConfig {
129 fn default() -> Self {
130 Self {
131 schema_version: default_schema_version(),
132 simulation: SimulationMeta::default(),
133 reproducibility: ReproducibilityConfig::default(),
134 domains: DomainsConfig::default(),
135 jidoka: JidokaConfig::default(),
136 replay: ReplayConfig::default(),
137 visualization: VisualizationConfig::default(),
138 falsification: FalsificationConfig::default(),
139 }
140 }
141}
142
143#[derive(Debug, Default)]
145pub struct SimConfigBuilder {
146 seed: Option<u64>,
147 timestep: Option<f64>,
148 jidoka: Option<JidokaConfig>,
149}
150
151impl SimConfigBuilder {
152 #[must_use]
154 pub const fn seed(mut self, seed: u64) -> Self {
155 self.seed = Some(seed);
156 self
157 }
158
159 #[must_use]
161 pub const fn timestep(mut self, dt: f64) -> Self {
162 self.timestep = Some(dt);
163 self
164 }
165
166 #[must_use]
168 #[allow(clippy::missing_const_for_fn)] pub fn jidoka(mut self, config: JidokaConfig) -> Self {
170 self.jidoka = Some(config);
171 self
172 }
173
174 #[must_use]
176 pub fn build(self) -> SimConfig {
177 let mut config = SimConfig::default();
178
179 if let Some(seed) = self.seed {
180 config.reproducibility.seed = seed;
181 }
182
183 if let Some(dt) = self.timestep {
184 config.domains.physics.timestep.dt = dt;
185 }
186
187 if let Some(jidoka) = self.jidoka {
188 config.jidoka = jidoka;
189 }
190
191 config
192 }
193}
194
195#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
197pub struct SimulationMeta {
198 #[serde(default)]
200 pub name: String,
201 #[serde(default)]
203 pub description: String,
204 #[serde(default = "default_version")]
206 pub version: String,
207}
208
209fn default_version() -> String {
210 "0.1.0".to_string()
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
215pub struct ReproducibilityConfig {
216 pub seed: u64,
218 #[serde(default = "default_true")]
220 pub ieee_strict: bool,
221 #[serde(default = "default_true")]
223 pub record_rng_state: bool,
224}
225
226const fn default_true() -> bool {
227 true
228}
229
230impl Default for ReproducibilityConfig {
231 fn default() -> Self {
232 Self {
233 seed: 42,
234 ieee_strict: true,
235 record_rng_state: true,
236 }
237 }
238}
239
240#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
242pub struct DomainsConfig {
243 #[validate(nested)]
245 #[serde(default)]
246 pub physics: PhysicsConfig,
247 #[validate(nested)]
249 #[serde(default)]
250 pub monte_carlo: MonteCarloConfig,
251 #[serde(default)]
253 pub optimization: OptimizationConfig,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
258pub struct PhysicsConfig {
259 #[serde(default = "default_true")]
261 pub enabled: bool,
262 #[serde(default)]
264 pub engine: PhysicsEngine,
265 #[serde(default)]
267 pub integrator: IntegratorConfig,
268 #[validate(nested)]
270 #[serde(default)]
271 pub timestep: TimestepConfig,
272}
273
274impl Default for PhysicsConfig {
275 fn default() -> Self {
276 Self {
277 enabled: true,
278 engine: PhysicsEngine::default(),
279 integrator: IntegratorConfig::default(),
280 timestep: TimestepConfig::default(),
281 }
282 }
283}
284
285#[derive(Debug, Clone, Default, Serialize, Deserialize)]
287#[serde(rename_all = "kebab-case")]
288pub enum PhysicsEngine {
289 #[default]
291 RigidBody,
292 Orbital,
294 Fluid,
296 Discrete,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct IntegratorConfig {
303 #[serde(default)]
305 pub integrator_type: IntegratorType,
306}
307
308impl Default for IntegratorConfig {
309 fn default() -> Self {
310 Self {
311 integrator_type: IntegratorType::Verlet,
312 }
313 }
314}
315
316#[derive(Debug, Clone, Default, Serialize, Deserialize)]
318#[serde(rename_all = "kebab-case")]
319pub enum IntegratorType {
320 Euler,
322 #[default]
324 Verlet,
325 Rk4,
327 Rk78,
329 SymplecticEuler,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
335pub struct TimestepConfig {
336 #[serde(default)]
338 pub mode: TimestepMode,
339 #[validate(range(min = 0.000_001, max = 1.0))]
341 #[serde(default = "default_timestep")]
342 pub dt: f64,
343 #[serde(default = "default_min_timestep")]
345 pub min_dt: f64,
346 #[serde(default = "default_max_timestep")]
348 pub max_dt: f64,
349 #[serde(default = "default_tolerance")]
351 pub tolerance: f64,
352}
353
354const fn default_timestep() -> f64 {
355 0.001
356}
357
358const fn default_min_timestep() -> f64 {
359 0.0001
360}
361
362const fn default_max_timestep() -> f64 {
363 0.01
364}
365
366const fn default_tolerance() -> f64 {
367 1e-9
368}
369
370impl Default for TimestepConfig {
371 fn default() -> Self {
372 Self {
373 mode: TimestepMode::Fixed,
374 dt: default_timestep(),
375 min_dt: default_min_timestep(),
376 max_dt: default_max_timestep(),
377 tolerance: default_tolerance(),
378 }
379 }
380}
381
382#[derive(Debug, Clone, Default, Serialize, Deserialize)]
384#[serde(rename_all = "kebab-case")]
385pub enum TimestepMode {
386 #[default]
388 Fixed,
389 Adaptive,
391}
392
393#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
395pub struct MonteCarloConfig {
396 #[serde(default)]
398 pub enabled: bool,
399 #[validate(range(min = 1))]
401 #[serde(default = "default_samples")]
402 pub samples: usize,
403 #[serde(default)]
405 pub variance_reduction: VarianceReductionMethod,
406}
407
408const fn default_samples() -> usize {
409 10_000
410}
411
412impl Default for MonteCarloConfig {
413 fn default() -> Self {
414 Self {
415 enabled: false,
416 samples: default_samples(),
417 variance_reduction: VarianceReductionMethod::None,
418 }
419 }
420}
421
422#[derive(Debug, Clone, Default, Serialize, Deserialize)]
424#[serde(rename_all = "kebab-case")]
425pub enum VarianceReductionMethod {
426 #[default]
428 None,
429 Antithetic,
431 ControlVariate,
433 Importance,
435 Stratified,
437}
438
439#[derive(Debug, Clone, Default, Serialize, Deserialize)]
441pub struct OptimizationConfig {
442 #[serde(default)]
444 pub enabled: bool,
445 #[serde(default)]
447 pub algorithm: OptimizationAlgorithm,
448}
449
450#[derive(Debug, Clone, Default, Serialize, Deserialize)]
452#[serde(rename_all = "kebab-case")]
453pub enum OptimizationAlgorithm {
454 #[default]
456 Bayesian,
457 CmaEs,
459 Genetic,
461 Gradient,
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
467pub struct ReplayConfig {
468 #[serde(default = "default_true")]
470 pub enabled: bool,
471 #[serde(default = "default_checkpoint_interval")]
473 pub checkpoint_interval: u64,
474 #[serde(default = "default_max_storage")]
476 pub max_storage: usize,
477 #[serde(default)]
479 pub compression: CompressionAlgorithm,
480 #[validate(range(min = 1, max = 22))]
482 #[serde(default = "default_compression_level")]
483 pub compression_level: i32,
484}
485
486const fn default_checkpoint_interval() -> u64 {
487 1000
488}
489
490const fn default_max_storage() -> usize {
491 1024 * 1024 * 1024 }
493
494const fn default_compression_level() -> i32 {
495 3
496}
497
498impl Default for ReplayConfig {
499 fn default() -> Self {
500 Self {
501 enabled: true,
502 checkpoint_interval: default_checkpoint_interval(),
503 max_storage: default_max_storage(),
504 compression: CompressionAlgorithm::Zstd,
505 compression_level: default_compression_level(),
506 }
507 }
508}
509
510#[derive(Debug, Clone, Default, Serialize, Deserialize)]
512#[serde(rename_all = "kebab-case")]
513pub enum CompressionAlgorithm {
514 None,
516 Lz4,
518 #[default]
520 Zstd,
521}
522
523#[derive(Debug, Clone, Default, Serialize, Deserialize)]
525pub struct VisualizationConfig {
526 #[serde(default)]
528 pub tui: TuiConfig,
529 #[serde(default)]
531 pub web: WebConfig,
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize)]
536pub struct TuiConfig {
537 #[serde(default)]
539 pub enabled: bool,
540 #[serde(default = "default_refresh_hz")]
542 pub refresh_hz: u32,
543}
544
545const fn default_refresh_hz() -> u32 {
546 30
547}
548
549impl Default for TuiConfig {
550 fn default() -> Self {
551 Self {
552 enabled: false,
553 refresh_hz: default_refresh_hz(),
554 }
555 }
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize)]
560pub struct WebConfig {
561 #[serde(default)]
563 pub enabled: bool,
564 #[serde(default = "default_port")]
566 pub port: u16,
567}
568
569const fn default_port() -> u16 {
570 8080
571}
572
573impl Default for WebConfig {
574 fn default() -> Self {
575 Self {
576 enabled: false,
577 port: default_port(),
578 }
579 }
580}
581
582#[derive(Debug, Clone, Default, Serialize, Deserialize)]
584pub struct FalsificationConfig {
585 #[serde(default)]
587 pub null_hypothesis: String,
588 #[serde(default = "default_significance")]
590 pub significance: f64,
591}
592
593const fn default_significance() -> f64 {
594 0.05
595}
596
597#[derive(Debug, Clone, Serialize)]
612pub struct Velocity {
613 pub meters_per_second: f64,
615 pub original_unit: String,
617}
618
619impl Velocity {
620 #[must_use]
622 pub fn from_mps(value: f64) -> Self {
623 Self {
624 meters_per_second: value,
625 original_unit: "m/s".to_string(),
626 }
627 }
628
629 #[must_use]
631 pub const fn as_mps(&self) -> f64 {
632 self.meters_per_second
633 }
634
635 #[must_use]
637 pub fn as_kps(&self) -> f64 {
638 self.meters_per_second / 1000.0
639 }
640
641 #[must_use]
643 pub fn as_kph(&self) -> f64 {
644 self.meters_per_second * 3.6
645 }
646}
647
648impl<'de> Deserialize<'de> for Velocity {
649 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
650 where
651 D: serde::Deserializer<'de>,
652 {
653 let s = String::deserialize(deserializer)?;
654 parse_velocity(&s).ok_or_else(|| {
655 serde::de::Error::custom(format!(
656 "Invalid velocity '{s}'. Expected format: '<number> <unit>' \
657 where unit is 'm/s', 'km/s', 'km/h', 'ft/s', or 'kn' (knots)"
658 ))
659 })
660 }
661}
662
663fn parse_velocity(s: &str) -> Option<Velocity> {
665 let parts: Vec<&str> = s.split_whitespace().collect();
666 if parts.len() != 2 {
667 return None;
668 }
669
670 let value: f64 = parts[0].parse().ok()?;
671 let unit = parts[1].to_lowercase();
672
673 let meters_per_second = match unit.as_str() {
674 "m/s" => value,
675 "km/s" => value * 1000.0,
676 "km/h" | "kph" => value / 3.6,
677 "ft/s" => value * 0.3048,
678 "kn" | "knots" | "kt" => value * 0.514_444,
679 "mph" => value * 0.447_04,
680 _ => return None,
681 };
682
683 Some(Velocity {
684 meters_per_second,
685 original_unit: parts[1].to_string(),
686 })
687}
688
689#[derive(Debug, Clone, Serialize)]
691pub struct Length {
692 pub meters: f64,
694 pub original_unit: String,
696}
697
698impl Length {
699 #[must_use]
701 pub fn from_meters(value: f64) -> Self {
702 Self {
703 meters: value,
704 original_unit: "m".to_string(),
705 }
706 }
707
708 #[must_use]
710 pub const fn as_meters(&self) -> f64 {
711 self.meters
712 }
713
714 #[must_use]
716 pub fn as_km(&self) -> f64 {
717 self.meters / 1000.0
718 }
719}
720
721impl<'de> Deserialize<'de> for Length {
722 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
723 where
724 D: serde::Deserializer<'de>,
725 {
726 let s = String::deserialize(deserializer)?;
727 parse_length(&s).ok_or_else(|| {
728 serde::de::Error::custom(format!(
729 "Invalid length '{s}'. Expected format: '<number> <unit>' \
730 where unit is 'm', 'km', 'cm', 'mm', 'ft', 'mi', or 'nm' (nautical miles)"
731 ))
732 })
733 }
734}
735
736fn parse_length(s: &str) -> Option<Length> {
738 let parts: Vec<&str> = s.split_whitespace().collect();
739 if parts.len() != 2 {
740 return None;
741 }
742
743 let value: f64 = parts[0].parse().ok()?;
744 let unit = parts[1].to_lowercase();
745
746 let meters = match unit.as_str() {
747 "m" | "meters" => value,
748 "km" | "kilometers" => value * 1000.0,
749 "cm" | "centimeters" => value / 100.0,
750 "mm" | "millimeters" => value / 1000.0,
751 "ft" | "feet" => value * 0.3048,
752 "mi" | "miles" => value * 1609.344,
753 "nm" | "nmi" => value * 1852.0, "au" => value * 149_597_870_700.0, _ => return None,
756 };
757
758 Some(Length {
759 meters,
760 original_unit: parts[1].to_string(),
761 })
762}
763
764#[derive(Debug, Clone, Serialize)]
766pub struct Mass {
767 pub kilograms: f64,
769 pub original_unit: String,
771}
772
773impl Mass {
774 #[must_use]
776 pub fn from_kg(value: f64) -> Self {
777 Self {
778 kilograms: value,
779 original_unit: "kg".to_string(),
780 }
781 }
782
783 #[must_use]
785 pub const fn as_kg(&self) -> f64 {
786 self.kilograms
787 }
788
789 #[must_use]
791 pub fn as_grams(&self) -> f64 {
792 self.kilograms * 1000.0
793 }
794}
795
796impl<'de> Deserialize<'de> for Mass {
797 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
798 where
799 D: serde::Deserializer<'de>,
800 {
801 let s = String::deserialize(deserializer)?;
802 parse_mass(&s).ok_or_else(|| {
803 serde::de::Error::custom(format!(
804 "Invalid mass '{s}'. Expected format: '<number> <unit>' \
805 where unit is 'kg', 'g', 'mg', 't' (metric ton), or 'lb'"
806 ))
807 })
808 }
809}
810
811fn parse_mass(s: &str) -> Option<Mass> {
813 let parts: Vec<&str> = s.split_whitespace().collect();
814 if parts.len() != 2 {
815 return None;
816 }
817
818 let value: f64 = parts[0].parse().ok()?;
819 let unit = parts[1].to_lowercase();
820
821 let kilograms = match unit.as_str() {
822 "kg" | "kilograms" => value,
823 "g" | "grams" => value / 1000.0,
824 "mg" | "milligrams" => value / 1_000_000.0,
825 "t" | "tonnes" | "metric_ton" => value * 1000.0,
826 "lb" | "lbs" | "pounds" => value * 0.453_592,
827 _ => return None,
828 };
829
830 Some(Mass {
831 kilograms,
832 original_unit: parts[1].to_string(),
833 })
834}
835
836#[derive(Debug, Clone, Serialize)]
838pub struct Duration {
839 pub seconds: f64,
841 pub original_unit: String,
843}
844
845impl Duration {
846 #[must_use]
848 pub fn from_seconds(value: f64) -> Self {
849 Self {
850 seconds: value,
851 original_unit: "s".to_string(),
852 }
853 }
854
855 #[must_use]
857 pub const fn as_seconds(&self) -> f64 {
858 self.seconds
859 }
860
861 #[must_use]
863 pub fn as_millis(&self) -> f64 {
864 self.seconds * 1000.0
865 }
866}
867
868impl<'de> Deserialize<'de> for Duration {
869 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
870 where
871 D: serde::Deserializer<'de>,
872 {
873 let s = String::deserialize(deserializer)?;
874 parse_duration(&s).ok_or_else(|| {
875 serde::de::Error::custom(format!(
876 "Invalid duration '{s}'. Expected format: '<number> <unit>' \
877 where unit is 's', 'ms', 'us', 'ns', 'min', 'h', or 'd'"
878 ))
879 })
880 }
881}
882
883fn parse_duration(s: &str) -> Option<Duration> {
885 let parts: Vec<&str> = s.split_whitespace().collect();
886 if parts.len() != 2 {
887 return None;
888 }
889
890 let value: f64 = parts[0].parse().ok()?;
891 let unit = parts[1].to_lowercase();
892
893 let seconds = match unit.as_str() {
894 "s" | "sec" | "seconds" => value,
895 "ms" | "milliseconds" => value / 1000.0,
896 "us" | "microseconds" | "µs" => value / 1_000_000.0,
897 "ns" | "nanoseconds" => value / 1_000_000_000.0,
898 "min" | "minutes" => value * 60.0,
899 "h" | "hr" | "hours" => value * 3600.0,
900 "d" | "days" => value * 86400.0,
901 _ => return None,
902 };
903
904 Some(Duration {
905 seconds,
906 original_unit: parts[1].to_string(),
907 })
908}
909
910#[derive(Debug, Clone, Serialize)]
912pub struct Angle {
913 pub radians: f64,
915 pub original_unit: String,
917}
918
919impl Angle {
920 #[must_use]
922 pub fn from_radians(value: f64) -> Self {
923 Self {
924 radians: value,
925 original_unit: "rad".to_string(),
926 }
927 }
928
929 #[must_use]
931 pub fn from_degrees(value: f64) -> Self {
932 Self {
933 radians: value.to_radians(),
934 original_unit: "deg".to_string(),
935 }
936 }
937
938 #[must_use]
940 pub const fn as_radians(&self) -> f64 {
941 self.radians
942 }
943
944 #[must_use]
946 pub fn as_degrees(&self) -> f64 {
947 self.radians.to_degrees()
948 }
949}
950
951impl<'de> Deserialize<'de> for Angle {
952 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
953 where
954 D: serde::Deserializer<'de>,
955 {
956 let s = String::deserialize(deserializer)?;
957 parse_angle(&s).ok_or_else(|| {
958 serde::de::Error::custom(format!(
959 "Invalid angle '{s}'. Expected format: '<number> <unit>' \
960 where unit is 'rad', 'deg', 'arcmin', or 'arcsec'"
961 ))
962 })
963 }
964}
965
966fn parse_angle(s: &str) -> Option<Angle> {
968 let parts: Vec<&str> = s.split_whitespace().collect();
969 if parts.len() != 2 {
970 return None;
971 }
972
973 let value: f64 = parts[0].parse().ok()?;
974 let unit = parts[1].to_lowercase();
975
976 let radians = match unit.as_str() {
977 "rad" | "radians" => value,
978 "deg" | "degrees" | "°" => value.to_radians(),
979 "arcmin" => (value / 60.0).to_radians(),
980 "arcsec" => (value / 3600.0).to_radians(),
981 _ => return None,
982 };
983
984 Some(Angle {
985 radians,
986 original_unit: parts[1].to_string(),
987 })
988}
989
990#[cfg(test)]
991mod tests {
992 use super::*;
993
994 #[test]
995 fn test_config_defaults() {
996 let config = SimConfig::default();
997
998 assert_eq!(config.schema_version, "1.0");
999 assert_eq!(config.reproducibility.seed, 42);
1000 assert!(config.reproducibility.ieee_strict);
1001 assert!((config.domains.physics.timestep.dt - 0.001).abs() < f64::EPSILON);
1002 }
1003
1004 #[test]
1005 fn test_config_builder() {
1006 let config = SimConfig::builder().seed(12345).timestep(0.01).build();
1007
1008 assert_eq!(config.reproducibility.seed, 12345);
1009 assert!((config.domains.physics.timestep.dt - 0.01).abs() < f64::EPSILON);
1010 }
1011
1012 #[test]
1013 fn test_config_yaml_parse() {
1014 let yaml = r"
1015reproducibility:
1016 seed: 42
1017domains:
1018 physics:
1019 enabled: true
1020 timestep:
1021 dt: 0.001
1022";
1023 let config = SimConfig::from_yaml(yaml);
1024 assert!(config.is_ok());
1025
1026 let config = config.ok();
1027 assert!(config.is_some());
1028 assert_eq!(config.as_ref().map(|c| c.reproducibility.seed), Some(42));
1029 }
1030
1031 #[test]
1032 fn test_config_validation_fails_invalid_samples() {
1033 let yaml = r"
1034reproducibility:
1035 seed: 42
1036domains:
1037 monte_carlo:
1038 enabled: true
1039 samples: 10
1040";
1041 let config = SimConfig::from_yaml(yaml);
1042 assert!(config.is_err());
1043 }
1044
1045 #[test]
1046 fn test_config_validation_fails_negative_timestep() {
1047 let yaml = r"
1048reproducibility:
1049 seed: 42
1050domains:
1051 physics:
1052 timestep:
1053 dt: -0.001
1054";
1055 let config = SimConfig::from_yaml(yaml);
1057 assert!(config.is_err());
1058 }
1059
1060 #[test]
1061 fn test_integrator_types() {
1062 let yaml_verlet = r"
1063reproducibility:
1064 seed: 42
1065domains:
1066 physics:
1067 integrator:
1068 integrator_type: verlet
1069";
1070 let config = SimConfig::from_yaml(yaml_verlet);
1071 assert!(config.is_ok());
1072
1073 let yaml_rk4 = r"
1074reproducibility:
1075 seed: 42
1076domains:
1077 physics:
1078 integrator:
1079 integrator_type: rk4
1080";
1081 let config = SimConfig::from_yaml(yaml_rk4);
1082 assert!(config.is_ok());
1083 }
1084
1085 #[test]
1088 fn test_velocity_parsing() {
1089 let v = parse_velocity("10.0 m/s");
1091 assert!(v.is_some());
1092 assert!((v.as_ref().unwrap().meters_per_second - 10.0).abs() < 0.01);
1093
1094 let v = parse_velocity("7.8 km/s");
1096 assert!(v.is_some());
1097 assert!((v.as_ref().unwrap().meters_per_second - 7800.0).abs() < 0.01);
1098
1099 let v = parse_velocity("100 km/h");
1101 assert!(v.is_some());
1102 assert!((v.as_ref().unwrap().meters_per_second - 27.778).abs() < 0.01);
1103
1104 let v = parse_velocity("10.0");
1106 assert!(v.is_none());
1107
1108 let v = parse_velocity("10.0 furlongs/fortnight");
1109 assert!(v.is_none());
1110 }
1111
1112 #[test]
1113 fn test_velocity_conversions() {
1114 let v = Velocity::from_mps(1000.0);
1115 assert!((v.as_kps() - 1.0).abs() < f64::EPSILON);
1116 assert!((v.as_kph() - 3600.0).abs() < 0.01);
1117 }
1118
1119 #[test]
1120 fn test_length_parsing() {
1121 let l = parse_length("100 m");
1123 assert!(l.is_some());
1124 assert!((l.as_ref().unwrap().meters - 100.0).abs() < 0.01);
1125
1126 let l = parse_length("1.5 km");
1128 assert!(l.is_some());
1129 assert!((l.as_ref().unwrap().meters - 1500.0).abs() < 0.01);
1130
1131 let l = parse_length("1 au");
1133 assert!(l.is_some());
1134 assert!((l.as_ref().unwrap().meters - 149_597_870_700.0).abs() < 1000.0);
1135
1136 let l = parse_length("100");
1138 assert!(l.is_none());
1139 }
1140
1141 #[test]
1142 fn test_mass_parsing() {
1143 let m = parse_mass("100 kg");
1145 assert!(m.is_some());
1146 assert!((m.as_ref().unwrap().kilograms - 100.0).abs() < 0.01);
1147
1148 let m = parse_mass("1.5 t");
1150 assert!(m.is_some());
1151 assert!((m.as_ref().unwrap().kilograms - 1500.0).abs() < 0.01);
1152
1153 let m = parse_mass("100 lb");
1155 assert!(m.is_some());
1156 assert!((m.as_ref().unwrap().kilograms - 45.36).abs() < 0.01);
1157 }
1158
1159 #[test]
1160 fn test_duration_parsing() {
1161 let d = parse_duration("10 s");
1163 assert!(d.is_some());
1164 assert!((d.as_ref().unwrap().seconds - 10.0).abs() < f64::EPSILON);
1165
1166 let d = parse_duration("1000 ms");
1168 assert!(d.is_some());
1169 assert!((d.as_ref().unwrap().seconds - 1.0).abs() < 0.001);
1170
1171 let d = parse_duration("2 h");
1173 assert!(d.is_some());
1174 assert!((d.as_ref().unwrap().seconds - 7200.0).abs() < f64::EPSILON);
1175 }
1176
1177 #[test]
1178 fn test_angle_parsing() {
1179 let a = parse_angle("3.14159 rad");
1181 assert!(a.is_some());
1182 assert!((a.as_ref().unwrap().radians - 3.14159).abs() < 0.00001);
1183
1184 let a = parse_angle("180 deg");
1186 assert!(a.is_some());
1187 assert!((a.as_ref().unwrap().radians - std::f64::consts::PI).abs() < 0.0001);
1188
1189 let a = parse_angle("60 arcmin");
1191 assert!(a.is_some());
1192 assert!((a.as_ref().unwrap().as_degrees() - 1.0).abs() < 0.001);
1193 }
1194
1195 #[test]
1196 fn test_poka_yoke_rejects_unitless() {
1197 assert!(parse_velocity("100").is_none());
1199 assert!(parse_length("100").is_none());
1200 assert!(parse_mass("100").is_none());
1201 assert!(parse_duration("100").is_none());
1202 assert!(parse_angle("100").is_none());
1203 }
1204
1205 #[test]
1206 fn test_poka_yoke_yaml_deserialization() {
1207 #[derive(Debug, Deserialize)]
1208 struct TestConfig {
1209 velocity: Velocity,
1210 length: Length,
1211 }
1212
1213 let yaml = r#"
1214velocity: "100 m/s"
1215length: "10 km"
1216"#;
1217 let config: Result<TestConfig, _> = serde_yaml::from_str(yaml);
1218 assert!(config.is_ok());
1219
1220 let config = config.ok().unwrap();
1221 assert!((config.velocity.meters_per_second - 100.0).abs() < 0.01);
1222 assert!((config.length.meters - 10000.0).abs() < 0.01);
1223 }
1224
1225 #[test]
1226 fn test_poka_yoke_yaml_rejects_invalid() {
1227 #[derive(Debug, Deserialize)]
1228 #[allow(dead_code)]
1229 struct TestConfig {
1230 velocity: Velocity,
1231 }
1232
1233 let yaml = r#"
1235velocity: "100"
1236"#;
1237 let config: Result<TestConfig, _> = serde_yaml::from_str(yaml);
1238 assert!(config.is_err());
1239
1240 let yaml = r#"
1242velocity: "100 parsecs"
1243"#;
1244 let config: Result<TestConfig, _> = serde_yaml::from_str(yaml);
1245 assert!(config.is_err());
1246 }
1247
1248 #[test]
1249 fn test_config_get_timestep() {
1250 let config = SimConfig::default();
1251 assert!((config.get_timestep() - 0.001).abs() < f64::EPSILON);
1252 }
1253
1254 #[test]
1255 fn test_config_builder_with_jidoka() {
1256 let jidoka = JidokaConfig::default();
1257 let config = SimConfig::builder().jidoka(jidoka).build();
1258 assert!(config.jidoka.energy_tolerance > 0.0);
1260 }
1261
1262 #[test]
1263 fn test_config_validation_fails_large_timestep() {
1264 let yaml = r"
1265reproducibility:
1266 seed: 42
1267domains:
1268 physics:
1269 timestep:
1270 dt: 2.0
1271";
1272 let config = SimConfig::from_yaml(yaml);
1273 assert!(config.is_err());
1274 }
1275
1276 #[test]
1277 fn test_variance_reduction_methods() {
1278 let _none = VarianceReductionMethod::None;
1279 let _anti = VarianceReductionMethod::Antithetic;
1280 let _control = VarianceReductionMethod::ControlVariate;
1281 let _importance = VarianceReductionMethod::Importance;
1282 let _strat = VarianceReductionMethod::Stratified;
1283 }
1284
1285 #[test]
1286 fn test_optimization_algorithms() {
1287 let _bayesian = OptimizationAlgorithm::Bayesian;
1288 let _cmaes = OptimizationAlgorithm::CmaEs;
1289 let _genetic = OptimizationAlgorithm::Genetic;
1290 let _gradient = OptimizationAlgorithm::Gradient;
1291 }
1292
1293 #[test]
1294 fn test_compression_algorithms() {
1295 let _none = CompressionAlgorithm::None;
1296 let _lz4 = CompressionAlgorithm::Lz4;
1297 let _zstd = CompressionAlgorithm::Zstd;
1298 }
1299
1300 #[test]
1301 fn test_velocity_all_units() {
1302 let v = parse_velocity("100 ft/s");
1304 assert!(v.is_some());
1305 assert!((v.as_ref().unwrap().meters_per_second - 30.48).abs() < 0.01);
1306
1307 let v = parse_velocity("60 mph");
1309 assert!(v.is_some());
1310 assert!((v.as_ref().unwrap().meters_per_second - 26.82).abs() < 0.1);
1311
1312 let v = parse_velocity("100 kn");
1314 assert!(v.is_some());
1315 assert!((v.as_ref().unwrap().meters_per_second - 51.44).abs() < 0.1);
1316
1317 let v = parse_velocity("36 kph");
1319 assert!(v.is_some());
1320 assert!((v.as_ref().unwrap().meters_per_second - 10.0).abs() < 0.1);
1321 }
1322
1323 #[test]
1324 fn test_length_all_units() {
1325 let l = parse_length("100 cm");
1327 assert!(l.is_some());
1328 assert!((l.as_ref().unwrap().meters - 1.0).abs() < 0.01);
1329
1330 let l = parse_length("1000 mm");
1332 assert!(l.is_some());
1333 assert!((l.as_ref().unwrap().meters - 1.0).abs() < 0.01);
1334
1335 let l = parse_length("100 ft");
1337 assert!(l.is_some());
1338 assert!((l.as_ref().unwrap().meters - 30.48).abs() < 0.01);
1339
1340 let l = parse_length("1 mi");
1342 assert!(l.is_some());
1343 assert!((l.as_ref().unwrap().meters - 1609.344).abs() < 0.01);
1344
1345 let l = parse_length("1 nm");
1347 assert!(l.is_some());
1348 assert!((l.as_ref().unwrap().meters - 1852.0).abs() < 0.01);
1349
1350 let l = parse_length("1 meters");
1352 assert!(l.is_some());
1353 }
1354
1355 #[test]
1356 fn test_length_conversions() {
1357 let l = Length::from_meters(1000.0);
1358 assert!((l.as_km() - 1.0).abs() < f64::EPSILON);
1359 }
1360
1361 #[test]
1362 fn test_mass_all_units() {
1363 let m = parse_mass("1000 g");
1365 assert!(m.is_some());
1366 assert!((m.as_ref().unwrap().kilograms - 1.0).abs() < 0.01);
1367
1368 let m = parse_mass("1000000 mg");
1370 assert!(m.is_some());
1371 assert!((m.as_ref().unwrap().kilograms - 1.0).abs() < 0.01);
1372
1373 let m = parse_mass("2.2 lbs");
1375 assert!(m.is_some());
1376 assert!((m.as_ref().unwrap().kilograms - 1.0).abs() < 0.01);
1377 }
1378
1379 #[test]
1380 fn test_mass_conversions() {
1381 let m = Mass::from_kg(1.0);
1382 assert!((m.as_grams() - 1000.0).abs() < f64::EPSILON);
1383 }
1384
1385 #[test]
1386 fn test_duration_all_units() {
1387 let d = parse_duration("1000000 us");
1389 assert!(d.is_some());
1390 assert!((d.as_ref().unwrap().seconds - 1.0).abs() < 0.001);
1391
1392 let d = parse_duration("1000000000 ns");
1394 assert!(d.is_some());
1395 assert!((d.as_ref().unwrap().seconds - 1.0).abs() < 0.001);
1396
1397 let d = parse_duration("1 min");
1399 assert!(d.is_some());
1400 assert!((d.as_ref().unwrap().seconds - 60.0).abs() < f64::EPSILON);
1401
1402 let d = parse_duration("1 d");
1404 assert!(d.is_some());
1405 assert!((d.as_ref().unwrap().seconds - 86400.0).abs() < f64::EPSILON);
1406
1407 let d = parse_duration("10 sec");
1409 assert!(d.is_some());
1410 }
1411
1412 #[test]
1413 fn test_duration_conversions() {
1414 let d = Duration::from_seconds(1.0);
1415 assert!((d.as_millis() - 1000.0).abs() < f64::EPSILON);
1416 }
1417
1418 #[test]
1419 fn test_angle_all_units() {
1420 let a = parse_angle("3600 arcsec");
1422 assert!(a.is_some());
1423 assert!((a.as_ref().unwrap().as_degrees() - 1.0).abs() < 0.001);
1424
1425 let a = parse_angle("90 degrees");
1427 assert!(a.is_some());
1428
1429 let a = parse_angle("1 radians");
1431 assert!(a.is_some());
1432 }
1433
1434 #[test]
1435 fn test_angle_conversions() {
1436 let a = Angle::from_degrees(180.0);
1437 assert!((a.as_radians() - std::f64::consts::PI).abs() < 0.0001);
1438 assert!((a.as_degrees() - 180.0).abs() < 0.0001);
1439
1440 let a2 = Angle::from_radians(std::f64::consts::PI);
1441 assert!((a2.as_degrees() - 180.0).abs() < 0.0001);
1442 }
1443
1444 #[test]
1445 fn test_parse_invalid_number() {
1446 assert!(parse_velocity("abc m/s").is_none());
1447 assert!(parse_length("abc m").is_none());
1448 assert!(parse_mass("abc kg").is_none());
1449 assert!(parse_duration("abc s").is_none());
1450 assert!(parse_angle("abc rad").is_none());
1451 }
1452
1453 #[test]
1454 fn test_parse_empty_string() {
1455 assert!(parse_velocity("").is_none());
1456 assert!(parse_length("").is_none());
1457 assert!(parse_mass("").is_none());
1458 assert!(parse_duration("").is_none());
1459 assert!(parse_angle("").is_none());
1460 }
1461
1462 #[test]
1463 fn test_parse_too_many_parts() {
1464 assert!(parse_velocity("100 m per second").is_none());
1465 assert!(parse_length("100 meters long").is_none());
1466 }
1467
1468 #[test]
1469 fn test_simulation_meta_default() {
1470 let meta = SimulationMeta::default();
1471 assert!(meta.name.is_empty());
1472 }
1473
1474 #[test]
1475 fn test_replay_config_default() {
1476 let config = ReplayConfig::default();
1477 assert!(config.enabled);
1478 assert_eq!(config.checkpoint_interval, 1000);
1479 }
1480
1481 #[test]
1482 fn test_tui_config_default() {
1483 let config = TuiConfig::default();
1484 assert!(!config.enabled);
1485 assert_eq!(config.refresh_hz, 30);
1486 }
1487
1488 #[test]
1489 fn test_web_config_default() {
1490 let config = WebConfig::default();
1491 assert!(!config.enabled);
1492 assert_eq!(config.port, 8080);
1493 }
1494
1495 #[test]
1496 fn test_falsification_config_default() {
1497 let config = FalsificationConfig::default();
1498 assert!(config.null_hypothesis.is_empty());
1499 assert!((config.significance - 0.0).abs() < f64::EPSILON);
1501 }
1502
1503 #[test]
1504 fn test_monte_carlo_config_default() {
1505 let config = MonteCarloConfig::default();
1506 assert!(!config.enabled);
1507 assert_eq!(config.samples, 10_000);
1508 }
1509
1510 #[test]
1511 fn test_optimization_config_default() {
1512 let config = OptimizationConfig::default();
1513 assert!(!config.enabled);
1514 }
1515
1516 #[test]
1517 fn test_domains_config_default() {
1518 let config = DomainsConfig::default();
1519 assert!(config.physics.enabled);
1520 assert!(!config.monte_carlo.enabled);
1521 }
1522
1523 #[test]
1524 fn test_reproducibility_config_default() {
1525 let config = ReproducibilityConfig::default();
1526 assert_eq!(config.seed, 42);
1527 assert!(config.ieee_strict);
1528 }
1529}