Rust#
modewise is a Cargo workspace of small crates: domain types (modewise_signals,
modewise_decomposition, …), one crate per method (modewise_mvmd, modewise_emd, …), and the
pipeline engine (modewise_pipeline, modewise_operations). The crates are not on crates.io yet;
depend on them from the repository:
[dependencies]
modewise_mvmd = { git = "https://github.com/ipeglin/modewise" }
modewise_signals = { git = "https://github.com/ipeglin/modewise" }
modewise_decomposition = { git = "https://github.com/ipeglin/modewise" }
The full API is in the Rust reference.
Decompose a signal#
A MultichannelSeries holds channels × samples with a sampling rate and channel labels. Every
decomposition implements ModeDecomposer.
use std::error::Error;
use std::f64::consts::PI;
use modewise_decomposition::ModeDecomposer;
use modewise_mvmd::{FrequencyInit, Mvmd, MvmdParams};
use modewise_signals::{MultichannelSeries, SamplingRate};
use ndarray::Array2;
fn main() -> Result<(), Box<dyn Error>> {
let fs = 1.25; // Hz, a repetition time of 0.8 s
let data = Array2::from_shape_fn((2, 600), |(c, i)| {
let t = i as f64 / fs;
(1.0 - 0.5 * c as f64) * (2.0 * PI * 0.02 * t).sin() + (2.0 * PI * 0.2 * t).sin()
});
let signal = MultichannelSeries::new(data, SamplingRate::new(fs)?, vec!["left".into(), "right".into()])?;
let params = MvmdParams { init: FrequencyInit::Linear, tau: 0.0, tolerance: 1e-7, ..MvmdParams::new(2, 2000.0) };
let modes = Mvmd::new(params)?.decompose(&signal)?;
assert_eq!(modes.modes().dim(), (2, 2, 600)); // mode × channel × sample
assert!((modes.centre_frequencies_hz()[0] - 0.02).abs() < 0.002);
Ok(())
}
Run a pipeline#
The pipeline engine plans a TOML spec against the registry of operations and runs it. Results go
to an ArtifactStore: MemoryStore here, or the Zarr store that the CLI and Python use for
results folders.
use std::collections::BTreeMap;
use std::error::Error;
use modewise_operations::builtin_registry;
use modewise_pipeline::{Artifact, ArtifactStore, PipelineSpec, Plan, run};
use modewise_signals::{MultichannelSeries, SamplingRate};
use modewise_store_memory::MemoryStore;
use ndarray::Array2;
const PIPELINE: &str = r#"
[pipeline]
name = "spectra"
inputs = ["bold"]
[[step]]
id = "psd"
op = "spectrum"
input = "bold"
"#;
fn main() -> Result<(), Box<dyn Error>> {
let data = Array2::from_shape_fn((1, 400), |(_, i)| (i as f64 * 0.3).sin());
let bold = MultichannelSeries::new(data, SamplingRate::new(1.0)?, vec!["a".into()])?;
let plan = Plan::new(&PipelineSpec::from_toml(PIPELINE)?, &builtin_registry())?;
let store = MemoryStore::new();
let result = run(&plan, &BTreeMap::from([("bold".to_string(), Artifact::Series(bold))]), &store)?;
let key = result.output_key("psd").ok_or("no output psd")?;
let Some(artifact) = store.get(key)? else { return Err("not stored".into()) };
let Artifact::TimeFrequency(psd) = artifact.as_ref() else { return Err("not a spectrum".into()) };
assert_eq!(psd.frequencies_hz().len(), 201);
Ok(())
}
Write your own method#
A new decomposition implements ModeDecomposer (descriptor and decompose), and an operation
wraps it for pipelines: Operation with descriptor, resolve (check parameters and fill
defaults), run, and summary and params_schema for the generated reference. The operations in
application/operations/src/ are worked examples, and CLAUDE.md lists the conventions every
method follows (sources, tests, no single trusted reference).