1use crate::common::ApiState;
7use base64::{engine::general_purpose, Engine as _};
8use feagi_config::load_config;
9use feagi_services::types::CreateCorticalAreaParams;
10use feagi_structures::genomic::cortical_area::descriptors::{
11 CorticalSubUnitIndex, CorticalUnitIndex,
12};
13use feagi_structures::genomic::cortical_area::io_cortical_area_configuration_flag::{
14 FrameChangeHandling, PercentageNeuronPositioning,
15};
16use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
17use serde_json::Value;
18use std::collections::{HashMap, HashSet};
19use tracing::{info, warn};
20
21const MOTOR_AREA_X_GAP_VOXELS: i32 = 10;
22const SEGMENTED_VISION_GROUP_X_GAP_VOXELS: i32 = 10;
23
24fn build_friendly_unit_name(unit_label: &str, group: u8, sub_unit_index: usize) -> String {
25 format!("{unit_label}-{}-{}", group, sub_unit_index)
26}
27
28fn non_empty_string(value: Option<&Value>) -> Option<String> {
29 value
30 .and_then(|v| v.as_str())
31 .map(str::trim)
32 .filter(|s| !s.is_empty())
33 .map(ToString::to_string)
34}
35
36fn extract_grouping_array(unit_def: &Value) -> &[Value] {
37 unit_def
38 .get("device_grouping")
39 .and_then(|v| v.as_array())
40 .map(Vec::as_slice)
41 .unwrap_or(&[])
42}
43
44fn first_grouping_property(unit_def: &Value, key: &str) -> Option<String> {
45 extract_grouping_array(unit_def)
46 .iter()
47 .find_map(|grouping| {
48 non_empty_string(
49 grouping
50 .get("device_properties")
51 .and_then(|v| v.as_object())
52 .and_then(|props| props.get(key)),
53 )
54 })
55}
56
57fn resolve_registration_name(unit_def: &Value, default_name: &str) -> String {
58 non_empty_string(unit_def.get("friendly_name"))
59 .or_else(|| first_grouping_property(unit_def, "bundle_id"))
60 .or_else(|| {
61 non_empty_string(
62 extract_grouping_array(unit_def)
63 .first()?
64 .get("friendly_name"),
65 )
66 })
67 .unwrap_or_else(|| default_name.to_string())
68}
69
70fn should_auto_rename(current_name: &str, cortical_id: &str, legacy_default_name: &str) -> bool {
71 current_name == cortical_id || current_name == legacy_default_name
72}
73
74fn build_io_config_map() -> Result<serde_json::Map<String, serde_json::Value>, String> {
75 let mut config = serde_json::Map::new();
76 config.insert(
77 "frame_change_handling".to_string(),
78 serde_json::to_value(FrameChangeHandling::Absolute)
79 .map_err(|e| format!("Failed to serialize FrameChangeHandling: {}", e))?,
80 );
81 config.insert(
82 "percentage_neuron_positioning".to_string(),
83 serde_json::to_value(PercentageNeuronPositioning::Linear)
84 .map_err(|e| format!("Failed to serialize PercentageNeuronPositioning: {}", e))?,
85 );
86 Ok(config)
87}
88
89fn build_io_config_map_from_unit_def(
90 unit_def: &Value,
91) -> Result<serde_json::Map<String, serde_json::Value>, String> {
92 let io_flags = unit_def
93 .get("io_configuration_flags")
94 .and_then(|v| v.as_object());
95
96 let frame_value = io_flags
97 .and_then(|flags| flags.get("frame_change_handling"))
98 .cloned()
99 .or_else(|| unit_def.get("frame_change_handling").cloned())
100 .ok_or_else(|| "unit_def missing frame_change_handling".to_string())?;
101 let positioning_value = io_flags
102 .and_then(|flags| flags.get("percentage_neuron_positioning"))
103 .cloned()
104 .or_else(|| unit_def.get("percentage_neuron_positioning").cloned())
105 .unwrap_or_else(|| serde_json::json!(PercentageNeuronPositioning::Linear));
108
109 let frame: FrameChangeHandling = serde_json::from_value(frame_value)
110 .map_err(|e| format!("Invalid frame_change_handling value: {}", e))?;
111 let positioning: PercentageNeuronPositioning = serde_json::from_value(positioning_value)
112 .map_err(|e| format!("Invalid percentage_neuron_positioning value: {}", e))?;
113
114 let mut config = serde_json::Map::new();
115 config.insert(
116 "frame_change_handling".to_string(),
117 serde_json::to_value(frame)
118 .map_err(|e| format!("Failed to serialize FrameChangeHandling: {}", e))?,
119 );
120 config.insert(
121 "percentage_neuron_positioning".to_string(),
122 serde_json::to_value(positioning)
123 .map_err(|e| format!("Failed to serialize PercentageNeuronPositioning: {}", e))?,
124 );
125 Ok(config)
126}
127
128fn as_nonzero_usize(value: Option<&Value>) -> Option<usize> {
129 value
130 .and_then(|v| v.as_u64())
131 .map(|v| v as usize)
132 .filter(|v| *v > 0)
133}
134
135fn color_channel_count_from_value(value: Option<&Value>) -> Option<usize> {
136 match value {
137 Some(Value::String(layout)) => match layout.as_str() {
138 "GrayScale" => Some(1),
139 "RG" => Some(2),
140 "RGB" => Some(3),
141 "RGBA" => Some(4),
142 _ => None,
143 },
144 Some(Value::Number(number)) => number
145 .as_u64()
146 .map(|v| v as usize)
147 .filter(|v| (1..=4).contains(v)),
148 _ => None,
149 }
150}
151
152fn encoder_variant_payload<'a>(encoder_properties: &'a Value, variant: &str) -> Option<&'a Value> {
153 let object = encoder_properties.as_object()?;
154 if let Some(payload) = object.get(variant) {
155 return Some(payload);
156 }
157 if variant == "CartesianPlane" && object.contains_key("image_resolution") {
159 return Some(encoder_properties);
160 }
161 if variant == "SegmentedImageFrame" && object.contains_key("segment_xy_resolutions") {
162 return Some(encoder_properties);
163 }
164 None
165}
166
167fn extract_cartesian_plane_dimensions(encoder_properties: &Value) -> Option<(usize, usize, usize)> {
168 let payload = encoder_variant_payload(encoder_properties, "CartesianPlane")?;
169 let resolution = payload.get("image_resolution")?;
170 let width = as_nonzero_usize(resolution.get("width"))?;
171 let height = as_nonzero_usize(resolution.get("height"))?;
172 let channels = color_channel_count_from_value(payload.get("color_channel_layout"))?;
173 Some((width, height, channels))
174}
175
176fn extract_segmented_vision_dimensions(
177 encoder_properties: &Value,
178 sub_unit_index: usize,
179) -> Option<(usize, usize, usize)> {
180 let payload = encoder_variant_payload(encoder_properties, "SegmentedImageFrame")?;
181 let resolutions = payload.get("segment_xy_resolutions")?.as_object()?;
182 let segment_key = match sub_unit_index {
183 0 => "lower_left",
184 1 => "lower_middle",
185 2 => "lower_right",
186 3 => "middle_left",
187 4 => "center",
188 5 => "middle_right",
189 6 => "upper_left",
190 7 => "upper_middle",
191 8 => "upper_right",
192 _ => return None,
193 };
194 let segment_resolution = resolutions.get(segment_key)?;
195 let width = as_nonzero_usize(segment_resolution.get("width"))?;
196 let height = as_nonzero_usize(segment_resolution.get("height"))?;
197 let channels = if sub_unit_index == 4 {
198 color_channel_count_from_value(payload.get("center_color_channel"))?
199 } else {
200 color_channel_count_from_value(payload.get("peripheral_color_channels"))?
201 };
202 Some((width, height, channels))
203}
204
205fn resolve_sensory_dimensions_from_encoder_properties(
206 encoder_properties: Option<&Value>,
207 sub_unit_index: usize,
208 fallback: (usize, usize, usize),
209) -> (usize, usize, usize) {
210 let Some(encoder_properties) = encoder_properties else {
211 return fallback;
212 };
213 extract_cartesian_plane_dimensions(encoder_properties)
214 .or_else(|| extract_segmented_vision_dimensions(encoder_properties, sub_unit_index))
215 .unwrap_or(fallback)
216}
217
218pub async fn auto_create_cortical_areas_from_device_registrations(
219 state: &ApiState,
220 device_registrations: &serde_json::Value,
221) {
222 let config = match load_config(None, None) {
223 Ok(config) => config,
224 Err(e) => {
225 warn!(
226 "⚠️ [API] Failed to load FEAGI configuration for auto-create: {}",
227 e
228 );
229 return;
230 }
231 };
232
233 if !config.agent.auto_create_missing_cortical_areas {
234 return;
235 }
236
237 let connectome_service = state.connectome_service.as_ref();
238 let genome_service = state.genome_service.as_ref();
239
240 let root_region_id = connectome_service.get_root_region_id().await.ok().flatten();
242 let existing_segmented_vision_yz_by_subunit = connectome_service
243 .list_cortical_areas()
244 .await
245 .ok()
246 .and_then(|areas| {
247 let mut grouped_yz_by_group: HashMap<u8, HashMap<u8, (i32, i32)>> = HashMap::new();
248
249 for area in areas {
250 let Ok(bytes) = general_purpose::STANDARD.decode(&area.cortical_id) else {
251 continue;
252 };
253 if bytes.len() != 8 || bytes[0] != b'i' || &bytes[1..4] != b"svi" {
254 continue;
255 }
256 let subunit_index = bytes[6];
257 let group_index = bytes[7];
258 grouped_yz_by_group
259 .entry(group_index)
260 .or_default()
261 .insert(subunit_index, (area.position.1, area.position.2));
262 }
263
264 let selected_group = grouped_yz_by_group
267 .iter()
268 .max_by(|(group_a, map_a), (group_b, map_b)| {
269 map_a
270 .len()
271 .cmp(&map_b.len())
272 .then_with(|| group_b.cmp(group_a))
273 })
274 .map(|(group_index, _)| *group_index)?;
275 let selected_map = grouped_yz_by_group.remove(&selected_group)?;
276 if selected_map.len()
277 == SensoryCorticalUnit::SegmentedVision.get_number_cortical_areas()
278 {
279 Some(selected_map)
280 } else {
281 None
282 }
283 });
284
285 let output_units = device_registrations
286 .get("output_units_and_decoder_properties")
287 .and_then(|v| v.as_object());
288 let input_units = device_registrations
289 .get("input_units_and_encoder_properties")
290 .and_then(|v| v.as_object());
291 if output_units.is_none() && input_units.is_none() {
292 return;
293 }
294
295 let mut to_create: Vec<CreateCorticalAreaParams> = Vec::new();
297
298 if let Some(output_units) = output_units {
299 for (motor_unit_key, unit_defs) in output_units {
300 let motor_unit: MotorCorticalUnit = match serde_json::from_value::<MotorCorticalUnit>(
302 serde_json::Value::String(motor_unit_key.clone()),
303 ) {
304 Ok(v) => v,
305 Err(e) => {
306 warn!(
307 "⚠️ [API] Unable to parse MotorCorticalUnit key '{}' from device_registrations: {}",
308 motor_unit_key, e
309 );
310 continue;
311 }
312 };
313
314 let Some(unit_defs_arr) = unit_defs.as_array() else {
315 continue;
316 };
317
318 for entry in unit_defs_arr {
319 let Some(pair) = entry.as_array() else {
321 continue;
322 };
323 let Some(unit_def) = pair.first() else {
324 continue;
325 };
326 let Some(group_u64) = unit_def.get("cortical_unit_index").and_then(|v| v.as_u64())
327 else {
328 continue;
329 };
330 let group_u8: u8 = match group_u64.try_into() {
331 Ok(v) => v,
332 Err(_) => continue,
333 };
334 let group: CorticalUnitIndex = group_u8.into();
335
336 let device_count = unit_def
337 .get("device_grouping")
338 .and_then(|v| v.as_array())
339 .map(|a| a.len())
340 .unwrap_or(0);
341 if device_count == 0 {
342 warn!(
343 "⚠️ [API] device_grouping is empty for motor unit '{}' group {}; skipping auto-create",
344 motor_unit_key, group_u8
345 );
346 continue;
347 }
348
349 let config_map = match build_io_config_map_from_unit_def(unit_def) {
350 Ok(map) => map,
351 Err(e) => {
352 warn!(
353 "⚠️ [API] Failed to build motor IO config map from registration for '{}' group {}: {}",
354 motor_unit_key, group_u8, e
355 );
356 continue;
357 }
358 };
359 let topology = motor_unit.get_unit_default_topology();
360
361 let cortical_ids = match motor_unit
362 .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
363 group, config_map,
364 ) {
365 Ok(ids) => ids,
366 Err(e) => {
367 warn!(
368 "⚠️ [API] Failed to derive motor cortical IDs for '{}' group {}: {}",
369 motor_unit_key, group_u8, e
370 );
371 continue;
372 }
373 };
374
375 let mut expected_dimensions_by_sub: Vec<Option<(usize, usize, usize)>> =
379 vec![None; cortical_ids.len()];
380 let mut expected_position_by_sub: Vec<Option<(i32, i32, i32)>> =
381 vec![None; cortical_ids.len()];
382 let mut previous_position_x: Option<i32> = None;
383 let mut previous_width: Option<i32> = None;
384 for i in 0..cortical_ids.len() {
385 let sub_index = CorticalSubUnitIndex::from(i as u8);
386 let Some(unit_topology) = topology.get(&sub_index) else {
387 continue;
388 };
389 let per_channel_width = unit_topology.channel_dimensions_default[0] as usize;
390 let per_channel_height = unit_topology.channel_dimensions_default[1] as usize;
391 let per_channel_depth = unit_topology.channel_dimensions_default[2] as usize;
392 let expected_dimensions = (
393 (per_channel_width * device_count).max(1),
394 per_channel_height,
395 per_channel_depth,
396 );
397 expected_dimensions_by_sub[i] = Some(expected_dimensions);
398
399 let y = unit_topology.relative_position[1] + (group_u8 as i32 * 20);
400 let z = unit_topology.relative_position[2];
401 let width_i32 = expected_dimensions.0 as i32;
402 let x = if let (Some(prev_x), Some(_prev_w)) =
403 (previous_position_x, previous_width)
404 {
405 prev_x - width_i32 - MOTOR_AREA_X_GAP_VOXELS
410 } else {
411 unit_topology.relative_position[0]
412 };
413 expected_position_by_sub[i] = Some((x, y, z));
414 previous_position_x = Some(x);
415 previous_width = Some(width_i32);
416 }
417
418 for (i, cortical_id) in cortical_ids.iter().enumerate() {
419 let cortical_id_b64 = cortical_id.as_base_64();
420 let legacy_default_name =
421 build_friendly_unit_name(motor_unit.get_friendly_name(), group_u8, i);
422 let resolved_base_name =
423 resolve_registration_name(unit_def, &legacy_default_name);
424 let resolved_name =
425 if resolved_base_name == legacy_default_name || cortical_ids.len() == 1 {
426 resolved_base_name.clone()
427 } else {
428 format!("{}-{}", resolved_base_name, i)
429 };
430 let exists = match connectome_service
431 .cortical_area_exists(&cortical_id_b64)
432 .await
433 {
434 Ok(v) => v,
435 Err(e) => {
436 warn!(
437 "⚠️ [API] Failed to check cortical area existence for '{}': {}",
438 cortical_id_b64, e
439 );
440 continue;
441 }
442 };
443
444 let sub_index = CorticalSubUnitIndex::from(i as u8);
445 let unit_topology = match topology.get(&sub_index) {
446 Some(t) => t,
447 None => {
448 warn!(
449 "⚠️ [API] Missing unit topology for motor unit '{}' subunit {}; skipping",
450 motor_unit_key, i
451 );
452 continue;
453 }
454 };
455 let expected_position = match expected_position_by_sub.get(i).and_then(|v| *v) {
456 Some(pos) => pos,
457 None => {
458 warn!(
459 "⚠️ [API] Missing precomputed motor position for '{}' subunit {}; skipping",
460 motor_unit_key, i
461 );
462 continue;
463 }
464 };
465 let expected_dimensions = match expected_dimensions_by_sub
466 .get(i)
467 .and_then(|v| *v)
468 {
469 Some(dims) => dims,
470 None => {
471 warn!(
472 "⚠️ [API] Missing precomputed motor dimensions for '{}' subunit {}; skipping",
473 motor_unit_key, i
474 );
475 continue;
476 }
477 };
478
479 if exists {
480 let current =
485 match connectome_service.get_cortical_area(&cortical_id_b64).await {
486 Ok(v) => v,
487 Err(e) => {
488 warn!(
489 "⚠️ [API] Failed to fetch existing cortical area '{}': {}",
490 cortical_id_b64, e
491 );
492 continue;
493 }
494 };
495
496 let current_dev_count = current
497 .properties
498 .get("dev_count")
499 .and_then(|v| v.as_u64())
500 .map(|u| u as usize)
501 .or(current.dev_count);
502 let dimensions_mismatch = current.dimensions != expected_dimensions;
503 let dev_count_mismatch = current_dev_count != Some(device_count);
504
505 if dimensions_mismatch || dev_count_mismatch {
506 let mut changes: HashMap<String, serde_json::Value> = HashMap::new();
507 changes.insert(
510 "dimensions".to_string(),
511 serde_json::json!([
512 expected_dimensions.0,
513 expected_dimensions.1,
514 expected_dimensions.2
515 ]),
516 );
517 changes.insert(
518 "dev_count".to_string(),
519 serde_json::Value::Number(serde_json::Number::from(device_count)),
520 );
521 if let Err(e) = genome_service
522 .update_cortical_area(&cortical_id_b64, changes)
523 .await
524 {
525 warn!(
526 "⚠️ [API] Failed to update cortical area '{}' dimensions/dev_count: {}",
527 cortical_id_b64, e
528 );
529 } else {
530 info!(
531 "[API] Updated cortical area '{}' to {} channels (dimensions {:?})",
532 cortical_id_b64, device_count, expected_dimensions
533 );
534 }
535 }
536
537 if should_auto_rename(¤t.name, &cortical_id_b64, &legacy_default_name)
539 {
540 let desired_name = resolved_name.clone();
541 let mut changes: HashMap<String, serde_json::Value> = HashMap::new();
542 changes.insert(
543 "name".to_string(),
544 serde_json::Value::String(desired_name),
545 );
546 if let Err(e) = genome_service
547 .update_cortical_area(&cortical_id_b64, changes)
548 .await
549 {
550 warn!(
551 "⚠️ [API] Failed to auto-rename existing motor cortical area '{}': {}",
552 cortical_id_b64, e
553 );
554 }
555 }
556 continue;
557 }
558
559 let friendly_name = resolved_name;
560 let (per_channel_width, per_channel_height, per_channel_depth) = (
563 unit_topology.channel_dimensions_default[0] as usize,
564 unit_topology.channel_dimensions_default[1] as usize,
565 unit_topology.channel_dimensions_default[2] as usize,
566 );
567 let dimensions = expected_dimensions;
568 let per_device_dims =
569 (per_channel_width, per_channel_height, per_channel_depth);
570 let position = expected_position;
571
572 let mut properties = HashMap::new();
573 properties.insert(
574 "dev_count".to_string(),
575 serde_json::Value::Number(serde_json::Number::from(device_count)),
576 );
577 properties.insert(
578 "cortical_dimensions_per_device".to_string(),
579 serde_json::json!([
580 per_device_dims.0,
581 per_device_dims.1,
582 per_device_dims.2
583 ]),
584 );
585 if let Some(unit_name) = non_empty_string(unit_def.get("friendly_name")) {
586 properties.insert(
587 "registration_unit_friendly_name".to_string(),
588 serde_json::Value::String(unit_name),
589 );
590 }
591 if let Some(bundle_id) = first_grouping_property(unit_def, "bundle_id") {
592 properties.insert(
593 "registration_bundle_id".to_string(),
594 serde_json::Value::String(bundle_id),
595 );
596 }
597 if let Some(bundle_type) = first_grouping_property(unit_def, "bundle_type") {
598 properties.insert(
599 "registration_bundle_type".to_string(),
600 serde_json::Value::String(bundle_type),
601 );
602 }
603 if let Some(ref rid) = root_region_id {
604 properties.insert(
605 "parent_region_id".to_string(),
606 serde_json::Value::String(rid.clone()),
607 );
608 }
609
610 to_create.push(CreateCorticalAreaParams {
611 cortical_id: cortical_id_b64.clone(),
612 name: friendly_name,
613 dimensions,
614 position,
615 area_type: "motor".to_string(),
616 visible: None,
617 sub_group: None,
618 neurons_per_voxel: None,
619 postsynaptic_current: None,
620 plasticity_constant: None,
621 degeneration: None,
622 psp_uniform_distribution: None,
623 firing_threshold_increment: None,
624 firing_threshold_limit: None,
625 consecutive_fire_count: None,
626 snooze_period: None,
627 refractory_period: None,
628 leak_coefficient: None,
629 leak_variability: None,
630 burst_engine_active: None,
631 properties: Some(properties),
632 });
633 }
634 }
635 }
636 }
637
638 if let Some(input_units) = input_units {
640 for (sensory_unit_key, unit_defs) in input_units {
641 let sensory_unit: SensoryCorticalUnit = match serde_json::from_value::<
642 SensoryCorticalUnit,
643 >(serde_json::Value::String(
644 sensory_unit_key.clone(),
645 )) {
646 Ok(v) => v,
647 Err(e) => {
648 warn!(
649 "⚠️ [API] Unable to parse SensoryCorticalUnit key '{}' from device_registrations: {}",
650 sensory_unit_key, e
651 );
652 continue;
653 }
654 };
655
656 let Some(unit_defs_arr) = unit_defs.as_array() else {
657 continue;
658 };
659
660 for entry in unit_defs_arr {
661 let Some(pair) = entry.as_array() else {
663 continue;
664 };
665 let Some(unit_def) = pair.first() else {
666 continue;
667 };
668 let encoder_properties = pair.get(1);
669 let Some(group_u64) = unit_def.get("cortical_unit_index").and_then(|v| v.as_u64())
670 else {
671 continue;
672 };
673 let group_u8: u8 = match group_u64.try_into() {
674 Ok(v) => v,
675 Err(_) => continue,
676 };
677 let group: CorticalUnitIndex = group_u8.into();
678
679 let device_count = unit_def
680 .get("device_grouping")
681 .and_then(|v| v.as_array())
682 .map(|a| a.len())
683 .unwrap_or(0);
684 if device_count == 0 {
685 warn!(
686 "⚠️ [API] device_grouping is empty for sensory unit '{}' group {}; skipping auto-create",
687 sensory_unit_key, group_u8
688 );
689 continue;
690 }
691
692 let config_map = match build_io_config_map() {
693 Ok(map) => map,
694 Err(e) => {
695 warn!(
696 "⚠️ [API] Failed to build sensory IO config map for '{}' group {}: {}",
697 sensory_unit_key, group_u8, e
698 );
699 continue;
700 }
701 };
702 let cortical_ids = match sensory_unit
703 .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
704 group, config_map,
705 ) {
706 Ok(ids) => ids,
707 Err(e) => {
708 warn!(
709 "⚠️ [API] Failed to derive sensory cortical IDs for '{}' group {}: {}",
710 sensory_unit_key, group_u8, e
711 );
712 continue;
713 }
714 };
715 let topology = sensory_unit.get_unit_default_topology();
716 let segmented_group_x_offsets =
717 if sensory_unit == SensoryCorticalUnit::SegmentedVision {
718 let mut bounds_by_group: Vec<(u8, i32, i32)> = Vec::new();
721 for grouped_entry in unit_defs_arr {
722 let Some(grouped_pair) = grouped_entry.as_array() else {
723 continue;
724 };
725 let Some(grouped_def) = grouped_pair.first() else {
726 continue;
727 };
728 let Some(grouped_u64) = grouped_def
729 .get("cortical_unit_index")
730 .and_then(|v| v.as_u64())
731 else {
732 continue;
733 };
734 let Ok(grouped_u8) = u8::try_from(grouped_u64) else {
735 continue;
736 };
737 let grouped_encoder_properties = grouped_pair.get(1);
738
739 let mut assembly_min_x: Option<i32> = None;
740 let mut assembly_max_x: Option<i32> = None;
741 for (sub_index, unit_topology) in &topology {
742 let sub_idx_usize = sub_index.get() as usize;
743 let dimensions = resolve_sensory_dimensions_from_encoder_properties(
744 grouped_encoder_properties,
745 sub_idx_usize,
746 (
747 unit_topology.channel_dimensions_default[0] as usize,
748 unit_topology.channel_dimensions_default[1] as usize,
749 unit_topology.channel_dimensions_default[2] as usize,
750 ),
751 );
752 let rel_x = unit_topology.relative_position[0];
753 let right_edge_x = rel_x.saturating_add(dimensions.0 as i32);
754
755 assembly_min_x = Some(match assembly_min_x {
756 Some(current) => current.min(rel_x),
757 None => rel_x,
758 });
759 assembly_max_x = Some(match assembly_max_x {
760 Some(current) => current.max(right_edge_x),
761 None => right_edge_x,
762 });
763 }
764
765 if let (Some(min_x), Some(max_x)) = (assembly_min_x, assembly_max_x) {
766 bounds_by_group.push((grouped_u8, min_x, max_x));
767 }
768 }
769
770 bounds_by_group.sort_by_key(|(grouped_u8, _, _)| *grouped_u8);
773
774 let mut offsets: HashMap<u8, i32> = HashMap::new();
775 let mut previous_shifted_max_x: Option<i32> = None;
776 for (grouped_u8, min_x, max_x) in bounds_by_group {
777 let offset_x = if let Some(prev_max_x) = previous_shifted_max_x {
778 prev_max_x + SEGMENTED_VISION_GROUP_X_GAP_VOXELS - min_x
779 } else {
780 0
781 };
782 previous_shifted_max_x = Some(max_x.saturating_add(offset_x));
783 offsets.insert(grouped_u8, offset_x);
784 }
785 offsets
786 } else {
787 HashMap::new()
788 };
789
790 for (i, cortical_id) in cortical_ids.iter().enumerate() {
791 let cortical_id_b64 = cortical_id.as_base_64();
792 let sub_index = CorticalSubUnitIndex::from(i as u8);
793 let unit_topology = match topology.get(&sub_index) {
794 Some(topology) => topology,
795 None => {
796 warn!(
797 "⚠️ [API] Missing unit topology for sensory unit '{}' subunit {} (agent device_registrations); cannot auto-create/update '{}'",
798 sensory_unit_key, i, cortical_id_b64
799 );
800 continue;
801 }
802 };
803 let expected_dimensions = resolve_sensory_dimensions_from_encoder_properties(
804 encoder_properties,
805 i,
806 (
807 unit_topology.channel_dimensions_default[0] as usize,
808 unit_topology.channel_dimensions_default[1] as usize,
809 unit_topology.channel_dimensions_default[2] as usize,
810 ),
811 );
812 let group_x_offset = *segmented_group_x_offsets.get(&group_u8).unwrap_or(&0);
813 let existing_segmented_yz =
814 if sensory_unit == SensoryCorticalUnit::SegmentedVision {
815 existing_segmented_vision_yz_by_subunit
816 .as_ref()
817 .and_then(|yz_by_subunit| yz_by_subunit.get(&(i as u8)).copied())
818 } else {
819 None
820 };
821 let expected_position = (
822 unit_topology.relative_position[0] + group_x_offset,
823 existing_segmented_yz
824 .map(|(y, _)| y)
825 .unwrap_or(unit_topology.relative_position[1]),
826 existing_segmented_yz
827 .map(|(_, z)| z)
828 .unwrap_or(unit_topology.relative_position[2]),
829 );
830 let legacy_default_name =
831 build_friendly_unit_name(sensory_unit.get_friendly_name(), group_u8, i);
832 let resolved_base_name =
833 resolve_registration_name(unit_def, &legacy_default_name);
834 let resolved_name =
835 if resolved_base_name == legacy_default_name || cortical_ids.len() == 1 {
836 resolved_base_name.clone()
837 } else {
838 format!("{}-{}", resolved_base_name, i)
839 };
840 let exists = match connectome_service
841 .cortical_area_exists(&cortical_id_b64)
842 .await
843 {
844 Ok(v) => v,
845 Err(e) => {
846 warn!(
847 "⚠️ [API] Failed to check cortical area existence for '{}': {}",
848 cortical_id_b64, e
849 );
850 continue;
851 }
852 };
853
854 if exists {
855 let current = match connectome_service
859 .get_cortical_area(&cortical_id_b64)
860 .await
861 {
862 Ok(v) => v,
863 Err(e) => {
864 warn!(
865 "⚠️ [API] Failed to fetch existing cortical area '{}' for potential rename: {}",
866 cortical_id_b64, e
867 );
868 continue;
869 }
870 };
871 let current_dev_count = current
872 .properties
873 .get("dev_count")
874 .and_then(|v| v.as_u64())
875 .map(|u| u as usize)
876 .or(current.dev_count);
877 let dimensions_mismatch = current.dimensions != expected_dimensions;
878 let dev_count_mismatch = current_dev_count != Some(device_count);
879 if dimensions_mismatch || dev_count_mismatch {
880 let mut changes: HashMap<String, serde_json::Value> = HashMap::new();
881 changes.insert(
882 "dimensions".to_string(),
883 serde_json::json!([
884 expected_dimensions.0,
885 expected_dimensions.1,
886 expected_dimensions.2
887 ]),
888 );
889 changes.insert(
890 "dev_count".to_string(),
891 serde_json::Value::Number(serde_json::Number::from(device_count)),
892 );
893 if let Err(e) = genome_service
894 .update_cortical_area(&cortical_id_b64, changes)
895 .await
896 {
897 warn!(
898 "⚠️ [API] Failed to update sensory cortical area '{}' dimensions/dev_count: {}",
899 cortical_id_b64, e
900 );
901 } else {
902 info!(
903 "[API] Updated sensory cortical area '{}' to registration dimensions {:?} (dev_count {})",
904 cortical_id_b64, expected_dimensions, device_count
905 );
906 }
907 }
908
909 if should_auto_rename(¤t.name, &cortical_id_b64, &legacy_default_name)
913 {
914 let desired_name = resolved_name.clone();
915 let mut changes: HashMap<String, serde_json::Value> = HashMap::new();
916 changes.insert(
917 "name".to_string(),
918 serde_json::Value::String(desired_name),
919 );
920 if let Err(e) = genome_service
921 .update_cortical_area(&cortical_id_b64, changes)
922 .await
923 {
924 warn!(
925 "⚠️ [API] Failed to auto-rename existing sensory cortical area '{}': {}",
926 cortical_id_b64, e
927 );
928 }
929 }
930 continue;
931 }
932
933 let friendly_name = resolved_name;
934 let dimensions = expected_dimensions;
935 let position = expected_position;
936 let mut properties: HashMap<String, serde_json::Value> = HashMap::new();
937 properties.insert(
938 "cortical_subunit_index".to_string(),
939 serde_json::Value::Number(serde_json::Number::from(sub_index.get())),
940 );
941 properties.insert(
942 "dev_count".to_string(),
943 serde_json::Value::Number(serde_json::Number::from(device_count)),
944 );
945 if let Some(unit_name) = non_empty_string(unit_def.get("friendly_name")) {
946 properties.insert(
947 "registration_unit_friendly_name".to_string(),
948 serde_json::Value::String(unit_name),
949 );
950 }
951 if let Some(bundle_id) = first_grouping_property(unit_def, "bundle_id") {
952 properties.insert(
953 "registration_bundle_id".to_string(),
954 serde_json::Value::String(bundle_id),
955 );
956 }
957 if let Some(bundle_type) = first_grouping_property(unit_def, "bundle_type") {
958 properties.insert(
959 "registration_bundle_type".to_string(),
960 serde_json::Value::String(bundle_type),
961 );
962 }
963 if let Some(ref rid) = root_region_id {
964 properties.insert(
965 "parent_region_id".to_string(),
966 serde_json::Value::String(rid.clone()),
967 );
968 }
969 if let Some(default_firing_threshold) =
970 sensory_unit.get_default_firing_threshold()
971 {
972 properties.insert(
973 "firing_threshold".to_string(),
974 serde_json::json!(default_firing_threshold),
975 );
976 }
977 if let Some(default_mp_charge_accumulation) =
978 sensory_unit.get_default_mp_charge_accumulation()
979 {
980 properties.insert(
981 "mp_charge_accumulation".to_string(),
982 serde_json::json!(default_mp_charge_accumulation),
983 );
984 }
985
986 to_create.push(CreateCorticalAreaParams {
987 cortical_id: cortical_id_b64.clone(),
988 name: friendly_name,
989 dimensions,
990 position,
991 area_type: "sensory".to_string(),
992 visible: None,
993 sub_group: None,
994 neurons_per_voxel: None,
995 postsynaptic_current: None,
996 plasticity_constant: None,
997 degeneration: None,
998 psp_uniform_distribution: None,
999 firing_threshold_increment: None,
1000 firing_threshold_limit: None,
1001 consecutive_fire_count: None,
1002 snooze_period: None,
1003 refractory_period: None,
1004 leak_coefficient: None,
1005 leak_variability: None,
1006 burst_engine_active: None,
1007 properties: Some(properties),
1008 });
1009 }
1010 }
1011 }
1012 }
1013
1014 if to_create.is_empty() {
1015 return;
1016 }
1017
1018 info!(
1019 "🦀 [API] Auto-creating {} missing cortical areas from device registrations",
1020 to_create.len()
1021 );
1022
1023 if let Err(e) = genome_service.create_cortical_areas(to_create).await {
1024 warn!(
1025 "⚠️ [API] Failed to auto-create cortical areas from device registrations: {}",
1026 e
1027 );
1028 }
1029}
1030
1031pub fn derive_motor_cortical_ids_from_device_registrations(
1032 device_registrations: &serde_json::Value,
1033) -> Result<HashSet<String>, String> {
1034 let output_units = device_registrations
1035 .get("output_units_and_decoder_properties")
1036 .and_then(|v| v.as_object())
1037 .ok_or_else(|| {
1038 "device_registrations missing output_units_and_decoder_properties".to_string()
1039 })?;
1040
1041 let mut cortical_ids: HashSet<String> = HashSet::new();
1042
1043 for (motor_unit_key, unit_defs) in output_units {
1044 let motor_unit: MotorCorticalUnit = serde_json::from_value::<MotorCorticalUnit>(
1045 serde_json::Value::String(motor_unit_key.clone()),
1046 )
1047 .map_err(|e| {
1048 format!(
1049 "Unable to parse MotorCorticalUnit key '{}': {}",
1050 motor_unit_key, e
1051 )
1052 })?;
1053
1054 let unit_defs_arr = unit_defs
1055 .as_array()
1056 .ok_or_else(|| "Motor unit definitions must be an array".to_string())?;
1057
1058 for entry in unit_defs_arr {
1059 let pair = entry
1060 .as_array()
1061 .ok_or_else(|| "Motor unit definition entries must be arrays".to_string())?;
1062 let unit_def = pair
1063 .first()
1064 .ok_or_else(|| "Motor unit definition entry missing unit_def".to_string())?;
1065 let group_u64 = unit_def
1066 .get("cortical_unit_index")
1067 .and_then(|v| v.as_u64())
1068 .ok_or_else(|| "Motor unit definition missing cortical_unit_index".to_string())?;
1069 let group_u8: u8 = group_u64
1070 .try_into()
1071 .map_err(|_| "Motor unit cortical_unit_index out of range for u8".to_string())?;
1072 let group: CorticalUnitIndex = group_u8.into();
1073
1074 let device_count = unit_def
1075 .get("device_grouping")
1076 .and_then(|v| v.as_array())
1077 .map(|a| a.len())
1078 .unwrap_or(0);
1079 if device_count == 0 {
1080 return Err(format!(
1081 "device_grouping is empty for motor unit '{}' group {}",
1082 motor_unit_key, group_u8
1083 ));
1084 }
1085
1086 let config = build_io_config_map_from_unit_def(unit_def).map_err(|e| {
1087 format!(
1088 "Failed to build motor IO config map from registration for '{}' group {}: {}",
1089 motor_unit_key, group_u8, e
1090 )
1091 })?;
1092 let unit_cortical_ids = motor_unit
1093 .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(group, config)
1094 .map_err(|e| format!("Failed to derive cortical IDs: {}", e))?;
1095 for cortical_id in unit_cortical_ids {
1096 cortical_ids.insert(cortical_id.as_base_64());
1097 }
1098 }
1099 }
1100
1101 Ok(cortical_ids)
1102}
1103
1104pub fn derive_sensory_cortical_ids_from_device_registrations(
1105 device_registrations: &serde_json::Value,
1106) -> Result<HashSet<String>, String> {
1107 let input_units = device_registrations
1108 .get("input_units_and_encoder_properties")
1109 .and_then(|v| v.as_object())
1110 .ok_or_else(|| {
1111 "device_registrations missing input_units_and_encoder_properties".to_string()
1112 })?;
1113
1114 let mut cortical_ids: HashSet<String> = HashSet::new();
1115
1116 for (sensory_unit_key, unit_defs) in input_units {
1117 let sensory_unit: SensoryCorticalUnit = serde_json::from_value::<SensoryCorticalUnit>(
1118 serde_json::Value::String(sensory_unit_key.clone()),
1119 )
1120 .map_err(|e| {
1121 format!(
1122 "Unable to parse SensoryCorticalUnit key '{}': {}",
1123 sensory_unit_key, e
1124 )
1125 })?;
1126
1127 let unit_defs_arr = unit_defs
1128 .as_array()
1129 .ok_or_else(|| "Sensory unit definitions must be an array".to_string())?;
1130
1131 for entry in unit_defs_arr {
1132 let pair = entry
1133 .as_array()
1134 .ok_or_else(|| "Sensory unit definition entries must be arrays".to_string())?;
1135 let unit_def = pair
1136 .first()
1137 .ok_or_else(|| "Sensory unit definition entry missing unit_def".to_string())?;
1138 let group_u64 = unit_def
1139 .get("cortical_unit_index")
1140 .and_then(|v| v.as_u64())
1141 .ok_or_else(|| "Sensory unit definition missing cortical_unit_index".to_string())?;
1142 let group_u8: u8 = group_u64
1143 .try_into()
1144 .map_err(|_| "Sensory unit cortical_unit_index out of range for u8".to_string())?;
1145 let group: CorticalUnitIndex = group_u8.into();
1146
1147 let device_count = unit_def
1148 .get("device_grouping")
1149 .and_then(|v| v.as_array())
1150 .map(|a| a.len())
1151 .unwrap_or(0);
1152 if device_count == 0 {
1153 return Err(format!(
1154 "device_grouping is empty for sensory unit '{}' group {}",
1155 sensory_unit_key, group_u8
1156 ));
1157 }
1158
1159 let config = build_io_config_map()
1160 .map_err(|e| format!("Failed to build sensory IO config map: {}", e))?;
1161 let unit_cortical_ids = sensory_unit
1162 .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(group, config)
1163 .map_err(|e| format!("Failed to derive cortical IDs: {}", e))?;
1164 for cortical_id in unit_cortical_ids {
1165 cortical_ids.insert(cortical_id.as_base_64());
1166 }
1167 }
1168 }
1169
1170 Ok(cortical_ids)
1171}