Skip to main content

stationxml_rs/
sensor.rs

1//! Built-in sensor library.
2//!
3//! Provides a database of common seismometer and accelerometer
4//! specifications, loaded from an embedded JSON file.
5
6use serde::{Deserialize, Serialize};
7use std::sync::OnceLock;
8
9const SENSORS_JSON: &str = include_str!("../data/sensors.json");
10
11static SENSOR_DB: OnceLock<Vec<SensorEntry>> = OnceLock::new();
12
13/// A sensor specification from the built-in database.
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct SensorEntry {
16    /// Model name (e.g. "GS-11D", "STS-2")
17    pub model: String,
18    /// Manufacturer name
19    pub manufacturer: String,
20    /// Sensor type (e.g. "Geophone", "Broadband")
21    pub sensor_type: String,
22    /// Human-readable description
23    #[serde(default)]
24    pub description: Option<String>,
25    /// Sensitivity in V per (m/s) or V per (m/s^2)
26    pub sensitivity: f64,
27    /// Sensitivity unit: "M/S" or "M/S**2"
28    pub sensitivity_unit: String,
29    /// Operating frequency range as (low_hz, high_hz)
30    pub frequency_range: (f64, f64),
31    /// Natural period in seconds (for geophones)
32    pub natural_period: Option<f64>,
33    /// Damping ratio (fraction of critical damping)
34    pub damping: Option<f64>,
35}
36
37/// Load the built-in sensor library.
38///
39/// Returns a slice of all sensor entries. The library is lazily initialized
40/// and cached for the lifetime of the program.
41pub fn load_sensor_library() -> &'static [SensorEntry] {
42    SENSOR_DB
43        .get_or_init(|| serde_json::from_str(SENSORS_JSON).expect("embedded sensors.json is valid"))
44}
45
46/// Find a sensor by model name (case-insensitive).
47///
48/// Returns `None` if no matching sensor is found.
49pub fn find_sensor(model: &str) -> Option<&'static SensorEntry> {
50    let model_lower = model.to_lowercase();
51    load_sensor_library()
52        .iter()
53        .find(|s| s.model.to_lowercase() == model_lower)
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn load_library_count() {
62        let sensors = load_sensor_library();
63        assert_eq!(sensors.len(), 9);
64    }
65
66    #[test]
67    fn find_gs11d() {
68        let sensor = find_sensor("GS-11D").unwrap();
69        assert_eq!(sensor.manufacturer, "Geospace");
70        assert_eq!(sensor.sensitivity, 32.0);
71        assert_eq!(sensor.sensitivity_unit, "M/S");
72    }
73
74    #[test]
75    fn find_case_insensitive() {
76        assert!(find_sensor("gs-11d").is_some());
77        assert!(find_sensor("sts-2").is_some());
78        assert!(find_sensor("STS-2").is_some());
79    }
80
81    #[test]
82    fn find_nonexistent() {
83        assert!(find_sensor("NonExistentSensor").is_none());
84    }
85
86    #[test]
87    fn all_entries_valid() {
88        for sensor in load_sensor_library() {
89            assert!(sensor.sensitivity > 0.0, "sensitivity must be positive");
90            assert!(!sensor.model.is_empty(), "model must not be empty");
91            assert!(!sensor.manufacturer.is_empty());
92            assert!(
93                sensor.frequency_range.0 < sensor.frequency_range.1,
94                "freq range must be low < high"
95            );
96        }
97    }
98
99    #[test]
100    fn broadband_vs_geophone() {
101        let sts2 = find_sensor("STS-2").unwrap();
102        let gs11d = find_sensor("GS-11D").unwrap();
103
104        // Broadband has much higher sensitivity
105        assert!(sts2.sensitivity > gs11d.sensitivity);
106        // Broadband has much wider frequency range (lower low-freq)
107        assert!(sts2.frequency_range.0 < gs11d.frequency_range.0);
108    }
109}