1use modewise_hilbert::{Padding, Weight, hilbert_spectrum, instantaneous};
2use modewise_pipeline::{
3 Artifact, ArtifactKind, Inputs, Operation, OperationDescriptor, Outputs, Port, resolve_params,
4};
5use modewise_spectral::{FrequencyGrid, GridScale};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10use crate::input;
11
12#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, Default)]
13#[serde(rename_all = "snake_case")]
14enum PaddingName {
15 #[default]
16 Mirror,
17 None,
18}
19
20#[derive(Serialize, Deserialize, JsonSchema)]
21#[serde(deny_unknown_fields)]
22struct HilbertParams {
23 #[serde(default)]
25 padding: PaddingName,
26}
27
28pub struct HilbertOperation;
30
31impl Operation for HilbertOperation {
32 fn summary(&self) -> &'static str {
33 "Instantaneous amplitude and frequency of every mode from its analytic signal (docs/time-frequency.md)."
34 }
35
36 fn params_schema(&self) -> serde_json::Value {
37 modewise_pipeline::schema_of::<HilbertParams>()
38 }
39
40 fn descriptor(&self) -> OperationDescriptor {
41 OperationDescriptor {
42 name: "hilbert",
43 version: env!("CARGO_PKG_VERSION"),
44 inputs: &[Port {
45 name: "modes",
46 kinds: &[ArtifactKind::Decomposition],
47 }],
48 outputs: &[Port {
49 name: "attributes",
50 kinds: &[ArtifactKind::Instantaneous],
51 }],
52 }
53 }
54
55 fn resolve(&self, params: Value) -> Result<Value, String> {
56 resolve_params::<HilbertParams>(params).map(|(_, v)| v)
57 }
58
59 fn run(&self, params: &Value, inputs: &Inputs) -> Result<Outputs, String> {
60 let p: HilbertParams = serde_json::from_value(params.clone()).map_err(|e| e.to_string())?;
61 let Artifact::Decomposition(modes) = input(inputs, "modes")? else {
62 return Err("input \"modes\" must be a decomposition".into());
63 };
64 let padding = match p.padding {
65 PaddingName::Mirror => Padding::Mirror,
66 PaddingName::None => Padding::None,
67 };
68 let attributes = instantaneous(modes, padding).map_err(|e| e.to_string())?;
69 Ok(Outputs::from([(
70 "attributes".into(),
71 Artifact::Instantaneous(attributes),
72 )]))
73 }
74}
75
76#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, Default)]
77#[serde(rename_all = "snake_case")]
78enum ScaleName {
79 #[default]
80 Linear,
81 Log,
82}
83
84#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, Default)]
85#[serde(rename_all = "snake_case")]
86enum WeightName {
87 #[default]
88 Power,
89 Amplitude,
90}
91
92#[derive(Serialize, Deserialize, JsonSchema)]
93#[serde(deny_unknown_fields)]
94struct SpectrumParams {
95 f_min: f64,
97 f_max: f64,
99 n_bins: usize,
101 #[serde(default)]
103 scale: ScaleName,
104 #[serde(default)]
106 weight: WeightName,
107}
108
109impl SpectrumParams {
110 fn grid(&self) -> Result<FrequencyGrid, String> {
111 let scale = match self.scale {
112 ScaleName::Linear => GridScale::Linear,
113 ScaleName::Log => GridScale::Log,
114 };
115 FrequencyGrid::new(self.f_min, self.f_max, self.n_bins, scale).map_err(|e| e.to_string())
116 }
117}
118
119pub struct HilbertSpectrumOperation;
121
122impl Operation for HilbertSpectrumOperation {
123 fn summary(&self) -> &'static str {
124 "Hilbert spectrum: instantaneous amplitude or power of the modes binned on a frequency grid over time (docs/time-frequency.md)."
125 }
126
127 fn params_schema(&self) -> serde_json::Value {
128 modewise_pipeline::schema_of::<SpectrumParams>()
129 }
130
131 fn descriptor(&self) -> OperationDescriptor {
132 OperationDescriptor {
133 name: "hilbert_spectrum",
134 version: env!("CARGO_PKG_VERSION"),
135 inputs: &[Port {
136 name: "attributes",
137 kinds: &[ArtifactKind::Instantaneous],
138 }],
139 outputs: &[Port {
140 name: "power",
141 kinds: &[ArtifactKind::TimeFrequency],
142 }],
143 }
144 }
145
146 fn resolve(&self, params: Value) -> Result<Value, String> {
147 let (typed, resolved) = resolve_params::<SpectrumParams>(params)?;
148 typed.grid()?;
149 Ok(resolved)
150 }
151
152 fn run(&self, params: &Value, inputs: &Inputs) -> Result<Outputs, String> {
153 let p: SpectrumParams =
154 serde_json::from_value(params.clone()).map_err(|e| e.to_string())?;
155 let Artifact::Instantaneous(attributes) = input(inputs, "attributes")? else {
156 return Err("input \"attributes\" must be instantaneous attributes".into());
157 };
158 let weight = match p.weight {
159 WeightName::Power => Weight::Power,
160 WeightName::Amplitude => Weight::Amplitude,
161 };
162 let spectrum =
163 hilbert_spectrum(attributes, &p.grid()?, weight).map_err(|e| e.to_string())?;
164 Ok(Outputs::from([(
165 "power".into(),
166 Artifact::TimeFrequency(spectrum.map),
167 )]))
168 }
169}