aranet_core/
validation.rs

1//! Data validation and bounds checking for sensor readings.
2//!
3//! This module provides validation utilities to detect anomalous readings
4//! and flag potential sensor issues.
5//!
6//! # Example
7//!
8//! ```
9//! use aranet_core::ReadingValidator;
10//! use aranet_core::validation::ValidatorConfig;
11//! use aranet_types::{CurrentReading, Status};
12//!
13//! // Create a validator with default config
14//! let validator = ReadingValidator::default();
15//!
16//! // Create a reading to validate
17//! let reading = CurrentReading {
18//!     co2: 800,
19//!     temperature: 22.5,
20//!     pressure: 1013.0,
21//!     humidity: 45,
22//!     battery: 85,
23//!     status: Status::Green,
24//!     interval: 300,
25//!     age: 60,
26//!     captured_at: None,
27//!     radon: None,
28//!     radiation_rate: None,
29//!     radiation_total: None,
30//! };
31//!
32//! let result = validator.validate(&reading);
33//! assert!(result.is_valid);
34//! assert!(!result.has_warnings());
35//! ```
36
37use serde::{Deserialize, Serialize};
38
39use aranet_types::{CurrentReading, DeviceType};
40
41/// Warning types for validation issues.
42///
43/// This enum is marked `#[non_exhaustive]` to allow adding new warning types
44/// in future versions without breaking downstream code.
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46#[non_exhaustive]
47pub enum ValidationWarning {
48    /// CO2 reading is below minimum expected value.
49    Co2TooLow { value: u16, min: u16 },
50    /// CO2 reading is above maximum expected value.
51    Co2TooHigh { value: u16, max: u16 },
52    /// Temperature is below minimum expected value.
53    TemperatureTooLow { value: f32, min: f32 },
54    /// Temperature is above maximum expected value.
55    TemperatureTooHigh { value: f32, max: f32 },
56    /// Pressure is below minimum expected value.
57    PressureTooLow { value: f32, min: f32 },
58    /// Pressure is above maximum expected value.
59    PressureTooHigh { value: f32, max: f32 },
60    /// Humidity is above 100%.
61    HumidityOutOfRange { value: u8 },
62    /// Battery level is above 100%.
63    BatteryOutOfRange { value: u8 },
64    /// CO2 is zero which may indicate sensor error.
65    Co2Zero,
66    /// All values are zero which may indicate communication error.
67    AllZeros,
68    /// Radon reading is above maximum expected value.
69    RadonTooHigh { value: u32, max: u32 },
70    /// Radiation rate is above maximum expected value.
71    RadiationRateTooHigh { value: f32, max: f32 },
72    /// Radiation total is above maximum expected value.
73    RadiationTotalTooHigh { value: f64, max: f64 },
74}
75
76impl std::fmt::Display for ValidationWarning {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        match self {
79            ValidationWarning::Co2TooLow { value, min } => {
80                write!(f, "CO2 {} ppm is below minimum {} ppm", value, min)
81            }
82            ValidationWarning::Co2TooHigh { value, max } => {
83                write!(f, "CO2 {} ppm exceeds maximum {} ppm", value, max)
84            }
85            ValidationWarning::TemperatureTooLow { value, min } => {
86                write!(f, "Temperature {}°C is below minimum {}°C", value, min)
87            }
88            ValidationWarning::TemperatureTooHigh { value, max } => {
89                write!(f, "Temperature {}°C exceeds maximum {}°C", value, max)
90            }
91            ValidationWarning::PressureTooLow { value, min } => {
92                write!(f, "Pressure {} hPa is below minimum {} hPa", value, min)
93            }
94            ValidationWarning::PressureTooHigh { value, max } => {
95                write!(f, "Pressure {} hPa exceeds maximum {} hPa", value, max)
96            }
97            ValidationWarning::HumidityOutOfRange { value } => {
98                write!(f, "Humidity {}% is out of valid range (0-100)", value)
99            }
100            ValidationWarning::BatteryOutOfRange { value } => {
101                write!(f, "Battery {}% is out of valid range (0-100)", value)
102            }
103            ValidationWarning::Co2Zero => {
104                write!(f, "CO2 reading is zero - possible sensor error")
105            }
106            ValidationWarning::AllZeros => {
107                write!(f, "All readings are zero - possible communication error")
108            }
109            ValidationWarning::RadonTooHigh { value, max } => {
110                write!(f, "Radon {} Bq/m³ exceeds maximum {} Bq/m³", value, max)
111            }
112            ValidationWarning::RadiationRateTooHigh { value, max } => {
113                write!(
114                    f,
115                    "Radiation rate {} µSv/h exceeds maximum {} µSv/h",
116                    value, max
117                )
118            }
119            ValidationWarning::RadiationTotalTooHigh { value, max } => {
120                write!(
121                    f,
122                    "Radiation total {} µSv exceeds maximum {} µSv",
123                    value, max
124                )
125            }
126        }
127    }
128}
129
130/// Result of validating a reading.
131#[derive(Debug, Clone)]
132pub struct ValidationResult {
133    /// Whether the reading passed validation.
134    pub is_valid: bool,
135    /// List of warnings (may be non-empty even if valid).
136    pub warnings: Vec<ValidationWarning>,
137}
138
139impl ValidationResult {
140    /// Create a successful validation result with no warnings.
141    pub fn valid() -> Self {
142        Self {
143            is_valid: true,
144            warnings: Vec::new(),
145        }
146    }
147
148    /// Create an invalid result with the given warnings.
149    pub fn invalid(warnings: Vec<ValidationWarning>) -> Self {
150        Self {
151            is_valid: false,
152            warnings,
153        }
154    }
155
156    /// Create a valid result with warnings.
157    pub fn valid_with_warnings(warnings: Vec<ValidationWarning>) -> Self {
158        Self {
159            is_valid: true,
160            warnings,
161        }
162    }
163
164    /// Check if there are any warnings.
165    pub fn has_warnings(&self) -> bool {
166        !self.warnings.is_empty()
167    }
168}
169
170/// Configuration for reading validation.
171#[derive(Debug, Clone)]
172pub struct ValidatorConfig {
173    /// Minimum expected CO2 value (ppm).
174    pub co2_min: u16,
175    /// Maximum expected CO2 value (ppm).
176    pub co2_max: u16,
177    /// Minimum expected temperature (°C).
178    pub temperature_min: f32,
179    /// Maximum expected temperature (°C).
180    pub temperature_max: f32,
181    /// Minimum expected pressure (hPa).
182    pub pressure_min: f32,
183    /// Maximum expected pressure (hPa).
184    pub pressure_max: f32,
185    /// Maximum expected radon value (Bq/m³).
186    pub radon_max: u32,
187    /// Maximum expected radiation rate (µSv/h).
188    pub radiation_rate_max: f32,
189    /// Maximum expected radiation total (mSv).
190    pub radiation_total_max: f64,
191    /// Treat CO2 = 0 as an error.
192    pub warn_on_zero_co2: bool,
193    /// Treat all zeros as an error.
194    pub warn_on_all_zeros: bool,
195}
196
197impl Default for ValidatorConfig {
198    fn default() -> Self {
199        Self {
200            co2_min: 300,   // Outdoor ambient is ~400 ppm
201            co2_max: 10000, // Very high but possible in some scenarios
202            temperature_min: -40.0,
203            temperature_max: 85.0,
204            pressure_min: 300.0,           // Very high altitude
205            pressure_max: 1100.0,          // Sea level or below
206            radon_max: 1000,               // WHO action level is 100-300 Bq/m³
207            radiation_rate_max: 100.0,     // Normal background is ~0.1-0.2 µSv/h
208            radiation_total_max: 100000.0, // Reasonable upper bound for accumulated dose
209            warn_on_zero_co2: true,
210            warn_on_all_zeros: true,
211        }
212    }
213}
214
215impl ValidatorConfig {
216    /// Create new validator config with defaults.
217    #[must_use]
218    pub fn new() -> Self {
219        Self::default()
220    }
221
222    /// Set minimum CO2 value (ppm).
223    #[must_use]
224    pub fn co2_min(mut self, min: u16) -> Self {
225        self.co2_min = min;
226        self
227    }
228
229    /// Set maximum CO2 value (ppm).
230    #[must_use]
231    pub fn co2_max(mut self, max: u16) -> Self {
232        self.co2_max = max;
233        self
234    }
235
236    /// Set CO2 range (min, max).
237    #[must_use]
238    pub fn co2_range(mut self, min: u16, max: u16) -> Self {
239        self.co2_min = min;
240        self.co2_max = max;
241        self
242    }
243
244    /// Set minimum temperature (°C).
245    #[must_use]
246    pub fn temperature_min(mut self, min: f32) -> Self {
247        self.temperature_min = min;
248        self
249    }
250
251    /// Set maximum temperature (°C).
252    #[must_use]
253    pub fn temperature_max(mut self, max: f32) -> Self {
254        self.temperature_max = max;
255        self
256    }
257
258    /// Set temperature range (min, max).
259    #[must_use]
260    pub fn temperature_range(mut self, min: f32, max: f32) -> Self {
261        self.temperature_min = min;
262        self.temperature_max = max;
263        self
264    }
265
266    /// Set minimum pressure (hPa).
267    #[must_use]
268    pub fn pressure_min(mut self, min: f32) -> Self {
269        self.pressure_min = min;
270        self
271    }
272
273    /// Set maximum pressure (hPa).
274    #[must_use]
275    pub fn pressure_max(mut self, max: f32) -> Self {
276        self.pressure_max = max;
277        self
278    }
279
280    /// Set pressure range (min, max).
281    #[must_use]
282    pub fn pressure_range(mut self, min: f32, max: f32) -> Self {
283        self.pressure_min = min;
284        self.pressure_max = max;
285        self
286    }
287
288    /// Set whether to warn on CO2 = 0.
289    #[must_use]
290    pub fn warn_on_zero_co2(mut self, warn: bool) -> Self {
291        self.warn_on_zero_co2 = warn;
292        self
293    }
294
295    /// Set whether to warn on all zeros.
296    #[must_use]
297    pub fn warn_on_all_zeros(mut self, warn: bool) -> Self {
298        self.warn_on_all_zeros = warn;
299        self
300    }
301
302    /// Set maximum radon value (Bq/m³).
303    #[must_use]
304    pub fn radon_max(mut self, max: u32) -> Self {
305        self.radon_max = max;
306        self
307    }
308
309    /// Set maximum radiation rate (µSv/h).
310    #[must_use]
311    pub fn radiation_rate_max(mut self, max: f32) -> Self {
312        self.radiation_rate_max = max;
313        self
314    }
315
316    /// Set maximum radiation total (mSv).
317    #[must_use]
318    pub fn radiation_total_max(mut self, max: f64) -> Self {
319        self.radiation_total_max = max;
320        self
321    }
322
323    /// Create strict validation config (narrow ranges).
324    pub fn strict() -> Self {
325        Self {
326            co2_min: 350,
327            co2_max: 5000,
328            temperature_min: -10.0,
329            temperature_max: 50.0,
330            pressure_min: 800.0,
331            pressure_max: 1100.0,
332            radon_max: 300, // WHO action level
333            radiation_rate_max: 10.0,
334            radiation_total_max: 10000.0,
335            warn_on_zero_co2: true,
336            warn_on_all_zeros: true,
337        }
338    }
339
340    /// Create relaxed validation config (wide ranges).
341    pub fn relaxed() -> Self {
342        Self {
343            co2_min: 0,
344            co2_max: 20000,
345            temperature_min: -50.0,
346            temperature_max: 100.0,
347            pressure_min: 200.0,
348            pressure_max: 1200.0,
349            radon_max: 5000,
350            radiation_rate_max: 1000.0,
351            radiation_total_max: 1000000.0,
352            warn_on_zero_co2: false,
353            warn_on_all_zeros: false,
354        }
355    }
356
357    /// Create validation config optimized for Aranet4 (CO2 sensor).
358    ///
359    /// Aranet4 measures CO2, temperature, humidity, and pressure.
360    /// This preset uses appropriate ranges for indoor air quality monitoring.
361    pub fn for_aranet4() -> Self {
362        Self {
363            co2_min: 300,   // Outdoor ambient is ~400 ppm
364            co2_max: 10000, // Aranet4 max range
365            temperature_min: -40.0,
366            temperature_max: 60.0, // Aranet4 operating range
367            pressure_min: 300.0,
368            pressure_max: 1100.0,
369            radon_max: 0,             // Not applicable
370            radiation_rate_max: 0.0,  // Not applicable
371            radiation_total_max: 0.0, // Not applicable
372            warn_on_zero_co2: true,
373            warn_on_all_zeros: true,
374        }
375    }
376
377    /// Create validation config optimized for Aranet2 (temperature/humidity sensor).
378    ///
379    /// Aranet2 measures only temperature and humidity.
380    /// CO2 and pressure validation is disabled.
381    ///
382    /// Note: This preset is based on device specifications. Actual testing
383    /// with an Aranet2 device may reveal adjustments needed.
384    pub fn for_aranet2() -> Self {
385        Self {
386            co2_min: 0,     // Not applicable - disable CO2 validation
387            co2_max: 65535, // Not applicable
388            temperature_min: -40.0,
389            temperature_max: 60.0,
390            pressure_min: 0.0,        // Not applicable
391            pressure_max: 2000.0,     // Not applicable
392            radon_max: 0,             // Not applicable
393            radiation_rate_max: 0.0,  // Not applicable
394            radiation_total_max: 0.0, // Not applicable
395            warn_on_zero_co2: false,  // CO2 is not measured
396            warn_on_all_zeros: false,
397        }
398    }
399
400    /// Create validation config optimized for AranetRn+ (radon sensor).
401    ///
402    /// AranetRn+ measures radon, temperature, humidity, and pressure.
403    /// CO2 validation is disabled.
404    ///
405    /// Note: This preset is based on device specifications. Actual testing
406    /// with an AranetRn+ device may reveal adjustments needed.
407    pub fn for_aranet_radon() -> Self {
408        Self {
409            co2_min: 0,     // Not applicable
410            co2_max: 65535, // Not applicable
411            temperature_min: -40.0,
412            temperature_max: 60.0,
413            pressure_min: 300.0,
414            pressure_max: 1100.0,
415            radon_max: 1000,          // WHO action level is 100-300 Bq/m³
416            radiation_rate_max: 0.0,  // Not applicable
417            radiation_total_max: 0.0, // Not applicable
418            warn_on_zero_co2: false,
419            warn_on_all_zeros: false,
420        }
421    }
422
423    /// Create validation config optimized for Aranet Radiation sensor.
424    ///
425    /// Aranet Radiation measures gamma radiation rate and accumulated dose.
426    /// CO2 and radon validation is disabled.
427    ///
428    /// Note: This preset is based on device specifications. Actual testing
429    /// with an Aranet Radiation device may reveal adjustments needed.
430    pub fn for_aranet_radiation() -> Self {
431        Self {
432            co2_min: 0,     // Not applicable
433            co2_max: 65535, // Not applicable
434            temperature_min: -40.0,
435            temperature_max: 60.0,
436            pressure_min: 300.0,
437            pressure_max: 1100.0,
438            radon_max: 0,                  // Not applicable
439            radiation_rate_max: 100.0,     // Normal background is ~0.1-0.2 µSv/h
440            radiation_total_max: 100000.0, // Reasonable upper bound
441            warn_on_zero_co2: false,
442            warn_on_all_zeros: false,
443        }
444    }
445
446    /// Create validation config for a specific device type.
447    ///
448    /// Automatically selects the appropriate preset based on the device type:
449    /// - [`DeviceType::Aranet4`] → [`for_aranet4()`](Self::for_aranet4)
450    /// - [`DeviceType::Aranet2`] → [`for_aranet2()`](Self::for_aranet2)
451    /// - [`DeviceType::AranetRadon`] → [`for_aranet_radon()`](Self::for_aranet_radon)
452    /// - [`DeviceType::AranetRadiation`] → [`for_aranet_radiation()`](Self::for_aranet_radiation)
453    /// - Unknown types → default config
454    ///
455    /// # Example
456    ///
457    /// ```
458    /// use aranet_core::validation::ValidatorConfig;
459    /// use aranet_types::DeviceType;
460    ///
461    /// let config = ValidatorConfig::for_device(DeviceType::Aranet4);
462    /// assert_eq!(config.co2_max, 10000);
463    ///
464    /// let config = ValidatorConfig::for_device(DeviceType::AranetRadon);
465    /// assert_eq!(config.radon_max, 1000);
466    /// ```
467    #[must_use]
468    pub fn for_device(device_type: DeviceType) -> Self {
469        match device_type {
470            DeviceType::Aranet4 => Self::for_aranet4(),
471            DeviceType::Aranet2 => Self::for_aranet2(),
472            DeviceType::AranetRadon => Self::for_aranet_radon(),
473            DeviceType::AranetRadiation => Self::for_aranet_radiation(),
474            _ => Self::default(),
475        }
476    }
477}
478
479/// Validator for sensor readings.
480#[derive(Debug, Clone, Default)]
481pub struct ReadingValidator {
482    config: ValidatorConfig,
483}
484
485impl ReadingValidator {
486    /// Create a new validator with the given configuration.
487    pub fn new(config: ValidatorConfig) -> Self {
488        Self { config }
489    }
490
491    /// Get the configuration.
492    pub fn config(&self) -> &ValidatorConfig {
493        &self.config
494    }
495
496    /// Validate a sensor reading.
497    pub fn validate(&self, reading: &CurrentReading) -> ValidationResult {
498        let mut warnings = Vec::new();
499
500        // Check for all zeros (use approximate comparison for floats)
501        if self.config.warn_on_all_zeros
502            && reading.co2 == 0
503            && reading.temperature.abs() < f32::EPSILON
504            && reading.pressure.abs() < f32::EPSILON
505            && reading.humidity == 0
506        {
507            warnings.push(ValidationWarning::AllZeros);
508            return ValidationResult::invalid(warnings);
509        }
510
511        // Check CO2
512        if reading.co2 > 0 {
513            if reading.co2 < self.config.co2_min {
514                warnings.push(ValidationWarning::Co2TooLow {
515                    value: reading.co2,
516                    min: self.config.co2_min,
517                });
518            }
519            if reading.co2 > self.config.co2_max {
520                warnings.push(ValidationWarning::Co2TooHigh {
521                    value: reading.co2,
522                    max: self.config.co2_max,
523                });
524            }
525        } else if self.config.warn_on_zero_co2 {
526            warnings.push(ValidationWarning::Co2Zero);
527        }
528
529        // Check temperature
530        if reading.temperature < self.config.temperature_min {
531            warnings.push(ValidationWarning::TemperatureTooLow {
532                value: reading.temperature,
533                min: self.config.temperature_min,
534            });
535        }
536        if reading.temperature > self.config.temperature_max {
537            warnings.push(ValidationWarning::TemperatureTooHigh {
538                value: reading.temperature,
539                max: self.config.temperature_max,
540            });
541        }
542
543        // Check pressure (skip if 0, might be Aranet2)
544        if reading.pressure > 0.0 {
545            if reading.pressure < self.config.pressure_min {
546                warnings.push(ValidationWarning::PressureTooLow {
547                    value: reading.pressure,
548                    min: self.config.pressure_min,
549                });
550            }
551            if reading.pressure > self.config.pressure_max {
552                warnings.push(ValidationWarning::PressureTooHigh {
553                    value: reading.pressure,
554                    max: self.config.pressure_max,
555                });
556            }
557        }
558
559        // Check humidity
560        if reading.humidity > 100 {
561            warnings.push(ValidationWarning::HumidityOutOfRange {
562                value: reading.humidity,
563            });
564        }
565
566        // Check battery
567        if reading.battery > 100 {
568            warnings.push(ValidationWarning::BatteryOutOfRange {
569                value: reading.battery,
570            });
571        }
572
573        // Check radon (if present)
574        if let Some(radon) = reading.radon
575            && radon > self.config.radon_max
576        {
577            warnings.push(ValidationWarning::RadonTooHigh {
578                value: radon,
579                max: self.config.radon_max,
580            });
581        }
582
583        // Check radiation rate (if present)
584        if let Some(rate) = reading.radiation_rate
585            && rate > self.config.radiation_rate_max
586        {
587            warnings.push(ValidationWarning::RadiationRateTooHigh {
588                value: rate,
589                max: self.config.radiation_rate_max,
590            });
591        }
592
593        // Check radiation total (if present)
594        if let Some(total) = reading.radiation_total
595            && total > self.config.radiation_total_max
596        {
597            warnings.push(ValidationWarning::RadiationTotalTooHigh {
598                value: total,
599                max: self.config.radiation_total_max,
600            });
601        }
602
603        if warnings.is_empty() {
604            ValidationResult::valid()
605        } else {
606            // Determine if any warnings are critical
607            let has_critical = warnings.iter().any(|w| {
608                matches!(
609                    w,
610                    ValidationWarning::AllZeros
611                        | ValidationWarning::Co2TooHigh { .. }
612                        | ValidationWarning::TemperatureTooHigh { .. }
613                        | ValidationWarning::RadonTooHigh { .. }
614                        | ValidationWarning::RadiationRateTooHigh { .. }
615                )
616            });
617
618            if has_critical {
619                ValidationResult::invalid(warnings)
620            } else {
621                ValidationResult::valid_with_warnings(warnings)
622            }
623        }
624    }
625
626    /// Quick check if a CO2 value is within expected range.
627    pub fn is_co2_valid(&self, co2: u16) -> bool {
628        co2 >= self.config.co2_min && co2 <= self.config.co2_max
629    }
630
631    /// Quick check if a temperature value is within expected range.
632    pub fn is_temperature_valid(&self, temp: f32) -> bool {
633        temp >= self.config.temperature_min && temp <= self.config.temperature_max
634    }
635}
636
637#[cfg(test)]
638mod tests {
639    use super::*;
640    use aranet_types::Status;
641
642    fn make_reading(co2: u16, temp: f32, pressure: f32, humidity: u8) -> CurrentReading {
643        CurrentReading {
644            co2,
645            temperature: temp,
646            pressure,
647            humidity,
648            battery: 80,
649            status: Status::Green,
650            interval: 300,
651            age: 60,
652            captured_at: None,
653            radon: None,
654            radiation_rate: None,
655            radiation_total: None,
656        }
657    }
658
659    #[test]
660    fn test_valid_reading() {
661        let validator = ReadingValidator::default();
662        let reading = make_reading(800, 22.5, 1013.2, 50);
663        let result = validator.validate(&reading);
664        assert!(result.is_valid);
665        assert!(result.warnings.is_empty());
666    }
667
668    #[test]
669    fn test_co2_too_high() {
670        let validator = ReadingValidator::default();
671        let reading = make_reading(15000, 22.5, 1013.2, 50);
672        let result = validator.validate(&reading);
673        assert!(!result.is_valid);
674        assert!(
675            result
676                .warnings
677                .iter()
678                .any(|w| matches!(w, ValidationWarning::Co2TooHigh { .. }))
679        );
680    }
681
682    #[test]
683    fn test_all_zeros() {
684        let validator = ReadingValidator::default();
685        let reading = make_reading(0, 0.0, 0.0, 0);
686        let result = validator.validate(&reading);
687        assert!(!result.is_valid);
688        assert!(
689            result
690                .warnings
691                .iter()
692                .any(|w| matches!(w, ValidationWarning::AllZeros))
693        );
694    }
695
696    #[test]
697    fn test_humidity_out_of_range() {
698        let validator = ReadingValidator::default();
699        let reading = make_reading(800, 22.5, 1013.2, 150);
700        let result = validator.validate(&reading);
701        assert!(result.has_warnings());
702        assert!(
703            result
704                .warnings
705                .iter()
706                .any(|w| matches!(w, ValidationWarning::HumidityOutOfRange { .. }))
707        );
708    }
709
710    #[test]
711    fn test_for_device_aranet4() {
712        let config = ValidatorConfig::for_device(DeviceType::Aranet4);
713        assert_eq!(config.co2_min, 300);
714        assert_eq!(config.co2_max, 10000);
715        assert!(config.warn_on_zero_co2);
716    }
717
718    #[test]
719    fn test_for_device_aranet2() {
720        let config = ValidatorConfig::for_device(DeviceType::Aranet2);
721        assert_eq!(config.co2_min, 0); // CO2 validation disabled
722        assert!(!config.warn_on_zero_co2);
723    }
724
725    #[test]
726    fn test_for_device_aranet_radon() {
727        let config = ValidatorConfig::for_device(DeviceType::AranetRadon);
728        assert_eq!(config.radon_max, 1000);
729        assert!(!config.warn_on_zero_co2);
730    }
731
732    #[test]
733    fn test_for_device_aranet_radiation() {
734        let config = ValidatorConfig::for_device(DeviceType::AranetRadiation);
735        assert_eq!(config.radiation_rate_max, 100.0);
736        assert_eq!(config.radiation_total_max, 100000.0);
737        assert!(!config.warn_on_zero_co2);
738    }
739}