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}