clawft_kernel/sensor_graph/
fixture.rs1use super::graph::{SensorGraph, SensorNode, PHYSICS_DIM};
13
14pub const CLASS_AMBIENT: usize = 0;
16pub const CLASS_VESSEL: usize = 1;
17pub const CLASS_MAMMAL: usize = 2;
18
19pub fn class_name(label: usize) -> &'static str {
21 match label {
22 CLASS_AMBIENT => "ambient",
23 CLASS_VESSEL => "vessel",
24 CLASS_MAMMAL => "mammal_call",
25 _ => "unknown",
26 }
27}
28
29#[derive(Debug, Clone)]
31pub struct SyntheticConfig {
32 pub n_buoys: usize,
34 pub series_len: usize,
36 pub connect_radius: f64,
38 pub noise: f32,
40 pub seed: u64,
42}
43
44impl Default for SyntheticConfig {
45 fn default() -> Self {
46 Self {
47 n_buoys: 6,
48 series_len: 32,
49 connect_radius: 3.5,
50 noise: 0.05,
51 seed: 0x50b007,
52 }
53 }
54}
55
56#[derive(Debug, Clone)]
58pub struct SyntheticSample {
59 pub node_idx: usize,
60 pub label: usize,
61}
62
63#[derive(Debug, Clone)]
65pub struct SyntheticDataset {
66 pub graph: SensorGraph,
67 pub samples: Vec<SyntheticSample>,
68 pub config: SyntheticConfig,
69}
70
71pub fn generate_synthetic_array(cfg: SyntheticConfig) -> SyntheticDataset {
78 let mut rng = cfg.seed;
79 let mut graph = SensorGraph::new();
80
81 let cols = (cfg.n_buoys as f64).sqrt().ceil() as usize;
83 let mut labels = Vec::with_capacity(cfg.n_buoys);
84
85 for i in 0..cfg.n_buoys {
86 let row = i / cols;
87 let col = i % cols;
88 let jx = f64::from(unit(&mut rng)) * 0.3;
90 let jy = f64::from(unit(&mut rng)) * 0.3;
91 let lat = row as f64 + jx;
92 let lon = col as f64 + jy;
93 let depth = 8.0 + f64::from(unit(&mut rng)) * 4.0;
94
95 let features = vec![
97 0.4 + 0.2 * unit(&mut rng), 0.3 + 0.3 * unit(&mut rng), 0.1 + 0.4 * unit(&mut rng), unit(&mut rng) * 0.5, if unit(&mut rng) > 0.5 { 0.8 } else { 0.2 }, ];
103 debug_assert_eq!(features.len(), PHYSICS_DIM);
104
105 graph.add_buoy(SensorNode {
106 id: format!("buoy-{i:02}"),
107 position: (lat, lon, depth),
108 features,
109 });
110
111 let label = match i % 3 {
113 0 => CLASS_AMBIENT,
114 1 => CLASS_VESSEL,
115 _ => CLASS_MAMMAL,
116 };
117 labels.push(label);
118 }
119
120 graph.connect_within_radius(cfg.connect_radius);
121 if graph.edge_count() == 0 && cfg.n_buoys > 1 {
123 graph.connect_fully();
124 }
125
126 for (i, &label) in labels.iter().enumerate() {
128 let series = match label {
129 CLASS_VESSEL => vessel_series(cfg.series_len, cfg.noise, &mut rng),
130 CLASS_MAMMAL => mammal_series(cfg.series_len, cfg.noise, &mut rng),
131 _ => ambient_series(cfg.series_len, cfg.noise, &mut rng),
132 };
133 graph.push_temporal(i, &series);
134 }
135
136 let bleed: Vec<(usize, Vec<f32>)> = (0..cfg.n_buoys)
139 .filter(|&i| labels[i] != CLASS_AMBIENT)
140 .map(|i| {
141 let src = graph.temporal_buffers[i].clone();
142 (i, src)
143 })
144 .collect();
145 for (src_idx, src_series) in bleed {
146 for nb in graph.neighbor_indices(src_idx) {
147 let buf = &mut graph.temporal_buffers[nb];
148 let n = buf.len().min(src_series.len());
149 for t in 0..n {
150 buf[t] += 0.2 * src_series[t];
151 }
152 }
153 }
154
155 let samples = labels
156 .into_iter()
157 .enumerate()
158 .map(|(node_idx, label)| SyntheticSample { node_idx, label })
159 .collect();
160
161 SyntheticDataset {
162 graph,
163 samples,
164 config: cfg,
165 }
166}
167
168fn ambient_series(len: usize, noise: f32, rng: &mut u64) -> Vec<f32> {
169 (0..len).map(|_| (unit(rng) - 0.5) * 2.0 * noise).collect()
170}
171
172fn vessel_series(len: usize, noise: f32, rng: &mut u64) -> Vec<f32> {
173 let period = 6.0f32;
175 (0..len)
176 .map(|t| {
177 let phase = (t as f32) * std::f32::consts::TAU / period;
178 let sig = 0.35 * (phase.sin().max(0.0));
179 sig + (unit(rng) - 0.5) * 2.0 * noise
180 })
181 .collect()
182}
183
184fn mammal_series(len: usize, noise: f32, rng: &mut u64) -> Vec<f32> {
185 let mut s: Vec<f32> = (0..len)
187 .map(|_| (unit(rng) - 0.5) * 2.0 * noise)
188 .collect();
189 let n_clicks = 2 + (*rng as usize % 3);
190 for _ in 0..n_clicks {
191 let t = (*rng as usize) % len;
192 *rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
193 s[t] += 0.9;
194 if t + 1 < len {
195 s[t + 1] += 0.4;
196 }
197 }
198 s
199}
200
201fn unit(rng: &mut u64) -> f32 {
203 *rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
204 ((*rng >> 33) as f32) / (u32::MAX as f32)
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn generates_expected_counts() {
213 let ds = generate_synthetic_array(SyntheticConfig {
214 n_buoys: 9,
215 series_len: 24,
216 connect_radius: 2.5,
217 noise: 0.05,
218 seed: 1,
219 });
220 assert_eq!(ds.graph.node_count(), 9);
221 assert_eq!(ds.samples.len(), 9);
222 assert!(ds.graph.edge_count() > 0);
223 for s in &ds.samples {
224 assert!(s.label < 3);
225 assert_eq!(
226 ds.graph.temporal_buffers[s.node_idx].len(),
227 24
228 );
229 }
230 }
231
232 #[test]
233 fn class_names() {
234 assert_eq!(class_name(0), "ambient");
235 assert_eq!(class_name(1), "vessel");
236 assert_eq!(class_name(2), "mammal_call");
237 }
238
239 #[test]
240 fn vessel_nodes_have_higher_energy_than_ambient() {
241 let ds = generate_synthetic_array(SyntheticConfig {
242 n_buoys: 6,
243 series_len: 32,
244 connect_radius: 5.0,
245 noise: 0.02,
246 seed: 42,
247 });
248 let mut amb_e = 0.0f32;
249 let mut amb_n = 0usize;
250 let mut ves_e = 0.0f32;
251 let mut ves_n = 0usize;
252 for s in &ds.samples {
253 let e: f32 = ds.graph.temporal_buffers[s.node_idx]
254 .iter()
255 .map(|v| v.abs())
256 .sum();
257 if s.label == CLASS_AMBIENT {
258 amb_e += e;
259 amb_n += 1;
260 } else if s.label == CLASS_VESSEL {
261 ves_e += e;
262 ves_n += 1;
263 }
264 }
265 assert!(amb_n > 0 && ves_n > 0);
266 assert!(ves_e / ves_n as f32 > amb_e / amb_n as f32);
267 }
268}