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_evolutionary::extract_memory_properties;
15use feagi_structures::genomic::cortical_area::descriptors::CorticalSubUnitIndex;
16use feagi_structures::genomic::cortical_area::CorticalID;
17use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
18use utoipa::{IntoParams, ToSchema};
19
20#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
25pub struct CorticalAreaIdListResponse {
26 pub cortical_ids: Vec<String>,
27}
28
29#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
30pub struct CorticalAreaNameListResponse {
31 pub cortical_area_name_list: Vec<String>,
32}
33
34#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
36pub struct CorticalAreaResetRequest {
37 pub area_list: Vec<String>,
38}
39
40#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
42pub struct CorticalAreaResetItem {
43 pub cortical_idx: u32,
44 pub neurons_reset: usize,
45}
46
47#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
48pub struct CorticalAreaResetResponse {
49 pub message: String,
50 pub results: Vec<CorticalAreaResetItem>,
51}
52
53#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
54pub struct UnitTopologyData {
55 pub relative_position: [i32; 3],
56 pub dimensions: [u32; 3],
57}
58
59#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
60pub struct CorticalTypeMetadata {
61 pub description: String,
62 pub encodings: Vec<String>,
63 pub formats: Vec<String>,
64 pub units: u32,
65 pub resolution: Vec<i32>,
66 pub structure: String,
67 pub unit_default_topology: HashMap<usize, UnitTopologyData>,
68}
69
70pub const VOXEL_NEURON_SYNAPSES_PER_DIRECTION_PER_PAGE: usize = 50;
72
73fn synapse_page_window(total: usize, page: u32) -> (usize, usize, bool) {
75 let page = page as usize;
76 let page_size = VOXEL_NEURON_SYNAPSES_PER_DIRECTION_PER_PAGE;
77 let start = page.saturating_mul(page_size);
78 if start >= total {
79 return (0, 0, false);
80 }
81 let end = (start + page_size).min(total);
82 let has_more = total > end;
83 (start, end, has_more)
84}
85
86#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
88#[into_params(parameter_in = Query)]
89pub struct VoxelNeuronsQuery {
90 pub cortical_id: String,
92 pub x: u32,
93 pub y: u32,
94 pub z: u32,
95 #[serde(default)]
97 pub synapse_page: u32,
98}
99
100#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
102pub struct VoxelNeuronsBody {
103 pub cortical_id: String,
104 pub x: u32,
105 pub y: u32,
106 pub z: u32,
107 #[serde(default)]
108 pub synapse_page: u32,
109}
110
111pub const MEMORY_CORTICAL_NEURON_IDS_PAGE_SIZE_DEFAULT: u32 = 50;
113pub const MEMORY_CORTICAL_NEURON_IDS_PAGE_SIZE_MAX: u32 = 500;
115
116fn default_memory_cortical_page_size() -> u32 {
117 MEMORY_CORTICAL_NEURON_IDS_PAGE_SIZE_DEFAULT
118}
119
120#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
122#[into_params(parameter_in = Query)]
123pub struct MemoryCorticalAreaQuery {
124 pub cortical_id: String,
126 #[serde(default)]
128 pub page: u32,
129 #[serde(default = "default_memory_cortical_page_size")]
131 pub page_size: u32,
132}
133
134#[derive(Debug, Clone, Serialize, ToSchema)]
136pub struct MemoryCorticalAreaParamsResponse {
137 pub temporal_depth: u32,
138 pub longterm_mem_threshold: u32,
139 pub lifespan_growth_rate: f32,
140 pub init_lifespan: u32,
141}
142
143#[derive(Debug, Clone, Serialize, ToSchema)]
145pub struct MemoryCorticalAreaResponse {
146 pub cortical_id: String,
147 pub cortical_idx: u32,
148 pub cortical_name: String,
149 pub short_term_neuron_count: usize,
150 pub long_term_neuron_count: usize,
151 pub memory_parameters: MemoryCorticalAreaParamsResponse,
152 pub upstream_cortical_area_indices: Vec<u32>,
154 pub upstream_cortical_area_count: usize,
155 pub upstream_pattern_cache_size: usize,
157 pub incoming_synapse_count: usize,
158 pub outgoing_synapse_count: usize,
159 pub total_memory_neuron_ids: usize,
160 pub page: u32,
161 pub page_size: u32,
162 pub memory_neuron_ids: Vec<u64>,
163 pub has_more: bool,
164}
165
166pub(crate) fn peer_cortical_voxel_fields(
168 mgr: &feagi_brain_development::ConnectomeManager,
169 peer_id: u64,
170 prefix: &str,
171) -> serde_json::Map<String, serde_json::Value> {
172 let mut m = serde_json::Map::new();
173 let peer_cortical = mgr.get_neuron_cortical_id(peer_id);
174 let cortical_id = peer_cortical.map(|c| c.as_base_64());
175 m.insert(
176 format!("{prefix}_cortical_id"),
177 cortical_id.map_or(serde_json::Value::Null, serde_json::Value::String),
178 );
179 let cortical_name = peer_cortical
180 .and_then(|cid| mgr.get_cortical_area(&cid))
181 .map(|a| a.name.clone());
182 m.insert(
183 format!("{prefix}_cortical_name"),
184 cortical_name.map_or(serde_json::Value::Null, serde_json::Value::String),
185 );
186 m.insert(
187 format!("{prefix}_cortical_idx"),
188 mgr.get_neuron_cortical_idx_opt(peer_id)
189 .map_or(serde_json::Value::Null, |v| serde_json::json!(v)),
190 );
191 let (vx, vy, vz) = mgr.get_neuron_coordinates(peer_id);
192 m.insert(format!("{prefix}_x"), serde_json::json!(vx));
193 m.insert(format!("{prefix}_y"), serde_json::json!(vy));
194 m.insert(format!("{prefix}_z"), serde_json::json!(vz));
195 m
196}
197
198pub(crate) fn synapse_details_for_neuron(
201 mgr: &feagi_brain_development::ConnectomeManager,
202 neuron_id: u32,
203 outgoing: &[(u32, f32, f32, u8)],
204 incoming: &[(u32, f32, f32, u8)],
205) -> (serde_json::Value, serde_json::Value) {
206 let outgoing_json: Vec<serde_json::Value> = outgoing
207 .iter()
208 .map(|&(target_id, weight, psp, synapse_type)| {
209 let mut obj = serde_json::Map::new();
210 obj.insert("source_neuron_id".to_string(), serde_json::json!(neuron_id));
211 obj.insert("target_neuron_id".to_string(), serde_json::json!(target_id));
212 obj.insert("weight".to_string(), serde_json::json!(weight));
213 obj.insert("postsynaptic_potential".to_string(), serde_json::json!(psp));
214 obj.insert("synapse_type".to_string(), serde_json::json!(synapse_type));
215 obj.extend(peer_cortical_voxel_fields(mgr, target_id as u64, "target"));
216 serde_json::Value::Object(obj)
217 })
218 .collect();
219 let incoming_json: Vec<serde_json::Value> = incoming
220 .iter()
221 .map(|&(source_id, weight, psp, synapse_type)| {
222 let mut obj = serde_json::Map::new();
223 obj.insert("source_neuron_id".to_string(), serde_json::json!(source_id));
224 obj.insert("target_neuron_id".to_string(), serde_json::json!(neuron_id));
225 obj.insert("weight".to_string(), serde_json::json!(weight));
226 obj.insert("postsynaptic_potential".to_string(), serde_json::json!(psp));
227 obj.insert("synapse_type".to_string(), serde_json::json!(synapse_type));
228 obj.extend(peer_cortical_voxel_fields(mgr, source_id as u64, "source"));
229 serde_json::Value::Object(obj)
230 })
231 .collect();
232 (
233 serde_json::Value::Array(outgoing_json),
234 serde_json::Value::Array(incoming_json),
235 )
236}
237
238#[derive(Debug, Clone, Serialize, ToSchema)]
240pub struct VoxelNeuronsResponse {
241 pub cortical_id: String,
242 pub cortical_name: String,
244 pub cortical_idx: u32,
245 pub voxel_coordinate: [u32; 3],
247 pub x: u32,
248 pub y: u32,
249 pub z: u32,
250 pub synapse_page: u32,
252 pub neuron_count: usize,
253 pub neurons: Vec<serde_json::Value>,
254}
255
256async fn resolve_voxel_neurons(
260 state: &ApiState,
261 cortical_id: String,
262 x: u32,
263 y: u32,
264 z: u32,
265 synapse_page: u32,
266) -> ApiResult<VoxelNeuronsResponse> {
267 let connectome_service = state.connectome_service.as_ref();
268 let area = connectome_service
269 .get_cortical_area(&cortical_id)
270 .await
271 .map_err(ApiError::from)?;
272
273 let cortical_idx = area.cortical_idx;
274 let cortical_name = area.name.clone();
275
276 let matching_ids: Vec<u32> = {
277 let manager = feagi_brain_development::ConnectomeManager::instance();
278 let manager_lock = manager.read();
279 let npu_arc = manager_lock
280 .get_npu()
281 .ok_or_else(|| ApiError::internal("NPU not initialized"))?;
282 let npu_lock = npu_arc.lock().map_err(|_| {
283 ApiError::internal("NPU mutex poisoned; restart FEAGI or wait for burst recovery")
284 })?;
285
286 let mut ids: Vec<u32> = npu_lock
287 .get_neurons_in_cortical_area(cortical_idx)
288 .into_iter()
289 .filter(|&nid| {
290 npu_lock
291 .get_neuron_coordinates(nid)
292 .map(|(nx, ny, nz)| nx == x && ny == y && nz == z)
293 .unwrap_or(false)
294 })
295 .collect();
296 ids.sort_unstable();
297 ids
298 };
299
300 let mut neurons: Vec<serde_json::Value> = Vec::with_capacity(matching_ids.len());
301 for neuron_id in &matching_ids {
302 let nid = *neuron_id;
303 let mut props = connectome_service
304 .get_neuron_properties(nid as u64)
305 .await
306 .map_err(ApiError::from)?;
307 props.insert(
308 "cortical_id".to_string(),
309 serde_json::Value::String(cortical_id.clone()),
310 );
311 props.insert("cortical_idx".to_string(), serde_json::json!(cortical_idx));
312
313 let (
314 outgoing_synapse_count,
315 incoming_synapse_count,
316 out_json,
317 in_json,
318 outgoing_synapses_has_more,
319 incoming_synapses_has_more,
320 ) = {
321 let manager = feagi_brain_development::ConnectomeManager::instance();
322 let mgr = manager.read();
323 let outgoing_full = mgr.get_outgoing_synapses(nid as u64);
324 let incoming_full = mgr.get_incoming_synapses(nid as u64);
325 let oc = outgoing_full.len();
326 let ic = incoming_full.len();
327 let (o_start, o_end, out_has_more) = synapse_page_window(oc, synapse_page);
328 let (i_start, i_end, in_has_more) = synapse_page_window(ic, synapse_page);
329 let out_slice = &outgoing_full[o_start..o_end];
330 let in_slice = &incoming_full[i_start..i_end];
331 let (out_json, in_json) = synapse_details_for_neuron(&mgr, nid, out_slice, in_slice);
332 (oc, ic, out_json, in_json, out_has_more, in_has_more)
333 };
334 props.insert("synapse_page".to_string(), serde_json::json!(synapse_page));
335 props.insert(
336 "outgoing_synapse_count".to_string(),
337 serde_json::json!(outgoing_synapse_count),
338 );
339 props.insert(
340 "incoming_synapse_count".to_string(),
341 serde_json::json!(incoming_synapse_count),
342 );
343 props.insert(
344 "outgoing_synapses_has_more".to_string(),
345 serde_json::json!(outgoing_synapses_has_more),
346 );
347 props.insert(
348 "incoming_synapses_has_more".to_string(),
349 serde_json::json!(incoming_synapses_has_more),
350 );
351 props.insert("outgoing_synapses".to_string(), out_json);
352 props.insert("incoming_synapses".to_string(), in_json);
353
354 neurons.push(serde_json::to_value(&props).map_err(|e| {
355 ApiError::internal(format!("Failed to serialize neuron properties: {}", e))
356 })?);
357 }
358
359 Ok(VoxelNeuronsResponse {
360 cortical_id,
361 cortical_name,
362 cortical_idx,
363 voxel_coordinate: [x, y, z],
364 x,
365 y,
366 z,
367 synapse_page,
368 neuron_count: neurons.len(),
369 neurons,
370 })
371}
372
373#[utoipa::path(get, path = "/v1/cortical_area/ipu", tag = "cortical_area")]
379pub async fn get_ipu(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
380 let connectome_service = state.connectome_service.as_ref();
381 match connectome_service.list_cortical_areas().await {
382 Ok(areas) => {
383 let ipu_areas: Vec<String> = areas
384 .into_iter()
385 .filter(|a| a.area_type == "sensory" || a.area_type == "IPU")
386 .map(|a| a.cortical_id)
387 .collect();
388 Ok(Json(ipu_areas))
389 }
390 Err(e) => Err(ApiError::internal(format!(
391 "Failed to get IPU areas: {}",
392 e
393 ))),
394 }
395}
396
397#[utoipa::path(get, path = "/v1/cortical_area/opu", tag = "cortical_area")]
399pub async fn get_opu(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
400 let connectome_service = state.connectome_service.as_ref();
401 match connectome_service.list_cortical_areas().await {
402 Ok(areas) => {
403 let opu_areas: Vec<String> = areas
404 .into_iter()
405 .filter(|a| a.area_type == "motor" || a.area_type == "OPU")
406 .map(|a| a.cortical_id)
407 .collect();
408 Ok(Json(opu_areas))
409 }
410 Err(e) => Err(ApiError::internal(format!(
411 "Failed to get OPU areas: {}",
412 e
413 ))),
414 }
415}
416
417#[utoipa::path(
419 get,
420 path = "/v1/cortical_area/cortical_area_id_list",
421 tag = "cortical_area",
422 responses(
423 (status = 200, description = "Cortical area IDs retrieved successfully", body = CorticalAreaIdListResponse),
424 (status = 500, description = "Internal server error", body = ApiError)
425 )
426)]
427pub async fn get_cortical_area_id_list(
428 State(state): State<ApiState>,
429) -> ApiResult<Json<CorticalAreaIdListResponse>> {
430 tracing::debug!(target: "feagi-api", "🔍 GET /v1/cortical_area/cortical_area_id_list - handler called");
431 let connectome_service = state.connectome_service.as_ref();
432 match connectome_service.get_cortical_area_ids().await {
433 Ok(ids) => {
434 tracing::info!(target: "feagi-api", "✅ GET /v1/cortical_area/cortical_area_id_list - success, returning {} IDs", ids.len());
435 tracing::debug!(target: "feagi-api", "📋 Cortical area IDs: {:?}", ids.iter().take(20).collect::<Vec<_>>());
436 let response = CorticalAreaIdListResponse {
437 cortical_ids: ids.clone(),
438 };
439 match serde_json::to_string(&response) {
440 Ok(json_str) => {
441 tracing::debug!(target: "feagi-api", "📤 Response JSON: {}", json_str);
442 }
443 Err(e) => {
444 tracing::warn!(target: "feagi-api", "⚠️ Failed to serialize response: {}", e);
445 }
446 }
447 Ok(Json(response))
448 }
449 Err(e) => {
450 tracing::error!(target: "feagi-api", "❌ GET /v1/cortical_area/cortical_area_id_list - error: {}", e);
451 Err(ApiError::internal(format!(
452 "Failed to get cortical IDs: {}",
453 e
454 )))
455 }
456 }
457}
458
459#[utoipa::path(
461 get,
462 path = "/v1/cortical_area/cortical_area_name_list",
463 tag = "cortical_area",
464 responses(
465 (status = 200, description = "Cortical area names retrieved successfully", body = CorticalAreaNameListResponse),
466 (status = 500, description = "Internal server error")
467 )
468)]
469pub async fn get_cortical_area_name_list(
470 State(state): State<ApiState>,
471) -> ApiResult<Json<CorticalAreaNameListResponse>> {
472 let connectome_service = state.connectome_service.as_ref();
473 match connectome_service.list_cortical_areas().await {
474 Ok(areas) => {
475 let names: Vec<String> = areas.into_iter().map(|a| a.name).collect();
476 Ok(Json(CorticalAreaNameListResponse {
477 cortical_area_name_list: names,
478 }))
479 }
480 Err(e) => Err(ApiError::internal(format!(
481 "Failed to get cortical names: {}",
482 e
483 ))),
484 }
485}
486
487#[utoipa::path(
489 get,
490 path = "/v1/cortical_area/cortical_id_name_mapping",
491 tag = "cortical_area"
492)]
493pub async fn get_cortical_id_name_mapping(
494 State(state): State<ApiState>,
495) -> ApiResult<Json<HashMap<String, String>>> {
496 let connectome_service = state.connectome_service.as_ref();
497 let ids = connectome_service
498 .get_cortical_area_ids()
499 .await
500 .map_err(|e| ApiError::internal(format!("Failed to get IDs: {}", e)))?;
501
502 let mut mapping = HashMap::new();
503 for id in ids {
504 if let Ok(area) = connectome_service.get_cortical_area(&id).await {
505 mapping.insert(id, area.name);
506 }
507 }
508 Ok(Json(mapping))
509}
510
511#[utoipa::path(get, path = "/v1/cortical_area/cortical_types", tag = "cortical_area")]
513pub async fn get_cortical_types(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
514 Ok(Json(vec![
515 "sensory".to_string(),
516 "motor".to_string(),
517 "memory".to_string(),
518 "custom".to_string(),
519 ]))
520}
521
522#[utoipa::path(
524 get,
525 path = "/v1/cortical_area/cortical_map_detailed",
526 tag = "cortical_area",
527 responses(
528 (status = 200, description = "Detailed cortical area mapping data", body = HashMap<String, serde_json::Value>),
529 (status = 500, description = "Internal server error")
530 )
531)]
532pub async fn get_cortical_map_detailed(
533 State(state): State<ApiState>,
534) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
535 let connectome_service = state.connectome_service.as_ref();
536 match connectome_service.list_cortical_areas().await {
537 Ok(areas) => {
538 let mut map: HashMap<String, serde_json::Value> = HashMap::new();
539
540 for area in areas {
541 if let Some(cortical_mapping_dst) = area.properties.get("cortical_mapping_dst") {
543 if !cortical_mapping_dst.is_null()
544 && cortical_mapping_dst
545 .as_object()
546 .is_some_and(|obj| !obj.is_empty())
547 {
548 map.insert(area.cortical_id.clone(), cortical_mapping_dst.clone());
549 }
550 }
551 }
552
553 Ok(Json(map))
554 }
555 Err(e) => Err(ApiError::internal(format!(
556 "Failed to get detailed map: {}",
557 e
558 ))),
559 }
560}
561
562#[utoipa::path(
564 get,
565 path = "/v1/cortical_area/cortical_locations_2d",
566 tag = "cortical_area"
567)]
568pub async fn get_cortical_locations_2d(
569 State(state): State<ApiState>,
570) -> ApiResult<Json<HashMap<String, (i32, i32)>>> {
571 let connectome_service = state.connectome_service.as_ref();
572 match connectome_service.list_cortical_areas().await {
573 Ok(areas) => {
574 let locations: HashMap<String, (i32, i32)> = areas
575 .into_iter()
576 .map(|area| (area.cortical_id, (area.position.0, area.position.1)))
577 .collect();
578 Ok(Json(locations))
579 }
580 Err(e) => Err(ApiError::internal(format!(
581 "Failed to get 2D locations: {}",
582 e
583 ))),
584 }
585}
586
587#[utoipa::path(
589 get,
590 path = "/v1/cortical_area/cortical_area/geometry",
591 tag = "cortical_area"
592)]
593pub async fn get_cortical_area_geometry(
594 State(state): State<ApiState>,
595) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
596 let connectome_service = state.connectome_service.as_ref();
597 match connectome_service.list_cortical_areas().await {
598 Ok(areas) => {
599 let geometry: HashMap<String, serde_json::Value> = areas.into_iter()
600 .map(|area| {
601 let coordinate_2d = area
604 .properties
605 .get("coordinate_2d")
606 .or_else(|| area.properties.get("coordinates_2d"))
607 .cloned()
608 .unwrap_or_else(|| serde_json::json!([0, 0]));
609 let data = serde_json::json!({
610 "cortical_id": area.cortical_id,
611 "cortical_name": area.name,
612 "cortical_group": area.cortical_group,
613 "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],
616 "coordinates_2d": coordinate_2d,
617 "cortical_dimensions": [area.dimensions.0, area.dimensions.1, area.dimensions.2],
618 "cortical_neuron_per_vox_count": area.neurons_per_voxel,
619 "visualization": area.visible,
620 "visible": area.visible,
621 "dimensions": {
623 "x": area.dimensions.0,
624 "y": area.dimensions.1,
625 "z": area.dimensions.2
626 },
627 "position": {
628 "x": area.position.0,
629 "y": area.position.1,
630 "z": area.position.2
631 },
632 "neuron_post_synaptic_potential": area.postsynaptic_current,
634 "neuron_fire_threshold": area.firing_threshold,
636 "neuron_firing_threshold_limit": area.firing_threshold_limit,
637 "plasticity_constant": area.plasticity_constant,
638 "degeneration": area.degeneration,
639 "leak_coefficient": area.leak_coefficient,
640 "refractory_period": area.refractory_period,
641 "snooze_period": area.snooze_period,
642 "parent_region_id": area.parent_region_id,
644 "visualization_voxel_granularity": area.visualization_voxel_granularity.map(|(x, y, z)| serde_json::json!([x, y, z])),
646 });
647 (area.cortical_id.clone(), data)
648 })
649 .collect();
650 Ok(Json(geometry))
651 }
652 Err(e) => Err(ApiError::internal(format!("Failed to get geometry: {}", e))),
653 }
654}
655
656#[utoipa::path(
658 get,
659 path = "/v1/cortical_area/cortical_visibility",
660 tag = "cortical_area"
661)]
662pub async fn get_cortical_visibility(
663 State(state): State<ApiState>,
664) -> ApiResult<Json<HashMap<String, bool>>> {
665 let connectome_service = state.connectome_service.as_ref();
666 match connectome_service.list_cortical_areas().await {
667 Ok(areas) => {
668 let visibility: HashMap<String, bool> = areas
669 .into_iter()
670 .map(|area| (area.cortical_id, area.visible))
671 .collect();
672 Ok(Json(visibility))
673 }
674 Err(e) => Err(ApiError::internal(format!(
675 "Failed to get visibility: {}",
676 e
677 ))),
678 }
679}
680
681#[utoipa::path(
683 post,
684 path = "/v1/cortical_area/cortical_name_location",
685 tag = "cortical_area"
686)]
687#[allow(unused_variables)] pub async fn post_cortical_name_location(
689 State(state): State<ApiState>,
690 Json(request): Json<HashMap<String, String>>,
691) -> ApiResult<Json<HashMap<String, (i32, i32)>>> {
692 let connectome_service = state.connectome_service.as_ref();
693 let cortical_name = request
694 .get("cortical_name")
695 .ok_or_else(|| ApiError::invalid_input("cortical_name required"))?;
696
697 match connectome_service.get_cortical_area(cortical_name).await {
698 Ok(area) => Ok(Json(HashMap::from([(
699 area.cortical_id,
700 (area.position.0, area.position.1),
701 )]))),
702 Err(e) => Err(ApiError::internal(format!("Failed to get location: {}", e))),
703 }
704}
705
706#[utoipa::path(
708 post,
709 path = "/v1/cortical_area/cortical_area_properties",
710 tag = "cortical_area"
711)]
712#[allow(unused_variables)] pub async fn post_cortical_area_properties(
714 State(state): State<ApiState>,
715 Json(request): Json<HashMap<String, String>>,
716) -> ApiResult<Json<serde_json::Value>> {
717 let connectome_service = state.connectome_service.as_ref();
718 let cortical_id = request
719 .get("cortical_id")
720 .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?;
721
722 match connectome_service.get_cortical_area(cortical_id).await {
723 Ok(area_info) => {
724 tracing::debug!(target: "feagi-api", "Cortical area properties for {}: cortical_group={}, area_type={}, cortical_type={}",
725 cortical_id, area_info.cortical_group, area_info.area_type, area_info.cortical_type);
726 tracing::info!(target: "feagi-api", "[API-RESPONSE] Returning mp_driven_psp={} for area {}", area_info.mp_driven_psp, cortical_id);
727 let json_value = serde_json::to_value(&area_info).unwrap_or_default();
728 tracing::debug!(target: "feagi-api", "Serialized JSON keys: {:?}", json_value.as_object().map(|o| o.keys().collect::<Vec<_>>()));
729 tracing::debug!(target: "feagi-api", "Serialized cortical_type value: {:?}", json_value.get("cortical_type"));
730 Ok(Json(json_value))
731 }
732 Err(e) => Err(ApiError::internal(format!(
733 "Failed to get properties: {}",
734 e
735 ))),
736 }
737}
738
739#[utoipa::path(
741 post,
742 path = "/v1/cortical_area/multi/cortical_area_properties",
743 tag = "cortical_area"
744)]
745#[allow(unused_variables)] pub async fn post_multi_cortical_area_properties(
747 State(state): State<ApiState>,
748 Json(request): Json<serde_json::Value>,
749) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
750 let connectome_service = state.connectome_service.as_ref();
751 let mut result = HashMap::new();
752
753 let cortical_ids: Vec<String> = if request.is_array() {
755 request
757 .as_array()
758 .unwrap()
759 .iter()
760 .filter_map(|v| v.as_str().map(|s| s.to_string()))
761 .collect()
762 } else if request.is_object() {
763 request
765 .get("cortical_id_list")
766 .and_then(|v| v.as_array())
767 .ok_or_else(|| ApiError::invalid_input("cortical_id_list required in object format"))?
768 .iter()
769 .filter_map(|v| v.as_str().map(|s| s.to_string()))
770 .collect()
771 } else {
772 return Err(ApiError::invalid_input(
773 "Request must be an array of IDs or object with cortical_id_list",
774 ));
775 };
776
777 for cortical_id in cortical_ids {
778 if let Ok(area_info) = connectome_service.get_cortical_area(&cortical_id).await {
779 tracing::trace!(target: "feagi-api",
780 "[MULTI] Area {}: cortical_type={}, cortical_group={}, is_mem_type={:?}",
781 cortical_id, area_info.cortical_type, area_info.cortical_group,
782 area_info.properties.get("is_mem_type")
783 );
784 let json_value = serde_json::to_value(&area_info).unwrap_or_default();
785 tracing::trace!(target: "feagi-api",
786 "[MULTI] Serialized has cortical_type: {}",
787 json_value.get("cortical_type").is_some()
788 );
789 result.insert(cortical_id, json_value);
790 }
791 }
792 Ok(Json(result))
793}
794
795#[utoipa::path(post, path = "/v1/cortical_area/cortical_area", tag = "cortical_area")]
797#[allow(unused_variables)] pub async fn post_cortical_area(
799 State(state): State<ApiState>,
800 Json(request): Json<HashMap<String, serde_json::Value>>,
801) -> ApiResult<Json<serde_json::Value>> {
802 use feagi_services::types::CreateCorticalAreaParams;
803 use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
804
805 let genome_service = state.genome_service.as_ref();
807
808 let cortical_type_key = request
810 .get("cortical_id")
811 .and_then(|v| v.as_str())
812 .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?;
813
814 let mut group_id = request
815 .get("group_id")
816 .and_then(|v| v.as_u64())
817 .unwrap_or(0) as u8;
818
819 let device_count = request
820 .get("device_count")
821 .and_then(|v| v.as_u64())
822 .ok_or_else(|| ApiError::invalid_input("device_count required"))?
823 as usize;
824
825 let coordinates_3d: Vec<i32> = request
826 .get("coordinates_3d")
827 .and_then(|v| v.as_array())
828 .and_then(|arr| {
829 if arr.len() == 3 {
830 Some(vec![
831 arr[0].as_i64()? as i32,
832 arr[1].as_i64()? as i32,
833 arr[2].as_i64()? as i32,
834 ])
835 } else {
836 None
837 }
838 })
839 .ok_or_else(|| ApiError::invalid_input("coordinates_3d must be [x, y, z]"))?;
840
841 let cortical_type_str = request
842 .get("cortical_type")
843 .and_then(|v| v.as_str())
844 .ok_or_else(|| ApiError::invalid_input("cortical_type required"))?;
845
846 let unit_id: Option<u8> = request
847 .get("unit_id")
848 .and_then(|v| v.as_u64())
849 .map(|value| {
850 value
851 .try_into()
852 .map_err(|_| ApiError::invalid_input("unit_id out of range"))
853 })
854 .transpose()?;
855 if let Some(unit_id) = unit_id {
856 group_id = unit_id;
857 }
858
859 let neurons_per_voxel = request
861 .get("neurons_per_voxel")
862 .and_then(|v| v.as_u64())
863 .unwrap_or(1) as u32;
864
865 let per_device_dimensions_override: Option<(usize, usize, usize)> = request
869 .get("per_device_dimensions")
870 .and_then(|v| v.as_array())
871 .and_then(|arr| {
872 if arr.len() == 3 {
873 Some((
874 arr[0].as_u64()? as usize,
875 arr[1].as_u64()? as usize,
876 arr[2].as_u64()? as usize,
877 ))
878 } else {
879 None
880 }
881 });
882
883 let raw_configs = request
890 .get("data_type_configs_by_subunit")
891 .and_then(|v| v.as_object())
892 .ok_or_else(|| ApiError::invalid_input("data_type_configs_by_subunit (object) required"))?;
893
894 let mut data_type_configs_by_subunit: HashMap<u8, u16> = HashMap::new();
895
896 for (k, v) in raw_configs {
897 let subunit_idx_u64 = k.parse::<u64>().map_err(|_| {
898 ApiError::invalid_input("data_type_configs_by_subunit keys must be integers")
899 })?;
900 let subunit_idx: u8 = subunit_idx_u64.try_into().map_err(|_| {
901 ApiError::invalid_input("data_type_configs_by_subunit key out of range")
902 })?;
903
904 let parsed_u64 = if let Some(u) = v.as_u64() {
905 Some(u)
906 } else if let Some(i) = v.as_i64() {
907 if i >= 0 {
908 Some(i as u64)
909 } else {
910 None
911 }
912 } else if let Some(f) = v.as_f64() {
913 if f >= 0.0 {
914 Some(f.round() as u64)
915 } else {
916 None
917 }
918 } else if let Some(s) = v.as_str() {
919 s.parse::<u64>().ok()
920 } else {
921 None
922 }
923 .ok_or_else(|| {
924 ApiError::invalid_input("data_type_configs_by_subunit values must be numeric")
925 })?;
926
927 if parsed_u64 > u16::MAX as u64 {
928 return Err(ApiError::invalid_input(
929 "data_type_configs_by_subunit value exceeds u16::MAX",
930 ));
931 }
932
933 data_type_configs_by_subunit.insert(subunit_idx, parsed_u64 as u16);
934 }
935
936 tracing::info!(
937 target: "feagi-api",
938 "Creating cortical areas for {} with neurons_per_voxel={}, data_type_configs_by_subunit={:?}",
939 cortical_type_key,
940 neurons_per_voxel,
941 data_type_configs_by_subunit
942 );
943
944 let (num_units, unit_topology) = if cortical_type_str == "IPU" {
946 let unit = SensoryCorticalUnit::list_all()
948 .iter()
949 .find(|u| {
950 let id_ref = u.get_cortical_id_unit_reference();
951 let key = format!("i{}", std::str::from_utf8(&id_ref).unwrap_or(""));
952 key == cortical_type_key
953 })
954 .ok_or_else(|| {
955 ApiError::invalid_input(format!("Unknown IPU type: {}", cortical_type_key))
956 })?;
957
958 (
959 unit.get_number_cortical_areas(),
960 unit.get_unit_default_topology(),
961 )
962 } else if cortical_type_str == "OPU" {
963 let unit = MotorCorticalUnit::list_all()
965 .iter()
966 .find(|u| {
967 let id_ref = u.get_cortical_id_unit_reference();
968 let key = format!("o{}", std::str::from_utf8(&id_ref).unwrap_or(""));
969 key == cortical_type_key
970 })
971 .ok_or_else(|| {
972 ApiError::invalid_input(format!("Unknown OPU type: {}", cortical_type_key))
973 })?;
974
975 (
976 unit.get_number_cortical_areas(),
977 unit.get_unit_default_topology(),
978 )
979 } else {
980 return Err(ApiError::invalid_input("cortical_type must be IPU or OPU"));
981 };
982
983 tracing::info!(
984 "Creating {} units for cortical type: {}",
985 num_units,
986 cortical_type_key
987 );
988
989 let mut creation_params = Vec::new();
991 for unit_idx in 0..num_units {
992 let data_type_config = data_type_configs_by_subunit
993 .get(&(unit_idx as u8))
994 .copied()
995 .ok_or_else(|| {
996 ApiError::invalid_input(format!(
997 "data_type_configs_by_subunit missing entry for subunit {}",
998 unit_idx
999 ))
1000 })?;
1001
1002 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) = if let Some(override_dims) =
1010 per_device_dimensions_override
1011 {
1012 let total_x = override_dims.0.saturating_mul(device_count);
1014 (override_dims, (total_x, override_dims.1, override_dims.2))
1015 } else if let Some(topo) = unit_topology.get(&CorticalSubUnitIndex::from(unit_idx as u8)) {
1016 let dims = topo.channel_dimensions_default;
1018 let per_device = (dims[0] as usize, dims[1] as usize, dims[2] as usize);
1019 let total_x = per_device.0.saturating_mul(device_count);
1020 (per_device, (total_x, per_device.1, per_device.2))
1021 } else {
1022 ((1, 1, 1), (device_count.max(1), 1, 1)) };
1024
1025 let position =
1027 if let Some(topo) = unit_topology.get(&CorticalSubUnitIndex::from(unit_idx as u8)) {
1028 let rel_pos = topo.relative_position;
1029 (
1030 coordinates_3d[0] + rel_pos[0],
1031 coordinates_3d[1] + rel_pos[1],
1032 coordinates_3d[2] + rel_pos[2],
1033 )
1034 } else {
1035 (coordinates_3d[0], coordinates_3d[1], coordinates_3d[2])
1036 };
1037
1038 let subtype_bytes = if cortical_type_key.len() >= 4 {
1042 let subtype_str = &cortical_type_key[1..4]; let mut bytes = [0u8; 3];
1044 for (i, c) in subtype_str.chars().take(3).enumerate() {
1045 bytes[i] = c as u8;
1046 }
1047 bytes
1048 } else {
1049 return Err(ApiError::invalid_input("Invalid cortical_type_key"));
1050 };
1051
1052 let cortical_id_bytes = [
1054 if cortical_type_str == "IPU" {
1055 b'i'
1056 } else {
1057 b'o'
1058 }, subtype_bytes[0], subtype_bytes[1], subtype_bytes[2], config_byte_4, config_byte_5, unit_idx as u8, group_id, ];
1067
1068 let cortical_id = general_purpose::STANDARD.encode(cortical_id_bytes);
1070
1071 tracing::debug!(target: "feagi-api",
1072 " Unit {}: dims={}x{}x{}, neurons_per_voxel={}, total_neurons={}",
1073 unit_idx, dimensions.0, dimensions.1, dimensions.2, neurons_per_voxel,
1074 dimensions.0 * dimensions.1 * dimensions.2 * neurons_per_voxel as usize
1075 );
1076
1077 let mut properties = HashMap::new();
1079 properties.insert(
1080 "dev_count".to_string(),
1081 serde_json::Value::Number(serde_json::Number::from(device_count)),
1082 );
1083 properties.insert(
1084 "cortical_dimensions_per_device".to_string(),
1085 serde_json::json!([
1086 per_device_dimensions.0,
1087 per_device_dimensions.1,
1088 per_device_dimensions.2
1089 ]),
1090 );
1091
1092 let params = CreateCorticalAreaParams {
1093 cortical_id: cortical_id.clone(),
1094 name: format!("{} Unit {}", cortical_type_key, unit_idx),
1095 dimensions,
1096 position,
1097 area_type: cortical_type_str.to_string(),
1098 visible: Some(true),
1099 sub_group: None,
1100 neurons_per_voxel: Some(neurons_per_voxel),
1101 postsynaptic_current: Some(0.0),
1102 plasticity_constant: Some(0.0),
1103 degeneration: Some(0.0),
1104 psp_uniform_distribution: Some(false),
1105 firing_threshold_increment: Some(0.0),
1106 firing_threshold_limit: Some(0.0),
1107 consecutive_fire_count: Some(0),
1108 snooze_period: Some(0),
1109 refractory_period: Some(0),
1110 leak_coefficient: Some(0.0),
1111 leak_variability: Some(0.0),
1112 burst_engine_active: Some(true),
1113 properties: Some(properties),
1114 };
1115
1116 creation_params.push(params);
1117 }
1118
1119 tracing::info!(
1120 "Calling GenomeService to create {} cortical areas",
1121 creation_params.len()
1122 );
1123
1124 let areas_details = genome_service
1127 .create_cortical_areas(creation_params)
1128 .await
1129 .map_err(|e| ApiError::internal(format!("Failed to create cortical areas: {}", e)))?;
1130
1131 tracing::info!(
1132 "✅ Successfully created {} cortical areas via GenomeService",
1133 areas_details.len()
1134 );
1135
1136 let areas_json = serde_json::to_value(&areas_details).unwrap_or_default();
1138
1139 let created_ids: Vec<String> = areas_details
1141 .iter()
1142 .map(|a| a.cortical_id.clone())
1143 .collect();
1144
1145 let first_id = created_ids.first().cloned().unwrap_or_default();
1147 let mut response = serde_json::Map::new();
1148 response.insert(
1149 "message".to_string(),
1150 serde_json::Value::String(format!("Created {} cortical areas", created_ids.len())),
1151 );
1152 response.insert(
1153 "cortical_id".to_string(),
1154 serde_json::Value::String(first_id),
1155 ); response.insert(
1157 "cortical_ids".to_string(),
1158 serde_json::Value::String(created_ids.join(", ")),
1159 );
1160 response.insert(
1161 "unit_count".to_string(),
1162 serde_json::Value::Number(created_ids.len().into()),
1163 );
1164 response.insert("areas".to_string(), areas_json); Ok(Json(serde_json::Value::Object(response)))
1167}
1168
1169#[utoipa::path(put, path = "/v1/cortical_area/cortical_area", tag = "cortical_area")]
1171pub async fn put_cortical_area(
1172 State(state): State<ApiState>,
1173 Json(mut request): Json<HashMap<String, serde_json::Value>>,
1174) -> ApiResult<Json<HashMap<String, String>>> {
1175 let genome_service = state.genome_service.as_ref();
1176
1177 let cortical_id = request
1179 .get("cortical_id")
1180 .and_then(|v| v.as_str())
1181 .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?
1182 .to_string();
1183
1184 tracing::debug!(
1185 target: "feagi-api",
1186 "PUT /v1/cortical_area/cortical_area - received update for area: {} (keys: {:?})",
1187 cortical_id,
1188 request.keys().collect::<Vec<_>>()
1189 );
1190
1191 request.remove("cortical_id");
1193
1194 match genome_service
1196 .update_cortical_area(&cortical_id, request)
1197 .await
1198 {
1199 Ok(area_info) => {
1200 let updated_id = area_info.cortical_id.clone();
1201 tracing::debug!(
1202 target: "feagi-api",
1203 "PUT /v1/cortical_area/cortical_area - success for {} (updated_id={})",
1204 cortical_id,
1205 updated_id
1206 );
1207 Ok(Json(HashMap::from([
1208 ("message".to_string(), "Cortical area updated".to_string()),
1209 ("cortical_id".to_string(), updated_id),
1210 ("previous_cortical_id".to_string(), cortical_id),
1211 ])))
1212 }
1213 Err(e) => {
1214 tracing::error!(target: "feagi-api", "PUT /v1/cortical_area/cortical_area - failed for {}: {}", cortical_id, e);
1215 Err(ApiError::internal(format!("Failed to update: {}", e)))
1216 }
1217 }
1218}
1219
1220#[utoipa::path(
1222 delete,
1223 path = "/v1/cortical_area/cortical_area",
1224 tag = "cortical_area"
1225)]
1226#[allow(unused_variables)] pub async fn delete_cortical_area(
1228 State(state): State<ApiState>,
1229 Json(request): Json<HashMap<String, String>>,
1230) -> ApiResult<Json<HashMap<String, String>>> {
1231 let connectome_service = state.connectome_service.as_ref();
1232 let cortical_id = request
1233 .get("cortical_id")
1234 .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?;
1235
1236 match connectome_service.delete_cortical_area(cortical_id).await {
1237 Ok(_) => Ok(Json(HashMap::from([(
1238 "message".to_string(),
1239 "Cortical area deleted".to_string(),
1240 )]))),
1241 Err(e) => Err(ApiError::internal(format!("Failed to delete: {}", e))),
1242 }
1243}
1244
1245#[utoipa::path(
1247 post,
1248 path = "/v1/cortical_area/custom_cortical_area",
1249 tag = "cortical_area"
1250)]
1251pub async fn post_custom_cortical_area(
1252 State(state): State<ApiState>,
1253 Json(request): Json<HashMap<String, serde_json::Value>>,
1254) -> ApiResult<Json<HashMap<String, String>>> {
1255 use feagi_services::types::CreateCorticalAreaParams;
1256 use std::time::{SystemTime, UNIX_EPOCH};
1257
1258 let is_memory_area_requested = request
1270 .get("sub_group_id")
1271 .and_then(|v| v.as_str())
1272 .map(|s| s.eq_ignore_ascii_case("MEMORY"))
1273 .unwrap_or(false);
1274
1275 let cortical_name = request
1277 .get("cortical_name")
1278 .and_then(|v| v.as_str())
1279 .ok_or_else(|| ApiError::invalid_input("cortical_name required"))?;
1280
1281 let cortical_dimensions: Vec<u32> = request
1282 .get("cortical_dimensions")
1283 .and_then(|v| v.as_array())
1284 .and_then(|arr| {
1285 if arr.len() == 3 {
1286 Some(vec![
1287 arr[0].as_u64()? as u32,
1288 arr[1].as_u64()? as u32,
1289 arr[2].as_u64()? as u32,
1290 ])
1291 } else {
1292 None
1293 }
1294 })
1295 .ok_or_else(|| ApiError::invalid_input("cortical_dimensions must be [x, y, z]"))?;
1296
1297 let coordinates_3d: Vec<i32> = request
1298 .get("coordinates_3d")
1299 .and_then(|v| v.as_array())
1300 .and_then(|arr| {
1301 if arr.len() == 3 {
1302 Some(vec![
1303 arr[0].as_i64()? as i32,
1304 arr[1].as_i64()? as i32,
1305 arr[2].as_i64()? as i32,
1306 ])
1307 } else {
1308 None
1309 }
1310 })
1311 .ok_or_else(|| ApiError::invalid_input("coordinates_3d must be [x, y, z]"))?;
1312
1313 let brain_region_id = request
1316 .get("brain_region_id")
1317 .and_then(|v| v.as_str())
1318 .map(str::trim)
1319 .filter(|s| !s.is_empty())
1320 .map(|s| s.to_string())
1321 .ok_or_else(|| {
1322 ApiError::invalid_input(
1323 "brain_region_id is required for custom and memory cortical areas",
1324 )
1325 })?;
1326
1327 let cortical_sub_group = request
1328 .get("cortical_sub_group")
1329 .and_then(|v| v.as_str())
1330 .filter(|s| !s.is_empty())
1331 .map(|s| s.to_string());
1332
1333 tracing::info!(target: "feagi-api",
1334 "Creating {} cortical area '{}' with dimensions: {}x{}x{}, position: ({}, {}, {})",
1335 if is_memory_area_requested { "memory" } else { "custom" },
1336 cortical_name, cortical_dimensions[0], cortical_dimensions[1], cortical_dimensions[2],
1337 coordinates_3d[0], coordinates_3d[1], coordinates_3d[2]
1338 );
1339
1340 let timestamp = SystemTime::now()
1344 .duration_since(UNIX_EPOCH)
1345 .unwrap()
1346 .as_millis() as u64;
1347
1348 let mut cortical_id_bytes = [0u8; 8];
1353 cortical_id_bytes[0] = if is_memory_area_requested { b'm' } else { b'c' };
1354
1355 let name_bytes = cortical_name.as_bytes();
1357 for i in 1..7 {
1358 cortical_id_bytes[i] = if i - 1 < name_bytes.len() {
1359 let c = name_bytes[i - 1];
1361 if c.is_ascii_alphanumeric() || c == b'_' {
1362 c
1363 } else {
1364 b'_'
1365 }
1366 } else {
1367 b'_' };
1369 }
1370
1371 cortical_id_bytes[7] = (timestamp & 0xFF) as u8;
1373
1374 let cortical_id = general_purpose::STANDARD.encode(cortical_id_bytes);
1376
1377 tracing::debug!(target: "feagi-api",
1378 "Generated cortical_id: {} (raw bytes: {:?})",
1379 cortical_id, cortical_id_bytes
1380 );
1381
1382 let mut properties = HashMap::new();
1384 properties.insert(
1385 "parent_region_id".to_string(),
1386 serde_json::Value::String(brain_region_id.clone()),
1387 );
1388
1389 let params = CreateCorticalAreaParams {
1391 cortical_id: cortical_id.clone(),
1392 name: cortical_name.to_string(),
1393 dimensions: (
1394 cortical_dimensions[0] as usize,
1395 cortical_dimensions[1] as usize,
1396 cortical_dimensions[2] as usize,
1397 ),
1398 position: (coordinates_3d[0], coordinates_3d[1], coordinates_3d[2]),
1399 area_type: if is_memory_area_requested {
1400 "Memory".to_string()
1401 } else {
1402 "Custom".to_string()
1403 },
1404 visible: Some(true),
1405 sub_group: cortical_sub_group,
1406 neurons_per_voxel: Some(1),
1407 postsynaptic_current: None,
1408 plasticity_constant: Some(0.0),
1409 degeneration: Some(0.0),
1410 psp_uniform_distribution: Some(false),
1411 firing_threshold_increment: Some(0.0),
1412 firing_threshold_limit: Some(0.0),
1413 consecutive_fire_count: Some(0),
1414 snooze_period: Some(0),
1415 refractory_period: Some(0),
1416 leak_coefficient: Some(0.0),
1417 leak_variability: Some(0.0),
1418 burst_engine_active: Some(true),
1419 properties: Some(properties),
1420 };
1421
1422 let genome_service = state.genome_service.as_ref();
1423
1424 tracing::info!(target: "feagi-api", "Calling GenomeService to create custom cortical area");
1425
1426 let areas_details = genome_service
1428 .create_cortical_areas(vec![params])
1429 .await
1430 .map_err(|e| ApiError::internal(format!("Failed to create custom cortical area: {}", e)))?;
1431
1432 let created_area = areas_details
1433 .first()
1434 .ok_or_else(|| ApiError::internal("No cortical area was created"))?;
1435
1436 tracing::info!(target: "feagi-api",
1437 "✅ Successfully created custom cortical area '{}' with ID: {}",
1438 cortical_name, created_area.cortical_id
1439 );
1440
1441 let mut response = HashMap::new();
1443 response.insert(
1444 "message".to_string(),
1445 "Custom cortical area created successfully".to_string(),
1446 );
1447 response.insert("cortical_id".to_string(), created_area.cortical_id.clone());
1448 response.insert("cortical_name".to_string(), cortical_name.to_string());
1449
1450 Ok(Json(response))
1451}
1452
1453#[utoipa::path(post, path = "/v1/cortical_area/clone", tag = "cortical_area")]
1455pub async fn post_clone(
1456 State(state): State<ApiState>,
1457 Json(request): Json<CloneCorticalAreaRequest>,
1458) -> ApiResult<Json<HashMap<String, String>>> {
1459 use base64::{engine::general_purpose, Engine as _};
1460 use feagi_services::types::CreateCorticalAreaParams;
1461 use feagi_structures::genomic::cortical_area::CorticalID;
1462 use serde_json::Value;
1463 use std::time::{SystemTime, UNIX_EPOCH};
1464
1465 let genome_service = state.genome_service.as_ref();
1466 let connectome_service = state.connectome_service.as_ref();
1467
1468 let source_id = request.source_area_id.clone();
1470 let source_typed = CorticalID::try_from_base_64(&source_id)
1471 .map_err(|e| ApiError::invalid_input(e.to_string()))?;
1472 let src_first_byte = source_typed.as_bytes()[0];
1473 if src_first_byte != b'c' && src_first_byte != b'm' {
1474 return Err(ApiError::invalid_input(format!(
1475 "Cloning is only supported for custom ('c') and memory ('m') cortical areas (got prefix byte: {})",
1476 src_first_byte
1477 )));
1478 }
1479
1480 let source_area = connectome_service
1482 .get_cortical_area(&source_id)
1483 .await
1484 .map_err(|e| ApiError::not_found("CorticalArea", &e.to_string()))?;
1485
1486 let source_parent_region_id = source_area
1493 .parent_region_id
1494 .clone()
1495 .or_else(|| {
1496 source_area
1497 .properties
1498 .get("parent_region_id")
1499 .and_then(|v| v.as_str())
1500 .map(|s| s.to_string())
1501 })
1502 .ok_or_else(|| {
1503 ApiError::internal(format!(
1504 "Source cortical area {} is missing parent_region_id; cannot determine region membership for clone",
1505 source_id
1506 ))
1507 })?;
1508
1509 if let Some(client_parent_region_id) = request.parent_region_id.as_ref() {
1510 if client_parent_region_id != &source_parent_region_id {
1511 return Err(ApiError::invalid_input(format!(
1512 "parent_region_id mismatch for clone request: client sent '{}', but FEAGI source area {} belongs to '{}'",
1513 client_parent_region_id, source_id, source_parent_region_id
1514 )));
1515 }
1516 }
1517
1518 let outgoing_mapping_dst = source_area
1520 .properties
1521 .get("cortical_mapping_dst")
1522 .and_then(|v| v.as_object())
1523 .cloned();
1524
1525 let timestamp = SystemTime::now()
1532 .duration_since(UNIX_EPOCH)
1533 .map_err(|e| ApiError::internal(format!("System clock error: {}", e)))?
1534 .as_millis() as u64;
1535
1536 let mut cortical_id_bytes = [0u8; 8];
1537 cortical_id_bytes[0] = src_first_byte;
1538
1539 let name_bytes = request.new_name.as_bytes();
1540 for i in 1..7 {
1541 cortical_id_bytes[i] = if i - 1 < name_bytes.len() {
1542 let c = name_bytes[i - 1];
1543 if c.is_ascii_alphanumeric() || c == b'_' {
1544 c
1545 } else {
1546 b'_'
1547 }
1548 } else {
1549 b'_'
1550 };
1551 }
1552 cortical_id_bytes[7] = (timestamp & 0xFF) as u8;
1553
1554 let new_area_id = general_purpose::STANDARD.encode(cortical_id_bytes);
1555
1556 let mut cloned_properties = source_area.properties.clone();
1559 cloned_properties.remove("cortical_mapping_dst");
1560
1561 cloned_properties.insert(
1563 "parent_region_id".to_string(),
1564 Value::String(source_parent_region_id),
1565 );
1566 cloned_properties.insert(
1567 "coordinate_2d".to_string(),
1568 serde_json::json!([request.coordinates_2d[0], request.coordinates_2d[1]]),
1569 );
1570
1571 let params = CreateCorticalAreaParams {
1572 cortical_id: new_area_id.clone(),
1573 name: request.new_name.clone(),
1574 dimensions: source_area.dimensions,
1575 position: (
1576 request.coordinates_3d[0],
1577 request.coordinates_3d[1],
1578 request.coordinates_3d[2],
1579 ),
1580 area_type: source_area.area_type.clone(),
1581 visible: Some(source_area.visible),
1582 sub_group: source_area.sub_group.clone(),
1583 neurons_per_voxel: Some(source_area.neurons_per_voxel),
1584 postsynaptic_current: Some(source_area.postsynaptic_current),
1585 plasticity_constant: Some(source_area.plasticity_constant),
1586 degeneration: Some(source_area.degeneration),
1587 psp_uniform_distribution: Some(source_area.psp_uniform_distribution),
1588 firing_threshold_increment: None,
1591 firing_threshold_limit: Some(source_area.firing_threshold_limit),
1592 consecutive_fire_count: Some(source_area.consecutive_fire_count),
1593 snooze_period: Some(source_area.snooze_period),
1594 refractory_period: Some(source_area.refractory_period),
1595 leak_coefficient: Some(source_area.leak_coefficient),
1596 leak_variability: Some(source_area.leak_variability),
1597 burst_engine_active: Some(source_area.burst_engine_active),
1598 properties: Some(cloned_properties),
1599 };
1600
1601 let created_areas = genome_service
1603 .create_cortical_areas(vec![params])
1604 .await
1605 .map_err(|e| ApiError::internal(format!("Failed to clone cortical area: {}", e)))?;
1606
1607 if let Some(created_area) = created_areas.first() {
1609 tracing::info!(target: "feagi-api",
1610 "Clone created area {} with position {:?} (requested {:?})",
1611 new_area_id, created_area.position, request.coordinates_3d
1612 );
1613 }
1614
1615 if request.clone_cortical_mapping {
1617 if let Some(dst_map) = outgoing_mapping_dst {
1619 for (dst_id, rules) in dst_map {
1620 let dst_effective = if dst_id == source_id {
1621 new_area_id.clone()
1623 } else {
1624 dst_id.clone()
1625 };
1626
1627 let Some(rules_array) = rules.as_array() else {
1628 return Err(ApiError::invalid_input(format!(
1629 "Invalid cortical_mapping_dst value for dst '{}': expected array, got {}",
1630 dst_id, rules
1631 )));
1632 };
1633
1634 connectome_service
1635 .update_cortical_mapping(
1636 new_area_id.clone(),
1637 dst_effective,
1638 rules_array.clone(),
1639 )
1640 .await
1641 .map_err(|e| {
1642 ApiError::internal(format!(
1643 "Failed to clone outgoing mapping from {}: {}",
1644 source_id, e
1645 ))
1646 })?;
1647 }
1648 }
1649
1650 let all_areas = connectome_service
1653 .list_cortical_areas()
1654 .await
1655 .map_err(|e| ApiError::internal(format!("Failed to list cortical areas: {}", e)))?;
1656
1657 for area in all_areas {
1658 if area.cortical_id == source_id {
1660 continue;
1661 }
1662
1663 let Some(dst_map) = area
1664 .properties
1665 .get("cortical_mapping_dst")
1666 .and_then(|v| v.as_object())
1667 else {
1668 continue;
1669 };
1670
1671 let Some(rules) = dst_map.get(&source_id) else {
1672 continue;
1673 };
1674
1675 let Some(rules_array) = rules.as_array() else {
1676 return Err(ApiError::invalid_input(format!(
1677 "Invalid cortical_mapping_dst value for src '{}', dst '{}': expected array, got {}",
1678 area.cortical_id, source_id, rules
1679 )));
1680 };
1681
1682 connectome_service
1683 .update_cortical_mapping(
1684 area.cortical_id.clone(),
1685 new_area_id.clone(),
1686 rules_array.clone(),
1687 )
1688 .await
1689 .map_err(|e| {
1690 ApiError::internal(format!(
1691 "Failed to clone incoming mapping into {} from {}: {}",
1692 source_id, area.cortical_id, e
1693 ))
1694 })?;
1695 }
1696 }
1697
1698 Ok(Json(HashMap::from([
1699 ("message".to_string(), "Cortical area cloned".to_string()),
1700 ("new_area_id".to_string(), new_area_id),
1701 ])))
1702}
1703
1704#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
1706pub struct CloneCorticalAreaRequest {
1707 pub source_area_id: String,
1709 pub new_name: String,
1711 pub coordinates_3d: [i32; 3],
1713 pub coordinates_2d: [i32; 2],
1715 #[serde(default)]
1720 pub parent_region_id: Option<String>,
1721 pub clone_cortical_mapping: bool,
1723}
1724
1725#[utoipa::path(
1727 put,
1728 path = "/v1/cortical_area/multi/cortical_area",
1729 tag = "cortical_area"
1730)]
1731pub async fn put_multi_cortical_area(
1732 State(state): State<ApiState>,
1733 Json(mut request): Json<HashMap<String, serde_json::Value>>,
1734) -> ApiResult<Json<HashMap<String, String>>> {
1735 let genome_service = state.genome_service.as_ref();
1736
1737 let cortical_ids: Vec<String> = request
1739 .get("cortical_id_list")
1740 .and_then(|v| v.as_array())
1741 .ok_or_else(|| ApiError::invalid_input("cortical_id_list required"))?
1742 .iter()
1743 .filter_map(|v| v.as_str().map(String::from))
1744 .collect();
1745
1746 if cortical_ids.is_empty() {
1747 return Err(ApiError::invalid_input("cortical_id_list cannot be empty"));
1748 }
1749
1750 tracing::debug!(
1751 target: "feagi-api",
1752 "PUT /v1/cortical_area/multi/cortical_area - received update for {} areas (keys: {:?})",
1753 cortical_ids.len(),
1754 request.keys().collect::<Vec<_>>()
1755 );
1756
1757 request.remove("cortical_id_list");
1759
1760 let mut shared_properties = request.clone();
1762 for cortical_id in &cortical_ids {
1763 shared_properties.remove(cortical_id);
1764 }
1765
1766 for cortical_id in &cortical_ids {
1768 tracing::debug!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - updating area: {}", cortical_id);
1769 let mut properties = shared_properties.clone();
1770 if let Some(serde_json::Value::Object(per_id_map)) = request.get(cortical_id) {
1771 for (key, value) in per_id_map {
1772 properties.insert(key.clone(), value.clone());
1773 }
1774 }
1775 match genome_service
1776 .update_cortical_area(cortical_id, properties)
1777 .await
1778 {
1779 Ok(_) => {
1780 tracing::debug!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - success for {}", cortical_id);
1781 }
1782 Err(e) => {
1783 tracing::error!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - failed for {}: {}", cortical_id, e);
1784 return Err(ApiError::internal(format!(
1785 "Failed to update cortical area {}: {}",
1786 cortical_id, e
1787 )));
1788 }
1789 }
1790 }
1791
1792 Ok(Json(HashMap::from([
1793 (
1794 "message".to_string(),
1795 format!("Updated {} cortical areas", cortical_ids.len()),
1796 ),
1797 ("cortical_ids".to_string(), cortical_ids.join(", ")),
1798 ])))
1799}
1800
1801#[utoipa::path(
1803 delete,
1804 path = "/v1/cortical_area/multi/cortical_area",
1805 tag = "cortical_area"
1806)]
1807#[allow(unused_variables)] pub async fn delete_multi_cortical_area(
1809 State(state): State<ApiState>,
1810 Json(request): Json<Vec<String>>,
1811) -> ApiResult<Json<HashMap<String, String>>> {
1812 Err(ApiError::internal("Not yet implemented"))
1814}
1815
1816#[utoipa::path(put, path = "/v1/cortical_area/coord_2d", tag = "cortical_area")]
1818#[allow(unused_variables)] pub async fn put_coord_2d(
1820 State(state): State<ApiState>,
1821 Json(request): Json<HashMap<String, serde_json::Value>>,
1822) -> ApiResult<Json<HashMap<String, String>>> {
1823 Err(ApiError::internal("Not yet implemented"))
1825}
1826
1827#[utoipa::path(
1829 put,
1830 path = "/v1/cortical_area/suppress_cortical_visibility",
1831 tag = "cortical_area"
1832)]
1833#[allow(unused_variables)] pub async fn put_suppress_cortical_visibility(
1835 State(state): State<ApiState>,
1836 Json(request): Json<HashMap<String, serde_json::Value>>,
1837) -> ApiResult<Json<HashMap<String, String>>> {
1838 Err(ApiError::internal("Not yet implemented"))
1840}
1841
1842#[utoipa::path(
1845 put,
1846 path = "/v1/cortical_area/reset",
1847 tag = "cortical_area",
1848 request_body = CorticalAreaResetRequest,
1849 responses(
1850 (status = 200, description = "Reset applied", body = CorticalAreaResetResponse),
1851 )
1852)]
1853pub async fn put_reset(
1854 State(state): State<ApiState>,
1855 Json(request): Json<CorticalAreaResetRequest>,
1856) -> ApiResult<Json<CorticalAreaResetResponse>> {
1857 use tracing::info;
1858
1859 if request.area_list.is_empty() {
1860 return Err(ApiError::invalid_input("area_list cannot be empty"));
1861 }
1862
1863 info!(
1864 target: "feagi-api",
1865 "[RESET] Received reset request for {} cortical areas: {:?}",
1866 request.area_list.len(),
1867 request.area_list
1868 );
1869
1870 let connectome_service = state.connectome_service.as_ref();
1871 let mut cortical_indices: Vec<u32> = Vec::with_capacity(request.area_list.len());
1872 for id in &request.area_list {
1873 let area = connectome_service
1874 .get_cortical_area(id)
1875 .await
1876 .map_err(ApiError::from)?;
1877 cortical_indices.push(area.cortical_idx);
1878 info!(
1879 target: "feagi-api",
1880 "[RESET] Resolved cortical ID '{}' to index {}",
1881 id,
1882 area.cortical_idx
1883 );
1884 }
1885
1886 info!(
1887 target: "feagi-api",
1888 "[RESET] Calling runtime service to reset indices: {:?}",
1889 cortical_indices
1890 );
1891
1892 let reset_pairs = state
1893 .runtime_service
1894 .reset_cortical_area_states(&cortical_indices)
1895 .await
1896 .map_err(ApiError::from)?;
1897
1898 let results: Vec<CorticalAreaResetItem> = reset_pairs
1899 .into_iter()
1900 .map(|(cortical_idx, neurons_reset)| {
1901 info!(
1902 target: "feagi-api",
1903 "[RESET] Cortical area {} reset: {} neurons cleared",
1904 cortical_idx,
1905 neurons_reset
1906 );
1907 CorticalAreaResetItem {
1908 cortical_idx,
1909 neurons_reset,
1910 }
1911 })
1912 .collect();
1913
1914 info!(
1915 target: "feagi-api",
1916 "[RESET] Reset complete for {} areas",
1917 results.len()
1918 );
1919
1920 Ok(Json(CorticalAreaResetResponse {
1921 message: "ok".to_string(),
1922 results,
1923 }))
1924}
1925
1926#[utoipa::path(get, path = "/v1/cortical_area/visualization", tag = "cortical_area")]
1928pub async fn get_visualization(
1929 State(_state): State<ApiState>,
1930) -> ApiResult<Json<HashMap<String, bool>>> {
1931 let mut response = HashMap::new();
1932 response.insert("enabled".to_string(), true);
1933 Ok(Json(response))
1934}
1935
1936#[utoipa::path(
1938 post,
1939 path = "/v1/cortical_area/batch_operations",
1940 tag = "cortical_area"
1941)]
1942pub async fn post_batch_operations(
1943 State(_state): State<ApiState>,
1944 Json(_ops): Json<Vec<HashMap<String, serde_json::Value>>>,
1945) -> ApiResult<Json<HashMap<String, i32>>> {
1946 let mut response = HashMap::new();
1947 response.insert("processed".to_string(), 0);
1948 Ok(Json(response))
1949}
1950
1951#[utoipa::path(get, path = "/v1/cortical_area/ipu/list", tag = "cortical_area")]
1953pub async fn get_ipu_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1954 get_ipu(State(state)).await
1955}
1956
1957#[utoipa::path(get, path = "/v1/cortical_area/opu/list", tag = "cortical_area")]
1959pub async fn get_opu_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1960 get_opu(State(state)).await
1961}
1962
1963#[utoipa::path(put, path = "/v1/cortical_area/coordinates_3d", tag = "cortical_area")]
1965pub async fn put_coordinates_3d(
1966 State(_state): State<ApiState>,
1967 Json(_req): Json<HashMap<String, serde_json::Value>>,
1968) -> ApiResult<Json<HashMap<String, String>>> {
1969 Ok(Json(HashMap::from([(
1970 "message".to_string(),
1971 "Not yet implemented".to_string(),
1972 )])))
1973}
1974
1975#[utoipa::path(delete, path = "/v1/cortical_area/bulk_delete", tag = "cortical_area")]
1977pub async fn delete_bulk(
1978 State(_state): State<ApiState>,
1979 Json(_ids): Json<Vec<String>>,
1980) -> ApiResult<Json<HashMap<String, i32>>> {
1981 let mut response = HashMap::new();
1982 response.insert("deleted_count".to_string(), 0);
1983 Ok(Json(response))
1984}
1985
1986#[utoipa::path(post, path = "/v1/cortical_area/resize", tag = "cortical_area")]
1988pub async fn post_resize(
1989 State(_state): State<ApiState>,
1990 Json(_req): Json<HashMap<String, serde_json::Value>>,
1991) -> ApiResult<Json<HashMap<String, String>>> {
1992 Ok(Json(HashMap::from([(
1993 "message".to_string(),
1994 "Not yet implemented".to_string(),
1995 )])))
1996}
1997
1998#[utoipa::path(post, path = "/v1/cortical_area/reposition", tag = "cortical_area")]
2000pub async fn post_reposition(
2001 State(_state): State<ApiState>,
2002 Json(_req): Json<HashMap<String, serde_json::Value>>,
2003) -> ApiResult<Json<HashMap<String, String>>> {
2004 Ok(Json(HashMap::from([(
2005 "message".to_string(),
2006 "Not yet implemented".to_string(),
2007 )])))
2008}
2009
2010#[utoipa::path(
2012 get,
2013 path = "/v1/cortical_area/voxel_neurons",
2014 tag = "cortical_area",
2015 params(VoxelNeuronsQuery),
2016 responses(
2017 (status = 200, description = "Neurons in voxel", body = VoxelNeuronsResponse),
2018 (status = 404, description = "Cortical area or neuron data not found"),
2019 (status = 500, description = "Internal server error")
2020 )
2021)]
2022pub async fn get_voxel_neurons(
2023 State(state): State<ApiState>,
2024 Query(params): Query<VoxelNeuronsQuery>,
2025) -> ApiResult<Json<VoxelNeuronsResponse>> {
2026 resolve_voxel_neurons(
2027 &state,
2028 params.cortical_id,
2029 params.x,
2030 params.y,
2031 params.z,
2032 params.synapse_page,
2033 )
2034 .await
2035 .map(Json)
2036}
2037
2038#[utoipa::path(
2040 post,
2041 path = "/v1/cortical_area/voxel_neurons",
2042 tag = "cortical_area",
2043 request_body = VoxelNeuronsBody,
2044 responses(
2045 (status = 200, description = "Neurons in voxel", body = VoxelNeuronsResponse),
2046 (status = 404, description = "Cortical area or neuron data not found"),
2047 (status = 500, description = "Internal server error")
2048 )
2049)]
2050pub async fn post_voxel_neurons(
2051 State(state): State<ApiState>,
2052 Json(body): Json<VoxelNeuronsBody>,
2053) -> ApiResult<Json<VoxelNeuronsResponse>> {
2054 resolve_voxel_neurons(
2055 &state,
2056 body.cortical_id,
2057 body.x,
2058 body.y,
2059 body.z,
2060 body.synapse_page,
2061 )
2062 .await
2063 .map(Json)
2064}
2065
2066#[utoipa::path(
2068 get,
2069 path = "/v1/cortical_area/memory",
2070 tag = "cortical_area",
2071 params(MemoryCorticalAreaQuery),
2072 responses(
2073 (status = 200, description = "Memory cortical area details", body = MemoryCorticalAreaResponse),
2074 (status = 400, description = "Invalid cortical id or not a memory area"),
2075 (status = 500, description = "Internal server error")
2076 )
2077)]
2078pub async fn get_memory_cortical_area(
2079 State(state): State<ApiState>,
2080 Query(params): Query<MemoryCorticalAreaQuery>,
2081) -> ApiResult<Json<MemoryCorticalAreaResponse>> {
2082 let connectome_service = state.connectome_service.as_ref();
2083 let area = connectome_service
2084 .get_cortical_area(¶ms.cortical_id)
2085 .await
2086 .map_err(ApiError::from)?;
2087
2088 let mem_props = extract_memory_properties(&area.properties).ok_or_else(|| {
2089 ApiError::invalid_input(
2090 "cortical area is not a memory area (expected is_mem_type memory properties)",
2091 )
2092 })?;
2093
2094 let cortical_idx = area.cortical_idx;
2095 let cortical_name = area.name.clone();
2096
2097 let cid = CorticalID::try_from_base_64(¶ms.cortical_id)
2098 .map_err(|e| ApiError::invalid_input(format!("Invalid cortical_id: {}", e)))?;
2099
2100 let page_size_u32 = params
2101 .page_size
2102 .clamp(1, MEMORY_CORTICAL_NEURON_IDS_PAGE_SIZE_MAX);
2103 let page_size = page_size_u32 as usize;
2104 let offset = (params.page as usize).saturating_mul(page_size);
2105
2106 let manager = feagi_brain_development::ConnectomeManager::instance();
2107 let mgr = manager.read();
2108
2109 let upstream_cortical_area_indices = mgr.get_upstream_cortical_areas(&cid);
2110 let upstream_cortical_area_count = upstream_cortical_area_indices.len();
2111
2112 let exec = mgr
2113 .get_plasticity_executor()
2114 .ok_or_else(|| ApiError::internal("Plasticity executor not available"))?;
2115 let ex = exec
2116 .lock()
2117 .map_err(|_| ApiError::internal("Plasticity executor lock poisoned"))?;
2118
2119 let runtime = ex
2120 .memory_cortical_area_runtime_info(cortical_idx)
2121 .ok_or_else(|| ApiError::internal("Plasticity service not initialized"))?;
2122
2123 let (memory_neuron_ids_u32, total_memory_neuron_ids) = ex
2124 .paginated_memory_neuron_ids_in_area(cortical_idx, offset, page_size)
2125 .unwrap_or((Vec::new(), 0));
2126
2127 let has_more = offset.saturating_add(memory_neuron_ids_u32.len()) < total_memory_neuron_ids;
2128
2129 let memory_neuron_ids: Vec<u64> = memory_neuron_ids_u32
2130 .into_iter()
2131 .map(|id| id as u64)
2132 .collect();
2133
2134 Ok(Json(MemoryCorticalAreaResponse {
2135 cortical_id: params.cortical_id,
2136 cortical_idx,
2137 cortical_name,
2138 short_term_neuron_count: runtime.short_term_neuron_count,
2139 long_term_neuron_count: runtime.long_term_neuron_count,
2140 memory_parameters: MemoryCorticalAreaParamsResponse {
2141 temporal_depth: mem_props.temporal_depth,
2142 longterm_mem_threshold: mem_props.longterm_threshold,
2143 lifespan_growth_rate: mem_props.lifespan_growth_rate,
2144 init_lifespan: mem_props.init_lifespan,
2145 },
2146 upstream_cortical_area_indices,
2147 upstream_cortical_area_count,
2148 upstream_pattern_cache_size: runtime.upstream_pattern_cache_size,
2149 incoming_synapse_count: area.incoming_synapse_count,
2150 outgoing_synapse_count: area.outgoing_synapse_count,
2151 total_memory_neuron_ids,
2152 page: params.page,
2153 page_size: page_size_u32,
2154 memory_neuron_ids,
2155 has_more,
2156 }))
2157}
2158
2159#[utoipa::path(
2161 get,
2162 path = "/v1/cortical_area/ipu/types",
2163 tag = "cortical_area",
2164 responses(
2165 (status = 200, description = "IPU type metadata", body = HashMap<String, CorticalTypeMetadata>),
2166 (status = 500, description = "Internal server error")
2167 )
2168)]
2169pub async fn get_ipu_types(
2170 State(_state): State<ApiState>,
2171) -> ApiResult<Json<HashMap<String, CorticalTypeMetadata>>> {
2172 let mut types = HashMap::new();
2173
2174 for unit in SensoryCorticalUnit::list_all() {
2176 let id_ref = unit.get_cortical_id_unit_reference();
2177 let key = format!("i{}", std::str::from_utf8(&id_ref).unwrap_or("???"));
2178
2179 let encodings = vec!["absolute".to_string(), "incremental".to_string()];
2181
2182 let snake_name = unit.get_snake_case_name();
2187 let formats = if snake_name == "vision"
2188 || snake_name == "segmented_vision"
2189 || snake_name == "miscellaneous"
2190 {
2191 vec![]
2192 } else {
2193 vec!["linear".to_string(), "fractional".to_string()]
2194 };
2195
2196 let resolution = if snake_name == "vision" {
2198 vec![64, 64, 1] } else if snake_name == "segmented_vision" {
2200 vec![32, 32, 1] } else {
2202 vec![1, 1, 1] };
2204
2205 let structure = "asymmetric".to_string();
2207
2208 let topology_map = unit.get_unit_default_topology();
2210 let unit_default_topology: HashMap<usize, UnitTopologyData> = topology_map
2211 .into_iter()
2212 .map(|(idx, topo)| {
2213 (
2214 *idx as usize,
2215 UnitTopologyData {
2216 relative_position: topo.relative_position,
2217 dimensions: topo.channel_dimensions_default,
2218 },
2219 )
2220 })
2221 .collect();
2222
2223 types.insert(
2224 key,
2225 CorticalTypeMetadata {
2226 description: unit.get_friendly_name().to_string(),
2227 encodings,
2228 formats,
2229 units: unit.get_number_cortical_areas() as u32,
2230 resolution,
2231 structure,
2232 unit_default_topology,
2233 },
2234 );
2235 }
2236
2237 Ok(Json(types))
2238}
2239
2240#[utoipa::path(
2242 get,
2243 path = "/v1/cortical_area/opu/types",
2244 tag = "cortical_area",
2245 responses(
2246 (status = 200, description = "OPU type metadata", body = HashMap<String, CorticalTypeMetadata>),
2247 (status = 500, description = "Internal server error")
2248 )
2249)]
2250pub async fn get_opu_types(
2251 State(_state): State<ApiState>,
2252) -> ApiResult<Json<HashMap<String, CorticalTypeMetadata>>> {
2253 let mut types = HashMap::new();
2254
2255 for unit in MotorCorticalUnit::list_all() {
2257 let id_ref = unit.get_cortical_id_unit_reference();
2258 let key = format!("o{}", std::str::from_utf8(&id_ref).unwrap_or("???"));
2259
2260 let encodings = vec!["absolute".to_string(), "incremental".to_string()];
2262
2263 let snake_name = unit.get_snake_case_name();
2267 let formats = if snake_name == "miscellaneous" {
2268 vec![]
2269 } else {
2270 vec!["linear".to_string(), "fractional".to_string()]
2271 };
2272
2273 let resolution = vec![1, 1, 1];
2275
2276 let structure = "asymmetric".to_string();
2278
2279 let topology_map = unit.get_unit_default_topology();
2281 let unit_default_topology: HashMap<usize, UnitTopologyData> = topology_map
2282 .into_iter()
2283 .map(|(idx, topo)| {
2284 (
2285 *idx as usize,
2286 UnitTopologyData {
2287 relative_position: topo.relative_position,
2288 dimensions: topo.channel_dimensions_default,
2289 },
2290 )
2291 })
2292 .collect();
2293
2294 types.insert(
2295 key,
2296 CorticalTypeMetadata {
2297 description: unit.get_friendly_name().to_string(),
2298 encodings,
2299 formats,
2300 units: unit.get_number_cortical_areas() as u32,
2301 resolution,
2302 structure,
2303 unit_default_topology,
2304 },
2305 );
2306 }
2307
2308 Ok(Json(types))
2309}
2310
2311#[utoipa::path(
2313 get,
2314 path = "/v1/cortical_area/cortical_area_index_list",
2315 tag = "cortical_area"
2316)]
2317pub async fn get_cortical_area_index_list(
2318 State(state): State<ApiState>,
2319) -> ApiResult<Json<Vec<u32>>> {
2320 let connectome_service = state.connectome_service.as_ref();
2321 let areas = connectome_service
2322 .list_cortical_areas()
2323 .await
2324 .map_err(|e| ApiError::internal(format!("{}", e)))?;
2325 let indices: Vec<u32> = areas.iter().map(|a| a.cortical_idx).collect();
2327 Ok(Json(indices))
2328}
2329
2330#[utoipa::path(
2332 get,
2333 path = "/v1/cortical_area/cortical_idx_mapping",
2334 tag = "cortical_area"
2335)]
2336pub async fn get_cortical_idx_mapping(
2337 State(state): State<ApiState>,
2338) -> ApiResult<Json<std::collections::BTreeMap<String, u32>>> {
2339 use std::collections::BTreeMap;
2340
2341 let connectome_service = state.connectome_service.as_ref();
2342 let areas = connectome_service
2343 .list_cortical_areas()
2344 .await
2345 .map_err(|e| ApiError::internal(format!("{}", e)))?;
2346 let mapping: BTreeMap<String, u32> = areas
2349 .iter()
2350 .map(|a| (a.cortical_id.clone(), a.cortical_idx))
2351 .collect();
2352 Ok(Json(mapping))
2353}
2354
2355#[utoipa::path(
2357 get,
2358 path = "/v1/cortical_area/mapping_restrictions",
2359 tag = "cortical_area"
2360)]
2361pub async fn get_mapping_restrictions_query(
2362 State(_state): State<ApiState>,
2363 Query(_params): Query<HashMap<String, String>>,
2364) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
2365 Ok(Json(HashMap::new()))
2366}
2367
2368#[utoipa::path(
2370 get,
2371 path = "/v1/cortical_area/{cortical_id}/memory_usage",
2372 tag = "cortical_area"
2373)]
2374pub async fn get_memory_usage(
2375 State(state): State<ApiState>,
2376 Path(cortical_id): Path<String>,
2377) -> ApiResult<Json<HashMap<String, i64>>> {
2378 let connectome_service = state.connectome_service.as_ref();
2379
2380 let area_info = connectome_service
2382 .get_cortical_area(&cortical_id)
2383 .await
2384 .map_err(|_| ApiError::not_found("CorticalArea", &cortical_id))?;
2385
2386 const BYTES_PER_NEURON: i64 = 48;
2389 let memory_bytes = (area_info.neuron_count as i64) * BYTES_PER_NEURON;
2390
2391 let mut response = HashMap::new();
2392 response.insert("memory_bytes".to_string(), memory_bytes);
2393 Ok(Json(response))
2394}
2395
2396#[utoipa::path(
2398 get,
2399 path = "/v1/cortical_area/{cortical_id}/neuron_count",
2400 tag = "cortical_area"
2401)]
2402pub async fn get_area_neuron_count(
2403 State(state): State<ApiState>,
2404 Path(cortical_id): Path<String>,
2405) -> ApiResult<Json<i64>> {
2406 let connectome_service = state.connectome_service.as_ref();
2407
2408 let area_info = connectome_service
2410 .get_cortical_area(&cortical_id)
2411 .await
2412 .map_err(|_| ApiError::not_found("CorticalArea", &cortical_id))?;
2413
2414 Ok(Json(area_info.neuron_count as i64))
2415}
2416
2417#[utoipa::path(
2419 post,
2420 path = "/v1/cortical_area/cortical_type_options",
2421 tag = "cortical_area"
2422)]
2423pub async fn post_cortical_type_options(
2424 State(_state): State<ApiState>,
2425) -> ApiResult<Json<Vec<String>>> {
2426 Ok(Json(vec![
2427 "Sensory".to_string(),
2428 "Motor".to_string(),
2429 "Custom".to_string(),
2430 "Memory".to_string(),
2431 ]))
2432}
2433
2434#[utoipa::path(
2436 post,
2437 path = "/v1/cortical_area/mapping_restrictions",
2438 tag = "cortical_area"
2439)]
2440pub async fn post_mapping_restrictions(
2441 State(_state): State<ApiState>,
2442 Json(_req): Json<HashMap<String, String>>,
2443) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
2444 Ok(Json(HashMap::new()))
2445}
2446
2447#[utoipa::path(
2449 post,
2450 path = "/v1/cortical_area/mapping_restrictions_between_areas",
2451 tag = "cortical_area"
2452)]
2453pub async fn post_mapping_restrictions_between_areas(
2454 State(_state): State<ApiState>,
2455 Json(_req): Json<HashMap<String, String>>,
2456) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
2457 Ok(Json(HashMap::new()))
2458}
2459
2460#[utoipa::path(put, path = "/v1/cortical_area/coord_3d", tag = "cortical_area")]
2462pub async fn put_coord_3d(
2463 State(_state): State<ApiState>,
2464 Json(_req): Json<HashMap<String, serde_json::Value>>,
2465) -> ApiResult<Json<HashMap<String, String>>> {
2466 Ok(Json(HashMap::from([(
2467 "message".to_string(),
2468 "Not yet implemented".to_string(),
2469 )])))
2470}
2471
2472#[cfg(test)]
2473mod voxel_neurons_dto_tests {
2474 use super::{
2475 synapse_details_for_neuron, synapse_page_window, VoxelNeuronsBody, VoxelNeuronsResponse,
2476 };
2477
2478 #[test]
2479 fn synapse_page_window_paginates_fifty_per_direction() {
2480 let (s, e, more) = synapse_page_window(120, 0);
2481 assert_eq!((s, e, more), (0, 50, true));
2482 let (s, e, more) = synapse_page_window(120, 1);
2483 assert_eq!((s, e, more), (50, 100, true));
2484 let (s, e, more) = synapse_page_window(120, 2);
2485 assert_eq!((s, e, more), (100, 120, false));
2486 let (s, e, more) = synapse_page_window(120, 3);
2487 assert_eq!((s, e, more), (0, 0, false));
2488 }
2489
2490 #[test]
2491 fn synapse_details_matches_connectome_shape() {
2492 let mgr = feagi_brain_development::ConnectomeManager::new_for_testing();
2493 let out_full = vec![(10, 2.0, 5.0, 1)];
2494 let inc_full = vec![(3, 4.0, 6.0, 0)];
2495 let (out, inc) = synapse_details_for_neuron(&mgr, 7, &out_full, &inc_full);
2496 let out_a = out.as_array().expect("outgoing array");
2497 assert_eq!(out_a[0]["source_neuron_id"], serde_json::json!(7));
2498 assert_eq!(out_a[0]["target_neuron_id"], serde_json::json!(10));
2499 assert_eq!(out_a[0]["weight"], serde_json::json!(2.0));
2500 assert_eq!(out_a[0]["postsynaptic_potential"], serde_json::json!(5.0));
2501 assert_eq!(out_a[0]["synapse_type"], serde_json::json!(1));
2502 assert!(out_a[0].get("target_cortical_id").is_some());
2503 assert!(out_a[0].get("target_cortical_name").is_some());
2504 assert!(out_a[0].get("target_x").is_some());
2505 let in_a = inc.as_array().expect("incoming array");
2506 assert_eq!(in_a[0]["source_neuron_id"], serde_json::json!(3));
2507 assert_eq!(in_a[0]["target_neuron_id"], serde_json::json!(7));
2508 assert!(in_a[0].get("source_cortical_id").is_some());
2509 assert!(in_a[0].get("source_cortical_name").is_some());
2510 assert!(in_a[0].get("source_x").is_some());
2511 }
2512
2513 #[test]
2514 fn voxel_neurons_body_deserializes_from_json() {
2515 let j = r#"{"cortical_id":"X19fcG93ZXI=","x":0,"y":0,"z":0}"#;
2516 let b: VoxelNeuronsBody = serde_json::from_str(j).expect("deserialize body");
2517 assert_eq!(b.cortical_id, "X19fcG93ZXI=");
2518 assert_eq!((b.x, b.y, b.z), (0, 0, 0));
2519 assert_eq!(b.synapse_page, 0);
2520 }
2521
2522 #[test]
2523 fn voxel_neurons_response_serializes() {
2524 let r = VoxelNeuronsResponse {
2525 cortical_id: "id".to_string(),
2526 cortical_name: "test_area".to_string(),
2527 cortical_idx: 2,
2528 voxel_coordinate: [1, 2, 3],
2529 x: 1,
2530 y: 2,
2531 z: 3,
2532 synapse_page: 0,
2533 neuron_count: 0,
2534 neurons: vec![],
2535 };
2536 let v = serde_json::to_value(&r).expect("serialize");
2537 assert_eq!(v["cortical_name"], serde_json::json!("test_area"));
2538 assert_eq!(v["voxel_coordinate"], serde_json::json!([1, 2, 3]));
2539 assert_eq!(v["neuron_count"], serde_json::json!(0));
2540 assert_eq!(v["synapse_page"], serde_json::json!(0));
2541 assert_eq!(v["neurons"], serde_json::json!([]));
2542 }
2543}