luna-rs 0.1.0

LUNA EEG Foundation Model — inference in Rust (Burn and/or RLX)
Documentation
//! Generate synthetic EEG CSV files for testing luna-rs without real data.
//!
//! ```sh
//! cargo run --bin gen_sample_eeg --release -- --output sample.csv
//! cargo run --bin gen_sample_eeg --release -- --output sample.csv --channels 8 --duration 30
//! ```

use clap::Parser;
use std::io::Write;

#[derive(Parser, Debug)]
#[command(about = "Generate synthetic EEG CSV for testing")]
struct Args {
    /// Output CSV file path.
    #[arg(long, short, default_value = "data/sample.csv")]
    output: String,

    /// Number of channels (max 22 for TUEG bipolar).
    #[arg(long, default_value_t = 22)]
    channels: usize,

    /// Duration in seconds.
    #[arg(long, default_value_t = 10.0)]
    duration: f32,

    /// Sampling rate in Hz.
    #[arg(long, default_value_t = 256.0)]
    sfreq: f32,
}

const TUEG_NAMES: &[&str] = &[
    "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",
];

fn main() -> anyhow::Result<()> {
    let args = Args::parse();
    let n_ch = args.channels.min(TUEG_NAMES.len());
    let n_t = (args.duration * args.sfreq) as usize;
    let dt = 1.0 / args.sfreq;

    if let Some(parent) = std::path::Path::new(&args.output).parent() {
        std::fs::create_dir_all(parent)?;
    }
    let mut f = std::fs::File::create(&args.output)?;

    // Header comment
    writeln!(f, "# Generated by luna-rs gen_sample_eeg")?;
    writeln!(f, "# sfreq={} channels={} duration={}s samples={}",
        args.sfreq, n_ch, args.duration, n_t)?;

    // Column header
    write!(f, "timestamp")?;
    for name in TUEG_NAMES.iter().take(n_ch) {
        write!(f, ",{name}")?;
    }
    writeln!(f)?;

    // Data: realistic EEG with alpha (10 Hz), theta (5 Hz), noise
    let mut noise_state: u32 = 0xDEAD_BEEF;
    for t in 0..n_t {
        let time = t as f32 * dt;
        write!(f, "{time:.9e}")?;
        for ch in 0..n_ch {
            let alpha = (2.0 * std::f32::consts::PI * (9.5 + ch as f32 * 0.3) * time).sin() * 20e-6;
            let theta = (2.0 * std::f32::consts::PI * (5.0 + ch as f32 * 0.1) * time).sin() * 15e-6;
            let beta  = (2.0 * std::f32::consts::PI * (20.0 + ch as f32 * 0.7) * time).sin() * 5e-6;
            noise_state ^= noise_state << 13;
            noise_state ^= noise_state >> 17;
            noise_state ^= noise_state << 5;
            let noise = (noise_state as f32 / u32::MAX as f32 - 0.5) * 3e-6;
            let val = alpha + theta + beta + noise;
            write!(f, ",{val:.9e}")?;
        }
        writeln!(f)?;
    }

    let size = std::fs::metadata(&args.output)?.len();
    println!("Generated {n_ch} channels × {n_t} samples ({:.1}s @ {} Hz)",
        args.duration, args.sfreq);
    println!("{} ({:.1} KB)", args.output, size as f64 / 1024.0);
    Ok(())
}