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