feagi_evolutionary/genome/normalizers/
v3.rs1use serde_json::{json, Value};
35
36use super::{NormalizationDiagnostics, Normalizer};
37use crate::genome::migration::MigrationError;
38use crate::genome::schema::{GenomeSchemaVersion, CURRENT_SCHEMA_VERSION};
39
40const DEFAULT_QUANTIZATION_PRECISION: &str = "fp32";
44
45const DEFAULT_SIMULATION_TIMESTEP: f64 = 0.025;
48
49const DEFAULT_MAX_AGE: u64 = 10_000_000;
52
53#[derive(Debug, Default, Clone, Copy)]
54pub struct V3Normalizer;
55
56impl V3Normalizer {
57 pub const fn new() -> Self {
58 Self
59 }
60}
61
62impl Normalizer for V3Normalizer {
63 fn schema_version(&self) -> GenomeSchemaVersion {
64 CURRENT_SCHEMA_VERSION
65 }
66
67 fn name(&self) -> &'static str {
68 "v3_normalizer"
69 }
70
71 fn normalize(&self, genome: &mut Value) -> Result<NormalizationDiagnostics, MigrationError> {
72 let mut diag = NormalizationDiagnostics::new(CURRENT_SCHEMA_VERSION);
73
74 normalize_physiology(genome, &mut diag);
75 normalize_blueprint(genome, &mut diag);
76
77 Ok(diag)
78 }
79}
80
81fn normalize_physiology(genome: &mut Value, diag: &mut NormalizationDiagnostics) {
85 let physiology = match genome.get_mut("physiology").and_then(Value::as_object_mut) {
86 Some(p) => p,
87 None => return,
88 };
89
90 if let Some(ts) = physiology
91 .get("simulation_timestep")
92 .and_then(Value::as_f64)
93 {
94 if ts <= 0.0 {
95 physiology.insert(
96 "simulation_timestep".to_string(),
97 json!(DEFAULT_SIMULATION_TIMESTEP),
98 );
99 diag.record(format!(
100 "physiology.simulation_timestep {ts} -> {DEFAULT_SIMULATION_TIMESTEP} (default)"
101 ));
102 }
103 }
104
105 if let Some(age) = physiology.get("max_age").and_then(Value::as_u64) {
106 if age == 0 {
107 physiology.insert("max_age".to_string(), json!(DEFAULT_MAX_AGE));
108 diag.record(format!(
109 "physiology.max_age 0 -> {DEFAULT_MAX_AGE} (default)"
110 ));
111 }
112 }
113
114 let precision_action = match physiology
115 .get("quantization_precision")
116 .and_then(Value::as_str)
117 {
118 Some("") => Some(PrecisionAction::ReplaceWithDefault {
119 previous: String::new(),
120 }),
121 Some(other) => match canonicalize_precision(other) {
122 Some(canonical) if canonical != other => Some(PrecisionAction::Normalize {
123 previous: other.to_string(),
124 canonical,
125 }),
126 Some(_) => None,
127 None => Some(PrecisionAction::ReplaceWithDefault {
128 previous: other.to_string(),
129 }),
130 },
131 None => None,
132 };
133
134 if let Some(action) = precision_action {
135 match action {
136 PrecisionAction::Normalize {
137 previous,
138 canonical,
139 } => {
140 physiology.insert(
141 "quantization_precision".to_string(),
142 Value::String(canonical.clone()),
143 );
144 diag.record(format!(
145 "physiology.quantization_precision '{previous}' -> '{canonical}' (normalized)"
146 ));
147 }
148 PrecisionAction::ReplaceWithDefault { previous } => {
149 physiology.insert(
150 "quantization_precision".to_string(),
151 Value::String(DEFAULT_QUANTIZATION_PRECISION.to_string()),
152 );
153 diag.record(format!(
154 "physiology.quantization_precision '{previous}' -> '{DEFAULT_QUANTIZATION_PRECISION}' (default)"
155 ));
156 }
157 }
158 }
159}
160
161enum PrecisionAction {
162 Normalize { previous: String, canonical: String },
163 ReplaceWithDefault { previous: String },
164}
165
166fn canonicalize_precision(input: &str) -> Option<String> {
173 match input.to_lowercase().as_str() {
174 "fp32" | "f32" => Some("fp32".to_string()),
175 "fp16" | "f16" => Some("fp16".to_string()),
176 "int8" => Some("int8".to_string()),
177 _ => None,
178 }
179}
180
181fn normalize_blueprint(genome: &mut Value, diag: &mut NormalizationDiagnostics) {
184 let blueprint = match genome.get_mut("blueprint").and_then(Value::as_object_mut) {
185 Some(b) => b,
186 None => return,
187 };
188
189 let area_ids: Vec<String> = blueprint.keys().cloned().collect();
190 for cortical_id in area_ids {
191 let area = match blueprint
192 .get_mut(&cortical_id)
193 .and_then(Value::as_object_mut)
194 {
195 Some(a) => a,
196 None => continue,
197 };
198
199 normalize_block_boundaries(area, &cortical_id, diag);
200 normalize_per_voxel_neuron_cnt(area, &cortical_id, diag);
201 }
202}
203
204fn normalize_block_boundaries(
205 area: &mut serde_json::Map<String, Value>,
206 cortical_id: &str,
207 diag: &mut NormalizationDiagnostics,
208) {
209 let boundaries = match area
210 .get_mut("block_boundaries")
211 .and_then(Value::as_array_mut)
212 {
213 Some(b) if b.len() == 3 => b,
214 _ => return,
215 };
216
217 static AXIS_NAMES: [&str; 3] = ["width", "height", "depth"];
218 for (i, slot) in boundaries.iter_mut().enumerate() {
219 if slot.as_u64() == Some(0) {
220 *slot = json!(1u32);
221 diag.record(format!(
222 "blueprint['{cortical_id}'].block_boundaries[{i}] ({}) 0 -> 1",
223 AXIS_NAMES[i]
224 ));
225 }
226 }
227}
228
229fn normalize_per_voxel_neuron_cnt(
230 area: &mut serde_json::Map<String, Value>,
231 cortical_id: &str,
232 diag: &mut NormalizationDiagnostics,
233) {
234 if area.get("per_voxel_neuron_cnt").and_then(Value::as_u64) == Some(0) {
235 area.insert("per_voxel_neuron_cnt".to_string(), json!(1u32));
236 diag.record(format!(
237 "blueprint['{cortical_id}'].per_voxel_neuron_cnt 0 -> 1"
238 ));
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use serde_json::json;
246
247 #[test]
248 fn reports_current_schema_version() {
249 let n = V3Normalizer::new();
250 assert_eq!(n.schema_version(), CURRENT_SCHEMA_VERSION);
251 assert_eq!(n.name(), "v3_normalizer");
252 }
253
254 #[test]
255 fn clean_genome_yields_clean_diagnostics() {
256 let n = V3Normalizer::new();
257 let mut g = json!({
258 "physiology": {
259 "simulation_timestep": 0.025,
260 "max_age": 10_000_000,
261 "quantization_precision": "fp32"
262 },
263 "blueprint": {
264 "abc12345": {
265 "block_boundaries": [10, 10, 10],
266 "per_voxel_neuron_cnt": 1
267 }
268 }
269 });
270 let d = n.normalize(&mut g).unwrap();
271 assert!(d.is_clean());
272 }
273
274 #[test]
275 fn fixes_negative_simulation_timestep() {
276 let n = V3Normalizer::new();
277 let mut g = json!({
278 "physiology": { "simulation_timestep": -0.1 }
279 });
280 let d = n.normalize(&mut g).unwrap();
281 assert_eq!(g["physiology"]["simulation_timestep"], json!(0.025));
282 assert_eq!(d.transformations.len(), 1);
283 assert!(d.transformations[0].contains("simulation_timestep"));
284 }
285
286 #[test]
287 fn fixes_zero_simulation_timestep() {
288 let n = V3Normalizer::new();
289 let mut g = json!({ "physiology": { "simulation_timestep": 0.0 } });
290 let d = n.normalize(&mut g).unwrap();
291 assert_eq!(g["physiology"]["simulation_timestep"], json!(0.025));
292 assert!(!d.is_clean());
293 }
294
295 #[test]
296 fn leaves_burst_delay_alone() {
297 let n = V3Normalizer::new();
301 let mut g = json!({ "physiology": { "burst_delay": 0.030 } });
302 let d = n.normalize(&mut g).unwrap();
303 assert!(d.is_clean());
304 assert_eq!(g["physiology"]["burst_delay"], json!(0.030));
305 assert!(g["physiology"].get("simulation_timestep").is_none());
306 }
307
308 #[test]
309 fn fixes_zero_max_age() {
310 let n = V3Normalizer::new();
311 let mut g = json!({ "physiology": { "max_age": 0 } });
312 let d = n.normalize(&mut g).unwrap();
313 assert_eq!(g["physiology"]["max_age"], json!(DEFAULT_MAX_AGE));
314 assert!(!d.is_clean());
315 }
316
317 #[test]
318 fn replaces_empty_precision_with_default() {
319 let n = V3Normalizer::new();
320 let mut g = json!({ "physiology": { "quantization_precision": "" } });
321 let d = n.normalize(&mut g).unwrap();
322 assert_eq!(g["physiology"]["quantization_precision"], json!("fp32"));
323 assert_eq!(d.transformations.len(), 1);
324 }
325
326 #[test]
327 fn normalizes_uppercase_precision() {
328 let n = V3Normalizer::new();
329 let mut g = json!({ "physiology": { "quantization_precision": "FP32" } });
330 let d = n.normalize(&mut g).unwrap();
331 assert_eq!(g["physiology"]["quantization_precision"], json!("fp32"));
332 assert!(d.transformations[0].contains("normalized"));
333 }
334
335 #[test]
336 fn normalizes_f32_alias_precision() {
337 let n = V3Normalizer::new();
338 let mut g = json!({ "physiology": { "quantization_precision": "f32" } });
339 let d = n.normalize(&mut g).unwrap();
340 assert_eq!(g["physiology"]["quantization_precision"], json!("fp32"));
341 assert!(d.transformations[0].contains("normalized"));
342 }
343
344 #[test]
345 fn replaces_invalid_precision_with_default() {
346 let n = V3Normalizer::new();
347 let mut g = json!({ "physiology": { "quantization_precision": "garbage" } });
348 let d = n.normalize(&mut g).unwrap();
349 assert_eq!(g["physiology"]["quantization_precision"], json!("fp32"));
350 assert!(d.transformations[0].contains("default"));
351 }
352
353 #[test]
354 fn fixes_zero_block_boundaries_per_axis() {
355 let n = V3Normalizer::new();
356 let mut g = json!({
357 "blueprint": {
358 "abc12345": { "block_boundaries": [0, 5, 0] }
359 }
360 });
361 let d = n.normalize(&mut g).unwrap();
362 assert_eq!(
363 g["blueprint"]["abc12345"]["block_boundaries"],
364 json!([1, 5, 1])
365 );
366 assert_eq!(d.transformations.len(), 2);
367 }
368
369 #[test]
370 fn fixes_zero_per_voxel_neuron_cnt() {
371 let n = V3Normalizer::new();
372 let mut g = json!({
373 "blueprint": {
374 "abc12345": { "per_voxel_neuron_cnt": 0 }
375 }
376 });
377 let d = n.normalize(&mut g).unwrap();
378 assert_eq!(g["blueprint"]["abc12345"]["per_voxel_neuron_cnt"], json!(1));
379 assert_eq!(d.transformations.len(), 1);
380 }
381
382 #[test]
383 fn handles_missing_fields_silently() {
384 let n = V3Normalizer::new();
387 let mut g = json!({});
388 let d = n.normalize(&mut g).unwrap();
389 assert!(d.is_clean());
390 assert_eq!(g, json!({}));
391 }
392
393 #[test]
394 fn is_idempotent() {
395 let n = V3Normalizer::new();
398 let mut g = json!({
399 "physiology": {
400 "simulation_timestep": 0.0,
401 "max_age": 0,
402 "quantization_precision": ""
403 },
404 "blueprint": {
405 "abc12345": {
406 "block_boundaries": [0, 0, 0],
407 "per_voxel_neuron_cnt": 0
408 }
409 }
410 });
411
412 let d1 = n.normalize(&mut g).unwrap();
413 assert!(!d1.is_clean());
414 let snapshot = g.clone();
415
416 let d2 = n.normalize(&mut g).unwrap();
417 assert!(d2.is_clean());
418 assert_eq!(g, snapshot);
419 }
420}