Skip to main content

gen_smoke_corpus/
gen_smoke_corpus.rs

1//! Generate the redistributable OpenECS smoke corpus.
2//!
3//! The smoke corpus is a tiny, license-clean, *synthetic* set of EEG-shaped
4//! EDF files that ships in-repo so OpenECS runs offline with zero external data.
5//! It is NOT patient data (CHB-MIT / TUH are not redistributable); it is a
6//! deterministic sum of band-placed sinusoids in the EDF i16 digital domain,
7//! reproducible byte-for-byte by re-running this generator.
8//!
9//! Run from the crate root:
10//!
11//! ```bash
12//! cargo run --example gen_smoke_corpus
13//! ```
14//!
15//! It writes `corpora/smoke/*.edf` and prints the `[[file]]` blocks (with
16//! real SHA-256 + shape) to paste into `corpora/ecs-smoke.toml`.
17
18use std::f64::consts::PI;
19use std::path::PathBuf;
20
21use open_eeg_codec_standard::corpus::sha256_hex;
22use open_eeg_codec_standard::subprocess::write_edf_bytes;
23
24/// One synthetic EEG-shaped channel: a deterministic sum of band-placed
25/// sinusoids (delta/theta/alpha/beta/gamma) + a small DC term, scaled to a
26/// few hundred ADC counts, well inside the i16 digital range. No RNG, so the
27/// output is byte-stable across runs and machines.
28fn channel(ch: usize, n: usize, fs: f64) -> Vec<i64> {
29    let amp = 1.0 + 0.2 * ch as f64;
30    let phase = 0.37 * ch as f64;
31    (0..n)
32        .map(|i| {
33            let t = i as f64 / fs;
34            let v = amp
35                * (30.0
36                    + 110.0 * (2.0 * PI * 2.0 * t + phase).sin()
37                    + 70.0 * (2.0 * PI * 6.0 * t).sin()
38                    + 55.0 * (2.0 * PI * 10.0 * t + phase).sin()
39                    + 28.0 * (2.0 * PI * 20.0 * t).sin()
40                    + 12.0 * (2.0 * PI * 40.0 * t).sin());
41            v.round() as i64
42        })
43        .collect()
44}
45
46fn signal(n_chan: usize, n: usize, fs: f64) -> Vec<Vec<i64>> {
47    (0..n_chan).map(|c| channel(c, n, fs)).collect()
48}
49
50fn main() {
51    // (file name, channels, samples/channel, fs) — small + diverse shapes.
52    let specs: &[(&str, usize, usize, f64)] = &[
53        ("synthetic_a.edf", 4, 1024, 256.0),
54        ("synthetic_b.edf", 6, 768, 128.0),
55        ("synthetic_c.edf", 2, 512, 256.0),
56    ];
57
58    let out_dir: PathBuf = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
59        .join("corpora")
60        .join("smoke");
61    std::fs::create_dir_all(&out_dir).expect("create corpora/smoke");
62
63    println!("# Generated by `cargo run --example gen_smoke_corpus`.");
64    println!("# Paste the [[file]] blocks below into corpora/ecs-smoke.toml.\n");
65    for (name, n_chan, n_samp, fs) in specs {
66        let sig = signal(*n_chan, *n_samp, *fs);
67        let edf = write_edf_bytes(&sig, *fs).expect("synthetic signal -> EDF");
68        let path = out_dir.join(name);
69        std::fs::write(&path, &edf).expect("write edf");
70        let sha = sha256_hex(&edf);
71        println!("[[file]]");
72        println!("path = \"smoke/{name}\"");
73        println!("sha256 = \"{sha}\"");
74        println!("fs = {fs:.1}");
75        println!("n_chan = {n_chan}");
76        println!("n_samples = {n_samp}");
77        println!();
78        eprintln!("wrote {} ({} bytes, sha {})", path.display(), edf.len(), &sha[..12]);
79    }
80}