modewise_operations/
cwt.rs

1use modewise_cwt::Cwt;
2use modewise_pipeline::{
3    Artifact, ArtifactKind, Inputs, Operation, OperationDescriptor, Outputs, Port, resolve_params,
4};
5use modewise_spectral::{FrequencyGrid, GridScale, TimeFrequencyTransform};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10use crate::input;
11
12#[derive(Serialize, Deserialize, JsonSchema)]
13#[serde(deny_unknown_fields)]
14struct Params {
15    /// Lowest frequency in Hz, above 0. The transform analyses the geometric centres of log-spaced bins.
16    f_min: f64,
17    /// Highest frequency in Hz, at most Nyquist.
18    f_max: f64,
19    /// Number of log-spaced frequencies.
20    n_bins: usize,
21    /// Morlet wavenumber (Torrence and Compo use 6).
22    #[serde(default = "six")]
23    omega0: f64,
24    /// Divide power by scale to remove the bias towards low frequencies (Liu et al. 2007).
25    #[serde(default)]
26    rectify: bool,
27}
28
29fn six() -> f64 {
30    6.0
31}
32
33impl Params {
34    /// Log-spaced bins; the transform analyses their geometric centres.
35    fn method(&self) -> Result<Cwt, String> {
36        let grid = FrequencyGrid::new(self.f_min, self.f_max, self.n_bins, GridScale::Log)
37            .map_err(|e| e.to_string())?;
38        Cwt::new(grid, self.omega0, self.rectify).map_err(|e| e.to_string())
39    }
40}
41
42/// Morlet continuous wavelet transform with a cone-of-influence mask (docs/time-frequency.md).
43pub struct CwtOperation;
44
45impl Operation for CwtOperation {
46    fn summary(&self) -> &'static str {
47        "Morlet continuous wavelet transform power on log-spaced frequencies, with a cone of influence (docs/time-frequency.md)."
48    }
49
50    fn params_schema(&self) -> serde_json::Value {
51        modewise_pipeline::schema_of::<Params>()
52    }
53
54    fn descriptor(&self) -> OperationDescriptor {
55        OperationDescriptor {
56            name: "cwt",
57            version: env!("CARGO_PKG_VERSION"),
58            inputs: &[Port {
59                name: "signal",
60                kinds: &[ArtifactKind::Series],
61            }],
62            outputs: &[Port {
63                name: "power",
64                kinds: &[ArtifactKind::TimeFrequency],
65            }],
66        }
67    }
68
69    fn resolve(&self, params: Value) -> Result<Value, String> {
70        let (typed, resolved) = resolve_params::<Params>(params)?;
71        typed.method()?;
72        Ok(resolved)
73    }
74
75    fn run(&self, params: &Value, inputs: &Inputs) -> Result<Outputs, String> {
76        let p: Params = serde_json::from_value(params.clone()).map_err(|e| e.to_string())?;
77        let Artifact::Series(signal) = input(inputs, "signal")? else {
78            return Err("input \"signal\" must be a series".into());
79        };
80        let map = p.method()?.transform(signal).map_err(|e| e.to_string())?;
81        Ok(Outputs::from([(
82            "power".into(),
83            Artifact::TimeFrequency(map),
84        )]))
85    }
86}