ground_motion_lib/
vectorized.rs

1//! # Vectorized Ground Motion Calculations
2//!
3//! This module provides parallelized routines for ground motion prediction calculations
4//! and post-processing over collections of site points. It leverages [`Rayon`] for efficient
5//! multi-threaded computation on large datasets.
6//!
7//! ## Features
8//!
9//! - **Parallel GMPE prediction:** Compute ground motion values for large sets of site points in parallel.
10//! - **Summary statistics computation:** Derive key statistical descriptors (mean, standard deviation, min, max, median)
11//!   from ground motion prediction results, with parallelism applied to key operations.
12//!
13//! ## Primary Types and Functions
14//!
15//! - [`calc_gmpe_vec`]: Perform parallel ground motion prediction for a vector of [`Vs30Point`] instances.
16//! - [`compute_stats`]: Calculate summary statistics over a collection of predicted [`GmpePoint`] values.
17//! - [`Stats`]: Struct representing the computed statistical summary.
18//!
19//! ## Parallelism
20//!
21//! This module uses [`Rayon`](https://docs.rs/rayon/latest/rayon/) for thread-safe, data-parallel operations:
22//!
23//! - `par_iter()` for distributing GMPE calculations and statistical reductions across threads.
24//! - Number of threads is controlled by the `RAYON_NUM_THREADS` environment variable or defaults
25//!   to the number of logical CPU cores.
26//!
27//! ## Usage Example
28//!
29//! ```rust
30//! use ground_motion_lib::gmm::{Vs30Point, Earthquake, Magnitude};
31//! use ground_motion_lib::configs::get_mf2013_lib_configs;
32//! use ground_motion_lib::vectorized::{calc_gmpe_vec, compute_stats};
33//!
34//! let points = vec![
35//!     Vs30Point::new(142.5, 50.0, 400., Some(200.), Some(0)),
36//!     Vs30Point::new(142.6, 50.1, 350., Some(150.), Some(1)),
37//! ];
38//!
39//! let eq = Earthquake {
40//!     lon: 142.4,
41//!     lat: 50.0,
42//!     depth: 10.0,
43//!     magnitude: 6.5,
44//!     magnitude_kind: Magnitude::Mw,
45//! };
46//!
47//! let gmpe_ref = get_mf2013_lib_configs().get("config_mf2013_crustal_pga").unwrap();
48//! let results = calc_gmpe_vec(&points, gmpe_ref, &eq);
49//! let stats = compute_stats(&results);
50//!
51//! println!("Stats: {stats:?}");
52//! ```
53//!
54//! ## See Also
55//!
56//! - [`crate::gmm::GmpePoint`]
57//! - [`crate::gmm::Vs30Point`]
58//! - [`crate::gmm::GroundMotionModeling`]
59//! - [`crate::mf2013::MF2013`]
60//!
61//! ## Thread Safety
62//!
63//! All operations in this module are thread-safe and make use of [`Rayon`] for concurrency.
64
65use crate::gmm::{Earthquake, GmpePoint, GroundMotionModeling, Vs30Point};
66use rayon::prelude::*;
67
68/// Calculate ground motion predictions for a set of site points in parallel.
69///
70/// This function takes a slice of `Vs30Point` site points, a reference to a ground motion prediction
71/// equation (GMPE) implementation, and an earthquake definition, and computes ground motion values
72/// (`GmpePoint`) for each site point using the provided GMPE model.
73///
74/// The calculation is performed in parallel using Rayon to improve performance for large datasets.
75///
76/// # Type Parameters
77///
78/// * `T` - A type implementing the `GroundMotionModeling` trait.
79///   Must also implement `Sync` to allow safe parallel access across threads.
80///
81/// # Arguments
82///
83/// * `points` - A slice of `Vs30Point` instances representing the site points for which
84///   ground motion predictions will be calculated.
85/// * `gmpe` - A reference to a type implementing the `GroundMotionModeling` trait, representing
86///   the GMPE model to be used for the calculations.
87/// * `eq` - A reference to the `Earthquake` instance describing the earthquake event.
88///
89/// # Returns
90///
91/// A `Vec<GmpePoint>` containing the calculated ground motion values for each input site point.
92///
93/// # Examples
94///
95/// ```rust
96/// use ground_motion_lib::gmm::{Vs30Point, Earthquake, Magnitude, GroundMotionModeling};
97/// use ground_motion_lib::mf2013::MF2013;
98/// use ground_motion_lib::configs::get_mf2013_lib_configs;
99/// use ground_motion_lib::vectorized::calc_gmpe_vec;
100///
101/// let points = vec![
102///     Vs30Point::new(142.5, 50.0, 400., Some(200.), Some(0)),
103///     Vs30Point::new(142.6, 50.1, 350., Some(150.), Some(1)),
104/// ];
105///
106/// let eq = Earthquake {
107///     lon: 142.4,
108///     lat: 50.0,
109///     depth: 10.0,
110///     magnitude: 6.5,
111///     magnitude_kind: Magnitude::Mw,
112/// };
113///
114/// let gmpe_ref = get_mf2013_lib_configs().get("config_mf2013_crustal_pga").unwrap();
115///
116/// let results = calc_gmpe_vec(&points, gmpe_ref, &eq);
117/// println!("{results:?}");
118/// ```
119///
120/// # Parallelism
121///
122/// This function uses Rayon’s `par_iter()` to distribute the workload of computing ground motion
123/// values across multiple threads. The number of threads is typically controlled by the
124/// `RAYON_NUM_THREADS` environment variable, or by default equals the number of logical CPU cores.
125/// - [`Rayon`](https://docs.rs/rayon/latest/rayon/)
126///
127/// # See Also
128///
129/// - [`GmpePoint`](crate::gmm::GmpePoint)
130/// - [`Vs30Point`](crate::gmm::Vs30Point)
131/// - [`GroundMotionModeling`](crate::gmm::GroundMotionModeling)
132///
133pub fn calc_gmpe_vec<T: GroundMotionModeling + Sync>(
134    points: &[Vs30Point],
135    gmpe: &T,
136    eq: &Earthquake,
137) -> Vec<GmpePoint> {
138    points
139        .par_iter()
140        .map(|point| point.get_gm(gmpe, eq))
141        .collect()
142}
143
144/// Struct for computed summary statistics
145#[derive(Debug, PartialEq)]
146pub struct Stats {
147    pub mean: f64,
148    pub std_dev: f64,
149    pub min: f64,
150    pub max: f64,
151    pub median: f64,
152}
153
154/// Compute summary statistics (mean, standard deviation, minimum, maximum, and median)
155/// for a list of `GmpePoint` values.
156///
157/// This function processes the `value` field from a slice of `GmpePoint` instances,
158/// computing key statistical measures.
159/// Most operations are parallelized using Rayon where possible (e.g., sum, variance, min, max)
160/// to improve performance on larger datasets.
161///
162/// # Arguments
163///
164/// * `points` - A slice of `GmpePoint` instances to analyze.
165///
166/// # Returns
167///
168/// A `Stats` struct containing:
169/// - `mean` — the arithmetic mean
170/// - `std_dev` — the sample standard deviation
171/// - `min` — the minimum value
172/// - `max` — the maximum value
173/// - `median` — the median value (sorted centrally)
174///
175/// # Example
176///
177/// ```rust
178/// use ground_motion_lib::gmm::GmpePoint;
179/// use ground_motion_lib::vectorized::{compute_stats, Stats};
180///
181/// let points = vec![
182///     GmpePoint::new_pga(147.1, 50.1, 1.1),
183///     GmpePoint::new_pga(147.2, 50.2, 1.2),
184///     GmpePoint::new_pga(147.3, 50.3, 1.3),
185/// ];
186///
187/// let stats = compute_stats(&points);
188///
189/// println!("Mean: {}", stats.mean);
190/// println!("Min: {}", stats.min);
191/// println!("Max: {}", stats.max);
192/// println!("Median: {}", stats.median);
193/// println!("Std Dev: {}", stats.std_dev);
194/// ```
195///
196/// # Parallelism
197///
198/// - Sum, variance, min, and max calculations use `Rayon`’s parallel iterators.
199/// - Median is computed single-threaded via an in-place sort since sorting isn’t parallelized here.
200///
201/// # Panics
202///
203/// This function will panic if called with an empty slice.
204///
205/// # See Also
206///
207/// - [`Rayon`](https://docs.rs/rayon/latest/rayon/)
208/// - [`GmpePoint`](crate::gmm::GmpePoint)
209/// - [`Stats`](crate::vectorized::Stats)
210///
211pub fn compute_stats(points: &[GmpePoint]) -> Stats {
212    let n = points.len() as f64;
213
214    // Extract values into a Vec<f64> to operate on
215    let mut values: Vec<f64> = points.iter().map(|p| p.value).collect();
216
217    // Compute sum in parallel, then mean
218    let sum: f64 = values.par_iter().sum();
219    let mean = sum / n;
220
221    // Compute variance (sample, denominator is n-1)
222    let variance: f64 = values
223        .par_iter()
224        .map(|v| {
225            let diff = v - mean;
226            diff * diff
227        })
228        .sum::<f64>()
229        / (n - 1.0);
230    let std_dev = variance.sqrt();
231
232    // Compute min and max via parallel reduction
233    let min = values
234        .par_iter()
235        .cloned()
236        .reduce(|| f64::INFINITY, f64::min);
237
238    let max = values
239        .par_iter()
240        .cloned()
241        .reduce(|| f64::NEG_INFINITY, f64::max);
242
243    // Compute median by sorting values locally (single-threaded)
244    values.sort_by(|a, b| a.partial_cmp(b).unwrap());
245    let median = if values.len() % 2 == 0 {
246        let mid = values.len() / 2;
247        (values[mid - 1] + values[mid]) / 2.0
248    } else {
249        values[values.len() / 2]
250    };
251
252    Stats {
253        mean,
254        std_dev,
255        min,
256        max,
257        median,
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use crate::gmm::GmpePointKind;
265
266    #[test]
267    fn test_compute_stats() {
268        let points = vec![
269            GmpePoint {
270                lon: 0.0,
271                lat: 0.0,
272                value: 1.0,
273                kind: GmpePointKind::Pga,
274            },
275            GmpePoint {
276                lon: 0.0,
277                lat: 0.0,
278                value: 2.0,
279                kind: GmpePointKind::Pga,
280            },
281            GmpePoint {
282                lon: 0.0,
283                lat: 0.0,
284                value: 3.0,
285                kind: GmpePointKind::Pga,
286            },
287            GmpePoint {
288                lon: 0.0,
289                lat: 0.0,
290                value: 4.0,
291                kind: GmpePointKind::Pga,
292            },
293            GmpePoint {
294                lon: 0.0,
295                lat: 0.0,
296                value: 5.0,
297                kind: GmpePointKind::Pga,
298            },
299        ];
300
301        let stats = compute_stats(&points);
302
303        // Expected values calculated by hand
304        let expected = Stats {
305            mean: 3.0,
306            std_dev: 1.5811388300841898, // sqrt(2.5)
307            min: 1.0,
308            max: 5.0,
309            median: 3.0,
310        };
311
312        assert!((stats.mean - expected.mean).abs() < 1e-10);
313        assert!((stats.std_dev - expected.std_dev).abs() < 1e-10);
314        assert_eq!(stats.min, expected.min);
315        assert_eq!(stats.max, expected.max);
316        assert_eq!(stats.median, expected.median);
317    }
318}