Skip to main content

stationxml_rs/
inventory.rs

1//! Core inventory types — format-agnostic representation of seismic station metadata.
2//!
3//! These types represent the internal model used by all format backends (FDSN, SC3ML, etc.).
4//! They follow FDSN naming conventions but are not tied to any specific XML structure.
5//!
6//! # Hierarchy
7//!
8//! ```text
9//! Inventory
10//!  └── Network
11//!       └── Station
12//!            └── Channel
13//!                 └── Response
14//!                      └── ResponseStage
15//! ```
16
17use chrono::{DateTime, Utc};
18use serde::{Deserialize, Serialize};
19
20// ─── Top-level ───────────────────────────────────────────────────────
21
22/// Top-level inventory — container for all station metadata.
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub struct Inventory {
25    /// Organization that generated this metadata (e.g. "IRIS", "Pena Bumi")
26    pub source: String,
27    /// Optional sender identifier
28    pub sender: Option<String>,
29    /// When this metadata document was created
30    pub created: Option<DateTime<Utc>>,
31    /// Networks contained in this inventory
32    pub networks: Vec<Network>,
33}
34
35// ─── Network / Station ──────────────────────────────────────────────
36
37/// A seismic network — a collection of stations operated together.
38///
39/// Network codes are typically 2 characters (e.g. "GE", "IU", "XX").
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41pub struct Network {
42    /// FDSN network code (e.g. "GE", "IU", "XX")
43    pub code: String,
44    /// Human-readable network description
45    pub description: Option<String>,
46    /// When this network epoch started
47    pub start_date: Option<DateTime<Utc>>,
48    /// When this network epoch ended (None = still active)
49    pub end_date: Option<DateTime<Utc>>,
50    /// Stations in this network
51    pub stations: Vec<Station>,
52}
53
54/// A seismic station — one physical location with one or more sensors.
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub struct Station {
57    /// Station code (e.g. "PBUMI", "ANMO")
58    pub code: String,
59    /// Human-readable description
60    pub description: Option<String>,
61    /// Geographic latitude in degrees (WGS84)
62    pub latitude: f64,
63    /// Geographic longitude in degrees (WGS84)
64    pub longitude: f64,
65    /// Elevation in meters above sea level
66    pub elevation: f64,
67    /// Site information (name, region, country, etc.)
68    pub site: Site,
69    /// When this station epoch started
70    pub start_date: Option<DateTime<Utc>>,
71    /// When this station epoch ended (None = still active)
72    pub end_date: Option<DateTime<Utc>>,
73    /// When this station was originally created
74    pub creation_date: Option<DateTime<Utc>>,
75    /// Channels (measurement components) at this station
76    pub channels: Vec<Channel>,
77}
78
79/// Site information for a station — describes the physical location.
80#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
81pub struct Site {
82    /// Site name (e.g. "Yogyakarta Seismic Shelter")
83    pub name: String,
84    /// Optional detailed description
85    pub description: Option<String>,
86    /// Town or city
87    pub town: Option<String>,
88    /// County or district
89    pub county: Option<String>,
90    /// Region or state/province
91    pub region: Option<String>,
92    /// Country name
93    pub country: Option<String>,
94}
95
96// ─── Channel ────────────────────────────────────────────────────────
97
98/// A channel — one measurement component at a station.
99///
100/// Channel codes are 3 characters following the SEED naming convention:
101/// - Band code (sample rate/response band): S, B, H, etc.
102/// - Instrument code (sensor type): H (seismometer), N (accelerometer), etc.
103/// - Orientation code (direction): Z (vertical), N (north), E (east), etc.
104///
105/// See `docs/guide/02-channel-codes.md` for the full breakdown.
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107pub struct Channel {
108    /// SEED channel code (e.g. "SHZ", "BHN", "HNE")
109    pub code: String,
110    /// Location code (e.g. "00", "10", "")
111    pub location_code: String,
112    /// Channel latitude in degrees (usually same as station)
113    pub latitude: f64,
114    /// Channel longitude in degrees (usually same as station)
115    pub longitude: f64,
116    /// Channel elevation in meters above sea level
117    pub elevation: f64,
118    /// Depth of sensor below surface in meters
119    pub depth: f64,
120    /// Azimuth in degrees from north (0=N, 90=E)
121    pub azimuth: f64,
122    /// Dip in degrees from horizontal (-90=up, 0=horizontal, 90=down)
123    pub dip: f64,
124    /// Sample rate in Hz
125    pub sample_rate: f64,
126    /// When this channel epoch started
127    pub start_date: Option<DateTime<Utc>>,
128    /// When this channel epoch ended (None = still active)
129    pub end_date: Option<DateTime<Utc>>,
130    /// Sensor (geophone, broadband, accelerometer, etc.)
131    pub sensor: Option<Equipment>,
132    /// Data logger / digitizer
133    pub data_logger: Option<Equipment>,
134    /// Instrument response (sensitivity, poles & zeros, etc.)
135    pub response: Option<Response>,
136}
137
138/// Equipment description — sensor, datalogger, or other instrument.
139#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
140pub struct Equipment {
141    /// Equipment type (e.g. "Geophone", "Datalogger")
142    pub equipment_type: Option<String>,
143    /// Human-readable description
144    pub description: Option<String>,
145    /// Manufacturer name (e.g. "Geospace", "Nanometrics")
146    pub manufacturer: Option<String>,
147    /// Vendor/distributor name
148    pub vendor: Option<String>,
149    /// Model name (e.g. "GS-11D", "Trillium 120")
150    pub model: Option<String>,
151    /// Serial number of this specific unit
152    pub serial_number: Option<String>,
153    /// When this equipment was installed
154    pub installation_date: Option<DateTime<Utc>>,
155    /// When this equipment was removed
156    pub removal_date: Option<DateTime<Utc>>,
157}
158
159// ─── Response ───────────────────────────────────────────────────────
160
161/// Full instrument response — describes how to convert counts to physical units.
162///
163/// Contains both a quick overall sensitivity and detailed per-stage information.
164/// See `docs/guide/03-instrument-response.md` for background.
165#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
166pub struct Response {
167    /// Overall sensitivity (product of all stage gains).
168    /// Used for quick counts-to-physical conversion at a single frequency.
169    pub instrument_sensitivity: Option<InstrumentSensitivity>,
170    /// Detailed per-stage response information.
171    /// Stage 1 is typically the sensor, stage 2+ are digitizer/filters.
172    pub stages: Vec<ResponseStage>,
173}
174
175/// Overall instrument sensitivity — a single-frequency approximation.
176///
177/// `value` is in units of `output_units / input_units` (e.g. counts per m/s).
178/// Only valid at the specified `frequency`.
179#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
180pub struct InstrumentSensitivity {
181    /// Sensitivity value (e.g. 53721548.8 counts/(m/s))
182    pub value: f64,
183    /// Frequency at which this sensitivity is valid (Hz)
184    pub frequency: f64,
185    /// Physical input units (e.g. M/S, M/S**2)
186    pub input_units: Units,
187    /// Digital output units (e.g. COUNTS)
188    pub output_units: Units,
189}
190
191/// Physical or digital units.
192#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
193pub struct Units {
194    /// Unit name following SEED convention (e.g. "M/S", "V", "COUNTS")
195    pub name: String,
196    /// Optional human-readable description (e.g. "Velocity in meters per second")
197    pub description: Option<String>,
198}
199
200// ─── Response stages ────────────────────────────────────────────────
201
202/// One stage in the instrument response chain.
203///
204/// Each stage has a gain and optionally one transfer function type
205/// (poles & zeros, coefficients, or FIR).
206#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
207pub struct ResponseStage {
208    /// Stage number (1-based). Stage 1 is typically the sensor.
209    pub number: u32,
210    /// Gain at a reference frequency for this stage
211    pub stage_gain: Option<StageGain>,
212    /// Poles & zeros transfer function (typically stage 1 — sensor)
213    pub poles_zeros: Option<PolesZeros>,
214    /// Coefficient transfer function
215    pub coefficients: Option<Coefficients>,
216    /// FIR filter
217    pub fir: Option<FIR>,
218    /// Decimation parameters (sample rate reduction)
219    pub decimation: Option<Decimation>,
220}
221
222/// Gain of a single stage at a reference frequency.
223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
224pub struct StageGain {
225    /// Gain value (e.g. 32.0 V/(m/s) for a sensor, 1678801.5 counts/V for an ADC)
226    pub value: f64,
227    /// Frequency at which this gain is valid (Hz)
228    pub frequency: f64,
229}
230
231// ─── Transfer functions ─────────────────────────────────────────────
232
233/// Poles and zeros representation of a transfer function.
234///
235/// Describes the frequency response as:
236/// ```text
237/// H(s) = A0 * product(s - z_i) / product(s - p_j)
238/// ```
239/// where s = j*2*pi*f for Laplace (radians) or s = j*f for Laplace (Hz).
240#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
241pub struct PolesZeros {
242    /// Input units for this stage (e.g. M/S for velocity)
243    pub input_units: Units,
244    /// Output units for this stage (e.g. V for voltage)
245    pub output_units: Units,
246    /// Transfer function type (Laplace in rad/s, Hz, or digital Z-transform)
247    pub pz_transfer_function_type: PzTransferFunction,
248    /// Normalization factor (A0) — scales the transfer function
249    pub normalization_factor: f64,
250    /// Frequency at which the normalization factor is computed (Hz)
251    pub normalization_frequency: f64,
252    /// Zeros of the transfer function (complex numbers)
253    pub zeros: Vec<PoleZero>,
254    /// Poles of the transfer function (complex numbers)
255    pub poles: Vec<PoleZero>,
256}
257
258/// A single complex pole or zero.
259#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
260pub struct PoleZero {
261    /// Stage-local index number
262    pub number: u32,
263    /// Real part of the complex value
264    pub real: f64,
265    /// Imaginary part of the complex value
266    pub imaginary: f64,
267}
268
269/// Transfer function type for poles & zeros.
270#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
271pub enum PzTransferFunction {
272    /// Laplace transform, angular frequency (radians/second)
273    LaplaceRadians,
274    /// Laplace transform, frequency in Hz
275    LaplaceHertz,
276    /// Digital (Z-transform)
277    DigitalZTransform,
278}
279
280/// Coefficient-based transfer function.
281#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
282pub struct Coefficients {
283    /// Input units for this stage
284    pub input_units: Units,
285    /// Output units for this stage
286    pub output_units: Units,
287    /// Transfer function type
288    pub cf_transfer_function_type: CfTransferFunction,
289    /// Numerator coefficients
290    pub numerators: Vec<f64>,
291    /// Denominator coefficients
292    pub denominators: Vec<f64>,
293}
294
295/// Transfer function type for coefficients.
296#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
297pub enum CfTransferFunction {
298    /// Analog, angular frequency (radians/second)
299    AnalogRadians,
300    /// Analog, frequency in Hz
301    AnalogHertz,
302    /// Digital
303    Digital,
304}
305
306/// FIR (Finite Impulse Response) filter.
307#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
308pub struct FIR {
309    /// Input units for this stage
310    pub input_units: Units,
311    /// Output units for this stage
312    pub output_units: Units,
313    /// Filter symmetry
314    pub symmetry: Symmetry,
315    /// Numerator coefficients
316    pub numerator_coefficients: Vec<f64>,
317}
318
319/// FIR filter symmetry type.
320#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
321pub enum Symmetry {
322    /// No symmetry — all coefficients specified
323    None,
324    /// Even symmetry — only first half specified
325    Even,
326    /// Odd symmetry — only first half specified
327    Odd,
328}
329
330/// Decimation parameters — describes how sample rate is reduced at this stage.
331#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
332pub struct Decimation {
333    /// Input sample rate to this stage (Hz)
334    pub input_sample_rate: f64,
335    /// Decimation factor (output rate = input rate / factor)
336    pub factor: u32,
337    /// Sample offset for decimation
338    pub offset: u32,
339    /// Estimated delay introduced by this stage (seconds)
340    pub delay: f64,
341    /// Applied correction for the delay (seconds)
342    pub correction: f64,
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn empty_inventory() {
351        let inv = Inventory {
352            source: "Test".into(),
353            sender: None,
354            created: None,
355            networks: vec![],
356        };
357        assert_eq!(inv.source, "Test");
358        assert!(inv.networks.is_empty());
359    }
360
361    #[test]
362    fn full_inventory_construction() {
363        let inv = Inventory {
364            source: "Pena Bumi".into(),
365            sender: Some("stationxml-rs".into()),
366            created: None,
367            networks: vec![Network {
368                code: "XX".into(),
369                description: Some("Local Test Network".into()),
370                start_date: None,
371                end_date: None,
372                stations: vec![Station {
373                    code: "PBUMI".into(),
374                    description: None,
375                    latitude: -7.7714,
376                    longitude: 110.3776,
377                    elevation: 150.0,
378                    site: Site {
379                        name: "Yogyakarta".into(),
380                        ..Default::default()
381                    },
382                    start_date: None,
383                    end_date: None,
384                    creation_date: None,
385                    channels: vec![Channel {
386                        code: "SHZ".into(),
387                        location_code: "00".into(),
388                        latitude: -7.7714,
389                        longitude: 110.3776,
390                        elevation: 150.0,
391                        depth: 0.0,
392                        azimuth: 0.0,
393                        dip: -90.0,
394                        sample_rate: 100.0,
395                        start_date: None,
396                        end_date: None,
397                        sensor: Some(Equipment {
398                            equipment_type: Some("Geophone".into()),
399                            model: Some("GS-11D".into()),
400                            manufacturer: Some("Geospace".into()),
401                            ..Default::default()
402                        }),
403                        data_logger: None,
404                        response: Some(Response {
405                            instrument_sensitivity: Some(InstrumentSensitivity {
406                                value: 53721548.8,
407                                frequency: 15.0,
408                                input_units: Units {
409                                    name: "M/S".into(),
410                                    description: None,
411                                },
412                                output_units: Units {
413                                    name: "COUNTS".into(),
414                                    description: None,
415                                },
416                            }),
417                            stages: vec![],
418                        }),
419                    }],
420                }],
421            }],
422        };
423
424        assert_eq!(inv.networks[0].code, "XX");
425        let sta = &inv.networks[0].stations[0];
426        assert_eq!(sta.code, "PBUMI");
427        assert_eq!(sta.latitude, -7.7714);
428        let ch = &sta.channels[0];
429        assert_eq!(ch.code, "SHZ");
430        assert_eq!(ch.dip, -90.0);
431        let sens = ch
432            .response
433            .as_ref()
434            .unwrap()
435            .instrument_sensitivity
436            .as_ref()
437            .unwrap();
438        assert!((sens.value - 53721548.8).abs() < 0.1);
439    }
440
441    #[test]
442    fn site_default() {
443        let site = Site::default();
444        assert!(site.name.is_empty());
445        assert!(site.country.is_none());
446    }
447
448    #[test]
449    fn equipment_default() {
450        let eq = Equipment::default();
451        assert!(eq.model.is_none());
452        assert!(eq.manufacturer.is_none());
453    }
454
455    #[test]
456    fn response_default() {
457        let resp = Response::default();
458        assert!(resp.instrument_sensitivity.is_none());
459        assert!(resp.stages.is_empty());
460    }
461}