ppsd-rs
Seismic PPSD (Probabilistic Power Spectral Density) computation in Rust, matching ObsPy output. Zero unsafe, zero C dependencies.
What is PPSD?
PPSD (McNamara & Buland, 2004) is the standard method for characterizing ambient seismic noise at a station. It computes power spectral density from continuous waveforms, removes the instrument response, and accumulates statistics over time. PPSD is used by IRIS, FDSN, BMKG, and most seismic networks for station quality monitoring.
This crate implements the exact same algorithm as ObsPy's PPSD, validated against ObsPy output within 0.5 dB tolerance. It uses stationxml-rs types directly for instrument response evaluation -- no intermediate types, no conversion boilerplate.
Features
- Full PPSD pipeline matching ObsPy's
PPSD.__processstep-by-step - Welch's method (mlab.psd compatible) with cosine taper and linear detrend
- Instrument response evaluation from
stationxml_rs::Response-- walks PAZ, FIR, and Coefficients stages automatically - FIR filter support with DTFT evaluation, DC normalization (matching evalresp), and symmetry expansion (None/Even/Odd)
- Konno-Ohmachi smoothing with configurable bandwidth
- Period binning with octave-based averaging (ObsPy style)
- Direct
stationxml-rsintegration -- takes&Responsefrom any FDSN StationXML or SC3ML inventory - Zero unsafe -- pure Rust math, no FFI
- Zero C dependencies -- compiles anywhere
rustcruns
Quick Start
Add to your Cargo.toml:
[]
= "0.1"
= "0.2"
Compute PSD for one segment
use ;
// Load inventory (auto-detects FDSN StationXML or SC3ML)
let inv = read_from_file.unwrap;
let channel = &inv.networks.stations.channels;
let response = channel.response.as_ref.unwrap;
let sample_rate = channel.sample_rate;
// ObsPy-compatible parameters
let ppsd_length = 3600.0; // 1-hour segments
let nfft = as usize;
let nlap = nfft / 2; // 50% overlap
// Period binning (matching ObsPy defaults)
let freqs: =
.map
.collect;
let psd_periods: = freqs.iter.rev.map.collect;
let =
setup_period_binning;
// Process one segment (3600s of data)
let samples: = load_your_data; // your data source
let result = process_segment.unwrap;
// result is Vec<Option<f64>> -- dB values per period bin
for in result.iter.enumerate
Evaluate instrument response only
use eval_response;
let inv = read_from_file.unwrap;
let response = inv.networks.stations.channels
.response.as_ref.unwrap;
let freqs: = .map.collect;
let h_squared = eval_response.unwrap;
// h_squared[i] = |H(f_i)|² including all stages (PAZ + FIR + ADC)
API Overview
use ;
| Function | Description |
|---|---|
process_segment() |
Full PPSD pipeline: Welch → response removal → dB → period binning |
eval_response() |
Evaluate stationxml_rs::Response at frequencies, returns |H(f)|² |
welch_psd() |
PSD via Welch's method (cosine taper, linear detrend, overlap-average) |
cosine_taper() |
Tukey window matching ObsPy's cosine_taper(npts, p) |
fft_power() |
One-sided power spectrum via real FFT |
konno_ohmachi_smooth() |
Konno-Ohmachi log-frequency smoothing |
period_bin_average() |
Average dB values into octave-spaced period bins |
setup_period_binning() |
Compute bin edges matching ObsPy PPSD._setup_period_binning |
Response Evaluation
eval_response() walks all stages in stationxml_rs::Response and computes the combined |H(f)|²:
| Stage type | stationxml-rs type | Evaluation method |
|---|---|---|
| Sensor (PAZ) | PolesZeros |
Laplace H(s) = A₀ × ∏(s-zᵢ)/∏(s-pⱼ) × gain |
| ADC / Digital | Coefficients (Digital) |
DTFT of numerators × gain |
| FIR filter | FIR |
DTFT with DC normalization × gain |
| Gain-only | (none of above) | gain² |
FIR coefficients are DC-normalized to match evalresp behavior. Symmetry expansion handles None (all coefficients), Even (mirror), and Odd (mirror with negation).
Architecture
src/
lib.rs -- single-file library: types, response eval, PSD math, tests
Design Decisions
- Direct
stationxml-rscoupling: takes&Responsedirectly -- no intermediateInstrumentResponsetype. Both crates share the same author; updates propagate naturally. - Single file: the crate is focused enough that one
lib.rskeeps everything discoverable without module navigation overhead. Result<T>over panic: response evaluation can fail (empty response, missing decimation, unsupported stage) -- errors are explicit, not hidden behind defaults.- DC normalization: FIR coefficients are normalized to unit DC gain, matching evalresp's internal behavior. This is critical for correct dB output.
&[f64]over stream: user provides pre-sliced sample arrays -- simple, composable, no hidden I/O or buffering.
TDD with ObsPy
Test vectors are generated by Python/ObsPy scripts, ensuring the Rust output matches ObsPy PPSD within 0.5 dB tolerance across all 7 pipeline stages:
&&
Development
References
- McNamara & Buland (2004) -- original PPSD method paper
- ObsPy PPSD -- Python reference implementation
- FDSN StationXML -- instrument response format specification
- evalresp -- IRIS response evaluation (FIR normalization behavior)
Sister Projects
- stationxml-rs -- FDSN StationXML and SC3ML reader/writer (provides
Responsetypes used by this crate) - miniseed-rs -- miniSEED v2/v3 decoder and encoder
- seedlink-rs -- SeedLink protocol client/server
License
Apache-2.0