1use base64::{engine::general_purpose, Engine as _};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12use crate::common::ApiState;
13use crate::common::{ApiError, ApiResult, Json, Path, Query, State};
14use feagi_structures::genomic::cortical_area::descriptors::CorticalSubUnitIndex;
15use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
16
17#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
22pub struct CorticalAreaIdListResponse {
23 pub cortical_ids: Vec<String>,
24}
25
26#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
27pub struct CorticalAreaNameListResponse {
28 pub cortical_area_name_list: Vec<String>,
29}
30
31#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
32pub struct UnitTopologyData {
33 pub relative_position: [i32; 3],
34 pub dimensions: [u32; 3],
35}
36
37#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
38pub struct CorticalTypeMetadata {
39 pub description: String,
40 pub encodings: Vec<String>,
41 pub formats: Vec<String>,
42 pub units: u32,
43 pub resolution: Vec<i32>,
44 pub structure: String,
45 pub unit_default_topology: HashMap<usize, UnitTopologyData>,
46}
47
48#[utoipa::path(get, path = "/v1/cortical_area/ipu", tag = "cortical_area")]
54pub async fn get_ipu(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
55 let connectome_service = state.connectome_service.as_ref();
56 match connectome_service.list_cortical_areas().await {
57 Ok(areas) => {
58 let ipu_areas: Vec<String> = areas
59 .into_iter()
60 .filter(|a| a.area_type == "sensory" || a.area_type == "IPU")
61 .map(|a| a.cortical_id)
62 .collect();
63 Ok(Json(ipu_areas))
64 }
65 Err(e) => Err(ApiError::internal(format!(
66 "Failed to get IPU areas: {}",
67 e
68 ))),
69 }
70}
71
72#[utoipa::path(get, path = "/v1/cortical_area/opu", tag = "cortical_area")]
74pub async fn get_opu(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
75 let connectome_service = state.connectome_service.as_ref();
76 match connectome_service.list_cortical_areas().await {
77 Ok(areas) => {
78 let opu_areas: Vec<String> = areas
79 .into_iter()
80 .filter(|a| a.area_type == "motor" || a.area_type == "OPU")
81 .map(|a| a.cortical_id)
82 .collect();
83 Ok(Json(opu_areas))
84 }
85 Err(e) => Err(ApiError::internal(format!(
86 "Failed to get OPU areas: {}",
87 e
88 ))),
89 }
90}
91
92#[utoipa::path(
94 get,
95 path = "/v1/cortical_area/cortical_area_id_list",
96 tag = "cortical_area",
97 responses(
98 (status = 200, description = "Cortical area IDs retrieved successfully", body = CorticalAreaIdListResponse),
99 (status = 500, description = "Internal server error", body = ApiError)
100 )
101)]
102pub async fn get_cortical_area_id_list(
103 State(state): State<ApiState>,
104) -> ApiResult<Json<CorticalAreaIdListResponse>> {
105 tracing::debug!(target: "feagi-api", "π GET /v1/cortical_area/cortical_area_id_list - handler called");
106 let connectome_service = state.connectome_service.as_ref();
107 match connectome_service.get_cortical_area_ids().await {
108 Ok(ids) => {
109 tracing::info!(target: "feagi-api", "β
GET /v1/cortical_area/cortical_area_id_list - success, returning {} IDs", ids.len());
110 tracing::debug!(target: "feagi-api", "π Cortical area IDs: {:?}", ids.iter().take(20).collect::<Vec<_>>());
111 let response = CorticalAreaIdListResponse {
112 cortical_ids: ids.clone(),
113 };
114 match serde_json::to_string(&response) {
115 Ok(json_str) => {
116 tracing::debug!(target: "feagi-api", "π€ Response JSON: {}", json_str);
117 }
118 Err(e) => {
119 tracing::warn!(target: "feagi-api", "β οΈ Failed to serialize response: {}", e);
120 }
121 }
122 Ok(Json(response))
123 }
124 Err(e) => {
125 tracing::error!(target: "feagi-api", "β GET /v1/cortical_area/cortical_area_id_list - error: {}", e);
126 Err(ApiError::internal(format!(
127 "Failed to get cortical IDs: {}",
128 e
129 )))
130 }
131 }
132}
133
134#[utoipa::path(
136 get,
137 path = "/v1/cortical_area/cortical_area_name_list",
138 tag = "cortical_area",
139 responses(
140 (status = 200, description = "Cortical area names retrieved successfully", body = CorticalAreaNameListResponse),
141 (status = 500, description = "Internal server error")
142 )
143)]
144pub async fn get_cortical_area_name_list(
145 State(state): State<ApiState>,
146) -> ApiResult<Json<CorticalAreaNameListResponse>> {
147 let connectome_service = state.connectome_service.as_ref();
148 match connectome_service.list_cortical_areas().await {
149 Ok(areas) => {
150 let names: Vec<String> = areas.into_iter().map(|a| a.name).collect();
151 Ok(Json(CorticalAreaNameListResponse {
152 cortical_area_name_list: names,
153 }))
154 }
155 Err(e) => Err(ApiError::internal(format!(
156 "Failed to get cortical names: {}",
157 e
158 ))),
159 }
160}
161
162#[utoipa::path(
164 get,
165 path = "/v1/cortical_area/cortical_id_name_mapping",
166 tag = "cortical_area"
167)]
168pub async fn get_cortical_id_name_mapping(
169 State(state): State<ApiState>,
170) -> ApiResult<Json<HashMap<String, String>>> {
171 let connectome_service = state.connectome_service.as_ref();
172 let ids = connectome_service
173 .get_cortical_area_ids()
174 .await
175 .map_err(|e| ApiError::internal(format!("Failed to get IDs: {}", e)))?;
176
177 let mut mapping = HashMap::new();
178 for id in ids {
179 if let Ok(area) = connectome_service.get_cortical_area(&id).await {
180 mapping.insert(id, area.name);
181 }
182 }
183 Ok(Json(mapping))
184}
185
186#[utoipa::path(get, path = "/v1/cortical_area/cortical_types", tag = "cortical_area")]
188pub async fn get_cortical_types(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
189 Ok(Json(vec![
190 "sensory".to_string(),
191 "motor".to_string(),
192 "memory".to_string(),
193 "custom".to_string(),
194 ]))
195}
196
197#[utoipa::path(
199 get,
200 path = "/v1/cortical_area/cortical_map_detailed",
201 tag = "cortical_area",
202 responses(
203 (status = 200, description = "Detailed cortical area mapping data", body = HashMap<String, serde_json::Value>),
204 (status = 500, description = "Internal server error")
205 )
206)]
207pub async fn get_cortical_map_detailed(
208 State(state): State<ApiState>,
209) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
210 let connectome_service = state.connectome_service.as_ref();
211 match connectome_service.list_cortical_areas().await {
212 Ok(areas) => {
213 let mut map: HashMap<String, serde_json::Value> = HashMap::new();
214
215 for area in areas {
216 if let Some(cortical_mapping_dst) = area.properties.get("cortical_mapping_dst") {
218 if !cortical_mapping_dst.is_null()
219 && cortical_mapping_dst
220 .as_object()
221 .is_some_and(|obj| !obj.is_empty())
222 {
223 map.insert(area.cortical_id.clone(), cortical_mapping_dst.clone());
224 }
225 }
226 }
227
228 Ok(Json(map))
229 }
230 Err(e) => Err(ApiError::internal(format!(
231 "Failed to get detailed map: {}",
232 e
233 ))),
234 }
235}
236
237#[utoipa::path(
239 get,
240 path = "/v1/cortical_area/cortical_locations_2d",
241 tag = "cortical_area"
242)]
243pub async fn get_cortical_locations_2d(
244 State(state): State<ApiState>,
245) -> ApiResult<Json<HashMap<String, (i32, i32)>>> {
246 let connectome_service = state.connectome_service.as_ref();
247 match connectome_service.list_cortical_areas().await {
248 Ok(areas) => {
249 let locations: HashMap<String, (i32, i32)> = areas
250 .into_iter()
251 .map(|area| (area.cortical_id, (area.position.0, area.position.1)))
252 .collect();
253 Ok(Json(locations))
254 }
255 Err(e) => Err(ApiError::internal(format!(
256 "Failed to get 2D locations: {}",
257 e
258 ))),
259 }
260}
261
262#[utoipa::path(
264 get,
265 path = "/v1/cortical_area/cortical_area/geometry",
266 tag = "cortical_area"
267)]
268pub async fn get_cortical_area_geometry(
269 State(state): State<ApiState>,
270) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
271 let connectome_service = state.connectome_service.as_ref();
272 match connectome_service.list_cortical_areas().await {
273 Ok(areas) => {
274 let geometry: HashMap<String, serde_json::Value> = areas.into_iter()
275 .map(|area| {
276 let coordinate_2d = area
279 .properties
280 .get("coordinate_2d")
281 .or_else(|| area.properties.get("coordinates_2d"))
282 .cloned()
283 .unwrap_or_else(|| serde_json::json!([0, 0]));
284 let data = serde_json::json!({
285 "cortical_id": area.cortical_id,
286 "cortical_name": area.name,
287 "cortical_group": area.cortical_group,
288 "cortical_type": area.cortical_type, "cortical_sub_group": area.sub_group.as_ref().unwrap_or(&String::new()), "coordinates_3d": [area.position.0, area.position.1, area.position.2],
291 "coordinates_2d": coordinate_2d,
292 "cortical_dimensions": [area.dimensions.0, area.dimensions.1, area.dimensions.2],
293 "cortical_neuron_per_vox_count": area.neurons_per_voxel,
294 "visualization": area.visible,
295 "visible": area.visible,
296 "dimensions": {
298 "x": area.dimensions.0,
299 "y": area.dimensions.1,
300 "z": area.dimensions.2
301 },
302 "position": {
303 "x": area.position.0,
304 "y": area.position.1,
305 "z": area.position.2
306 },
307 "neuron_post_synaptic_potential": area.postsynaptic_current,
309 "neuron_fire_threshold": area.firing_threshold,
311 "neuron_firing_threshold_limit": area.firing_threshold_limit,
312 "plasticity_constant": area.plasticity_constant,
313 "degeneration": area.degeneration,
314 "leak_coefficient": area.leak_coefficient,
315 "refractory_period": area.refractory_period,
316 "snooze_period": area.snooze_period,
317 "parent_region_id": area.parent_region_id,
319 "visualization_voxel_granularity": area.visualization_voxel_granularity.map(|(x, y, z)| serde_json::json!([x, y, z])),
321 });
322 (area.cortical_id.clone(), data)
323 })
324 .collect();
325 Ok(Json(geometry))
326 }
327 Err(e) => Err(ApiError::internal(format!("Failed to get geometry: {}", e))),
328 }
329}
330
331#[utoipa::path(
333 get,
334 path = "/v1/cortical_area/cortical_visibility",
335 tag = "cortical_area"
336)]
337pub async fn get_cortical_visibility(
338 State(state): State<ApiState>,
339) -> ApiResult<Json<HashMap<String, bool>>> {
340 let connectome_service = state.connectome_service.as_ref();
341 match connectome_service.list_cortical_areas().await {
342 Ok(areas) => {
343 let visibility: HashMap<String, bool> = areas
344 .into_iter()
345 .map(|area| (area.cortical_id, area.visible))
346 .collect();
347 Ok(Json(visibility))
348 }
349 Err(e) => Err(ApiError::internal(format!(
350 "Failed to get visibility: {}",
351 e
352 ))),
353 }
354}
355
356#[utoipa::path(
358 post,
359 path = "/v1/cortical_area/cortical_name_location",
360 tag = "cortical_area"
361)]
362#[allow(unused_variables)] pub async fn post_cortical_name_location(
364 State(state): State<ApiState>,
365 Json(request): Json<HashMap<String, String>>,
366) -> ApiResult<Json<HashMap<String, (i32, i32)>>> {
367 let connectome_service = state.connectome_service.as_ref();
368 let cortical_name = request
369 .get("cortical_name")
370 .ok_or_else(|| ApiError::invalid_input("cortical_name required"))?;
371
372 match connectome_service.get_cortical_area(cortical_name).await {
373 Ok(area) => Ok(Json(HashMap::from([(
374 area.cortical_id,
375 (area.position.0, area.position.1),
376 )]))),
377 Err(e) => Err(ApiError::internal(format!("Failed to get location: {}", e))),
378 }
379}
380
381#[utoipa::path(
383 post,
384 path = "/v1/cortical_area/cortical_area_properties",
385 tag = "cortical_area"
386)]
387#[allow(unused_variables)] pub async fn post_cortical_area_properties(
389 State(state): State<ApiState>,
390 Json(request): Json<HashMap<String, String>>,
391) -> ApiResult<Json<serde_json::Value>> {
392 let connectome_service = state.connectome_service.as_ref();
393 let cortical_id = request
394 .get("cortical_id")
395 .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?;
396
397 match connectome_service.get_cortical_area(cortical_id).await {
398 Ok(area_info) => {
399 tracing::debug!(target: "feagi-api", "Cortical area properties for {}: cortical_group={}, area_type={}, cortical_type={}",
400 cortical_id, area_info.cortical_group, area_info.area_type, area_info.cortical_type);
401 tracing::info!(target: "feagi-api", "[API-RESPONSE] Returning mp_driven_psp={} for area {}", area_info.mp_driven_psp, cortical_id);
402 let json_value = serde_json::to_value(&area_info).unwrap_or_default();
403 tracing::debug!(target: "feagi-api", "Serialized JSON keys: {:?}", json_value.as_object().map(|o| o.keys().collect::<Vec<_>>()));
404 tracing::debug!(target: "feagi-api", "Serialized cortical_type value: {:?}", json_value.get("cortical_type"));
405 Ok(Json(json_value))
406 }
407 Err(e) => Err(ApiError::internal(format!(
408 "Failed to get properties: {}",
409 e
410 ))),
411 }
412}
413
414#[utoipa::path(
416 post,
417 path = "/v1/cortical_area/multi/cortical_area_properties",
418 tag = "cortical_area"
419)]
420#[allow(unused_variables)] pub async fn post_multi_cortical_area_properties(
422 State(state): State<ApiState>,
423 Json(request): Json<serde_json::Value>,
424) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
425 let connectome_service = state.connectome_service.as_ref();
426 let mut result = HashMap::new();
427
428 let cortical_ids: Vec<String> = if request.is_array() {
430 request
432 .as_array()
433 .unwrap()
434 .iter()
435 .filter_map(|v| v.as_str().map(|s| s.to_string()))
436 .collect()
437 } else if request.is_object() {
438 request
440 .get("cortical_id_list")
441 .and_then(|v| v.as_array())
442 .ok_or_else(|| ApiError::invalid_input("cortical_id_list required in object format"))?
443 .iter()
444 .filter_map(|v| v.as_str().map(|s| s.to_string()))
445 .collect()
446 } else {
447 return Err(ApiError::invalid_input(
448 "Request must be an array of IDs or object with cortical_id_list",
449 ));
450 };
451
452 for cortical_id in cortical_ids {
453 if let Ok(area_info) = connectome_service.get_cortical_area(&cortical_id).await {
454 tracing::debug!(target: "feagi-api",
455 "[MULTI] Area {}: cortical_type={}, cortical_group={}, is_mem_type={:?}",
456 cortical_id, area_info.cortical_type, area_info.cortical_group,
457 area_info.properties.get("is_mem_type")
458 );
459 let json_value = serde_json::to_value(&area_info).unwrap_or_default();
460 tracing::debug!(target: "feagi-api",
461 "[MULTI] Serialized has cortical_type: {}",
462 json_value.get("cortical_type").is_some()
463 );
464 result.insert(cortical_id, json_value);
465 }
466 }
467 Ok(Json(result))
468}
469
470#[utoipa::path(post, path = "/v1/cortical_area/cortical_area", tag = "cortical_area")]
472#[allow(unused_variables)] pub async fn post_cortical_area(
474 State(state): State<ApiState>,
475 Json(request): Json<HashMap<String, serde_json::Value>>,
476) -> ApiResult<Json<serde_json::Value>> {
477 use feagi_services::types::CreateCorticalAreaParams;
478 use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
479
480 let genome_service = state.genome_service.as_ref();
482
483 let cortical_type_key = request
485 .get("cortical_id")
486 .and_then(|v| v.as_str())
487 .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?;
488
489 let mut group_id = request
490 .get("group_id")
491 .and_then(|v| v.as_u64())
492 .unwrap_or(0) as u8;
493
494 let device_count = request
495 .get("device_count")
496 .and_then(|v| v.as_u64())
497 .ok_or_else(|| ApiError::invalid_input("device_count required"))?
498 as usize;
499
500 let coordinates_3d: Vec<i32> = request
501 .get("coordinates_3d")
502 .and_then(|v| v.as_array())
503 .and_then(|arr| {
504 if arr.len() == 3 {
505 Some(vec![
506 arr[0].as_i64()? as i32,
507 arr[1].as_i64()? as i32,
508 arr[2].as_i64()? as i32,
509 ])
510 } else {
511 None
512 }
513 })
514 .ok_or_else(|| ApiError::invalid_input("coordinates_3d must be [x, y, z]"))?;
515
516 let cortical_type_str = request
517 .get("cortical_type")
518 .and_then(|v| v.as_str())
519 .ok_or_else(|| ApiError::invalid_input("cortical_type required"))?;
520
521 let unit_id: Option<u8> = request
522 .get("unit_id")
523 .and_then(|v| v.as_u64())
524 .map(|value| {
525 value
526 .try_into()
527 .map_err(|_| ApiError::invalid_input("unit_id out of range"))
528 })
529 .transpose()?;
530 if let Some(unit_id) = unit_id {
531 group_id = unit_id;
532 }
533
534 let neurons_per_voxel = request
536 .get("neurons_per_voxel")
537 .and_then(|v| v.as_u64())
538 .unwrap_or(1) as u32;
539
540 let raw_configs = request
547 .get("data_type_configs_by_subunit")
548 .and_then(|v| v.as_object())
549 .ok_or_else(|| ApiError::invalid_input("data_type_configs_by_subunit (object) required"))?;
550
551 let mut data_type_configs_by_subunit: HashMap<u8, u16> = HashMap::new();
552
553 for (k, v) in raw_configs {
554 let subunit_idx_u64 = k.parse::<u64>().map_err(|_| {
555 ApiError::invalid_input("data_type_configs_by_subunit keys must be integers")
556 })?;
557 let subunit_idx: u8 = subunit_idx_u64.try_into().map_err(|_| {
558 ApiError::invalid_input("data_type_configs_by_subunit key out of range")
559 })?;
560
561 let parsed_u64 = if let Some(u) = v.as_u64() {
562 Some(u)
563 } else if let Some(i) = v.as_i64() {
564 if i >= 0 {
565 Some(i as u64)
566 } else {
567 None
568 }
569 } else if let Some(f) = v.as_f64() {
570 if f >= 0.0 {
571 Some(f.round() as u64)
572 } else {
573 None
574 }
575 } else if let Some(s) = v.as_str() {
576 s.parse::<u64>().ok()
577 } else {
578 None
579 }
580 .ok_or_else(|| {
581 ApiError::invalid_input("data_type_configs_by_subunit values must be numeric")
582 })?;
583
584 if parsed_u64 > u16::MAX as u64 {
585 return Err(ApiError::invalid_input(
586 "data_type_configs_by_subunit value exceeds u16::MAX",
587 ));
588 }
589
590 data_type_configs_by_subunit.insert(subunit_idx, parsed_u64 as u16);
591 }
592
593 tracing::info!(
594 target: "feagi-api",
595 "Creating cortical areas for {} with neurons_per_voxel={}, data_type_configs_by_subunit={:?}",
596 cortical_type_key,
597 neurons_per_voxel,
598 data_type_configs_by_subunit
599 );
600
601 let (num_units, unit_topology) = if cortical_type_str == "IPU" {
603 let unit = SensoryCorticalUnit::list_all()
605 .iter()
606 .find(|u| {
607 let id_ref = u.get_cortical_id_unit_reference();
608 let key = format!("i{}", std::str::from_utf8(&id_ref).unwrap_or(""));
609 key == cortical_type_key
610 })
611 .ok_or_else(|| {
612 ApiError::invalid_input(format!("Unknown IPU type: {}", cortical_type_key))
613 })?;
614
615 (
616 unit.get_number_cortical_areas(),
617 unit.get_unit_default_topology(),
618 )
619 } else if cortical_type_str == "OPU" {
620 let unit = MotorCorticalUnit::list_all()
622 .iter()
623 .find(|u| {
624 let id_ref = u.get_cortical_id_unit_reference();
625 let key = format!("o{}", std::str::from_utf8(&id_ref).unwrap_or(""));
626 key == cortical_type_key
627 })
628 .ok_or_else(|| {
629 ApiError::invalid_input(format!("Unknown OPU type: {}", cortical_type_key))
630 })?;
631
632 (
633 unit.get_number_cortical_areas(),
634 unit.get_unit_default_topology(),
635 )
636 } else {
637 return Err(ApiError::invalid_input("cortical_type must be IPU or OPU"));
638 };
639
640 tracing::info!(
641 "Creating {} units for cortical type: {}",
642 num_units,
643 cortical_type_key
644 );
645
646 let mut creation_params = Vec::new();
648 for unit_idx in 0..num_units {
649 let data_type_config = data_type_configs_by_subunit
650 .get(&(unit_idx as u8))
651 .copied()
652 .ok_or_else(|| {
653 ApiError::invalid_input(format!(
654 "data_type_configs_by_subunit missing entry for subunit {}",
655 unit_idx
656 ))
657 })?;
658
659 let config_byte_4 = (data_type_config & 0xFF) as u8; let config_byte_5 = ((data_type_config >> 8) & 0xFF) as u8; let (per_device_dimensions, dimensions) =
666 if let Some(topo) = unit_topology.get(&CorticalSubUnitIndex::from(unit_idx as u8)) {
667 let dims = topo.channel_dimensions_default;
668 let per_device = (dims[0] as usize, dims[1] as usize, dims[2] as usize);
669 let total_x = per_device.0.saturating_mul(device_count);
670 (per_device, (total_x, per_device.1, per_device.2))
671 } else {
672 ((1, 1, 1), (device_count.max(1), 1, 1)) };
674
675 let position =
677 if let Some(topo) = unit_topology.get(&CorticalSubUnitIndex::from(unit_idx as u8)) {
678 let rel_pos = topo.relative_position;
679 (
680 coordinates_3d[0] + rel_pos[0],
681 coordinates_3d[1] + rel_pos[1],
682 coordinates_3d[2] + rel_pos[2],
683 )
684 } else {
685 (coordinates_3d[0], coordinates_3d[1], coordinates_3d[2])
686 };
687
688 let subtype_bytes = if cortical_type_key.len() >= 4 {
692 let subtype_str = &cortical_type_key[1..4]; let mut bytes = [0u8; 3];
694 for (i, c) in subtype_str.chars().take(3).enumerate() {
695 bytes[i] = c as u8;
696 }
697 bytes
698 } else {
699 return Err(ApiError::invalid_input("Invalid cortical_type_key"));
700 };
701
702 let cortical_id_bytes = [
704 if cortical_type_str == "IPU" {
705 b'i'
706 } else {
707 b'o'
708 }, subtype_bytes[0], subtype_bytes[1], subtype_bytes[2], config_byte_4, config_byte_5, unit_idx as u8, group_id, ];
717
718 let cortical_id = general_purpose::STANDARD.encode(cortical_id_bytes);
720
721 tracing::debug!(target: "feagi-api",
722 " Unit {}: dims={}x{}x{}, neurons_per_voxel={}, total_neurons={}",
723 unit_idx, dimensions.0, dimensions.1, dimensions.2, neurons_per_voxel,
724 dimensions.0 * dimensions.1 * dimensions.2 * neurons_per_voxel as usize
725 );
726
727 let mut properties = HashMap::new();
729 properties.insert(
730 "dev_count".to_string(),
731 serde_json::Value::Number(serde_json::Number::from(device_count)),
732 );
733 properties.insert(
734 "cortical_dimensions_per_device".to_string(),
735 serde_json::json!([
736 per_device_dimensions.0,
737 per_device_dimensions.1,
738 per_device_dimensions.2
739 ]),
740 );
741
742 let params = CreateCorticalAreaParams {
743 cortical_id: cortical_id.clone(),
744 name: format!("{} Unit {}", cortical_type_key, unit_idx),
745 dimensions,
746 position,
747 area_type: cortical_type_str.to_string(),
748 visible: Some(true),
749 sub_group: None,
750 neurons_per_voxel: Some(neurons_per_voxel),
751 postsynaptic_current: Some(0.0),
752 plasticity_constant: Some(0.0),
753 degeneration: Some(0.0),
754 psp_uniform_distribution: Some(false),
755 firing_threshold_increment: Some(0.0),
756 firing_threshold_limit: Some(0.0),
757 consecutive_fire_count: Some(0),
758 snooze_period: Some(0),
759 refractory_period: Some(0),
760 leak_coefficient: Some(0.0),
761 leak_variability: Some(0.0),
762 burst_engine_active: Some(true),
763 properties: Some(properties),
764 };
765
766 creation_params.push(params);
767 }
768
769 tracing::info!(
770 "Calling GenomeService to create {} cortical areas",
771 creation_params.len()
772 );
773
774 let areas_details = genome_service
777 .create_cortical_areas(creation_params)
778 .await
779 .map_err(|e| ApiError::internal(format!("Failed to create cortical areas: {}", e)))?;
780
781 tracing::info!(
782 "β
Successfully created {} cortical areas via GenomeService",
783 areas_details.len()
784 );
785
786 let areas_json = serde_json::to_value(&areas_details).unwrap_or_default();
788
789 let created_ids: Vec<String> = areas_details
791 .iter()
792 .map(|a| a.cortical_id.clone())
793 .collect();
794
795 let first_id = created_ids.first().cloned().unwrap_or_default();
797 let mut response = serde_json::Map::new();
798 response.insert(
799 "message".to_string(),
800 serde_json::Value::String(format!("Created {} cortical areas", created_ids.len())),
801 );
802 response.insert(
803 "cortical_id".to_string(),
804 serde_json::Value::String(first_id),
805 ); response.insert(
807 "cortical_ids".to_string(),
808 serde_json::Value::String(created_ids.join(", ")),
809 );
810 response.insert(
811 "unit_count".to_string(),
812 serde_json::Value::Number(created_ids.len().into()),
813 );
814 response.insert("areas".to_string(), areas_json); Ok(Json(serde_json::Value::Object(response)))
817}
818
819#[utoipa::path(put, path = "/v1/cortical_area/cortical_area", tag = "cortical_area")]
821pub async fn put_cortical_area(
822 State(state): State<ApiState>,
823 Json(mut request): Json<HashMap<String, serde_json::Value>>,
824) -> ApiResult<Json<HashMap<String, String>>> {
825 let genome_service = state.genome_service.as_ref();
826
827 let cortical_id = request
829 .get("cortical_id")
830 .and_then(|v| v.as_str())
831 .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?
832 .to_string();
833
834 tracing::debug!(
835 target: "feagi-api",
836 "PUT /v1/cortical_area/cortical_area - received update for area: {} (keys: {:?})",
837 cortical_id,
838 request.keys().collect::<Vec<_>>()
839 );
840
841 request.remove("cortical_id");
843
844 match genome_service
846 .update_cortical_area(&cortical_id, request)
847 .await
848 {
849 Ok(area_info) => {
850 let updated_id = area_info.cortical_id.clone();
851 tracing::debug!(
852 target: "feagi-api",
853 "PUT /v1/cortical_area/cortical_area - success for {} (updated_id={})",
854 cortical_id,
855 updated_id
856 );
857 Ok(Json(HashMap::from([
858 ("message".to_string(), "Cortical area updated".to_string()),
859 ("cortical_id".to_string(), updated_id),
860 ("previous_cortical_id".to_string(), cortical_id),
861 ])))
862 }
863 Err(e) => {
864 tracing::error!(target: "feagi-api", "PUT /v1/cortical_area/cortical_area - failed for {}: {}", cortical_id, e);
865 Err(ApiError::internal(format!("Failed to update: {}", e)))
866 }
867 }
868}
869
870#[utoipa::path(
872 delete,
873 path = "/v1/cortical_area/cortical_area",
874 tag = "cortical_area"
875)]
876#[allow(unused_variables)] pub async fn delete_cortical_area(
878 State(state): State<ApiState>,
879 Json(request): Json<HashMap<String, String>>,
880) -> ApiResult<Json<HashMap<String, String>>> {
881 let connectome_service = state.connectome_service.as_ref();
882 let cortical_id = request
883 .get("cortical_id")
884 .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?;
885
886 match connectome_service.delete_cortical_area(cortical_id).await {
887 Ok(_) => Ok(Json(HashMap::from([(
888 "message".to_string(),
889 "Cortical area deleted".to_string(),
890 )]))),
891 Err(e) => Err(ApiError::internal(format!("Failed to delete: {}", e))),
892 }
893}
894
895#[utoipa::path(
897 post,
898 path = "/v1/cortical_area/custom_cortical_area",
899 tag = "cortical_area"
900)]
901pub async fn post_custom_cortical_area(
902 State(state): State<ApiState>,
903 Json(request): Json<HashMap<String, serde_json::Value>>,
904) -> ApiResult<Json<HashMap<String, String>>> {
905 use feagi_services::types::CreateCorticalAreaParams;
906 use std::time::{SystemTime, UNIX_EPOCH};
907
908 let is_memory_area_requested = request
920 .get("sub_group_id")
921 .and_then(|v| v.as_str())
922 .map(|s| s.eq_ignore_ascii_case("MEMORY"))
923 .unwrap_or(false);
924
925 let cortical_name = request
927 .get("cortical_name")
928 .and_then(|v| v.as_str())
929 .ok_or_else(|| ApiError::invalid_input("cortical_name required"))?;
930
931 let cortical_dimensions: Vec<u32> = request
932 .get("cortical_dimensions")
933 .and_then(|v| v.as_array())
934 .and_then(|arr| {
935 if arr.len() == 3 {
936 Some(vec![
937 arr[0].as_u64()? as u32,
938 arr[1].as_u64()? as u32,
939 arr[2].as_u64()? as u32,
940 ])
941 } else {
942 None
943 }
944 })
945 .ok_or_else(|| ApiError::invalid_input("cortical_dimensions must be [x, y, z]"))?;
946
947 let coordinates_3d: Vec<i32> = request
948 .get("coordinates_3d")
949 .and_then(|v| v.as_array())
950 .and_then(|arr| {
951 if arr.len() == 3 {
952 Some(vec![
953 arr[0].as_i64()? as i32,
954 arr[1].as_i64()? as i32,
955 arr[2].as_i64()? as i32,
956 ])
957 } else {
958 None
959 }
960 })
961 .ok_or_else(|| ApiError::invalid_input("coordinates_3d must be [x, y, z]"))?;
962
963 let brain_region_id = request
964 .get("brain_region_id")
965 .and_then(|v| v.as_str())
966 .map(|s| s.to_string());
967
968 let cortical_sub_group = request
969 .get("cortical_sub_group")
970 .and_then(|v| v.as_str())
971 .filter(|s| !s.is_empty())
972 .map(|s| s.to_string());
973
974 tracing::info!(target: "feagi-api",
975 "Creating {} cortical area '{}' with dimensions: {}x{}x{}, position: ({}, {}, {})",
976 if is_memory_area_requested { "memory" } else { "custom" },
977 cortical_name, cortical_dimensions[0], cortical_dimensions[1], cortical_dimensions[2],
978 coordinates_3d[0], coordinates_3d[1], coordinates_3d[2]
979 );
980
981 let timestamp = SystemTime::now()
985 .duration_since(UNIX_EPOCH)
986 .unwrap()
987 .as_millis() as u64;
988
989 let mut cortical_id_bytes = [0u8; 8];
994 cortical_id_bytes[0] = if is_memory_area_requested { b'm' } else { b'c' };
995
996 let name_bytes = cortical_name.as_bytes();
998 for i in 1..7 {
999 cortical_id_bytes[i] = if i - 1 < name_bytes.len() {
1000 let c = name_bytes[i - 1];
1002 if c.is_ascii_alphanumeric() || c == b'_' {
1003 c
1004 } else {
1005 b'_'
1006 }
1007 } else {
1008 b'_' };
1010 }
1011
1012 cortical_id_bytes[7] = (timestamp & 0xFF) as u8;
1014
1015 let cortical_id = general_purpose::STANDARD.encode(cortical_id_bytes);
1017
1018 tracing::debug!(target: "feagi-api",
1019 "Generated cortical_id: {} (raw bytes: {:?})",
1020 cortical_id, cortical_id_bytes
1021 );
1022
1023 let mut properties = HashMap::new();
1025 if let Some(region_id) = brain_region_id.clone() {
1026 properties.insert(
1027 "parent_region_id".to_string(),
1028 serde_json::Value::String(region_id),
1029 );
1030 }
1031
1032 let params = CreateCorticalAreaParams {
1034 cortical_id: cortical_id.clone(),
1035 name: cortical_name.to_string(),
1036 dimensions: (
1037 cortical_dimensions[0] as usize,
1038 cortical_dimensions[1] as usize,
1039 cortical_dimensions[2] as usize,
1040 ),
1041 position: (coordinates_3d[0], coordinates_3d[1], coordinates_3d[2]),
1042 area_type: if is_memory_area_requested {
1043 "Memory".to_string()
1044 } else {
1045 "Custom".to_string()
1046 },
1047 visible: Some(true),
1048 sub_group: cortical_sub_group,
1049 neurons_per_voxel: Some(1),
1050 postsynaptic_current: None,
1051 plasticity_constant: Some(0.0),
1052 degeneration: Some(0.0),
1053 psp_uniform_distribution: Some(false),
1054 firing_threshold_increment: Some(0.0),
1055 firing_threshold_limit: Some(0.0),
1056 consecutive_fire_count: Some(0),
1057 snooze_period: Some(0),
1058 refractory_period: Some(0),
1059 leak_coefficient: Some(0.0),
1060 leak_variability: Some(0.0),
1061 burst_engine_active: Some(true),
1062 properties: Some(properties),
1063 };
1064
1065 let genome_service = state.genome_service.as_ref();
1066
1067 tracing::info!(target: "feagi-api", "Calling GenomeService to create custom cortical area");
1068
1069 let areas_details = genome_service
1071 .create_cortical_areas(vec![params])
1072 .await
1073 .map_err(|e| ApiError::internal(format!("Failed to create custom cortical area: {}", e)))?;
1074
1075 let created_area = areas_details
1076 .first()
1077 .ok_or_else(|| ApiError::internal("No cortical area was created"))?;
1078
1079 tracing::info!(target: "feagi-api",
1080 "β
Successfully created custom cortical area '{}' with ID: {}",
1081 cortical_name, created_area.cortical_id
1082 );
1083
1084 let mut response = HashMap::new();
1086 response.insert(
1087 "message".to_string(),
1088 "Custom cortical area created successfully".to_string(),
1089 );
1090 response.insert("cortical_id".to_string(), created_area.cortical_id.clone());
1091 response.insert("cortical_name".to_string(), cortical_name.to_string());
1092
1093 Ok(Json(response))
1094}
1095
1096#[utoipa::path(post, path = "/v1/cortical_area/clone", tag = "cortical_area")]
1098pub async fn post_clone(
1099 State(state): State<ApiState>,
1100 Json(request): Json<CloneCorticalAreaRequest>,
1101) -> ApiResult<Json<HashMap<String, String>>> {
1102 use base64::{engine::general_purpose, Engine as _};
1103 use feagi_services::types::CreateCorticalAreaParams;
1104 use feagi_structures::genomic::cortical_area::CorticalID;
1105 use serde_json::Value;
1106 use std::time::{SystemTime, UNIX_EPOCH};
1107
1108 let genome_service = state.genome_service.as_ref();
1109 let connectome_service = state.connectome_service.as_ref();
1110
1111 let source_id = request.source_area_id.clone();
1113 let source_typed = CorticalID::try_from_base_64(&source_id)
1114 .map_err(|e| ApiError::invalid_input(e.to_string()))?;
1115 let src_first_byte = source_typed.as_bytes()[0];
1116 if src_first_byte != b'c' && src_first_byte != b'm' {
1117 return Err(ApiError::invalid_input(format!(
1118 "Cloning is only supported for custom ('c') and memory ('m') cortical areas (got prefix byte: {})",
1119 src_first_byte
1120 )));
1121 }
1122
1123 let source_area = connectome_service
1125 .get_cortical_area(&source_id)
1126 .await
1127 .map_err(|e| ApiError::not_found("CorticalArea", &e.to_string()))?;
1128
1129 let source_parent_region_id = source_area
1136 .parent_region_id
1137 .clone()
1138 .or_else(|| {
1139 source_area
1140 .properties
1141 .get("parent_region_id")
1142 .and_then(|v| v.as_str())
1143 .map(|s| s.to_string())
1144 })
1145 .ok_or_else(|| {
1146 ApiError::internal(format!(
1147 "Source cortical area {} is missing parent_region_id; cannot determine region membership for clone",
1148 source_id
1149 ))
1150 })?;
1151
1152 if let Some(client_parent_region_id) = request.parent_region_id.as_ref() {
1153 if client_parent_region_id != &source_parent_region_id {
1154 return Err(ApiError::invalid_input(format!(
1155 "parent_region_id mismatch for clone request: client sent '{}', but FEAGI source area {} belongs to '{}'",
1156 client_parent_region_id, source_id, source_parent_region_id
1157 )));
1158 }
1159 }
1160
1161 let outgoing_mapping_dst = source_area
1163 .properties
1164 .get("cortical_mapping_dst")
1165 .and_then(|v| v.as_object())
1166 .cloned();
1167
1168 let timestamp = SystemTime::now()
1175 .duration_since(UNIX_EPOCH)
1176 .map_err(|e| ApiError::internal(format!("System clock error: {}", e)))?
1177 .as_millis() as u64;
1178
1179 let mut cortical_id_bytes = [0u8; 8];
1180 cortical_id_bytes[0] = src_first_byte;
1181
1182 let name_bytes = request.new_name.as_bytes();
1183 for i in 1..7 {
1184 cortical_id_bytes[i] = if i - 1 < name_bytes.len() {
1185 let c = name_bytes[i - 1];
1186 if c.is_ascii_alphanumeric() || c == b'_' {
1187 c
1188 } else {
1189 b'_'
1190 }
1191 } else {
1192 b'_'
1193 };
1194 }
1195 cortical_id_bytes[7] = (timestamp & 0xFF) as u8;
1196
1197 let new_area_id = general_purpose::STANDARD.encode(cortical_id_bytes);
1198
1199 let mut cloned_properties = source_area.properties.clone();
1202 cloned_properties.remove("cortical_mapping_dst");
1203
1204 cloned_properties.insert(
1206 "parent_region_id".to_string(),
1207 Value::String(source_parent_region_id),
1208 );
1209 cloned_properties.insert(
1210 "coordinate_2d".to_string(),
1211 serde_json::json!([request.coordinates_2d[0], request.coordinates_2d[1]]),
1212 );
1213
1214 let params = CreateCorticalAreaParams {
1215 cortical_id: new_area_id.clone(),
1216 name: request.new_name.clone(),
1217 dimensions: source_area.dimensions,
1218 position: (
1219 request.coordinates_3d[0],
1220 request.coordinates_3d[1],
1221 request.coordinates_3d[2],
1222 ),
1223 area_type: source_area.area_type.clone(),
1224 visible: Some(source_area.visible),
1225 sub_group: source_area.sub_group.clone(),
1226 neurons_per_voxel: Some(source_area.neurons_per_voxel),
1227 postsynaptic_current: Some(source_area.postsynaptic_current),
1228 plasticity_constant: Some(source_area.plasticity_constant),
1229 degeneration: Some(source_area.degeneration),
1230 psp_uniform_distribution: Some(source_area.psp_uniform_distribution),
1231 firing_threshold_increment: None,
1234 firing_threshold_limit: Some(source_area.firing_threshold_limit),
1235 consecutive_fire_count: Some(source_area.consecutive_fire_count),
1236 snooze_period: Some(source_area.snooze_period),
1237 refractory_period: Some(source_area.refractory_period),
1238 leak_coefficient: Some(source_area.leak_coefficient),
1239 leak_variability: Some(source_area.leak_variability),
1240 burst_engine_active: Some(source_area.burst_engine_active),
1241 properties: Some(cloned_properties),
1242 };
1243
1244 let created_areas = genome_service
1246 .create_cortical_areas(vec![params])
1247 .await
1248 .map_err(|e| ApiError::internal(format!("Failed to clone cortical area: {}", e)))?;
1249
1250 if let Some(created_area) = created_areas.first() {
1252 tracing::info!(target: "feagi-api",
1253 "Clone created area {} with position {:?} (requested {:?})",
1254 new_area_id, created_area.position, request.coordinates_3d
1255 );
1256 }
1257
1258 if request.clone_cortical_mapping {
1260 if let Some(dst_map) = outgoing_mapping_dst {
1262 for (dst_id, rules) in dst_map {
1263 let dst_effective = if dst_id == source_id {
1264 new_area_id.clone()
1266 } else {
1267 dst_id.clone()
1268 };
1269
1270 let Some(rules_array) = rules.as_array() else {
1271 return Err(ApiError::invalid_input(format!(
1272 "Invalid cortical_mapping_dst value for dst '{}': expected array, got {}",
1273 dst_id, rules
1274 )));
1275 };
1276
1277 connectome_service
1278 .update_cortical_mapping(
1279 new_area_id.clone(),
1280 dst_effective,
1281 rules_array.clone(),
1282 )
1283 .await
1284 .map_err(|e| {
1285 ApiError::internal(format!(
1286 "Failed to clone outgoing mapping from {}: {}",
1287 source_id, e
1288 ))
1289 })?;
1290 }
1291 }
1292
1293 let all_areas = connectome_service
1296 .list_cortical_areas()
1297 .await
1298 .map_err(|e| ApiError::internal(format!("Failed to list cortical areas: {}", e)))?;
1299
1300 for area in all_areas {
1301 if area.cortical_id == source_id {
1303 continue;
1304 }
1305
1306 let Some(dst_map) = area
1307 .properties
1308 .get("cortical_mapping_dst")
1309 .and_then(|v| v.as_object())
1310 else {
1311 continue;
1312 };
1313
1314 let Some(rules) = dst_map.get(&source_id) else {
1315 continue;
1316 };
1317
1318 let Some(rules_array) = rules.as_array() else {
1319 return Err(ApiError::invalid_input(format!(
1320 "Invalid cortical_mapping_dst value for src '{}', dst '{}': expected array, got {}",
1321 area.cortical_id, source_id, rules
1322 )));
1323 };
1324
1325 connectome_service
1326 .update_cortical_mapping(
1327 area.cortical_id.clone(),
1328 new_area_id.clone(),
1329 rules_array.clone(),
1330 )
1331 .await
1332 .map_err(|e| {
1333 ApiError::internal(format!(
1334 "Failed to clone incoming mapping into {} from {}: {}",
1335 source_id, area.cortical_id, e
1336 ))
1337 })?;
1338 }
1339 }
1340
1341 Ok(Json(HashMap::from([
1342 ("message".to_string(), "Cortical area cloned".to_string()),
1343 ("new_area_id".to_string(), new_area_id),
1344 ])))
1345}
1346
1347#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
1349pub struct CloneCorticalAreaRequest {
1350 pub source_area_id: String,
1352 pub new_name: String,
1354 pub coordinates_3d: [i32; 3],
1356 pub coordinates_2d: [i32; 2],
1358 #[serde(default)]
1363 pub parent_region_id: Option<String>,
1364 pub clone_cortical_mapping: bool,
1366}
1367
1368#[utoipa::path(
1370 put,
1371 path = "/v1/cortical_area/multi/cortical_area",
1372 tag = "cortical_area"
1373)]
1374pub async fn put_multi_cortical_area(
1375 State(state): State<ApiState>,
1376 Json(mut request): Json<HashMap<String, serde_json::Value>>,
1377) -> ApiResult<Json<HashMap<String, String>>> {
1378 let genome_service = state.genome_service.as_ref();
1379
1380 let cortical_ids: Vec<String> = request
1382 .get("cortical_id_list")
1383 .and_then(|v| v.as_array())
1384 .ok_or_else(|| ApiError::invalid_input("cortical_id_list required"))?
1385 .iter()
1386 .filter_map(|v| v.as_str().map(String::from))
1387 .collect();
1388
1389 if cortical_ids.is_empty() {
1390 return Err(ApiError::invalid_input("cortical_id_list cannot be empty"));
1391 }
1392
1393 tracing::debug!(
1394 target: "feagi-api",
1395 "PUT /v1/cortical_area/multi/cortical_area - received update for {} areas (keys: {:?})",
1396 cortical_ids.len(),
1397 request.keys().collect::<Vec<_>>()
1398 );
1399
1400 request.remove("cortical_id_list");
1402
1403 let mut shared_properties = request.clone();
1405 for cortical_id in &cortical_ids {
1406 shared_properties.remove(cortical_id);
1407 }
1408
1409 for cortical_id in &cortical_ids {
1411 tracing::debug!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - updating area: {}", cortical_id);
1412 let mut properties = shared_properties.clone();
1413 if let Some(serde_json::Value::Object(per_id_map)) = request.get(cortical_id) {
1414 for (key, value) in per_id_map {
1415 properties.insert(key.clone(), value.clone());
1416 }
1417 }
1418 match genome_service
1419 .update_cortical_area(cortical_id, properties)
1420 .await
1421 {
1422 Ok(_) => {
1423 tracing::debug!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - success for {}", cortical_id);
1424 }
1425 Err(e) => {
1426 tracing::error!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - failed for {}: {}", cortical_id, e);
1427 return Err(ApiError::internal(format!(
1428 "Failed to update cortical area {}: {}",
1429 cortical_id, e
1430 )));
1431 }
1432 }
1433 }
1434
1435 Ok(Json(HashMap::from([
1436 (
1437 "message".to_string(),
1438 format!("Updated {} cortical areas", cortical_ids.len()),
1439 ),
1440 ("cortical_ids".to_string(), cortical_ids.join(", ")),
1441 ])))
1442}
1443
1444#[utoipa::path(
1446 delete,
1447 path = "/v1/cortical_area/multi/cortical_area",
1448 tag = "cortical_area"
1449)]
1450#[allow(unused_variables)] pub async fn delete_multi_cortical_area(
1452 State(state): State<ApiState>,
1453 Json(request): Json<Vec<String>>,
1454) -> ApiResult<Json<HashMap<String, String>>> {
1455 Err(ApiError::internal("Not yet implemented"))
1457}
1458
1459#[utoipa::path(put, path = "/v1/cortical_area/coord_2d", tag = "cortical_area")]
1461#[allow(unused_variables)] pub async fn put_coord_2d(
1463 State(state): State<ApiState>,
1464 Json(request): Json<HashMap<String, serde_json::Value>>,
1465) -> ApiResult<Json<HashMap<String, String>>> {
1466 Err(ApiError::internal("Not yet implemented"))
1468}
1469
1470#[utoipa::path(
1472 put,
1473 path = "/v1/cortical_area/suppress_cortical_visibility",
1474 tag = "cortical_area"
1475)]
1476#[allow(unused_variables)] pub async fn put_suppress_cortical_visibility(
1478 State(state): State<ApiState>,
1479 Json(request): Json<HashMap<String, serde_json::Value>>,
1480) -> ApiResult<Json<HashMap<String, String>>> {
1481 Err(ApiError::internal("Not yet implemented"))
1483}
1484
1485#[utoipa::path(put, path = "/v1/cortical_area/reset", tag = "cortical_area")]
1487#[allow(unused_variables)] pub async fn put_reset(
1489 State(state): State<ApiState>,
1490 Json(request): Json<HashMap<String, String>>,
1491) -> ApiResult<Json<HashMap<String, String>>> {
1492 Err(ApiError::internal("Not yet implemented"))
1494}
1495
1496#[utoipa::path(get, path = "/v1/cortical_area/visualization", tag = "cortical_area")]
1498pub async fn get_visualization(
1499 State(_state): State<ApiState>,
1500) -> ApiResult<Json<HashMap<String, bool>>> {
1501 let mut response = HashMap::new();
1502 response.insert("enabled".to_string(), true);
1503 Ok(Json(response))
1504}
1505
1506#[utoipa::path(
1508 post,
1509 path = "/v1/cortical_area/batch_operations",
1510 tag = "cortical_area"
1511)]
1512pub async fn post_batch_operations(
1513 State(_state): State<ApiState>,
1514 Json(_ops): Json<Vec<HashMap<String, serde_json::Value>>>,
1515) -> ApiResult<Json<HashMap<String, i32>>> {
1516 let mut response = HashMap::new();
1517 response.insert("processed".to_string(), 0);
1518 Ok(Json(response))
1519}
1520
1521#[utoipa::path(get, path = "/v1/cortical_area/ipu/list", tag = "cortical_area")]
1523pub async fn get_ipu_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1524 get_ipu(State(state)).await
1525}
1526
1527#[utoipa::path(get, path = "/v1/cortical_area/opu/list", tag = "cortical_area")]
1529pub async fn get_opu_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1530 get_opu(State(state)).await
1531}
1532
1533#[utoipa::path(put, path = "/v1/cortical_area/coordinates_3d", tag = "cortical_area")]
1535pub async fn put_coordinates_3d(
1536 State(_state): State<ApiState>,
1537 Json(_req): Json<HashMap<String, serde_json::Value>>,
1538) -> ApiResult<Json<HashMap<String, String>>> {
1539 Ok(Json(HashMap::from([(
1540 "message".to_string(),
1541 "Not yet implemented".to_string(),
1542 )])))
1543}
1544
1545#[utoipa::path(delete, path = "/v1/cortical_area/bulk_delete", tag = "cortical_area")]
1547pub async fn delete_bulk(
1548 State(_state): State<ApiState>,
1549 Json(_ids): Json<Vec<String>>,
1550) -> ApiResult<Json<HashMap<String, i32>>> {
1551 let mut response = HashMap::new();
1552 response.insert("deleted_count".to_string(), 0);
1553 Ok(Json(response))
1554}
1555
1556#[utoipa::path(post, path = "/v1/cortical_area/resize", tag = "cortical_area")]
1558pub async fn post_resize(
1559 State(_state): State<ApiState>,
1560 Json(_req): Json<HashMap<String, serde_json::Value>>,
1561) -> ApiResult<Json<HashMap<String, String>>> {
1562 Ok(Json(HashMap::from([(
1563 "message".to_string(),
1564 "Not yet implemented".to_string(),
1565 )])))
1566}
1567
1568#[utoipa::path(post, path = "/v1/cortical_area/reposition", tag = "cortical_area")]
1570pub async fn post_reposition(
1571 State(_state): State<ApiState>,
1572 Json(_req): Json<HashMap<String, serde_json::Value>>,
1573) -> ApiResult<Json<HashMap<String, String>>> {
1574 Ok(Json(HashMap::from([(
1575 "message".to_string(),
1576 "Not yet implemented".to_string(),
1577 )])))
1578}
1579
1580#[utoipa::path(post, path = "/v1/cortical_area/voxel_neurons", tag = "cortical_area")]
1582pub async fn post_voxel_neurons(
1583 State(_state): State<ApiState>,
1584 Json(_req): Json<HashMap<String, serde_json::Value>>,
1585) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1586 let mut response = HashMap::new();
1587 response.insert("neurons".to_string(), serde_json::json!([]));
1588 Ok(Json(response))
1589}
1590
1591#[utoipa::path(
1593 get,
1594 path = "/v1/cortical_area/ipu/types",
1595 tag = "cortical_area",
1596 responses(
1597 (status = 200, description = "IPU type metadata", body = HashMap<String, CorticalTypeMetadata>),
1598 (status = 500, description = "Internal server error")
1599 )
1600)]
1601pub async fn get_ipu_types(
1602 State(_state): State<ApiState>,
1603) -> ApiResult<Json<HashMap<String, CorticalTypeMetadata>>> {
1604 let mut types = HashMap::new();
1605
1606 for unit in SensoryCorticalUnit::list_all() {
1608 let id_ref = unit.get_cortical_id_unit_reference();
1609 let key = format!("i{}", std::str::from_utf8(&id_ref).unwrap_or("???"));
1610
1611 let encodings = vec!["absolute".to_string(), "incremental".to_string()];
1613
1614 let snake_name = unit.get_snake_case_name();
1619 let formats = if snake_name == "vision"
1620 || snake_name == "segmented_vision"
1621 || snake_name == "miscellaneous"
1622 {
1623 vec![]
1624 } else {
1625 vec!["linear".to_string(), "fractional".to_string()]
1626 };
1627
1628 let resolution = if snake_name == "vision" {
1630 vec![64, 64, 1] } else if snake_name == "segmented_vision" {
1632 vec![32, 32, 1] } else {
1634 vec![1, 1, 1] };
1636
1637 let structure = "asymmetric".to_string();
1639
1640 let topology_map = unit.get_unit_default_topology();
1642 let unit_default_topology: HashMap<usize, UnitTopologyData> = topology_map
1643 .into_iter()
1644 .map(|(idx, topo)| {
1645 (
1646 *idx as usize,
1647 UnitTopologyData {
1648 relative_position: topo.relative_position,
1649 dimensions: topo.channel_dimensions_default,
1650 },
1651 )
1652 })
1653 .collect();
1654
1655 types.insert(
1656 key,
1657 CorticalTypeMetadata {
1658 description: unit.get_friendly_name().to_string(),
1659 encodings,
1660 formats,
1661 units: unit.get_number_cortical_areas() as u32,
1662 resolution,
1663 structure,
1664 unit_default_topology,
1665 },
1666 );
1667 }
1668
1669 Ok(Json(types))
1670}
1671
1672#[utoipa::path(
1674 get,
1675 path = "/v1/cortical_area/opu/types",
1676 tag = "cortical_area",
1677 responses(
1678 (status = 200, description = "OPU type metadata", body = HashMap<String, CorticalTypeMetadata>),
1679 (status = 500, description = "Internal server error")
1680 )
1681)]
1682pub async fn get_opu_types(
1683 State(_state): State<ApiState>,
1684) -> ApiResult<Json<HashMap<String, CorticalTypeMetadata>>> {
1685 let mut types = HashMap::new();
1686
1687 for unit in MotorCorticalUnit::list_all() {
1689 let id_ref = unit.get_cortical_id_unit_reference();
1690 let key = format!("o{}", std::str::from_utf8(&id_ref).unwrap_or("???"));
1691
1692 let encodings = vec!["absolute".to_string(), "incremental".to_string()];
1694
1695 let snake_name = unit.get_snake_case_name();
1699 let formats = if snake_name == "miscellaneous" {
1700 vec![]
1701 } else {
1702 vec!["linear".to_string(), "fractional".to_string()]
1703 };
1704
1705 let resolution = vec![1, 1, 1];
1707
1708 let structure = "asymmetric".to_string();
1710
1711 let topology_map = unit.get_unit_default_topology();
1713 let unit_default_topology: HashMap<usize, UnitTopologyData> = topology_map
1714 .into_iter()
1715 .map(|(idx, topo)| {
1716 (
1717 *idx as usize,
1718 UnitTopologyData {
1719 relative_position: topo.relative_position,
1720 dimensions: topo.channel_dimensions_default,
1721 },
1722 )
1723 })
1724 .collect();
1725
1726 types.insert(
1727 key,
1728 CorticalTypeMetadata {
1729 description: unit.get_friendly_name().to_string(),
1730 encodings,
1731 formats,
1732 units: unit.get_number_cortical_areas() as u32,
1733 resolution,
1734 structure,
1735 unit_default_topology,
1736 },
1737 );
1738 }
1739
1740 Ok(Json(types))
1741}
1742
1743#[utoipa::path(
1745 get,
1746 path = "/v1/cortical_area/cortical_area_index_list",
1747 tag = "cortical_area"
1748)]
1749pub async fn get_cortical_area_index_list(
1750 State(state): State<ApiState>,
1751) -> ApiResult<Json<Vec<u32>>> {
1752 let connectome_service = state.connectome_service.as_ref();
1753 let areas = connectome_service
1754 .list_cortical_areas()
1755 .await
1756 .map_err(|e| ApiError::internal(format!("{}", e)))?;
1757 let indices: Vec<u32> = areas.iter().map(|a| a.cortical_idx).collect();
1759 Ok(Json(indices))
1760}
1761
1762#[utoipa::path(
1764 get,
1765 path = "/v1/cortical_area/cortical_idx_mapping",
1766 tag = "cortical_area"
1767)]
1768pub async fn get_cortical_idx_mapping(
1769 State(state): State<ApiState>,
1770) -> ApiResult<Json<std::collections::BTreeMap<String, u32>>> {
1771 use std::collections::BTreeMap;
1772
1773 let connectome_service = state.connectome_service.as_ref();
1774 let areas = connectome_service
1775 .list_cortical_areas()
1776 .await
1777 .map_err(|e| ApiError::internal(format!("{}", e)))?;
1778 let mapping: BTreeMap<String, u32> = areas
1781 .iter()
1782 .map(|a| (a.cortical_id.clone(), a.cortical_idx))
1783 .collect();
1784 Ok(Json(mapping))
1785}
1786
1787#[utoipa::path(
1789 get,
1790 path = "/v1/cortical_area/mapping_restrictions",
1791 tag = "cortical_area"
1792)]
1793pub async fn get_mapping_restrictions_query(
1794 State(_state): State<ApiState>,
1795 Query(_params): Query<HashMap<String, String>>,
1796) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
1797 Ok(Json(HashMap::new()))
1798}
1799
1800#[utoipa::path(
1802 get,
1803 path = "/v1/cortical_area/{cortical_id}/memory_usage",
1804 tag = "cortical_area"
1805)]
1806pub async fn get_memory_usage(
1807 State(state): State<ApiState>,
1808 Path(cortical_id): Path<String>,
1809) -> ApiResult<Json<HashMap<String, i64>>> {
1810 let connectome_service = state.connectome_service.as_ref();
1811
1812 let area_info = connectome_service
1814 .get_cortical_area(&cortical_id)
1815 .await
1816 .map_err(|_| ApiError::not_found("CorticalArea", &cortical_id))?;
1817
1818 const BYTES_PER_NEURON: i64 = 48;
1821 let memory_bytes = (area_info.neuron_count as i64) * BYTES_PER_NEURON;
1822
1823 let mut response = HashMap::new();
1824 response.insert("memory_bytes".to_string(), memory_bytes);
1825 Ok(Json(response))
1826}
1827
1828#[utoipa::path(
1830 get,
1831 path = "/v1/cortical_area/{cortical_id}/neuron_count",
1832 tag = "cortical_area"
1833)]
1834pub async fn get_area_neuron_count(
1835 State(state): State<ApiState>,
1836 Path(cortical_id): Path<String>,
1837) -> ApiResult<Json<i64>> {
1838 let connectome_service = state.connectome_service.as_ref();
1839
1840 let area_info = connectome_service
1842 .get_cortical_area(&cortical_id)
1843 .await
1844 .map_err(|_| ApiError::not_found("CorticalArea", &cortical_id))?;
1845
1846 Ok(Json(area_info.neuron_count as i64))
1847}
1848
1849#[utoipa::path(
1851 post,
1852 path = "/v1/cortical_area/cortical_type_options",
1853 tag = "cortical_area"
1854)]
1855pub async fn post_cortical_type_options(
1856 State(_state): State<ApiState>,
1857) -> ApiResult<Json<Vec<String>>> {
1858 Ok(Json(vec![
1859 "Sensory".to_string(),
1860 "Motor".to_string(),
1861 "Custom".to_string(),
1862 "Memory".to_string(),
1863 ]))
1864}
1865
1866#[utoipa::path(
1868 post,
1869 path = "/v1/cortical_area/mapping_restrictions",
1870 tag = "cortical_area"
1871)]
1872pub async fn post_mapping_restrictions(
1873 State(_state): State<ApiState>,
1874 Json(_req): Json<HashMap<String, String>>,
1875) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
1876 Ok(Json(HashMap::new()))
1877}
1878
1879#[utoipa::path(
1881 post,
1882 path = "/v1/cortical_area/mapping_restrictions_between_areas",
1883 tag = "cortical_area"
1884)]
1885pub async fn post_mapping_restrictions_between_areas(
1886 State(_state): State<ApiState>,
1887 Json(_req): Json<HashMap<String, String>>,
1888) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
1889 Ok(Json(HashMap::new()))
1890}
1891
1892#[utoipa::path(put, path = "/v1/cortical_area/coord_3d", tag = "cortical_area")]
1894pub async fn put_coord_3d(
1895 State(_state): State<ApiState>,
1896 Json(_req): Json<HashMap<String, serde_json::Value>>,
1897) -> ApiResult<Json<HashMap<String, String>>> {
1898 Ok(Json(HashMap::from([(
1899 "message".to_string(),
1900 "Not yet implemented".to_string(),
1901 )])))
1902}