1use crate::engine::experience::experiment::{DomainType, Experiment, ParameterValue};
5use crate::engine::experience::runner::{ExperimentRunner, RunOutput};
6
7#[derive(Debug, Clone)]
9pub struct ValidationCase {
10 pub name: String,
12 pub domain: DomainType,
14 pub function: String,
16 pub params: Vec<(String, ParameterValue)>,
18 pub expected: f64,
20 pub tolerance: f64,
22 pub source: String,
24}
25
26impl ValidationCase {
27 pub fn new(
28 name: &str,
29 domain: DomainType,
30 function: &str,
31 params: Vec<(&str, f64)>,
32 expected: f64,
33 tolerance: f64,
34 source: &str,
35 ) -> Self {
36 Self {
37 name: name.into(),
38 domain,
39 function: function.into(),
40 params: params
41 .into_iter()
42 .map(|(k, v)| (k.to_string(), ParameterValue::Scalar(v)))
43 .collect(),
44 expected,
45 tolerance,
46 source: source.into(),
47 }
48 }
49}
50
51#[derive(Debug, Clone)]
53pub struct ValidationResult {
54 pub name: String,
56 pub passed: bool,
58 pub computed: f64,
60 pub expected: f64,
62 pub relative_error: f64,
64 pub tolerance: f64,
66 pub error_message: Option<String>,
68}
69
70#[derive(Debug, Clone)]
72pub struct ValidationReport {
73 pub results: Vec<ValidationResult>,
75}
76
77impl ValidationReport {
78 pub fn passed_count(&self) -> usize {
80 self.results.iter().filter(|r| r.passed).count()
81 }
82
83 pub fn failed_count(&self) -> usize {
85 self.results.iter().filter(|r| !r.passed).count()
86 }
87
88 pub fn total(&self) -> usize {
90 self.results.len()
91 }
92
93 pub fn all_passed(&self) -> bool {
95 self.results.iter().all(|r| r.passed)
96 }
97
98 pub fn failures(&self) -> Vec<&ValidationResult> {
100 self.results.iter().filter(|r| !r.passed).collect()
101 }
102
103 pub fn worst_error(&self) -> Option<&ValidationResult> {
105 self.results.iter().max_by(|a, b| {
106 a.relative_error
107 .partial_cmp(&b.relative_error)
108 .unwrap_or(std::cmp::Ordering::Equal)
109 })
110 }
111
112 pub fn to_markdown(&self) -> String {
114 let mut out = String::from("# Validation Report\n\n");
115 out.push_str(&format!(
116 "**{}/{} passed**\n\n",
117 self.passed_count(),
118 self.total()
119 ));
120 out.push_str("| Test | Computed | Expected | Rel. Error | Tol | Status |\n");
121 out.push_str("|------|----------|----------|------------|-----|--------|\n");
122 for r in &self.results {
123 let status = if r.passed { "✓" } else { "✗" };
124 out.push_str(&format!(
125 "| {} | {:.6e} | {:.6e} | {:.2e} | {:.0e} | {} |\n",
126 r.name, r.computed, r.expected, r.relative_error, r.tolerance, status,
127 ));
128 }
129 if let Some(w) = self.worst_error() {
130 out.push_str(&format!(
131 "\nWorst error: {} (rel={:.2e})\n",
132 w.name, w.relative_error
133 ));
134 }
135 out
136 }
137
138 pub fn to_csv(&self) -> String {
140 let mut out = String::from("name,computed,expected,relative_error,tolerance,passed\n");
141 for r in &self.results {
142 out.push_str(&format!(
143 "{},{:.10e},{:.10e},{:.6e},{:.0e},{}\n",
144 r.name, r.computed, r.expected, r.relative_error, r.tolerance, r.passed,
145 ));
146 }
147 out
148 }
149}
150
151pub fn run_validation(cases: &[ValidationCase]) -> ValidationReport {
153 let runner = ExperimentRunner::new();
154 let mut results = Vec::with_capacity(cases.len());
155
156 for case in cases {
157 let mut exp = Experiment::new(case.domain.clone(), &case.function);
158 for (k, v) in &case.params {
159 exp = exp.param(k, v.clone());
160 }
161
162 let vr = match runner.run(&exp) {
163 Ok(RunOutput::Scalar(v)) => {
164 let rel_err = if case.expected == 0.0 {
165 v.abs()
166 } else {
167 ((v - case.expected) / case.expected).abs()
168 };
169 ValidationResult {
170 name: case.name.clone(),
171 passed: rel_err <= case.tolerance,
172 computed: v,
173 expected: case.expected,
174 relative_error: rel_err,
175 tolerance: case.tolerance,
176 error_message: None,
177 }
178 }
179 Ok(other) => ValidationResult {
180 name: case.name.clone(),
181 passed: false,
182 computed: f64::NAN,
183 expected: case.expected,
184 relative_error: f64::INFINITY,
185 tolerance: case.tolerance,
186 error_message: Some(format!("expected Scalar, got {:?}", other)),
187 },
188 Err(e) => ValidationResult {
189 name: case.name.clone(),
190 passed: false,
191 computed: f64::NAN,
192 expected: case.expected,
193 relative_error: f64::INFINITY,
194 tolerance: case.tolerance,
195 error_message: Some(format!("{e}")),
196 },
197 };
198 results.push(vr);
199 }
200
201 ValidationReport { results }
202}
203
204pub fn check_nan_safety(domain: DomainType, function: &str, params: Vec<(&str, f64)>) -> bool {
206 let runner = ExperimentRunner::new();
207 let mut exp = Experiment::new(domain, function);
208 for (k, v) in params {
209 exp = exp.param(k, ParameterValue::Scalar(v));
210 }
211 match runner.run(&exp) {
212 Ok(RunOutput::Scalar(v)) => !v.is_nan(),
213 _ => true,
214 }
215}
216
217pub fn check_monotonicity(
219 domain: DomainType,
220 function: &str,
221 base_params: Vec<(&str, f64)>,
222 vary_param: &str,
223 values: &[f64],
224 increasing: bool,
225) -> bool {
226 let runner = ExperimentRunner::new();
227 let mut prev: Option<f64> = None;
228
229 for &v in values {
230 let mut exp = Experiment::new(domain.clone(), function);
231 for &(k, val) in &base_params {
232 if k == vary_param {
233 exp = exp.param(k, ParameterValue::Scalar(v));
234 } else {
235 exp = exp.param(k, ParameterValue::Scalar(val));
236 }
237 }
238 if let Ok(RunOutput::Scalar(out)) = runner.run(&exp) {
239 if let Some(p) = prev {
240 if increasing && out < p {
241 return false;
242 }
243 if !increasing && out > p {
244 return false;
245 }
246 }
247 prev = Some(out);
248 }
249 }
250 true
251}
252
253pub fn report_to_latex(report: &ValidationReport) -> String {
255 let mut out =
256 String::from("\\begin{table}[h]\n\\centering\n\\begin{tabular}{|l|r|r|r|c|}\n\\hline\n");
257 out.push_str("Test & Computed & Expected & Rel. Error & Pass \\\\\n\\hline\n");
258 for r in &report.results {
259 let status = if r.passed { "\\checkmark" } else { "\\times" };
260 out.push_str(&format!(
261 "{} & {:.6e} & {:.6e} & {:.2e} & ${status}$ \\\\\n",
262 r.name.replace('_', "\\_"),
263 r.computed,
264 r.expected,
265 r.relative_error,
266 ));
267 }
268 out.push_str("\\hline\n\\end{tabular}\n");
269 out.push_str(&format!(
270 "\\caption{{Validation: {}/{} passed}}\n",
271 report.passed_count(),
272 report.total()
273 ));
274 out.push_str("\\end{table}\n");
275 out
276}
277
278pub fn report_to_tsv(report: &ValidationReport) -> String {
280 let mut out = String::from("name\tcomputed\texpected\trelative_error\ttolerance\tpassed\n");
281 for r in &report.results {
282 out.push_str(&format!(
283 "{}\t{:.10e}\t{:.10e}\t{:.6e}\t{:.0e}\t{}\n",
284 r.name, r.computed, r.expected, r.relative_error, r.tolerance, r.passed,
285 ));
286 }
287 out
288}
289
290#[derive(Debug, Clone)]
292pub struct ValidationThresholds {
293 pub max_failures: usize,
295 pub max_relative_error: f64,
297}
298
299impl Default for ValidationThresholds {
300 fn default() -> Self {
301 Self {
302 max_failures: 0,
303 max_relative_error: 1e-6,
304 }
305 }
306}
307
308#[derive(Debug, Clone)]
310pub struct MonotonicityCheck {
311 pub label: String,
313 pub domain: DomainType,
315 pub function: String,
317 pub base_params: Vec<(String, f64)>,
319 pub vary_param: String,
321 pub values: Vec<f64>,
323 pub increasing: bool,
325}
326
327impl MonotonicityCheck {
328 pub fn new(
330 label: &str,
331 domain: DomainType,
332 function: &str,
333 base_params: Vec<(&str, f64)>,
334 vary_param: &str,
335 values: Vec<f64>,
336 increasing: bool,
337 ) -> Self {
338 Self {
339 label: label.into(),
340 domain,
341 function: function.into(),
342 base_params: base_params
343 .into_iter()
344 .map(|(k, v)| (k.to_string(), v))
345 .collect(),
346 vary_param: vary_param.into(),
347 values,
348 increasing,
349 }
350 }
351}
352
353#[derive(Debug, Clone)]
355pub struct NanSafetyCheck {
356 pub label: String,
358 pub domain: DomainType,
360 pub function: String,
362 pub params: Vec<(String, f64)>,
364}
365
366impl NanSafetyCheck {
367 pub fn new(label: &str, domain: DomainType, function: &str, params: Vec<(&str, f64)>) -> Self {
369 Self {
370 label: label.into(),
371 domain,
372 function: function.into(),
373 params: params
374 .into_iter()
375 .map(|(k, v)| (k.to_string(), v))
376 .collect(),
377 }
378 }
379}
380
381#[derive(Debug, Clone)]
383pub struct MonotonicityResult {
384 pub label: String,
386 pub passed: bool,
388}
389
390#[derive(Debug, Clone)]
392pub struct NanSafetyResult {
393 pub label: String,
395 pub passed: bool,
397}
398
399#[derive(Debug, Clone)]
401pub struct PipelineOutcome {
402 pub passed: bool,
404 pub report: ValidationReport,
406 pub blocked_by_failures: bool,
408 pub blocked_by_error: bool,
410 pub worst_relative_error: f64,
412 pub monotonicity_results: Vec<MonotonicityResult>,
414 pub nan_safety_results: Vec<NanSafetyResult>,
416 pub monotonicity_passed: bool,
418 pub nan_safety_passed: bool,
420}
421
422pub struct ValidationPipeline {
424 cases: Vec<ValidationCase>,
425 thresholds: ValidationThresholds,
426 monotonicity_checks: Vec<MonotonicityCheck>,
427 nan_checks: Vec<NanSafetyCheck>,
428}
429
430impl ValidationPipeline {
431 pub fn new(thresholds: ValidationThresholds) -> Self {
433 Self {
434 cases: Vec::new(),
435 thresholds,
436 monotonicity_checks: Vec::new(),
437 nan_checks: Vec::new(),
438 }
439 }
440
441 pub fn add_case(mut self, case: ValidationCase) -> Self {
443 self.cases.push(case);
444 self
445 }
446
447 pub fn add_cases(mut self, cases: Vec<ValidationCase>) -> Self {
449 self.cases.extend(cases);
450 self
451 }
452
453 pub fn add_monotonicity(mut self, check: MonotonicityCheck) -> Self {
455 self.monotonicity_checks.push(check);
456 self
457 }
458
459 pub fn add_nan_check(mut self, check: NanSafetyCheck) -> Self {
461 self.nan_checks.push(check);
462 self
463 }
464
465 pub fn with_default_cases(self) -> Self {
467 self.add_cases(default_cases())
468 }
469
470 pub fn with_default_monotonicity(self) -> Self {
472 let checks = default_monotonicity_checks();
473 let mut s = self;
474 for c in checks {
475 s = s.add_monotonicity(c);
476 }
477 s
478 }
479
480 pub fn with_default_nan_safety(self) -> Self {
482 let checks = default_nan_safety_checks();
483 let mut s = self;
484 for c in checks {
485 s = s.add_nan_check(c);
486 }
487 s
488 }
489
490 pub fn full_default(thresholds: ValidationThresholds) -> Self {
492 Self::new(thresholds)
493 .with_default_cases()
494 .with_default_monotonicity()
495 .with_default_nan_safety()
496 }
497
498 pub fn run(&self) -> PipelineOutcome {
500 let report = run_validation(&self.cases);
501
502 let worst_relative_error = report
503 .results
504 .iter()
505 .map(|r| r.relative_error)
506 .filter(|e| e.is_finite())
507 .fold(0.0_f64, f64::max);
508
509 let blocked_by_failures = report.failed_count() > self.thresholds.max_failures;
510 let blocked_by_error = worst_relative_error > self.thresholds.max_relative_error;
511
512 let monotonicity_results: Vec<MonotonicityResult> = self
513 .monotonicity_checks
514 .iter()
515 .map(|mc| {
516 let ok = check_monotonicity(
517 mc.domain.clone(),
518 &mc.function,
519 mc.base_params
520 .iter()
521 .map(|(k, v)| (k.as_str(), *v))
522 .collect(),
523 &mc.vary_param,
524 &mc.values,
525 mc.increasing,
526 );
527 MonotonicityResult {
528 label: mc.label.clone(),
529 passed: ok,
530 }
531 })
532 .collect();
533
534 let nan_safety_results: Vec<NanSafetyResult> = self
535 .nan_checks
536 .iter()
537 .map(|nc| {
538 let ok = check_nan_safety(
539 nc.domain.clone(),
540 &nc.function,
541 nc.params.iter().map(|(k, v)| (k.as_str(), *v)).collect(),
542 );
543 NanSafetyResult {
544 label: nc.label.clone(),
545 passed: ok,
546 }
547 })
548 .collect();
549
550 let monotonicity_passed = monotonicity_results.iter().all(|r| r.passed);
551 let nan_safety_passed = nan_safety_results.iter().all(|r| r.passed);
552 let passed =
553 !blocked_by_failures && !blocked_by_error && monotonicity_passed && nan_safety_passed;
554
555 PipelineOutcome {
556 passed,
557 report,
558 blocked_by_failures,
559 blocked_by_error,
560 worst_relative_error,
561 monotonicity_results,
562 nan_safety_results,
563 monotonicity_passed,
564 nan_safety_passed,
565 }
566 }
567
568 pub fn case_count(&self) -> usize {
570 self.cases.len()
571 }
572
573 pub fn monotonicity_count(&self) -> usize {
575 self.monotonicity_checks.len()
576 }
577
578 pub fn nan_check_count(&self) -> usize {
580 self.nan_checks.len()
581 }
582}
583
584pub fn default_cases() -> Vec<ValidationCase> {
586 let cases: [ValidationCase; _] = [
587 #[cfg(feature = "physics")]
588 ValidationCase::new(
589 "physics_carnot_efficiency",
590 DomainType::Physics,
591 "carnot_efficiency",
592 vec![("t_hot", 600.0), ("t_cold", 300.0)],
593 0.5,
594 1e-12,
595 "Carnot theorem",
596 ),
597 #[cfg(feature = "chemistry")]
598 ValidationCase::new(
599 "chemistry_strong_acid_ph",
600 DomainType::Chemistry,
601 "ph_strong_acid",
602 vec![("concentration", 0.01)],
603 2.0,
604 1e-12,
605 "pH = -log10(c)",
606 ),
607 #[cfg(feature = "biology")]
608 ValidationCase::new(
609 "biology_michaelis_menten",
610 DomainType::Biology,
611 "michaelis_menten",
612 vec![("s", 10.0), ("vmax", 100.0), ("km", 5.0)],
613 100.0 * 10.0 / 15.0,
614 1e-12,
615 "Michaelis-Menten equation",
616 ),
617 #[cfg(feature = "astronomy")]
618 ValidationCase::new(
619 "astronomy_escape_velocity_earth",
620 DomainType::Astronomy,
621 "escape_velocity",
622 vec![("mu", 3.986e14), ("r", 6.371e6)],
623 11_186.0,
624 2e-2,
625 "Earth escape velocity reference",
626 ),
627 #[cfg(feature = "geology")]
628 ValidationCase::new(
629 "geology_half_life",
630 DomainType::Geology,
631 "half_life",
632 vec![("lambda", std::f64::consts::LN_2 / 5730.0)],
633 5730.0,
634 1e-12,
635 "Half-life definition",
636 ),
637 #[cfg(feature = "meteorology")]
638 ValidationCase::new(
639 "meteorology_relative_humidity",
640 DomainType::Meteorology,
641 "relative_humidity",
642 vec![("e", 10.0), ("es", 20.0)],
643 50.0,
644 1e-12,
645 "Relative humidity percentage",
646 ),
647 #[cfg(feature = "maths")]
648 ValidationCase::new(
649 "maths_uniform_cdf",
650 DomainType::Maths,
651 "prob_uniform_cdf",
652 vec![("x", 0.5), ("a", 0.0), ("b", 1.0)],
653 0.5,
654 1e-12,
655 "Uniform CDF on [0,1]",
656 ),
657 #[cfg(feature = "cross_domain")]
658 ValidationCase::new(
659 "astrophysics_schwarzschild_radius_sun",
660 DomainType::Astrophysics,
661 "schwarzschild_radius",
662 vec![("mass", 1.989e30)],
663 2.0 * 6.674_30e-11 * 1.989e30 / (299_792_458.0 * 299_792_458.0),
664 1e-6,
665 "r_s = 2GM/c^2",
666 ),
667 #[cfg(feature = "cross_domain")]
668 ValidationCase::new(
669 "biochemistry_gibbs_free_energy",
670 DomainType::Biochemistry,
671 "gibbs_free_energy",
672 vec![
673 ("delta_h", -100_000.0),
674 ("delta_s", -200.0),
675 ("temperature", 298.15),
676 ],
677 -100_000.0 + 200.0 * 298.15,
678 1e-12,
679 "G = H - TS",
680 ),
681 #[cfg(feature = "cross_domain")]
682 ValidationCase::new(
683 "geochemistry_partition_coefficient",
684 DomainType::Geochemistry,
685 "partition_coefficient",
686 vec![("c_solid", 50.0), ("c_liquid", 10.0)],
687 5.0,
688 1e-12,
689 "Kd = Cs/Cl",
690 ),
691 #[cfg(feature = "cross_domain")]
692 ValidationCase::new(
693 "atmospheric_chemistry_photolysis_rate",
694 DomainType::AtmosphericChemistry,
695 "photolysis_rate",
696 vec![
697 ("cross_section", 1e-20),
698 ("quantum_yield", 0.5),
699 ("actinic_flux", 1e15),
700 ],
701 1e-20 * 0.5 * 1e15,
702 1e-12,
703 "J = sigma * phi * F",
704 ),
705 #[cfg(feature = "cross_domain")]
706 ValidationCase::new(
707 "atmospheric_physics_stefan_boltzmann",
708 DomainType::AtmosphericPhysics,
709 "stefan_boltzmann_flux",
710 vec![("temperature", 255.0)],
711 5.670_374_419e-8 * 255.0_f64.powi(4),
712 1e-6,
713 "F = sigma * T^4",
714 ),
715 #[cfg(feature = "cross_domain")]
716 ValidationCase::new(
717 "planetary_geology_impact_energy",
718 DomainType::PlanetaryGeology,
719 "impact_energy",
720 vec![("projectile_mass", 1e6), ("impact_velocity", 2e4)],
721 0.5 * 1e6 * 2e4 * 2e4,
722 1e-12,
723 "KE = 0.5 * m * v^2",
724 ),
725 #[cfg(feature = "cross_domain")]
726 ValidationCase::new(
727 "biomathematics_logistic_growth_zero_at_capacity",
728 DomainType::Biomathematics,
729 "logistic_growth_rate",
730 vec![
731 ("r", 0.5),
732 ("carrying_capacity", 1000.0),
733 ("population", 1000.0),
734 ],
735 0.0,
736 1e-12,
737 "dN/dt = 0 when N = K",
738 ),
739 #[cfg(feature = "cross_domain")]
740 ValidationCase::new(
741 "mathematical_physics_de_broglie",
742 DomainType::MathematicalPhysics,
743 "de_broglie_wavelength",
744 vec![("momentum", 1e-24)],
745 6.626_070_15e-34 / 1e-24,
746 1e-6,
747 "lambda = h/p",
748 ),
749 #[cfg(feature = "cross_domain")]
750 ValidationCase::new(
751 "biophysics_stokes_drag",
752 DomainType::Biophysics,
753 "stokes_drag_force",
754 vec![("viscosity", 1e-3), ("radius", 1e-6), ("velocity", 1e-4)],
755 6.0 * std::f64::consts::PI * 1e-3 * 1e-6 * 1e-4,
756 1e-6,
757 "F = 6*pi*eta*r*v",
758 ),
759 #[cfg(feature = "cross_domain")]
760 ValidationCase::new(
761 "geophysics_bouguer_anomaly_zero_elevation",
762 DomainType::Geophysics,
763 "bouguer_anomaly",
764 vec![
765 ("observed_gravity", 9.81),
766 ("reference_gravity", 9.80),
767 ("elevation", 0.0),
768 ("slab_density", 2670.0),
769 ],
770 9.81 - 9.80,
771 1e-6,
772 "At zero elevation: anomaly = g_obs - g_ref",
773 ),
774 #[cfg(feature = "cross_domain")]
775 ValidationCase::new(
776 "astrochemistry_freefall_time",
777 DomainType::Astrochemistry,
778 "freefall_time",
779 vec![("number_density", 1e4), ("mean_molecular_weight", 2.33)],
780 (3.0 * std::f64::consts::PI
781 / (32.0 * 6.674_30e-11 * 1e4 * 1e6 * 2.33 * 1.672_621_9e-27))
782 .sqrt(),
783 5e-2,
784 "t_ff = sqrt(3*pi/(32*G*rho))",
785 ),
786 #[cfg(feature = "cross_domain")]
787 ValidationCase::new(
788 "astrobiology_habitable_zone_sun",
789 DomainType::Astrobiology,
790 "habitable_zone_inner",
791 vec![("luminosity", 3.828e26)],
792 (3.828e26 / (4.0 * std::f64::consts::PI * 1.0e3)).sqrt()
793 * (3.828e26 / (16.0 * std::f64::consts::PI * 5.670_374_419e-8 * 373.0_f64.powi(4)))
794 .sqrt()
795 .recip()
796 * (3.828e26 / (4.0 * std::f64::consts::PI * 1.0e3)).sqrt(),
797 5e-1,
798 "Inner edge ~0.95 AU for Sun-like star",
799 ),
800 ];
801 cases.into_iter().collect()
802}
803
804pub fn default_monotonicity_checks() -> Vec<MonotonicityCheck> {
806 let checks: [MonotonicityCheck; _] = [
807 #[cfg(feature = "physics")]
808 MonotonicityCheck::new(
809 "gamma_increases_with_velocity",
810 DomainType::Physics,
811 "lorentz_gamma",
812 vec![],
813 "v",
814 vec![0.0, 1e7, 5e7, 1e8, 2e8, 2.5e8, 2.9e8],
815 true,
816 ),
817 #[cfg(feature = "chemistry")]
818 MonotonicityCheck::new(
819 "ph_decreases_with_concentration",
820 DomainType::Chemistry,
821 "ph_strong_acid",
822 vec![],
823 "concentration",
824 vec![1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 0.1, 1.0],
825 false,
826 ),
827 #[cfg(feature = "biology")]
828 MonotonicityCheck::new(
829 "michaelis_menten_increases_with_substrate",
830 DomainType::Biology,
831 "michaelis_menten",
832 vec![("vmax", 100.0), ("km", 5.0)],
833 "s",
834 vec![0.1, 0.5, 1.0, 5.0, 10.0, 50.0, 100.0],
835 true,
836 ),
837 #[cfg(feature = "physics")]
838 MonotonicityCheck::new(
839 "carnot_efficiency_increases_with_t_hot",
840 DomainType::Physics,
841 "carnot_efficiency",
842 vec![("t_cold", 300.0)],
843 "t_hot",
844 vec![301.0, 400.0, 500.0, 1000.0, 2000.0, 5000.0],
845 true,
846 ),
847 #[cfg(feature = "astronomy")]
848 MonotonicityCheck::new(
849 "escape_velocity_decreases_with_radius",
850 DomainType::Astronomy,
851 "escape_velocity",
852 vec![("mu", 3.986e14)],
853 "r",
854 vec![6.371e6, 1e7, 2e7, 5e7, 1e8],
855 false,
856 ),
857 ];
858 checks.into_iter().collect()
859}
860
861pub fn default_nan_safety_checks() -> Vec<NanSafetyCheck> {
863 let checks: [NanSafetyCheck; _] = [
864 #[cfg(feature = "physics")]
865 NanSafetyCheck::new(
866 "carnot_zero_temps",
867 DomainType::Physics,
868 "carnot_efficiency",
869 vec![("t_hot", 300.0), ("t_cold", 0.0)],
870 ),
871 #[cfg(feature = "biology")]
872 NanSafetyCheck::new(
873 "michaelis_menten_zero_substrate",
874 DomainType::Biology,
875 "michaelis_menten",
876 vec![("s", 0.0), ("vmax", 100.0), ("km", 5.0)],
877 ),
878 #[cfg(feature = "meteorology")]
879 NanSafetyCheck::new(
880 "relative_humidity_zero_es",
881 DomainType::Meteorology,
882 "relative_humidity",
883 vec![("e", 0.0), ("es", 0.0)],
884 ),
885 #[cfg(feature = "geology")]
886 NanSafetyCheck::new(
887 "half_life_zero_lambda",
888 DomainType::Geology,
889 "half_life",
890 vec![("lambda", 0.0)],
891 ),
892 #[cfg(feature = "cross_domain")]
893 NanSafetyCheck::new(
894 "logistic_growth_zero_population",
895 DomainType::Biomathematics,
896 "logistic_growth_rate",
897 vec![
898 ("r", 0.5),
899 ("carrying_capacity", 1000.0),
900 ("population", 0.0),
901 ],
902 ),
903 ];
904 checks.into_iter().collect()
905}