modewise_operations/
vlmd.rs

1use modewise_pipeline::{
2    Artifact, ArtifactKind, Inputs, Operation, OperationDescriptor, Outputs, Port, resolve_params,
3};
4use modewise_vlmd::{Vlmd, VlmdParams};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::input;
10
11#[derive(Serialize, Deserialize, JsonSchema)]
12#[serde(deny_unknown_fields)]
13struct Params {
14    /// Number of modes K.
15    num_modes: usize,
16    /// Number of latent components L, at most the number of channels; one per channel by default.
17    #[serde(default)]
18    num_latents: Option<usize>,
19    /// Bandwidth penalty; larger means narrower modes.
20    #[serde(default = "defaults::alpha")]
21    alpha: f64,
22    /// Sparsity of the loadings, on the reference code's scale.
23    #[serde(default = "defaults::reg_lambda")]
24    reg_lambda: f64,
25    /// ADMM penalty.
26    #[serde(default = "defaults::reg_rho")]
27    reg_rho: f64,
28    /// Dual step, in [0, 1].
29    #[serde(default = "defaults::tau")]
30    tau: f64,
31    /// Stop when the summed squared relative change of the centre frequencies falls to this.
32    #[serde(default = "defaults::tolerance")]
33    tolerance: f64,
34    /// Iteration cap.
35    #[serde(default = "defaults::max_iterations")]
36    max_iterations: usize,
37}
38
39mod defaults {
40    use modewise_vlmd::VlmdParams;
41
42    fn reference() -> VlmdParams {
43        VlmdParams::new(1)
44    }
45    pub fn alpha() -> f64 {
46        reference().alpha
47    }
48    pub fn reg_lambda() -> f64 {
49        reference().reg_lambda
50    }
51    pub fn reg_rho() -> f64 {
52        reference().reg_rho
53    }
54    pub fn tau() -> f64 {
55        reference().tau
56    }
57    pub fn tolerance() -> f64 {
58        reference().tolerance
59    }
60    pub fn max_iterations() -> usize {
61        reference().max_iterations
62    }
63}
64
65impl Params {
66    fn method(self) -> Result<Vlmd, String> {
67        Vlmd::new(VlmdParams {
68            num_modes: self.num_modes,
69            num_latents: self.num_latents,
70            alpha: self.alpha,
71            reg_lambda: self.reg_lambda,
72            reg_rho: self.reg_rho,
73            tau: self.tau,
74            tolerance: self.tolerance,
75            max_iterations: self.max_iterations,
76        })
77        .map_err(|e| e.to_string())
78    }
79}
80
81/// Variational Latent Mode Decomposition (docs/vlmd.md).
82pub struct VlmdOperation;
83
84impl Operation for VlmdOperation {
85    fn summary(&self) -> &'static str {
86        "Variational latent mode decomposition: modes of a few latent signals and sparse channel loadings (docs/vlmd.md)."
87    }
88
89    fn params_schema(&self) -> serde_json::Value {
90        modewise_pipeline::schema_of::<Params>()
91    }
92
93    fn descriptor(&self) -> OperationDescriptor {
94        OperationDescriptor {
95            name: "vlmd",
96            version: env!("CARGO_PKG_VERSION"),
97            inputs: &[Port {
98                name: "signal",
99                kinds: &[ArtifactKind::Series],
100            }],
101            outputs: &[
102                Port {
103                    name: "modes",
104                    kinds: &[ArtifactKind::Decomposition],
105                },
106                Port {
107                    name: "latent_modes",
108                    kinds: &[ArtifactKind::Decomposition],
109                },
110                Port {
111                    name: "loadings",
112                    kinds: &[ArtifactKind::Loadings],
113                },
114            ],
115        }
116    }
117
118    fn resolve(&self, params: Value) -> Result<Value, String> {
119        let (typed, resolved) = resolve_params::<Params>(params)?;
120        typed.method()?;
121        Ok(resolved)
122    }
123
124    fn run(&self, params: &Value, inputs: &Inputs) -> Result<Outputs, String> {
125        let method = serde_json::from_value::<Params>(params.clone())
126            .map_err(|e| e.to_string())?
127            .method()?;
128        let Artifact::Series(signal) = input(inputs, "signal")? else {
129            return Err("input \"signal\" must be a series".into());
130        };
131        let out = method.decompose_latent(signal).map_err(|e| e.to_string())?;
132        Ok(Outputs::from([
133            ("modes".into(), Artifact::Decomposition(out.modes)),
134            (
135                "latent_modes".into(),
136                Artifact::Decomposition(out.latent_modes),
137            ),
138            ("loadings".into(), Artifact::Loadings(out.loadings)),
139        ]))
140    }
141}