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 raw_configs = request
872 .get("data_type_configs_by_subunit")
873 .and_then(|v| v.as_object())
874 .ok_or_else(|| ApiError::invalid_input("data_type_configs_by_subunit (object) required"))?;
875
876 let mut data_type_configs_by_subunit: HashMap<u8, u16> = HashMap::new();
877
878 for (k, v) in raw_configs {
879 let subunit_idx_u64 = k.parse::<u64>().map_err(|_| {
880 ApiError::invalid_input("data_type_configs_by_subunit keys must be integers")
881 })?;
882 let subunit_idx: u8 = subunit_idx_u64.try_into().map_err(|_| {
883 ApiError::invalid_input("data_type_configs_by_subunit key out of range")
884 })?;
885
886 let parsed_u64 = if let Some(u) = v.as_u64() {
887 Some(u)
888 } else if let Some(i) = v.as_i64() {
889 if i >= 0 {
890 Some(i as u64)
891 } else {
892 None
893 }
894 } else if let Some(f) = v.as_f64() {
895 if f >= 0.0 {
896 Some(f.round() as u64)
897 } else {
898 None
899 }
900 } else if let Some(s) = v.as_str() {
901 s.parse::<u64>().ok()
902 } else {
903 None
904 }
905 .ok_or_else(|| {
906 ApiError::invalid_input("data_type_configs_by_subunit values must be numeric")
907 })?;
908
909 if parsed_u64 > u16::MAX as u64 {
910 return Err(ApiError::invalid_input(
911 "data_type_configs_by_subunit value exceeds u16::MAX",
912 ));
913 }
914
915 data_type_configs_by_subunit.insert(subunit_idx, parsed_u64 as u16);
916 }
917
918 tracing::info!(
919 target: "feagi-api",
920 "Creating cortical areas for {} with neurons_per_voxel={}, data_type_configs_by_subunit={:?}",
921 cortical_type_key,
922 neurons_per_voxel,
923 data_type_configs_by_subunit
924 );
925
926 let (num_units, unit_topology) = if cortical_type_str == "IPU" {
928 let unit = SensoryCorticalUnit::list_all()
930 .iter()
931 .find(|u| {
932 let id_ref = u.get_cortical_id_unit_reference();
933 let key = format!("i{}", std::str::from_utf8(&id_ref).unwrap_or(""));
934 key == cortical_type_key
935 })
936 .ok_or_else(|| {
937 ApiError::invalid_input(format!("Unknown IPU type: {}", cortical_type_key))
938 })?;
939
940 (
941 unit.get_number_cortical_areas(),
942 unit.get_unit_default_topology(),
943 )
944 } else if cortical_type_str == "OPU" {
945 let unit = MotorCorticalUnit::list_all()
947 .iter()
948 .find(|u| {
949 let id_ref = u.get_cortical_id_unit_reference();
950 let key = format!("o{}", std::str::from_utf8(&id_ref).unwrap_or(""));
951 key == cortical_type_key
952 })
953 .ok_or_else(|| {
954 ApiError::invalid_input(format!("Unknown OPU type: {}", cortical_type_key))
955 })?;
956
957 (
958 unit.get_number_cortical_areas(),
959 unit.get_unit_default_topology(),
960 )
961 } else {
962 return Err(ApiError::invalid_input("cortical_type must be IPU or OPU"));
963 };
964
965 tracing::info!(
966 "Creating {} units for cortical type: {}",
967 num_units,
968 cortical_type_key
969 );
970
971 let mut creation_params = Vec::new();
973 for unit_idx in 0..num_units {
974 let data_type_config = data_type_configs_by_subunit
975 .get(&(unit_idx as u8))
976 .copied()
977 .ok_or_else(|| {
978 ApiError::invalid_input(format!(
979 "data_type_configs_by_subunit missing entry for subunit {}",
980 unit_idx
981 ))
982 })?;
983
984 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) =
991 if let Some(topo) = unit_topology.get(&CorticalSubUnitIndex::from(unit_idx as u8)) {
992 let dims = topo.channel_dimensions_default;
993 let per_device = (dims[0] as usize, dims[1] as usize, dims[2] as usize);
994 let total_x = per_device.0.saturating_mul(device_count);
995 (per_device, (total_x, per_device.1, per_device.2))
996 } else {
997 ((1, 1, 1), (device_count.max(1), 1, 1)) };
999
1000 let position =
1002 if let Some(topo) = unit_topology.get(&CorticalSubUnitIndex::from(unit_idx as u8)) {
1003 let rel_pos = topo.relative_position;
1004 (
1005 coordinates_3d[0] + rel_pos[0],
1006 coordinates_3d[1] + rel_pos[1],
1007 coordinates_3d[2] + rel_pos[2],
1008 )
1009 } else {
1010 (coordinates_3d[0], coordinates_3d[1], coordinates_3d[2])
1011 };
1012
1013 let subtype_bytes = if cortical_type_key.len() >= 4 {
1017 let subtype_str = &cortical_type_key[1..4]; let mut bytes = [0u8; 3];
1019 for (i, c) in subtype_str.chars().take(3).enumerate() {
1020 bytes[i] = c as u8;
1021 }
1022 bytes
1023 } else {
1024 return Err(ApiError::invalid_input("Invalid cortical_type_key"));
1025 };
1026
1027 let cortical_id_bytes = [
1029 if cortical_type_str == "IPU" {
1030 b'i'
1031 } else {
1032 b'o'
1033 }, subtype_bytes[0], subtype_bytes[1], subtype_bytes[2], config_byte_4, config_byte_5, unit_idx as u8, group_id, ];
1042
1043 let cortical_id = general_purpose::STANDARD.encode(cortical_id_bytes);
1045
1046 tracing::debug!(target: "feagi-api",
1047 " Unit {}: dims={}x{}x{}, neurons_per_voxel={}, total_neurons={}",
1048 unit_idx, dimensions.0, dimensions.1, dimensions.2, neurons_per_voxel,
1049 dimensions.0 * dimensions.1 * dimensions.2 * neurons_per_voxel as usize
1050 );
1051
1052 let mut properties = HashMap::new();
1054 properties.insert(
1055 "dev_count".to_string(),
1056 serde_json::Value::Number(serde_json::Number::from(device_count)),
1057 );
1058 properties.insert(
1059 "cortical_dimensions_per_device".to_string(),
1060 serde_json::json!([
1061 per_device_dimensions.0,
1062 per_device_dimensions.1,
1063 per_device_dimensions.2
1064 ]),
1065 );
1066
1067 let params = CreateCorticalAreaParams {
1068 cortical_id: cortical_id.clone(),
1069 name: format!("{} Unit {}", cortical_type_key, unit_idx),
1070 dimensions,
1071 position,
1072 area_type: cortical_type_str.to_string(),
1073 visible: Some(true),
1074 sub_group: None,
1075 neurons_per_voxel: Some(neurons_per_voxel),
1076 postsynaptic_current: Some(0.0),
1077 plasticity_constant: Some(0.0),
1078 degeneration: Some(0.0),
1079 psp_uniform_distribution: Some(false),
1080 firing_threshold_increment: Some(0.0),
1081 firing_threshold_limit: Some(0.0),
1082 consecutive_fire_count: Some(0),
1083 snooze_period: Some(0),
1084 refractory_period: Some(0),
1085 leak_coefficient: Some(0.0),
1086 leak_variability: Some(0.0),
1087 burst_engine_active: Some(true),
1088 properties: Some(properties),
1089 };
1090
1091 creation_params.push(params);
1092 }
1093
1094 tracing::info!(
1095 "Calling GenomeService to create {} cortical areas",
1096 creation_params.len()
1097 );
1098
1099 let areas_details = genome_service
1102 .create_cortical_areas(creation_params)
1103 .await
1104 .map_err(|e| ApiError::internal(format!("Failed to create cortical areas: {}", e)))?;
1105
1106 tracing::info!(
1107 "✅ Successfully created {} cortical areas via GenomeService",
1108 areas_details.len()
1109 );
1110
1111 let areas_json = serde_json::to_value(&areas_details).unwrap_or_default();
1113
1114 let created_ids: Vec<String> = areas_details
1116 .iter()
1117 .map(|a| a.cortical_id.clone())
1118 .collect();
1119
1120 let first_id = created_ids.first().cloned().unwrap_or_default();
1122 let mut response = serde_json::Map::new();
1123 response.insert(
1124 "message".to_string(),
1125 serde_json::Value::String(format!("Created {} cortical areas", created_ids.len())),
1126 );
1127 response.insert(
1128 "cortical_id".to_string(),
1129 serde_json::Value::String(first_id),
1130 ); response.insert(
1132 "cortical_ids".to_string(),
1133 serde_json::Value::String(created_ids.join(", ")),
1134 );
1135 response.insert(
1136 "unit_count".to_string(),
1137 serde_json::Value::Number(created_ids.len().into()),
1138 );
1139 response.insert("areas".to_string(), areas_json); Ok(Json(serde_json::Value::Object(response)))
1142}
1143
1144#[utoipa::path(put, path = "/v1/cortical_area/cortical_area", tag = "cortical_area")]
1146pub async fn put_cortical_area(
1147 State(state): State<ApiState>,
1148 Json(mut request): Json<HashMap<String, serde_json::Value>>,
1149) -> ApiResult<Json<HashMap<String, String>>> {
1150 let genome_service = state.genome_service.as_ref();
1151
1152 let cortical_id = request
1154 .get("cortical_id")
1155 .and_then(|v| v.as_str())
1156 .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?
1157 .to_string();
1158
1159 tracing::debug!(
1160 target: "feagi-api",
1161 "PUT /v1/cortical_area/cortical_area - received update for area: {} (keys: {:?})",
1162 cortical_id,
1163 request.keys().collect::<Vec<_>>()
1164 );
1165
1166 request.remove("cortical_id");
1168
1169 match genome_service
1171 .update_cortical_area(&cortical_id, request)
1172 .await
1173 {
1174 Ok(area_info) => {
1175 let updated_id = area_info.cortical_id.clone();
1176 tracing::debug!(
1177 target: "feagi-api",
1178 "PUT /v1/cortical_area/cortical_area - success for {} (updated_id={})",
1179 cortical_id,
1180 updated_id
1181 );
1182 Ok(Json(HashMap::from([
1183 ("message".to_string(), "Cortical area updated".to_string()),
1184 ("cortical_id".to_string(), updated_id),
1185 ("previous_cortical_id".to_string(), cortical_id),
1186 ])))
1187 }
1188 Err(e) => {
1189 tracing::error!(target: "feagi-api", "PUT /v1/cortical_area/cortical_area - failed for {}: {}", cortical_id, e);
1190 Err(ApiError::internal(format!("Failed to update: {}", e)))
1191 }
1192 }
1193}
1194
1195#[utoipa::path(
1197 delete,
1198 path = "/v1/cortical_area/cortical_area",
1199 tag = "cortical_area"
1200)]
1201#[allow(unused_variables)] pub async fn delete_cortical_area(
1203 State(state): State<ApiState>,
1204 Json(request): Json<HashMap<String, String>>,
1205) -> ApiResult<Json<HashMap<String, String>>> {
1206 let connectome_service = state.connectome_service.as_ref();
1207 let cortical_id = request
1208 .get("cortical_id")
1209 .ok_or_else(|| ApiError::invalid_input("cortical_id required"))?;
1210
1211 match connectome_service.delete_cortical_area(cortical_id).await {
1212 Ok(_) => Ok(Json(HashMap::from([(
1213 "message".to_string(),
1214 "Cortical area deleted".to_string(),
1215 )]))),
1216 Err(e) => Err(ApiError::internal(format!("Failed to delete: {}", e))),
1217 }
1218}
1219
1220#[utoipa::path(
1222 post,
1223 path = "/v1/cortical_area/custom_cortical_area",
1224 tag = "cortical_area"
1225)]
1226pub async fn post_custom_cortical_area(
1227 State(state): State<ApiState>,
1228 Json(request): Json<HashMap<String, serde_json::Value>>,
1229) -> ApiResult<Json<HashMap<String, String>>> {
1230 use feagi_services::types::CreateCorticalAreaParams;
1231 use std::time::{SystemTime, UNIX_EPOCH};
1232
1233 let is_memory_area_requested = request
1245 .get("sub_group_id")
1246 .and_then(|v| v.as_str())
1247 .map(|s| s.eq_ignore_ascii_case("MEMORY"))
1248 .unwrap_or(false);
1249
1250 let cortical_name = request
1252 .get("cortical_name")
1253 .and_then(|v| v.as_str())
1254 .ok_or_else(|| ApiError::invalid_input("cortical_name required"))?;
1255
1256 let cortical_dimensions: Vec<u32> = request
1257 .get("cortical_dimensions")
1258 .and_then(|v| v.as_array())
1259 .and_then(|arr| {
1260 if arr.len() == 3 {
1261 Some(vec![
1262 arr[0].as_u64()? as u32,
1263 arr[1].as_u64()? as u32,
1264 arr[2].as_u64()? as u32,
1265 ])
1266 } else {
1267 None
1268 }
1269 })
1270 .ok_or_else(|| ApiError::invalid_input("cortical_dimensions must be [x, y, z]"))?;
1271
1272 let coordinates_3d: Vec<i32> = request
1273 .get("coordinates_3d")
1274 .and_then(|v| v.as_array())
1275 .and_then(|arr| {
1276 if arr.len() == 3 {
1277 Some(vec![
1278 arr[0].as_i64()? as i32,
1279 arr[1].as_i64()? as i32,
1280 arr[2].as_i64()? as i32,
1281 ])
1282 } else {
1283 None
1284 }
1285 })
1286 .ok_or_else(|| ApiError::invalid_input("coordinates_3d must be [x, y, z]"))?;
1287
1288 let brain_region_id = request
1289 .get("brain_region_id")
1290 .and_then(|v| v.as_str())
1291 .map(|s| s.to_string());
1292
1293 let cortical_sub_group = request
1294 .get("cortical_sub_group")
1295 .and_then(|v| v.as_str())
1296 .filter(|s| !s.is_empty())
1297 .map(|s| s.to_string());
1298
1299 tracing::info!(target: "feagi-api",
1300 "Creating {} cortical area '{}' with dimensions: {}x{}x{}, position: ({}, {}, {})",
1301 if is_memory_area_requested { "memory" } else { "custom" },
1302 cortical_name, cortical_dimensions[0], cortical_dimensions[1], cortical_dimensions[2],
1303 coordinates_3d[0], coordinates_3d[1], coordinates_3d[2]
1304 );
1305
1306 let timestamp = SystemTime::now()
1310 .duration_since(UNIX_EPOCH)
1311 .unwrap()
1312 .as_millis() as u64;
1313
1314 let mut cortical_id_bytes = [0u8; 8];
1319 cortical_id_bytes[0] = if is_memory_area_requested { b'm' } else { b'c' };
1320
1321 let name_bytes = cortical_name.as_bytes();
1323 for i in 1..7 {
1324 cortical_id_bytes[i] = if i - 1 < name_bytes.len() {
1325 let c = name_bytes[i - 1];
1327 if c.is_ascii_alphanumeric() || c == b'_' {
1328 c
1329 } else {
1330 b'_'
1331 }
1332 } else {
1333 b'_' };
1335 }
1336
1337 cortical_id_bytes[7] = (timestamp & 0xFF) as u8;
1339
1340 let cortical_id = general_purpose::STANDARD.encode(cortical_id_bytes);
1342
1343 tracing::debug!(target: "feagi-api",
1344 "Generated cortical_id: {} (raw bytes: {:?})",
1345 cortical_id, cortical_id_bytes
1346 );
1347
1348 let mut properties = HashMap::new();
1350 if let Some(region_id) = brain_region_id.clone() {
1351 properties.insert(
1352 "parent_region_id".to_string(),
1353 serde_json::Value::String(region_id),
1354 );
1355 }
1356
1357 let params = CreateCorticalAreaParams {
1359 cortical_id: cortical_id.clone(),
1360 name: cortical_name.to_string(),
1361 dimensions: (
1362 cortical_dimensions[0] as usize,
1363 cortical_dimensions[1] as usize,
1364 cortical_dimensions[2] as usize,
1365 ),
1366 position: (coordinates_3d[0], coordinates_3d[1], coordinates_3d[2]),
1367 area_type: if is_memory_area_requested {
1368 "Memory".to_string()
1369 } else {
1370 "Custom".to_string()
1371 },
1372 visible: Some(true),
1373 sub_group: cortical_sub_group,
1374 neurons_per_voxel: Some(1),
1375 postsynaptic_current: None,
1376 plasticity_constant: Some(0.0),
1377 degeneration: Some(0.0),
1378 psp_uniform_distribution: Some(false),
1379 firing_threshold_increment: Some(0.0),
1380 firing_threshold_limit: Some(0.0),
1381 consecutive_fire_count: Some(0),
1382 snooze_period: Some(0),
1383 refractory_period: Some(0),
1384 leak_coefficient: Some(0.0),
1385 leak_variability: Some(0.0),
1386 burst_engine_active: Some(true),
1387 properties: Some(properties),
1388 };
1389
1390 let genome_service = state.genome_service.as_ref();
1391
1392 tracing::info!(target: "feagi-api", "Calling GenomeService to create custom cortical area");
1393
1394 let areas_details = genome_service
1396 .create_cortical_areas(vec![params])
1397 .await
1398 .map_err(|e| ApiError::internal(format!("Failed to create custom cortical area: {}", e)))?;
1399
1400 let created_area = areas_details
1401 .first()
1402 .ok_or_else(|| ApiError::internal("No cortical area was created"))?;
1403
1404 tracing::info!(target: "feagi-api",
1405 "✅ Successfully created custom cortical area '{}' with ID: {}",
1406 cortical_name, created_area.cortical_id
1407 );
1408
1409 let mut response = HashMap::new();
1411 response.insert(
1412 "message".to_string(),
1413 "Custom cortical area created successfully".to_string(),
1414 );
1415 response.insert("cortical_id".to_string(), created_area.cortical_id.clone());
1416 response.insert("cortical_name".to_string(), cortical_name.to_string());
1417
1418 Ok(Json(response))
1419}
1420
1421#[utoipa::path(post, path = "/v1/cortical_area/clone", tag = "cortical_area")]
1423pub async fn post_clone(
1424 State(state): State<ApiState>,
1425 Json(request): Json<CloneCorticalAreaRequest>,
1426) -> ApiResult<Json<HashMap<String, String>>> {
1427 use base64::{engine::general_purpose, Engine as _};
1428 use feagi_services::types::CreateCorticalAreaParams;
1429 use feagi_structures::genomic::cortical_area::CorticalID;
1430 use serde_json::Value;
1431 use std::time::{SystemTime, UNIX_EPOCH};
1432
1433 let genome_service = state.genome_service.as_ref();
1434 let connectome_service = state.connectome_service.as_ref();
1435
1436 let source_id = request.source_area_id.clone();
1438 let source_typed = CorticalID::try_from_base_64(&source_id)
1439 .map_err(|e| ApiError::invalid_input(e.to_string()))?;
1440 let src_first_byte = source_typed.as_bytes()[0];
1441 if src_first_byte != b'c' && src_first_byte != b'm' {
1442 return Err(ApiError::invalid_input(format!(
1443 "Cloning is only supported for custom ('c') and memory ('m') cortical areas (got prefix byte: {})",
1444 src_first_byte
1445 )));
1446 }
1447
1448 let source_area = connectome_service
1450 .get_cortical_area(&source_id)
1451 .await
1452 .map_err(|e| ApiError::not_found("CorticalArea", &e.to_string()))?;
1453
1454 let source_parent_region_id = source_area
1461 .parent_region_id
1462 .clone()
1463 .or_else(|| {
1464 source_area
1465 .properties
1466 .get("parent_region_id")
1467 .and_then(|v| v.as_str())
1468 .map(|s| s.to_string())
1469 })
1470 .ok_or_else(|| {
1471 ApiError::internal(format!(
1472 "Source cortical area {} is missing parent_region_id; cannot determine region membership for clone",
1473 source_id
1474 ))
1475 })?;
1476
1477 if let Some(client_parent_region_id) = request.parent_region_id.as_ref() {
1478 if client_parent_region_id != &source_parent_region_id {
1479 return Err(ApiError::invalid_input(format!(
1480 "parent_region_id mismatch for clone request: client sent '{}', but FEAGI source area {} belongs to '{}'",
1481 client_parent_region_id, source_id, source_parent_region_id
1482 )));
1483 }
1484 }
1485
1486 let outgoing_mapping_dst = source_area
1488 .properties
1489 .get("cortical_mapping_dst")
1490 .and_then(|v| v.as_object())
1491 .cloned();
1492
1493 let timestamp = SystemTime::now()
1500 .duration_since(UNIX_EPOCH)
1501 .map_err(|e| ApiError::internal(format!("System clock error: {}", e)))?
1502 .as_millis() as u64;
1503
1504 let mut cortical_id_bytes = [0u8; 8];
1505 cortical_id_bytes[0] = src_first_byte;
1506
1507 let name_bytes = request.new_name.as_bytes();
1508 for i in 1..7 {
1509 cortical_id_bytes[i] = if i - 1 < name_bytes.len() {
1510 let c = name_bytes[i - 1];
1511 if c.is_ascii_alphanumeric() || c == b'_' {
1512 c
1513 } else {
1514 b'_'
1515 }
1516 } else {
1517 b'_'
1518 };
1519 }
1520 cortical_id_bytes[7] = (timestamp & 0xFF) as u8;
1521
1522 let new_area_id = general_purpose::STANDARD.encode(cortical_id_bytes);
1523
1524 let mut cloned_properties = source_area.properties.clone();
1527 cloned_properties.remove("cortical_mapping_dst");
1528
1529 cloned_properties.insert(
1531 "parent_region_id".to_string(),
1532 Value::String(source_parent_region_id),
1533 );
1534 cloned_properties.insert(
1535 "coordinate_2d".to_string(),
1536 serde_json::json!([request.coordinates_2d[0], request.coordinates_2d[1]]),
1537 );
1538
1539 let params = CreateCorticalAreaParams {
1540 cortical_id: new_area_id.clone(),
1541 name: request.new_name.clone(),
1542 dimensions: source_area.dimensions,
1543 position: (
1544 request.coordinates_3d[0],
1545 request.coordinates_3d[1],
1546 request.coordinates_3d[2],
1547 ),
1548 area_type: source_area.area_type.clone(),
1549 visible: Some(source_area.visible),
1550 sub_group: source_area.sub_group.clone(),
1551 neurons_per_voxel: Some(source_area.neurons_per_voxel),
1552 postsynaptic_current: Some(source_area.postsynaptic_current),
1553 plasticity_constant: Some(source_area.plasticity_constant),
1554 degeneration: Some(source_area.degeneration),
1555 psp_uniform_distribution: Some(source_area.psp_uniform_distribution),
1556 firing_threshold_increment: None,
1559 firing_threshold_limit: Some(source_area.firing_threshold_limit),
1560 consecutive_fire_count: Some(source_area.consecutive_fire_count),
1561 snooze_period: Some(source_area.snooze_period),
1562 refractory_period: Some(source_area.refractory_period),
1563 leak_coefficient: Some(source_area.leak_coefficient),
1564 leak_variability: Some(source_area.leak_variability),
1565 burst_engine_active: Some(source_area.burst_engine_active),
1566 properties: Some(cloned_properties),
1567 };
1568
1569 let created_areas = genome_service
1571 .create_cortical_areas(vec![params])
1572 .await
1573 .map_err(|e| ApiError::internal(format!("Failed to clone cortical area: {}", e)))?;
1574
1575 if let Some(created_area) = created_areas.first() {
1577 tracing::info!(target: "feagi-api",
1578 "Clone created area {} with position {:?} (requested {:?})",
1579 new_area_id, created_area.position, request.coordinates_3d
1580 );
1581 }
1582
1583 if request.clone_cortical_mapping {
1585 if let Some(dst_map) = outgoing_mapping_dst {
1587 for (dst_id, rules) in dst_map {
1588 let dst_effective = if dst_id == source_id {
1589 new_area_id.clone()
1591 } else {
1592 dst_id.clone()
1593 };
1594
1595 let Some(rules_array) = rules.as_array() else {
1596 return Err(ApiError::invalid_input(format!(
1597 "Invalid cortical_mapping_dst value for dst '{}': expected array, got {}",
1598 dst_id, rules
1599 )));
1600 };
1601
1602 connectome_service
1603 .update_cortical_mapping(
1604 new_area_id.clone(),
1605 dst_effective,
1606 rules_array.clone(),
1607 )
1608 .await
1609 .map_err(|e| {
1610 ApiError::internal(format!(
1611 "Failed to clone outgoing mapping from {}: {}",
1612 source_id, e
1613 ))
1614 })?;
1615 }
1616 }
1617
1618 let all_areas = connectome_service
1621 .list_cortical_areas()
1622 .await
1623 .map_err(|e| ApiError::internal(format!("Failed to list cortical areas: {}", e)))?;
1624
1625 for area in all_areas {
1626 if area.cortical_id == source_id {
1628 continue;
1629 }
1630
1631 let Some(dst_map) = area
1632 .properties
1633 .get("cortical_mapping_dst")
1634 .and_then(|v| v.as_object())
1635 else {
1636 continue;
1637 };
1638
1639 let Some(rules) = dst_map.get(&source_id) else {
1640 continue;
1641 };
1642
1643 let Some(rules_array) = rules.as_array() else {
1644 return Err(ApiError::invalid_input(format!(
1645 "Invalid cortical_mapping_dst value for src '{}', dst '{}': expected array, got {}",
1646 area.cortical_id, source_id, rules
1647 )));
1648 };
1649
1650 connectome_service
1651 .update_cortical_mapping(
1652 area.cortical_id.clone(),
1653 new_area_id.clone(),
1654 rules_array.clone(),
1655 )
1656 .await
1657 .map_err(|e| {
1658 ApiError::internal(format!(
1659 "Failed to clone incoming mapping into {} from {}: {}",
1660 source_id, area.cortical_id, e
1661 ))
1662 })?;
1663 }
1664 }
1665
1666 Ok(Json(HashMap::from([
1667 ("message".to_string(), "Cortical area cloned".to_string()),
1668 ("new_area_id".to_string(), new_area_id),
1669 ])))
1670}
1671
1672#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
1674pub struct CloneCorticalAreaRequest {
1675 pub source_area_id: String,
1677 pub new_name: String,
1679 pub coordinates_3d: [i32; 3],
1681 pub coordinates_2d: [i32; 2],
1683 #[serde(default)]
1688 pub parent_region_id: Option<String>,
1689 pub clone_cortical_mapping: bool,
1691}
1692
1693#[utoipa::path(
1695 put,
1696 path = "/v1/cortical_area/multi/cortical_area",
1697 tag = "cortical_area"
1698)]
1699pub async fn put_multi_cortical_area(
1700 State(state): State<ApiState>,
1701 Json(mut request): Json<HashMap<String, serde_json::Value>>,
1702) -> ApiResult<Json<HashMap<String, String>>> {
1703 let genome_service = state.genome_service.as_ref();
1704
1705 let cortical_ids: Vec<String> = request
1707 .get("cortical_id_list")
1708 .and_then(|v| v.as_array())
1709 .ok_or_else(|| ApiError::invalid_input("cortical_id_list required"))?
1710 .iter()
1711 .filter_map(|v| v.as_str().map(String::from))
1712 .collect();
1713
1714 if cortical_ids.is_empty() {
1715 return Err(ApiError::invalid_input("cortical_id_list cannot be empty"));
1716 }
1717
1718 tracing::debug!(
1719 target: "feagi-api",
1720 "PUT /v1/cortical_area/multi/cortical_area - received update for {} areas (keys: {:?})",
1721 cortical_ids.len(),
1722 request.keys().collect::<Vec<_>>()
1723 );
1724
1725 request.remove("cortical_id_list");
1727
1728 let mut shared_properties = request.clone();
1730 for cortical_id in &cortical_ids {
1731 shared_properties.remove(cortical_id);
1732 }
1733
1734 for cortical_id in &cortical_ids {
1736 tracing::debug!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - updating area: {}", cortical_id);
1737 let mut properties = shared_properties.clone();
1738 if let Some(serde_json::Value::Object(per_id_map)) = request.get(cortical_id) {
1739 for (key, value) in per_id_map {
1740 properties.insert(key.clone(), value.clone());
1741 }
1742 }
1743 match genome_service
1744 .update_cortical_area(cortical_id, properties)
1745 .await
1746 {
1747 Ok(_) => {
1748 tracing::debug!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - success for {}", cortical_id);
1749 }
1750 Err(e) => {
1751 tracing::error!(target: "feagi-api", "PUT /v1/cortical_area/multi/cortical_area - failed for {}: {}", cortical_id, e);
1752 return Err(ApiError::internal(format!(
1753 "Failed to update cortical area {}: {}",
1754 cortical_id, e
1755 )));
1756 }
1757 }
1758 }
1759
1760 Ok(Json(HashMap::from([
1761 (
1762 "message".to_string(),
1763 format!("Updated {} cortical areas", cortical_ids.len()),
1764 ),
1765 ("cortical_ids".to_string(), cortical_ids.join(", ")),
1766 ])))
1767}
1768
1769#[utoipa::path(
1771 delete,
1772 path = "/v1/cortical_area/multi/cortical_area",
1773 tag = "cortical_area"
1774)]
1775#[allow(unused_variables)] pub async fn delete_multi_cortical_area(
1777 State(state): State<ApiState>,
1778 Json(request): Json<Vec<String>>,
1779) -> ApiResult<Json<HashMap<String, String>>> {
1780 Err(ApiError::internal("Not yet implemented"))
1782}
1783
1784#[utoipa::path(put, path = "/v1/cortical_area/coord_2d", tag = "cortical_area")]
1786#[allow(unused_variables)] pub async fn put_coord_2d(
1788 State(state): State<ApiState>,
1789 Json(request): Json<HashMap<String, serde_json::Value>>,
1790) -> ApiResult<Json<HashMap<String, String>>> {
1791 Err(ApiError::internal("Not yet implemented"))
1793}
1794
1795#[utoipa::path(
1797 put,
1798 path = "/v1/cortical_area/suppress_cortical_visibility",
1799 tag = "cortical_area"
1800)]
1801#[allow(unused_variables)] pub async fn put_suppress_cortical_visibility(
1803 State(state): State<ApiState>,
1804 Json(request): Json<HashMap<String, serde_json::Value>>,
1805) -> ApiResult<Json<HashMap<String, String>>> {
1806 Err(ApiError::internal("Not yet implemented"))
1808}
1809
1810#[utoipa::path(
1813 put,
1814 path = "/v1/cortical_area/reset",
1815 tag = "cortical_area",
1816 request_body = CorticalAreaResetRequest,
1817 responses(
1818 (status = 200, description = "Reset applied", body = CorticalAreaResetResponse),
1819 )
1820)]
1821pub async fn put_reset(
1822 State(state): State<ApiState>,
1823 Json(request): Json<CorticalAreaResetRequest>,
1824) -> ApiResult<Json<CorticalAreaResetResponse>> {
1825 use tracing::info;
1826
1827 if request.area_list.is_empty() {
1828 return Err(ApiError::invalid_input("area_list cannot be empty"));
1829 }
1830
1831 info!(
1832 target: "feagi-api",
1833 "[RESET] Received reset request for {} cortical areas: {:?}",
1834 request.area_list.len(),
1835 request.area_list
1836 );
1837
1838 let connectome_service = state.connectome_service.as_ref();
1839 let mut cortical_indices: Vec<u32> = Vec::with_capacity(request.area_list.len());
1840 for id in &request.area_list {
1841 let area = connectome_service
1842 .get_cortical_area(id)
1843 .await
1844 .map_err(ApiError::from)?;
1845 cortical_indices.push(area.cortical_idx);
1846 info!(
1847 target: "feagi-api",
1848 "[RESET] Resolved cortical ID '{}' to index {}",
1849 id,
1850 area.cortical_idx
1851 );
1852 }
1853
1854 info!(
1855 target: "feagi-api",
1856 "[RESET] Calling runtime service to reset indices: {:?}",
1857 cortical_indices
1858 );
1859
1860 let reset_pairs = state
1861 .runtime_service
1862 .reset_cortical_area_states(&cortical_indices)
1863 .await
1864 .map_err(ApiError::from)?;
1865
1866 let results: Vec<CorticalAreaResetItem> = reset_pairs
1867 .into_iter()
1868 .map(|(cortical_idx, neurons_reset)| {
1869 info!(
1870 target: "feagi-api",
1871 "[RESET] Cortical area {} reset: {} neurons cleared",
1872 cortical_idx,
1873 neurons_reset
1874 );
1875 CorticalAreaResetItem {
1876 cortical_idx,
1877 neurons_reset,
1878 }
1879 })
1880 .collect();
1881
1882 info!(
1883 target: "feagi-api",
1884 "[RESET] Reset complete for {} areas",
1885 results.len()
1886 );
1887
1888 Ok(Json(CorticalAreaResetResponse {
1889 message: "ok".to_string(),
1890 results,
1891 }))
1892}
1893
1894#[utoipa::path(get, path = "/v1/cortical_area/visualization", tag = "cortical_area")]
1896pub async fn get_visualization(
1897 State(_state): State<ApiState>,
1898) -> ApiResult<Json<HashMap<String, bool>>> {
1899 let mut response = HashMap::new();
1900 response.insert("enabled".to_string(), true);
1901 Ok(Json(response))
1902}
1903
1904#[utoipa::path(
1906 post,
1907 path = "/v1/cortical_area/batch_operations",
1908 tag = "cortical_area"
1909)]
1910pub async fn post_batch_operations(
1911 State(_state): State<ApiState>,
1912 Json(_ops): Json<Vec<HashMap<String, serde_json::Value>>>,
1913) -> ApiResult<Json<HashMap<String, i32>>> {
1914 let mut response = HashMap::new();
1915 response.insert("processed".to_string(), 0);
1916 Ok(Json(response))
1917}
1918
1919#[utoipa::path(get, path = "/v1/cortical_area/ipu/list", tag = "cortical_area")]
1921pub async fn get_ipu_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1922 get_ipu(State(state)).await
1923}
1924
1925#[utoipa::path(get, path = "/v1/cortical_area/opu/list", tag = "cortical_area")]
1927pub async fn get_opu_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1928 get_opu(State(state)).await
1929}
1930
1931#[utoipa::path(put, path = "/v1/cortical_area/coordinates_3d", tag = "cortical_area")]
1933pub async fn put_coordinates_3d(
1934 State(_state): State<ApiState>,
1935 Json(_req): Json<HashMap<String, serde_json::Value>>,
1936) -> ApiResult<Json<HashMap<String, String>>> {
1937 Ok(Json(HashMap::from([(
1938 "message".to_string(),
1939 "Not yet implemented".to_string(),
1940 )])))
1941}
1942
1943#[utoipa::path(delete, path = "/v1/cortical_area/bulk_delete", tag = "cortical_area")]
1945pub async fn delete_bulk(
1946 State(_state): State<ApiState>,
1947 Json(_ids): Json<Vec<String>>,
1948) -> ApiResult<Json<HashMap<String, i32>>> {
1949 let mut response = HashMap::new();
1950 response.insert("deleted_count".to_string(), 0);
1951 Ok(Json(response))
1952}
1953
1954#[utoipa::path(post, path = "/v1/cortical_area/resize", tag = "cortical_area")]
1956pub async fn post_resize(
1957 State(_state): State<ApiState>,
1958 Json(_req): Json<HashMap<String, serde_json::Value>>,
1959) -> ApiResult<Json<HashMap<String, String>>> {
1960 Ok(Json(HashMap::from([(
1961 "message".to_string(),
1962 "Not yet implemented".to_string(),
1963 )])))
1964}
1965
1966#[utoipa::path(post, path = "/v1/cortical_area/reposition", tag = "cortical_area")]
1968pub async fn post_reposition(
1969 State(_state): State<ApiState>,
1970 Json(_req): Json<HashMap<String, serde_json::Value>>,
1971) -> ApiResult<Json<HashMap<String, String>>> {
1972 Ok(Json(HashMap::from([(
1973 "message".to_string(),
1974 "Not yet implemented".to_string(),
1975 )])))
1976}
1977
1978#[utoipa::path(
1980 get,
1981 path = "/v1/cortical_area/voxel_neurons",
1982 tag = "cortical_area",
1983 params(VoxelNeuronsQuery),
1984 responses(
1985 (status = 200, description = "Neurons in voxel", body = VoxelNeuronsResponse),
1986 (status = 404, description = "Cortical area or neuron data not found"),
1987 (status = 500, description = "Internal server error")
1988 )
1989)]
1990pub async fn get_voxel_neurons(
1991 State(state): State<ApiState>,
1992 Query(params): Query<VoxelNeuronsQuery>,
1993) -> ApiResult<Json<VoxelNeuronsResponse>> {
1994 resolve_voxel_neurons(
1995 &state,
1996 params.cortical_id,
1997 params.x,
1998 params.y,
1999 params.z,
2000 params.synapse_page,
2001 )
2002 .await
2003 .map(Json)
2004}
2005
2006#[utoipa::path(
2008 post,
2009 path = "/v1/cortical_area/voxel_neurons",
2010 tag = "cortical_area",
2011 request_body = VoxelNeuronsBody,
2012 responses(
2013 (status = 200, description = "Neurons in voxel", body = VoxelNeuronsResponse),
2014 (status = 404, description = "Cortical area or neuron data not found"),
2015 (status = 500, description = "Internal server error")
2016 )
2017)]
2018pub async fn post_voxel_neurons(
2019 State(state): State<ApiState>,
2020 Json(body): Json<VoxelNeuronsBody>,
2021) -> ApiResult<Json<VoxelNeuronsResponse>> {
2022 resolve_voxel_neurons(
2023 &state,
2024 body.cortical_id,
2025 body.x,
2026 body.y,
2027 body.z,
2028 body.synapse_page,
2029 )
2030 .await
2031 .map(Json)
2032}
2033
2034#[utoipa::path(
2036 get,
2037 path = "/v1/cortical_area/memory",
2038 tag = "cortical_area",
2039 params(MemoryCorticalAreaQuery),
2040 responses(
2041 (status = 200, description = "Memory cortical area details", body = MemoryCorticalAreaResponse),
2042 (status = 400, description = "Invalid cortical id or not a memory area"),
2043 (status = 500, description = "Internal server error")
2044 )
2045)]
2046pub async fn get_memory_cortical_area(
2047 State(state): State<ApiState>,
2048 Query(params): Query<MemoryCorticalAreaQuery>,
2049) -> ApiResult<Json<MemoryCorticalAreaResponse>> {
2050 let connectome_service = state.connectome_service.as_ref();
2051 let area = connectome_service
2052 .get_cortical_area(¶ms.cortical_id)
2053 .await
2054 .map_err(ApiError::from)?;
2055
2056 let mem_props = extract_memory_properties(&area.properties).ok_or_else(|| {
2057 ApiError::invalid_input(
2058 "cortical area is not a memory area (expected is_mem_type memory properties)",
2059 )
2060 })?;
2061
2062 let cortical_idx = area.cortical_idx;
2063 let cortical_name = area.name.clone();
2064
2065 let cid = CorticalID::try_from_base_64(¶ms.cortical_id)
2066 .map_err(|e| ApiError::invalid_input(format!("Invalid cortical_id: {}", e)))?;
2067
2068 let page_size_u32 = params
2069 .page_size
2070 .clamp(1, MEMORY_CORTICAL_NEURON_IDS_PAGE_SIZE_MAX);
2071 let page_size = page_size_u32 as usize;
2072 let offset = (params.page as usize).saturating_mul(page_size);
2073
2074 let manager = feagi_brain_development::ConnectomeManager::instance();
2075 let mgr = manager.read();
2076
2077 let upstream_cortical_area_indices = mgr.get_upstream_cortical_areas(&cid);
2078 let upstream_cortical_area_count = upstream_cortical_area_indices.len();
2079
2080 let exec = mgr
2081 .get_plasticity_executor()
2082 .ok_or_else(|| ApiError::internal("Plasticity executor not available"))?;
2083 let ex = exec
2084 .lock()
2085 .map_err(|_| ApiError::internal("Plasticity executor lock poisoned"))?;
2086
2087 let runtime = ex
2088 .memory_cortical_area_runtime_info(cortical_idx)
2089 .ok_or_else(|| ApiError::internal("Plasticity service not initialized"))?;
2090
2091 let (memory_neuron_ids_u32, total_memory_neuron_ids) = ex
2092 .paginated_memory_neuron_ids_in_area(cortical_idx, offset, page_size)
2093 .unwrap_or((Vec::new(), 0));
2094
2095 let has_more = offset.saturating_add(memory_neuron_ids_u32.len()) < total_memory_neuron_ids;
2096
2097 let memory_neuron_ids: Vec<u64> = memory_neuron_ids_u32
2098 .into_iter()
2099 .map(|id| id as u64)
2100 .collect();
2101
2102 Ok(Json(MemoryCorticalAreaResponse {
2103 cortical_id: params.cortical_id,
2104 cortical_idx,
2105 cortical_name,
2106 short_term_neuron_count: runtime.short_term_neuron_count,
2107 long_term_neuron_count: runtime.long_term_neuron_count,
2108 memory_parameters: MemoryCorticalAreaParamsResponse {
2109 temporal_depth: mem_props.temporal_depth,
2110 longterm_mem_threshold: mem_props.longterm_threshold,
2111 lifespan_growth_rate: mem_props.lifespan_growth_rate,
2112 init_lifespan: mem_props.init_lifespan,
2113 },
2114 upstream_cortical_area_indices,
2115 upstream_cortical_area_count,
2116 upstream_pattern_cache_size: runtime.upstream_pattern_cache_size,
2117 incoming_synapse_count: area.incoming_synapse_count,
2118 outgoing_synapse_count: area.outgoing_synapse_count,
2119 total_memory_neuron_ids,
2120 page: params.page,
2121 page_size: page_size_u32,
2122 memory_neuron_ids,
2123 has_more,
2124 }))
2125}
2126
2127#[utoipa::path(
2129 get,
2130 path = "/v1/cortical_area/ipu/types",
2131 tag = "cortical_area",
2132 responses(
2133 (status = 200, description = "IPU type metadata", body = HashMap<String, CorticalTypeMetadata>),
2134 (status = 500, description = "Internal server error")
2135 )
2136)]
2137pub async fn get_ipu_types(
2138 State(_state): State<ApiState>,
2139) -> ApiResult<Json<HashMap<String, CorticalTypeMetadata>>> {
2140 let mut types = HashMap::new();
2141
2142 for unit in SensoryCorticalUnit::list_all() {
2144 let id_ref = unit.get_cortical_id_unit_reference();
2145 let key = format!("i{}", std::str::from_utf8(&id_ref).unwrap_or("???"));
2146
2147 let encodings = vec!["absolute".to_string(), "incremental".to_string()];
2149
2150 let snake_name = unit.get_snake_case_name();
2155 let formats = if snake_name == "vision"
2156 || snake_name == "segmented_vision"
2157 || snake_name == "miscellaneous"
2158 {
2159 vec![]
2160 } else {
2161 vec!["linear".to_string(), "fractional".to_string()]
2162 };
2163
2164 let resolution = if snake_name == "vision" {
2166 vec![64, 64, 1] } else if snake_name == "segmented_vision" {
2168 vec![32, 32, 1] } else {
2170 vec![1, 1, 1] };
2172
2173 let structure = "asymmetric".to_string();
2175
2176 let topology_map = unit.get_unit_default_topology();
2178 let unit_default_topology: HashMap<usize, UnitTopologyData> = topology_map
2179 .into_iter()
2180 .map(|(idx, topo)| {
2181 (
2182 *idx as usize,
2183 UnitTopologyData {
2184 relative_position: topo.relative_position,
2185 dimensions: topo.channel_dimensions_default,
2186 },
2187 )
2188 })
2189 .collect();
2190
2191 types.insert(
2192 key,
2193 CorticalTypeMetadata {
2194 description: unit.get_friendly_name().to_string(),
2195 encodings,
2196 formats,
2197 units: unit.get_number_cortical_areas() as u32,
2198 resolution,
2199 structure,
2200 unit_default_topology,
2201 },
2202 );
2203 }
2204
2205 Ok(Json(types))
2206}
2207
2208#[utoipa::path(
2210 get,
2211 path = "/v1/cortical_area/opu/types",
2212 tag = "cortical_area",
2213 responses(
2214 (status = 200, description = "OPU type metadata", body = HashMap<String, CorticalTypeMetadata>),
2215 (status = 500, description = "Internal server error")
2216 )
2217)]
2218pub async fn get_opu_types(
2219 State(_state): State<ApiState>,
2220) -> ApiResult<Json<HashMap<String, CorticalTypeMetadata>>> {
2221 let mut types = HashMap::new();
2222
2223 for unit in MotorCorticalUnit::list_all() {
2225 let id_ref = unit.get_cortical_id_unit_reference();
2226 let key = format!("o{}", std::str::from_utf8(&id_ref).unwrap_or("???"));
2227
2228 let encodings = vec!["absolute".to_string(), "incremental".to_string()];
2230
2231 let snake_name = unit.get_snake_case_name();
2235 let formats = if snake_name == "miscellaneous" {
2236 vec![]
2237 } else {
2238 vec!["linear".to_string(), "fractional".to_string()]
2239 };
2240
2241 let resolution = vec![1, 1, 1];
2243
2244 let structure = "asymmetric".to_string();
2246
2247 let topology_map = unit.get_unit_default_topology();
2249 let unit_default_topology: HashMap<usize, UnitTopologyData> = topology_map
2250 .into_iter()
2251 .map(|(idx, topo)| {
2252 (
2253 *idx as usize,
2254 UnitTopologyData {
2255 relative_position: topo.relative_position,
2256 dimensions: topo.channel_dimensions_default,
2257 },
2258 )
2259 })
2260 .collect();
2261
2262 types.insert(
2263 key,
2264 CorticalTypeMetadata {
2265 description: unit.get_friendly_name().to_string(),
2266 encodings,
2267 formats,
2268 units: unit.get_number_cortical_areas() as u32,
2269 resolution,
2270 structure,
2271 unit_default_topology,
2272 },
2273 );
2274 }
2275
2276 Ok(Json(types))
2277}
2278
2279#[utoipa::path(
2281 get,
2282 path = "/v1/cortical_area/cortical_area_index_list",
2283 tag = "cortical_area"
2284)]
2285pub async fn get_cortical_area_index_list(
2286 State(state): State<ApiState>,
2287) -> ApiResult<Json<Vec<u32>>> {
2288 let connectome_service = state.connectome_service.as_ref();
2289 let areas = connectome_service
2290 .list_cortical_areas()
2291 .await
2292 .map_err(|e| ApiError::internal(format!("{}", e)))?;
2293 let indices: Vec<u32> = areas.iter().map(|a| a.cortical_idx).collect();
2295 Ok(Json(indices))
2296}
2297
2298#[utoipa::path(
2300 get,
2301 path = "/v1/cortical_area/cortical_idx_mapping",
2302 tag = "cortical_area"
2303)]
2304pub async fn get_cortical_idx_mapping(
2305 State(state): State<ApiState>,
2306) -> ApiResult<Json<std::collections::BTreeMap<String, u32>>> {
2307 use std::collections::BTreeMap;
2308
2309 let connectome_service = state.connectome_service.as_ref();
2310 let areas = connectome_service
2311 .list_cortical_areas()
2312 .await
2313 .map_err(|e| ApiError::internal(format!("{}", e)))?;
2314 let mapping: BTreeMap<String, u32> = areas
2317 .iter()
2318 .map(|a| (a.cortical_id.clone(), a.cortical_idx))
2319 .collect();
2320 Ok(Json(mapping))
2321}
2322
2323#[utoipa::path(
2325 get,
2326 path = "/v1/cortical_area/mapping_restrictions",
2327 tag = "cortical_area"
2328)]
2329pub async fn get_mapping_restrictions_query(
2330 State(_state): State<ApiState>,
2331 Query(_params): Query<HashMap<String, String>>,
2332) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
2333 Ok(Json(HashMap::new()))
2334}
2335
2336#[utoipa::path(
2338 get,
2339 path = "/v1/cortical_area/{cortical_id}/memory_usage",
2340 tag = "cortical_area"
2341)]
2342pub async fn get_memory_usage(
2343 State(state): State<ApiState>,
2344 Path(cortical_id): Path<String>,
2345) -> ApiResult<Json<HashMap<String, i64>>> {
2346 let connectome_service = state.connectome_service.as_ref();
2347
2348 let area_info = connectome_service
2350 .get_cortical_area(&cortical_id)
2351 .await
2352 .map_err(|_| ApiError::not_found("CorticalArea", &cortical_id))?;
2353
2354 const BYTES_PER_NEURON: i64 = 48;
2357 let memory_bytes = (area_info.neuron_count as i64) * BYTES_PER_NEURON;
2358
2359 let mut response = HashMap::new();
2360 response.insert("memory_bytes".to_string(), memory_bytes);
2361 Ok(Json(response))
2362}
2363
2364#[utoipa::path(
2366 get,
2367 path = "/v1/cortical_area/{cortical_id}/neuron_count",
2368 tag = "cortical_area"
2369)]
2370pub async fn get_area_neuron_count(
2371 State(state): State<ApiState>,
2372 Path(cortical_id): Path<String>,
2373) -> ApiResult<Json<i64>> {
2374 let connectome_service = state.connectome_service.as_ref();
2375
2376 let area_info = connectome_service
2378 .get_cortical_area(&cortical_id)
2379 .await
2380 .map_err(|_| ApiError::not_found("CorticalArea", &cortical_id))?;
2381
2382 Ok(Json(area_info.neuron_count as i64))
2383}
2384
2385#[utoipa::path(
2387 post,
2388 path = "/v1/cortical_area/cortical_type_options",
2389 tag = "cortical_area"
2390)]
2391pub async fn post_cortical_type_options(
2392 State(_state): State<ApiState>,
2393) -> ApiResult<Json<Vec<String>>> {
2394 Ok(Json(vec![
2395 "Sensory".to_string(),
2396 "Motor".to_string(),
2397 "Custom".to_string(),
2398 "Memory".to_string(),
2399 ]))
2400}
2401
2402#[utoipa::path(
2404 post,
2405 path = "/v1/cortical_area/mapping_restrictions",
2406 tag = "cortical_area"
2407)]
2408pub async fn post_mapping_restrictions(
2409 State(_state): State<ApiState>,
2410 Json(_req): Json<HashMap<String, String>>,
2411) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
2412 Ok(Json(HashMap::new()))
2413}
2414
2415#[utoipa::path(
2417 post,
2418 path = "/v1/cortical_area/mapping_restrictions_between_areas",
2419 tag = "cortical_area"
2420)]
2421pub async fn post_mapping_restrictions_between_areas(
2422 State(_state): State<ApiState>,
2423 Json(_req): Json<HashMap<String, String>>,
2424) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
2425 Ok(Json(HashMap::new()))
2426}
2427
2428#[utoipa::path(put, path = "/v1/cortical_area/coord_3d", tag = "cortical_area")]
2430pub async fn put_coord_3d(
2431 State(_state): State<ApiState>,
2432 Json(_req): Json<HashMap<String, serde_json::Value>>,
2433) -> ApiResult<Json<HashMap<String, String>>> {
2434 Ok(Json(HashMap::from([(
2435 "message".to_string(),
2436 "Not yet implemented".to_string(),
2437 )])))
2438}
2439
2440#[cfg(test)]
2441mod voxel_neurons_dto_tests {
2442 use super::{
2443 synapse_details_for_neuron, synapse_page_window, VoxelNeuronsBody, VoxelNeuronsResponse,
2444 };
2445
2446 #[test]
2447 fn synapse_page_window_paginates_fifty_per_direction() {
2448 let (s, e, more) = synapse_page_window(120, 0);
2449 assert_eq!((s, e, more), (0, 50, true));
2450 let (s, e, more) = synapse_page_window(120, 1);
2451 assert_eq!((s, e, more), (50, 100, true));
2452 let (s, e, more) = synapse_page_window(120, 2);
2453 assert_eq!((s, e, more), (100, 120, false));
2454 let (s, e, more) = synapse_page_window(120, 3);
2455 assert_eq!((s, e, more), (0, 0, false));
2456 }
2457
2458 #[test]
2459 fn synapse_details_matches_connectome_shape() {
2460 let mgr = feagi_brain_development::ConnectomeManager::new_for_testing();
2461 let out_full = vec![(10, 2.0, 5.0, 1)];
2462 let inc_full = vec![(3, 4.0, 6.0, 0)];
2463 let (out, inc) = synapse_details_for_neuron(&mgr, 7, &out_full, &inc_full);
2464 let out_a = out.as_array().expect("outgoing array");
2465 assert_eq!(out_a[0]["source_neuron_id"], serde_json::json!(7));
2466 assert_eq!(out_a[0]["target_neuron_id"], serde_json::json!(10));
2467 assert_eq!(out_a[0]["weight"], serde_json::json!(2.0));
2468 assert_eq!(out_a[0]["postsynaptic_potential"], serde_json::json!(5.0));
2469 assert_eq!(out_a[0]["synapse_type"], serde_json::json!(1));
2470 assert!(out_a[0].get("target_cortical_id").is_some());
2471 assert!(out_a[0].get("target_cortical_name").is_some());
2472 assert!(out_a[0].get("target_x").is_some());
2473 let in_a = inc.as_array().expect("incoming array");
2474 assert_eq!(in_a[0]["source_neuron_id"], serde_json::json!(3));
2475 assert_eq!(in_a[0]["target_neuron_id"], serde_json::json!(7));
2476 assert!(in_a[0].get("source_cortical_id").is_some());
2477 assert!(in_a[0].get("source_cortical_name").is_some());
2478 assert!(in_a[0].get("source_x").is_some());
2479 }
2480
2481 #[test]
2482 fn voxel_neurons_body_deserializes_from_json() {
2483 let j = r#"{"cortical_id":"X19fcG93ZXI=","x":0,"y":0,"z":0}"#;
2484 let b: VoxelNeuronsBody = serde_json::from_str(j).expect("deserialize body");
2485 assert_eq!(b.cortical_id, "X19fcG93ZXI=");
2486 assert_eq!((b.x, b.y, b.z), (0, 0, 0));
2487 assert_eq!(b.synapse_page, 0);
2488 }
2489
2490 #[test]
2491 fn voxel_neurons_response_serializes() {
2492 let r = VoxelNeuronsResponse {
2493 cortical_id: "id".to_string(),
2494 cortical_name: "test_area".to_string(),
2495 cortical_idx: 2,
2496 voxel_coordinate: [1, 2, 3],
2497 x: 1,
2498 y: 2,
2499 z: 3,
2500 synapse_page: 0,
2501 neuron_count: 0,
2502 neurons: vec![],
2503 };
2504 let v = serde_json::to_value(&r).expect("serialize");
2505 assert_eq!(v["cortical_name"], serde_json::json!("test_area"));
2506 assert_eq!(v["voxel_coordinate"], serde_json::json!([1, 2, 3]));
2507 assert_eq!(v["neuron_count"], serde_json::json!(0));
2508 assert_eq!(v["synapse_page"], serde_json::json!(0));
2509 assert_eq!(v["neurons"], serde_json::json!([]));
2510 }
2511}