1use modewise_decomposition::ModeDecomposer;
2use modewise_emd::{Ceemdan, Eemd, Emd, Memd, Stop};
3use modewise_pipeline::{
4 Artifact, ArtifactKind, Inputs, Operation, OperationDescriptor, Outputs, Port, resolve_params,
5};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10use crate::input;
11
12#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, PartialEq)]
13#[serde(rename_all = "snake_case")]
14enum StopName {
15 Rilling,
16 SNumber,
17 Fixed,
18}
19
20#[derive(Serialize, Deserialize, JsonSchema)]
21#[serde(deny_unknown_fields)]
22struct Params {
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 stop: Option<StopName>,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
28 sd: Option<f64>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 sd2: Option<f64>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 tol: Option<f64>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 count: Option<usize>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
40 siftings: Option<usize>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
43 max_imfs: Option<usize>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 num_directions: Option<usize>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 trials: Option<usize>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 noise_sd: Option<f64>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 epsilon: Option<f64>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 seed: Option<u64>,
59}
60
61#[derive(Clone, Copy, PartialEq)]
62enum Variant {
63 Emd,
64 Memd,
65 Eemd,
66 Ceemdan,
67}
68
69fn only(name: &str, owner: &str) -> String {
70 format!("`{name}` only applies to {owner}")
71}
72
73impl Params {
74 fn complete(mut self, variant: Variant) -> Result<Self, String> {
76 let default_stop = if variant == Variant::Eemd {
77 StopName::Fixed
78 } else {
79 StopName::Rilling
80 };
81 let stop = *self.stop.get_or_insert(default_stop);
82 let rilling = [self.sd, self.sd2, self.tol];
83 let foreign = match stop {
84 StopName::Rilling => self.count.is_some() || self.siftings.is_some(),
85 StopName::SNumber => rilling.iter().any(Option::is_some) || self.siftings.is_some(),
86 StopName::Fixed => rilling.iter().any(Option::is_some) || self.count.is_some(),
87 };
88 if foreign {
89 return Err("only the chosen stop's settings apply: sd, sd2 and tol for rilling, count for s_number, siftings for fixed".into());
90 }
91 match stop {
92 StopName::Rilling => {
93 let default = if variant == Variant::Memd {
94 Stop::RILLING_MEMD
95 } else {
96 Stop::RILLING_EMD
97 };
98 if let Stop::Rilling { sd, sd2, tol } = default {
99 self.sd.get_or_insert(sd);
100 self.sd2.get_or_insert(sd2);
101 self.tol.get_or_insert(tol);
102 }
103 }
104 StopName::SNumber => {
105 self.count.get_or_insert(2);
106 }
107 StopName::Fixed => {
108 self.siftings.get_or_insert(10);
109 }
110 }
111 let noisy = matches!(variant, Variant::Eemd | Variant::Ceemdan);
112 if variant == Variant::Memd {
113 self.num_directions.get_or_insert(64);
114 } else if self.num_directions.is_some() {
115 return Err(only("num_directions", "memd"));
116 }
117 if noisy {
118 self.trials.get_or_insert(100);
119 self.seed.get_or_insert(0);
120 } else if self.trials.is_some() || self.seed.is_some() {
121 return Err("`trials` and `seed` only apply to eemd and ceemdan".into());
122 }
123 if variant == Variant::Eemd {
124 self.noise_sd.get_or_insert(0.2);
125 } else if self.noise_sd.is_some() {
126 return Err(only("noise_sd", "eemd"));
127 }
128 if variant == Variant::Ceemdan {
129 self.epsilon.get_or_insert(0.2);
130 } else if self.epsilon.is_some() {
131 return Err(only("epsilon", "ceemdan"));
132 }
133 Ok(self)
134 }
135
136 fn stop(&self) -> Stop {
137 match self.stop.unwrap_or(StopName::Rilling) {
138 StopName::Rilling => Stop::Rilling {
139 sd: self.sd.unwrap_or_default(),
140 sd2: self.sd2.unwrap_or_default(),
141 tol: self.tol.unwrap_or_default(),
142 },
143 StopName::SNumber => Stop::SNumber {
144 count: self.count.unwrap_or_default(),
145 },
146 StopName::Fixed => Stop::Fixed {
147 siftings: self.siftings.unwrap_or_default(),
148 },
149 }
150 }
151
152 fn method(&self, variant: Variant) -> Result<Box<dyn ModeDecomposer + Send + Sync>, String> {
153 let (stop, max_imfs) = (self.stop(), self.max_imfs);
154 let (trials, seed) = (
155 self.trials.unwrap_or_default(),
156 self.seed.unwrap_or_default(),
157 );
158 let method: Result<Box<dyn ModeDecomposer + Send + Sync>, _> = match variant {
159 Variant::Emd => Emd::new(stop, max_imfs).map(|m| Box::new(m) as _),
160 Variant::Memd => Memd::new(stop, max_imfs, self.num_directions.unwrap_or_default())
161 .map(|m| Box::new(m) as _),
162 Variant::Eemd => {
163 let noise_sd = self.noise_sd.unwrap_or_default();
164 Eemd {
165 stop,
166 max_imfs,
167 trials,
168 noise_sd,
169 seed,
170 }
171 .validated()
172 .map(|m| Box::new(m) as _)
173 }
174 Variant::Ceemdan => {
175 let epsilon = self.epsilon.unwrap_or_default();
176 Ceemdan {
177 stop,
178 max_imfs,
179 trials,
180 epsilon,
181 seed,
182 }
183 .validated()
184 .map(|m| Box::new(m) as _)
185 }
186 };
187 method.map_err(|e| e.to_string())
188 }
189}
190
191fn variant_schema(variant: Variant) -> Value {
194 let mut schema = modewise_pipeline::schema_of::<Params>();
195 let applies = |field: &str| match field {
196 "num_directions" => variant == Variant::Memd,
197 "trials" | "seed" => matches!(variant, Variant::Eemd | Variant::Ceemdan),
198 "noise_sd" => variant == Variant::Eemd,
199 "epsilon" => variant == Variant::Ceemdan,
200 _ => true,
201 };
202 let defaults = serde_json::from_value::<Params>(Value::Object(Default::default()))
203 .ok()
204 .and_then(|p| p.complete(variant).ok())
205 .and_then(|p| serde_json::to_value(p).ok())
206 .unwrap_or_default();
207 if let Some(properties) = schema.get_mut("properties").and_then(Value::as_object_mut) {
208 properties.retain(|field, _| applies(field));
209 for (field, spec) in properties.iter_mut() {
210 if let (Some(default), Some(spec)) = (defaults.get(field), spec.as_object_mut()) {
211 spec.insert("default".into(), default.clone());
212 }
213 }
214 }
215 schema
216}
217
218pub struct EmdOperation;
220pub struct MemdOperation;
221pub struct EemdOperation;
222pub struct CeemdanOperation;
223
224fn descriptor(name: &'static str) -> OperationDescriptor {
225 OperationDescriptor {
226 name,
227 version: env!("CARGO_PKG_VERSION"),
228 inputs: &[Port {
229 name: "signal",
230 kinds: &[ArtifactKind::Series],
231 }],
232 outputs: &[Port {
233 name: "modes",
234 kinds: &[ArtifactKind::Decomposition],
235 }],
236 }
237}
238
239fn resolve(variant: Variant, params: Value) -> Result<Value, String> {
240 let (typed, _) = resolve_params::<Params>(params)?;
241 let typed = typed.complete(variant)?;
242 typed.method(variant)?;
243 serde_json::to_value(&typed).map_err(|e| e.to_string())
244}
245
246fn run(variant: Variant, params: &Value, inputs: &Inputs) -> Result<Outputs, String> {
247 let method = serde_json::from_value::<Params>(params.clone())
248 .map_err(|e| e.to_string())?
249 .complete(variant)?
250 .method(variant)?;
251 let Artifact::Series(signal) = input(inputs, "signal")? else {
252 return Err("input \"signal\" must be a series".into());
253 };
254 let modes = method.decompose(signal).map_err(|e| e.to_string())?;
255 Ok(Outputs::from([(
256 "modes".into(),
257 Artifact::Decomposition(modes),
258 )]))
259}
260
261macro_rules! emd_family {
262 ($operation:ident, $name:literal, $variant:expr, $summary:literal) => {
263 impl Operation for $operation {
264 fn summary(&self) -> &'static str {
265 $summary
266 }
267
268 fn params_schema(&self) -> serde_json::Value {
269 variant_schema($variant)
270 }
271
272 fn descriptor(&self) -> OperationDescriptor {
273 descriptor($name)
274 }
275
276 fn resolve(&self, params: Value) -> Result<Value, String> {
277 resolve($variant, params)
278 }
279
280 fn run(&self, params: &Value, inputs: &Inputs) -> Result<Outputs, String> {
281 run($variant, params, inputs)
282 }
283 }
284 };
285}
286
287emd_family!(
288 EmdOperation,
289 "emd",
290 Variant::Emd,
291 "Empirical mode decomposition of each channel on its own (docs/emd.md)."
292);
293emd_family!(
294 MemdOperation,
295 "memd",
296 Variant::Memd,
297 "Multivariate EMD: the same IMFs in every channel, from projections on many directions (docs/emd.md)."
298);
299emd_family!(
300 EemdOperation,
301 "eemd",
302 Variant::Eemd,
303 "Ensemble EMD: the mean of EMDs of the signal plus seeded white noise (docs/emd.md)."
304);
305emd_family!(
306 CeemdanOperation,
307 "ceemdan",
308 Variant::Ceemdan,
309 "CEEMDAN, improved form: noise-assisted EMD whose modes sum to the signal (docs/emd.md)."
310);