use std::time::Instant;
use clap::Parser;
use lumamba::init_threads;
use lumamba::rlx::{save_epochs, LuMambaEncoder, PreprocInfo, RlxEpoch};
#[derive(Parser, Debug)]
#[command(about = "LuMamba EEG foundation-model inference (RLX runtime)")]
struct Args {
#[arg(long, default_value = "cpu", value_parser = lumamba::parse_device)]
device: rlx::Device,
#[arg(long, env = "LUMAMBA_WEIGHTS")]
weights: String,
#[arg(long, env = "LUMAMBA_CONFIG")]
config: String,
#[arg(long)]
input: Option<String>,
#[arg(long)]
output: String,
#[arg(long)]
encode: bool,
#[arg(long, default_value_t = 1)]
repeat: usize,
#[arg(long, env = "RAYON_NUM_THREADS")]
threads: Option<usize>,
#[arg(long, short = 'v')]
verbose: bool,
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
let n_threads = init_threads(args.threads);
let device = args.device;
let t0 = Instant::now();
eprintln!("Device : {:?} ({n_threads} threads)", device);
let config_path = lumamba::hf::resolve(&args.config)?;
let weights_path = lumamba::hf::resolve(&args.weights)?;
let (mut model, ms_load) = LuMambaEncoder::load(&config_path, &weights_path, device)?;
eprintln!("Model : {} ({ms_load:.0} ms)", model.describe());
let t_pp = Instant::now();
let (epochs, info) = load_epochs(args.input.as_deref())?;
let ms_preproc = t_pp.elapsed().as_secs_f64() * 1000.0;
eprintln!(
"Input : {} epochs × {} ch × {} samples",
epochs.len(),
info.n_channels,
epochs.first().map(|e| e.n_samples).unwrap_or(0),
);
eprintln!("Preproc : {ms_preproc:.1} ms");
let runs = args.repeat.max(1);
let mut outputs = Vec::with_capacity(epochs.len());
let mut times = Vec::with_capacity(runs);
for _ in 0..runs {
outputs.clear();
let t = Instant::now();
for ep in &epochs {
if args.encode {
let (latent, shape) = model.encode(
&ep.signal,
&ep.chan_pos,
ep.channel_indices.as_deref(),
ep.n_channels,
ep.n_samples,
)?;
outputs.push(lumamba::rlx::EpochEmbedding {
output: latent,
shape,
chan_pos: ep.chan_pos.clone(),
n_channels: ep.n_channels,
});
} else {
outputs.push(model.run_rlx_epoch(ep)?);
}
}
times.push(t.elapsed().as_secs_f64() * 1000.0);
}
let n = outputs.len().max(1);
let cold = times[0];
if runs > 1 {
let warm: f64 = times[1..].iter().sum::<f64>() / (runs - 1) as f64;
eprintln!(
"Infer : cold {cold:.0} ms | warm {:.1} ms/epoch ({n} epochs × {} warm runs)",
warm / n as f64,
runs - 1,
);
} else {
eprintln!("Infer : {cold:.0} ms ({n} epochs; {:.0} ms/epoch)", cold / n as f64);
}
if args.verbose {
for (i, ep) in outputs.iter().enumerate() {
let n = ep.output.len().max(1);
let mean: f64 = ep.output.iter().map(|&v| v as f64).sum::<f64>() / n as f64;
let max = ep.output.iter().cloned().fold(f32::MIN, f32::max);
let min = ep.output.iter().cloned().fold(f32::MAX, f32::min);
eprintln!(" [ep {}] shape={:?} mean={mean:+.5} min={min:+.4} max={max:+.4}", i + 1, ep.shape);
}
}
save_epochs(&outputs, &args.output)?;
eprintln!(
"Output → {} ({:.0} ms total)",
args.output,
t0.elapsed().as_secs_f64() * 1000.0
);
Ok(())
}
fn load_epochs(input: Option<&str>) -> anyhow::Result<(Vec<RlxEpoch>, PreprocInfo)> {
match input {
Some(path) => load_recording(path),
None => synthetic_epoch(),
}
}
#[cfg(feature = "edf")]
fn load_recording(path: &str) -> anyhow::Result<(Vec<RlxEpoch>, PreprocInfo)> {
use lumamba::rlx::{load_edf, load_fif};
let p = std::path::Path::new(path);
if p.extension().and_then(|s| s.to_str()) == Some("edf") {
load_edf(p)
} else {
load_fif(p)
}
}
#[cfg(not(feature = "edf"))]
fn load_recording(_path: &str) -> anyhow::Result<(Vec<RlxEpoch>, PreprocInfo)> {
anyhow::bail!("EDF/FIF input requires building with `--features edf`")
}
fn synthetic_epoch() -> anyhow::Result<(Vec<RlxEpoch>, PreprocInfo)> {
use lumamba::channel_positions;
let n_channels = 22usize;
let n_samples = 5120usize; let names = [
"FP1-F7", "F7-T3", "T3-T5", "T5-O1", "FP2-F8", "F8-T4", "T4-T6", "T6-O2", "T3-C3", "C3-CZ",
"CZ-C4", "C4-T4", "FP1-F3", "F3-C3", "C3-P3", "P3-O1", "FP2-F4", "F4-C4", "C4-P4", "P4-O2",
"A1-T3", "T4-A2",
];
let positions: Vec<f32> = names
.iter()
.flat_map(|n| channel_positions::bipolar_channel_xyz(n).unwrap_or([0.0, 0.0, 0.0]).to_vec())
.collect();
let channel_indices: Vec<i32> =
names.iter().map(|n| lumamba::channel_index(n).unwrap_or(0) as i32).collect();
let mut signal = vec![0f32; n_channels * n_samples];
for ch in 0..n_channels {
for ti in 0..n_samples {
let f = 0.05 + 0.01 * ch as f32;
signal[ch * n_samples + ti] = (f * ti as f32).sin();
}
}
Ok((
vec![RlxEpoch {
signal,
chan_pos: positions,
channel_indices: Some(channel_indices),
n_channels,
n_samples,
}],
PreprocInfo {
ch_names: names.iter().map(|s| s.to_string()).collect(),
n_channels,
n_epochs: 1,
target_sfreq: 256.0,
epoch_dur: 20.0,
},
))
}