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)]
1807pub async fn delete_multi_cortical_area(
1808 State(state): State<ApiState>,
1809 Json(request): Json<Vec<String>>,
1810) -> ApiResult<Json<HashMap<String, String>>> {
1811 if request.is_empty() {
1812 return Err(ApiError::invalid_input(
1813 "Request must contain at least one cortical ID",
1814 ));
1815 }
1816
1817 let connectome_service = state.connectome_service.as_ref();
1818
1819 tracing::debug!(
1820 target: "feagi-api",
1821 "DELETE /v1/cortical_area/multi/cortical_area - deleting {} areas",
1822 request.len()
1823 );
1824
1825 for cortical_id in &request {
1826 match connectome_service.delete_cortical_area(cortical_id).await {
1827 Ok(_) => {
1828 tracing::debug!(
1829 target: "feagi-api",
1830 "DELETE /v1/cortical_area/multi/cortical_area - deleted {}",
1831 cortical_id
1832 );
1833 }
1834 Err(e) => {
1835 tracing::error!(
1836 target: "feagi-api",
1837 "DELETE /v1/cortical_area/multi/cortical_area - failed for {}: {}",
1838 cortical_id,
1839 e
1840 );
1841 return Err(ApiError::internal(format!(
1842 "Failed to delete cortical area {}: {}",
1843 cortical_id, e
1844 )));
1845 }
1846 }
1847 }
1848
1849 Ok(Json(HashMap::from([
1850 (
1851 "message".to_string(),
1852 format!("Deleted {} cortical areas", request.len()),
1853 ),
1854 ("cortical_ids".to_string(), request.join(", ")),
1855 ])))
1856}
1857
1858#[utoipa::path(put, path = "/v1/cortical_area/coord_2d", tag = "cortical_area")]
1860#[allow(unused_variables)] pub async fn put_coord_2d(
1862 State(state): State<ApiState>,
1863 Json(request): Json<HashMap<String, serde_json::Value>>,
1864) -> ApiResult<Json<HashMap<String, String>>> {
1865 Err(ApiError::internal("Not yet implemented"))
1867}
1868
1869#[utoipa::path(
1871 put,
1872 path = "/v1/cortical_area/suppress_cortical_visibility",
1873 tag = "cortical_area"
1874)]
1875#[allow(unused_variables)] pub async fn put_suppress_cortical_visibility(
1877 State(state): State<ApiState>,
1878 Json(request): Json<HashMap<String, serde_json::Value>>,
1879) -> ApiResult<Json<HashMap<String, String>>> {
1880 Err(ApiError::internal("Not yet implemented"))
1882}
1883
1884#[utoipa::path(
1887 put,
1888 path = "/v1/cortical_area/reset",
1889 tag = "cortical_area",
1890 request_body = CorticalAreaResetRequest,
1891 responses(
1892 (status = 200, description = "Reset applied", body = CorticalAreaResetResponse),
1893 )
1894)]
1895pub async fn put_reset(
1896 State(state): State<ApiState>,
1897 Json(request): Json<CorticalAreaResetRequest>,
1898) -> ApiResult<Json<CorticalAreaResetResponse>> {
1899 use tracing::info;
1900
1901 if request.area_list.is_empty() {
1902 return Err(ApiError::invalid_input("area_list cannot be empty"));
1903 }
1904
1905 info!(
1906 target: "feagi-api",
1907 "[RESET] Received reset request for {} cortical areas: {:?}",
1908 request.area_list.len(),
1909 request.area_list
1910 );
1911
1912 let connectome_service = state.connectome_service.as_ref();
1913 let mut cortical_indices: Vec<u32> = Vec::with_capacity(request.area_list.len());
1914 for id in &request.area_list {
1915 let area = connectome_service
1916 .get_cortical_area(id)
1917 .await
1918 .map_err(ApiError::from)?;
1919 cortical_indices.push(area.cortical_idx);
1920 info!(
1921 target: "feagi-api",
1922 "[RESET] Resolved cortical ID '{}' to index {}",
1923 id,
1924 area.cortical_idx
1925 );
1926 }
1927
1928 info!(
1929 target: "feagi-api",
1930 "[RESET] Calling runtime service to reset indices: {:?}",
1931 cortical_indices
1932 );
1933
1934 let reset_pairs = state
1935 .runtime_service
1936 .reset_cortical_area_states(&cortical_indices)
1937 .await
1938 .map_err(ApiError::from)?;
1939
1940 let results: Vec<CorticalAreaResetItem> = reset_pairs
1941 .into_iter()
1942 .map(|(cortical_idx, neurons_reset)| {
1943 info!(
1944 target: "feagi-api",
1945 "[RESET] Cortical area {} reset: {} neurons cleared",
1946 cortical_idx,
1947 neurons_reset
1948 );
1949 CorticalAreaResetItem {
1950 cortical_idx,
1951 neurons_reset,
1952 }
1953 })
1954 .collect();
1955
1956 info!(
1957 target: "feagi-api",
1958 "[RESET] Reset complete for {} areas",
1959 results.len()
1960 );
1961
1962 Ok(Json(CorticalAreaResetResponse {
1963 message: "ok".to_string(),
1964 results,
1965 }))
1966}
1967
1968#[utoipa::path(get, path = "/v1/cortical_area/visualization", tag = "cortical_area")]
1970pub async fn get_visualization(
1971 State(_state): State<ApiState>,
1972) -> ApiResult<Json<HashMap<String, bool>>> {
1973 let mut response = HashMap::new();
1974 response.insert("enabled".to_string(), true);
1975 Ok(Json(response))
1976}
1977
1978#[utoipa::path(
1980 post,
1981 path = "/v1/cortical_area/batch_operations",
1982 tag = "cortical_area"
1983)]
1984pub async fn post_batch_operations(
1985 State(_state): State<ApiState>,
1986 Json(_ops): Json<Vec<HashMap<String, serde_json::Value>>>,
1987) -> ApiResult<Json<HashMap<String, i32>>> {
1988 let mut response = HashMap::new();
1989 response.insert("processed".to_string(), 0);
1990 Ok(Json(response))
1991}
1992
1993#[utoipa::path(get, path = "/v1/cortical_area/ipu/list", tag = "cortical_area")]
1995pub async fn get_ipu_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1996 get_ipu(State(state)).await
1997}
1998
1999#[utoipa::path(get, path = "/v1/cortical_area/opu/list", tag = "cortical_area")]
2001pub async fn get_opu_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
2002 get_opu(State(state)).await
2003}
2004
2005#[utoipa::path(put, path = "/v1/cortical_area/coordinates_3d", tag = "cortical_area")]
2007pub async fn put_coordinates_3d(
2008 State(_state): State<ApiState>,
2009 Json(_req): Json<HashMap<String, serde_json::Value>>,
2010) -> ApiResult<Json<HashMap<String, String>>> {
2011 Ok(Json(HashMap::from([(
2012 "message".to_string(),
2013 "Not yet implemented".to_string(),
2014 )])))
2015}
2016
2017#[utoipa::path(delete, path = "/v1/cortical_area/bulk_delete", tag = "cortical_area")]
2019pub async fn delete_bulk(
2020 State(_state): State<ApiState>,
2021 Json(_ids): Json<Vec<String>>,
2022) -> ApiResult<Json<HashMap<String, i32>>> {
2023 let mut response = HashMap::new();
2024 response.insert("deleted_count".to_string(), 0);
2025 Ok(Json(response))
2026}
2027
2028#[utoipa::path(post, path = "/v1/cortical_area/resize", tag = "cortical_area")]
2030pub async fn post_resize(
2031 State(_state): State<ApiState>,
2032 Json(_req): Json<HashMap<String, serde_json::Value>>,
2033) -> ApiResult<Json<HashMap<String, String>>> {
2034 Ok(Json(HashMap::from([(
2035 "message".to_string(),
2036 "Not yet implemented".to_string(),
2037 )])))
2038}
2039
2040#[utoipa::path(post, path = "/v1/cortical_area/reposition", tag = "cortical_area")]
2042pub async fn post_reposition(
2043 State(_state): State<ApiState>,
2044 Json(_req): Json<HashMap<String, serde_json::Value>>,
2045) -> ApiResult<Json<HashMap<String, String>>> {
2046 Ok(Json(HashMap::from([(
2047 "message".to_string(),
2048 "Not yet implemented".to_string(),
2049 )])))
2050}
2051
2052#[utoipa::path(
2054 get,
2055 path = "/v1/cortical_area/voxel_neurons",
2056 tag = "cortical_area",
2057 params(VoxelNeuronsQuery),
2058 responses(
2059 (status = 200, description = "Neurons in voxel", body = VoxelNeuronsResponse),
2060 (status = 404, description = "Cortical area or neuron data not found"),
2061 (status = 500, description = "Internal server error")
2062 )
2063)]
2064pub async fn get_voxel_neurons(
2065 State(state): State<ApiState>,
2066 Query(params): Query<VoxelNeuronsQuery>,
2067) -> ApiResult<Json<VoxelNeuronsResponse>> {
2068 resolve_voxel_neurons(
2069 &state,
2070 params.cortical_id,
2071 params.x,
2072 params.y,
2073 params.z,
2074 params.synapse_page,
2075 )
2076 .await
2077 .map(Json)
2078}
2079
2080#[utoipa::path(
2082 post,
2083 path = "/v1/cortical_area/voxel_neurons",
2084 tag = "cortical_area",
2085 request_body = VoxelNeuronsBody,
2086 responses(
2087 (status = 200, description = "Neurons in voxel", body = VoxelNeuronsResponse),
2088 (status = 404, description = "Cortical area or neuron data not found"),
2089 (status = 500, description = "Internal server error")
2090 )
2091)]
2092pub async fn post_voxel_neurons(
2093 State(state): State<ApiState>,
2094 Json(body): Json<VoxelNeuronsBody>,
2095) -> ApiResult<Json<VoxelNeuronsResponse>> {
2096 resolve_voxel_neurons(
2097 &state,
2098 body.cortical_id,
2099 body.x,
2100 body.y,
2101 body.z,
2102 body.synapse_page,
2103 )
2104 .await
2105 .map(Json)
2106}
2107
2108#[utoipa::path(
2110 get,
2111 path = "/v1/cortical_area/memory",
2112 tag = "cortical_area",
2113 params(MemoryCorticalAreaQuery),
2114 responses(
2115 (status = 200, description = "Memory cortical area details", body = MemoryCorticalAreaResponse),
2116 (status = 400, description = "Invalid cortical id or not a memory area"),
2117 (status = 500, description = "Internal server error")
2118 )
2119)]
2120pub async fn get_memory_cortical_area(
2121 State(state): State<ApiState>,
2122 Query(params): Query<MemoryCorticalAreaQuery>,
2123) -> ApiResult<Json<MemoryCorticalAreaResponse>> {
2124 let connectome_service = state.connectome_service.as_ref();
2125 let area = connectome_service
2126 .get_cortical_area(¶ms.cortical_id)
2127 .await
2128 .map_err(ApiError::from)?;
2129
2130 let mem_props = extract_memory_properties(&area.properties).ok_or_else(|| {
2131 ApiError::invalid_input(
2132 "cortical area is not a memory area (expected is_mem_type memory properties)",
2133 )
2134 })?;
2135
2136 let cortical_idx = area.cortical_idx;
2137 let cortical_name = area.name.clone();
2138
2139 let cid = CorticalID::try_from_base_64(¶ms.cortical_id)
2140 .map_err(|e| ApiError::invalid_input(format!("Invalid cortical_id: {}", e)))?;
2141
2142 let page_size_u32 = params
2143 .page_size
2144 .clamp(1, MEMORY_CORTICAL_NEURON_IDS_PAGE_SIZE_MAX);
2145 let page_size = page_size_u32 as usize;
2146 let offset = (params.page as usize).saturating_mul(page_size);
2147
2148 let manager = feagi_brain_development::ConnectomeManager::instance();
2149 let mgr = manager.read();
2150
2151 let upstream_cortical_area_indices = mgr.get_upstream_cortical_areas(&cid);
2152 let upstream_cortical_area_count = upstream_cortical_area_indices.len();
2153
2154 let exec = mgr
2155 .get_plasticity_executor()
2156 .ok_or_else(|| ApiError::internal("Plasticity executor not available"))?;
2157 let ex = exec
2158 .lock()
2159 .map_err(|_| ApiError::internal("Plasticity executor lock poisoned"))?;
2160
2161 let runtime = ex
2162 .memory_cortical_area_runtime_info(cortical_idx)
2163 .ok_or_else(|| ApiError::internal("Plasticity service not initialized"))?;
2164
2165 let (memory_neuron_ids_u32, total_memory_neuron_ids) = ex
2166 .paginated_memory_neuron_ids_in_area(cortical_idx, offset, page_size)
2167 .unwrap_or((Vec::new(), 0));
2168
2169 let has_more = offset.saturating_add(memory_neuron_ids_u32.len()) < total_memory_neuron_ids;
2170
2171 let memory_neuron_ids: Vec<u64> = memory_neuron_ids_u32
2172 .into_iter()
2173 .map(|id| id as u64)
2174 .collect();
2175
2176 Ok(Json(MemoryCorticalAreaResponse {
2177 cortical_id: params.cortical_id,
2178 cortical_idx,
2179 cortical_name,
2180 short_term_neuron_count: runtime.short_term_neuron_count,
2181 long_term_neuron_count: runtime.long_term_neuron_count,
2182 memory_parameters: MemoryCorticalAreaParamsResponse {
2183 temporal_depth: mem_props.temporal_depth,
2184 longterm_mem_threshold: mem_props.longterm_threshold,
2185 lifespan_growth_rate: mem_props.lifespan_growth_rate,
2186 init_lifespan: mem_props.init_lifespan,
2187 },
2188 upstream_cortical_area_indices,
2189 upstream_cortical_area_count,
2190 upstream_pattern_cache_size: runtime.upstream_pattern_cache_size,
2191 incoming_synapse_count: area.incoming_synapse_count,
2192 outgoing_synapse_count: area.outgoing_synapse_count,
2193 total_memory_neuron_ids,
2194 page: params.page,
2195 page_size: page_size_u32,
2196 memory_neuron_ids,
2197 has_more,
2198 }))
2199}
2200
2201#[utoipa::path(
2203 get,
2204 path = "/v1/cortical_area/ipu/types",
2205 tag = "cortical_area",
2206 responses(
2207 (status = 200, description = "IPU type metadata", body = HashMap<String, CorticalTypeMetadata>),
2208 (status = 500, description = "Internal server error")
2209 )
2210)]
2211pub async fn get_ipu_types(
2212 State(_state): State<ApiState>,
2213) -> ApiResult<Json<HashMap<String, CorticalTypeMetadata>>> {
2214 let mut types = HashMap::new();
2215
2216 for unit in SensoryCorticalUnit::list_all() {
2218 let id_ref = unit.get_cortical_id_unit_reference();
2219 let key = format!("i{}", std::str::from_utf8(&id_ref).unwrap_or("???"));
2220
2221 let encodings = vec!["absolute".to_string(), "incremental".to_string()];
2223
2224 let snake_name = unit.get_snake_case_name();
2229 let formats = if snake_name == "vision"
2230 || snake_name == "segmented_vision"
2231 || snake_name == "miscellaneous"
2232 {
2233 vec![]
2234 } else {
2235 vec!["linear".to_string(), "fractional".to_string()]
2236 };
2237
2238 let resolution = if snake_name == "vision" {
2240 vec![64, 64, 1] } else if snake_name == "segmented_vision" {
2242 vec![32, 32, 1] } else {
2244 vec![1, 1, 1] };
2246
2247 let structure = "asymmetric".to_string();
2249
2250 let topology_map = unit.get_unit_default_topology();
2252 let unit_default_topology: HashMap<usize, UnitTopologyData> = topology_map
2253 .into_iter()
2254 .map(|(idx, topo)| {
2255 (
2256 *idx as usize,
2257 UnitTopologyData {
2258 relative_position: topo.relative_position,
2259 dimensions: topo.channel_dimensions_default,
2260 },
2261 )
2262 })
2263 .collect();
2264
2265 types.insert(
2266 key,
2267 CorticalTypeMetadata {
2268 description: unit.get_friendly_name().to_string(),
2269 encodings,
2270 formats,
2271 units: unit.get_number_cortical_areas() as u32,
2272 resolution,
2273 structure,
2274 unit_default_topology,
2275 },
2276 );
2277 }
2278
2279 Ok(Json(types))
2280}
2281
2282#[utoipa::path(
2284 get,
2285 path = "/v1/cortical_area/opu/types",
2286 tag = "cortical_area",
2287 responses(
2288 (status = 200, description = "OPU type metadata", body = HashMap<String, CorticalTypeMetadata>),
2289 (status = 500, description = "Internal server error")
2290 )
2291)]
2292pub async fn get_opu_types(
2293 State(_state): State<ApiState>,
2294) -> ApiResult<Json<HashMap<String, CorticalTypeMetadata>>> {
2295 let mut types = HashMap::new();
2296
2297 for unit in MotorCorticalUnit::list_all() {
2299 let id_ref = unit.get_cortical_id_unit_reference();
2300 let key = format!("o{}", std::str::from_utf8(&id_ref).unwrap_or("???"));
2301
2302 let encodings = vec!["absolute".to_string(), "incremental".to_string()];
2304
2305 let snake_name = unit.get_snake_case_name();
2309 let formats = if snake_name == "miscellaneous" {
2310 vec![]
2311 } else {
2312 vec!["linear".to_string(), "fractional".to_string()]
2313 };
2314
2315 let resolution = vec![1, 1, 1];
2317
2318 let structure = "asymmetric".to_string();
2320
2321 let topology_map = unit.get_unit_default_topology();
2323 let unit_default_topology: HashMap<usize, UnitTopologyData> = topology_map
2324 .into_iter()
2325 .map(|(idx, topo)| {
2326 (
2327 *idx as usize,
2328 UnitTopologyData {
2329 relative_position: topo.relative_position,
2330 dimensions: topo.channel_dimensions_default,
2331 },
2332 )
2333 })
2334 .collect();
2335
2336 types.insert(
2337 key,
2338 CorticalTypeMetadata {
2339 description: unit.get_friendly_name().to_string(),
2340 encodings,
2341 formats,
2342 units: unit.get_number_cortical_areas() as u32,
2343 resolution,
2344 structure,
2345 unit_default_topology,
2346 },
2347 );
2348 }
2349
2350 Ok(Json(types))
2351}
2352
2353#[utoipa::path(
2355 get,
2356 path = "/v1/cortical_area/cortical_area_index_list",
2357 tag = "cortical_area"
2358)]
2359pub async fn get_cortical_area_index_list(
2360 State(state): State<ApiState>,
2361) -> ApiResult<Json<Vec<u32>>> {
2362 let connectome_service = state.connectome_service.as_ref();
2363 let areas = connectome_service
2364 .list_cortical_areas()
2365 .await
2366 .map_err(|e| ApiError::internal(format!("{}", e)))?;
2367 let indices: Vec<u32> = areas.iter().map(|a| a.cortical_idx).collect();
2369 Ok(Json(indices))
2370}
2371
2372#[utoipa::path(
2374 get,
2375 path = "/v1/cortical_area/cortical_idx_mapping",
2376 tag = "cortical_area"
2377)]
2378pub async fn get_cortical_idx_mapping(
2379 State(state): State<ApiState>,
2380) -> ApiResult<Json<std::collections::BTreeMap<String, u32>>> {
2381 use std::collections::BTreeMap;
2382
2383 let connectome_service = state.connectome_service.as_ref();
2384 let areas = connectome_service
2385 .list_cortical_areas()
2386 .await
2387 .map_err(|e| ApiError::internal(format!("{}", e)))?;
2388 let mapping: BTreeMap<String, u32> = areas
2391 .iter()
2392 .map(|a| (a.cortical_id.clone(), a.cortical_idx))
2393 .collect();
2394 Ok(Json(mapping))
2395}
2396
2397#[utoipa::path(
2399 get,
2400 path = "/v1/cortical_area/mapping_restrictions",
2401 tag = "cortical_area"
2402)]
2403pub async fn get_mapping_restrictions_query(
2404 State(_state): State<ApiState>,
2405 Query(_params): Query<HashMap<String, String>>,
2406) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
2407 Ok(Json(HashMap::new()))
2408}
2409
2410#[utoipa::path(
2412 get,
2413 path = "/v1/cortical_area/{cortical_id}/memory_usage",
2414 tag = "cortical_area"
2415)]
2416pub async fn get_memory_usage(
2417 State(state): State<ApiState>,
2418 Path(cortical_id): Path<String>,
2419) -> ApiResult<Json<HashMap<String, i64>>> {
2420 let connectome_service = state.connectome_service.as_ref();
2421
2422 let area_info = connectome_service
2424 .get_cortical_area(&cortical_id)
2425 .await
2426 .map_err(|_| ApiError::not_found("CorticalArea", &cortical_id))?;
2427
2428 const BYTES_PER_NEURON: i64 = 48;
2431 let memory_bytes = (area_info.neuron_count as i64) * BYTES_PER_NEURON;
2432
2433 let mut response = HashMap::new();
2434 response.insert("memory_bytes".to_string(), memory_bytes);
2435 Ok(Json(response))
2436}
2437
2438#[utoipa::path(
2440 get,
2441 path = "/v1/cortical_area/{cortical_id}/neuron_count",
2442 tag = "cortical_area"
2443)]
2444pub async fn get_area_neuron_count(
2445 State(state): State<ApiState>,
2446 Path(cortical_id): Path<String>,
2447) -> ApiResult<Json<i64>> {
2448 let connectome_service = state.connectome_service.as_ref();
2449
2450 let area_info = connectome_service
2452 .get_cortical_area(&cortical_id)
2453 .await
2454 .map_err(|_| ApiError::not_found("CorticalArea", &cortical_id))?;
2455
2456 Ok(Json(area_info.neuron_count as i64))
2457}
2458
2459#[utoipa::path(
2461 post,
2462 path = "/v1/cortical_area/cortical_type_options",
2463 tag = "cortical_area"
2464)]
2465pub async fn post_cortical_type_options(
2466 State(_state): State<ApiState>,
2467) -> ApiResult<Json<Vec<String>>> {
2468 Ok(Json(vec![
2469 "Sensory".to_string(),
2470 "Motor".to_string(),
2471 "Custom".to_string(),
2472 "Memory".to_string(),
2473 ]))
2474}
2475
2476#[utoipa::path(
2478 post,
2479 path = "/v1/cortical_area/mapping_restrictions",
2480 tag = "cortical_area"
2481)]
2482pub async fn post_mapping_restrictions(
2483 State(_state): State<ApiState>,
2484 Json(_req): Json<HashMap<String, String>>,
2485) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
2486 Ok(Json(HashMap::new()))
2487}
2488
2489#[utoipa::path(
2491 post,
2492 path = "/v1/cortical_area/mapping_restrictions_between_areas",
2493 tag = "cortical_area"
2494)]
2495pub async fn post_mapping_restrictions_between_areas(
2496 State(_state): State<ApiState>,
2497 Json(_req): Json<HashMap<String, String>>,
2498) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
2499 Ok(Json(HashMap::new()))
2500}
2501
2502#[utoipa::path(put, path = "/v1/cortical_area/coord_3d", tag = "cortical_area")]
2504pub async fn put_coord_3d(
2505 State(_state): State<ApiState>,
2506 Json(_req): Json<HashMap<String, serde_json::Value>>,
2507) -> ApiResult<Json<HashMap<String, String>>> {
2508 Ok(Json(HashMap::from([(
2509 "message".to_string(),
2510 "Not yet implemented".to_string(),
2511 )])))
2512}
2513
2514#[cfg(test)]
2515mod voxel_neurons_dto_tests {
2516 use super::{
2517 synapse_details_for_neuron, synapse_page_window, VoxelNeuronsBody, VoxelNeuronsResponse,
2518 };
2519
2520 #[test]
2521 fn synapse_page_window_paginates_fifty_per_direction() {
2522 let (s, e, more) = synapse_page_window(120, 0);
2523 assert_eq!((s, e, more), (0, 50, true));
2524 let (s, e, more) = synapse_page_window(120, 1);
2525 assert_eq!((s, e, more), (50, 100, true));
2526 let (s, e, more) = synapse_page_window(120, 2);
2527 assert_eq!((s, e, more), (100, 120, false));
2528 let (s, e, more) = synapse_page_window(120, 3);
2529 assert_eq!((s, e, more), (0, 0, false));
2530 }
2531
2532 #[test]
2533 fn synapse_details_matches_connectome_shape() {
2534 let mgr = feagi_brain_development::ConnectomeManager::new_for_testing();
2535 let out_full = vec![(10, 2.0, 5.0, 1)];
2536 let inc_full = vec![(3, 4.0, 6.0, 0)];
2537 let (out, inc) = synapse_details_for_neuron(&mgr, 7, &out_full, &inc_full);
2538 let out_a = out.as_array().expect("outgoing array");
2539 assert_eq!(out_a[0]["source_neuron_id"], serde_json::json!(7));
2540 assert_eq!(out_a[0]["target_neuron_id"], serde_json::json!(10));
2541 assert_eq!(out_a[0]["weight"], serde_json::json!(2.0));
2542 assert_eq!(out_a[0]["postsynaptic_potential"], serde_json::json!(5.0));
2543 assert_eq!(out_a[0]["synapse_type"], serde_json::json!(1));
2544 assert!(out_a[0].get("target_cortical_id").is_some());
2545 assert!(out_a[0].get("target_cortical_name").is_some());
2546 assert!(out_a[0].get("target_x").is_some());
2547 let in_a = inc.as_array().expect("incoming array");
2548 assert_eq!(in_a[0]["source_neuron_id"], serde_json::json!(3));
2549 assert_eq!(in_a[0]["target_neuron_id"], serde_json::json!(7));
2550 assert!(in_a[0].get("source_cortical_id").is_some());
2551 assert!(in_a[0].get("source_cortical_name").is_some());
2552 assert!(in_a[0].get("source_x").is_some());
2553 }
2554
2555 #[test]
2556 fn voxel_neurons_body_deserializes_from_json() {
2557 let j = r#"{"cortical_id":"X19fcG93ZXI=","x":0,"y":0,"z":0}"#;
2558 let b: VoxelNeuronsBody = serde_json::from_str(j).expect("deserialize body");
2559 assert_eq!(b.cortical_id, "X19fcG93ZXI=");
2560 assert_eq!((b.x, b.y, b.z), (0, 0, 0));
2561 assert_eq!(b.synapse_page, 0);
2562 }
2563
2564 #[test]
2565 fn voxel_neurons_response_serializes() {
2566 let r = VoxelNeuronsResponse {
2567 cortical_id: "id".to_string(),
2568 cortical_name: "test_area".to_string(),
2569 cortical_idx: 2,
2570 voxel_coordinate: [1, 2, 3],
2571 x: 1,
2572 y: 2,
2573 z: 3,
2574 synapse_page: 0,
2575 neuron_count: 0,
2576 neurons: vec![],
2577 };
2578 let v = serde_json::to_value(&r).expect("serialize");
2579 assert_eq!(v["cortical_name"], serde_json::json!("test_area"));
2580 assert_eq!(v["voxel_coordinate"], serde_json::json!([1, 2, 3]));
2581 assert_eq!(v["neuron_count"], serde_json::json!(0));
2582 assert_eq!(v["synapse_page"], serde_json::json!(0));
2583 assert_eq!(v["neurons"], serde_json::json!([]));
2584 }
2585}