Skip to main content

feagi_api/endpoints/
cortical_mapping.rs

1// Copyright 2025 Neuraville Inc.
2// Licensed under the Apache License, Version 2.0
3
4//! Cortical Mapping API Endpoints - Exact port from Python `/v1/cortical_mapping/*`
5
6// Removed - using crate::common::State instead
7use crate::common::ApiState;
8use crate::common::{ApiError, ApiResult, Json, Query, State};
9use std::collections::HashMap;
10
11/// POST /v1/cortical_mapping/afferents
12#[utoipa::path(
13    post,
14    path = "/v1/cortical_mapping/afferents",
15    tag = "cortical_mapping"
16)]
17pub async fn post_afferents(
18    State(_state): State<ApiState>,
19    Json(_req): Json<HashMap<String, String>>,
20) -> ApiResult<Json<Vec<String>>> {
21    Err(ApiError::internal("Not yet implemented"))
22}
23
24/// POST /v1/cortical_mapping/efferents
25#[utoipa::path(
26    post,
27    path = "/v1/cortical_mapping/efferents",
28    tag = "cortical_mapping"
29)]
30pub async fn post_efferents(
31    State(_state): State<ApiState>,
32    Json(_req): Json<HashMap<String, String>>,
33) -> ApiResult<Json<Vec<String>>> {
34    Err(ApiError::internal("Not yet implemented"))
35}
36
37/// POST /v1/cortical_mapping/mapping_properties
38#[utoipa::path(
39    post,
40    path = "/v1/cortical_mapping/mapping_properties",
41    tag = "cortical_mapping",
42    responses(
43        (status = 200, description = "Cortical mapping connections", body = Vec<serde_json::Value>),
44        (status = 500, description = "Internal server error")
45    )
46)]
47pub async fn post_mapping_properties(
48    State(state): State<ApiState>,
49    Json(req): Json<HashMap<String, serde_json::Value>>,
50) -> ApiResult<Json<Vec<serde_json::Value>>> {
51    use tracing::debug;
52
53    let src_area = req
54        .get("src_cortical_area")
55        .and_then(|v| v.as_str())
56        .ok_or_else(|| ApiError::invalid_input("Missing src_cortical_area"))?;
57
58    let dst_area = req
59        .get("dst_cortical_area")
60        .and_then(|v| v.as_str())
61        .ok_or_else(|| ApiError::invalid_input("Missing dst_cortical_area"))?;
62
63    debug!(target: "feagi-api", "Getting mapping properties: {} -> {}", src_area, dst_area);
64
65    let connectome_service = state.connectome_service.as_ref();
66
67    // Get source cortical area
68    let src_area_info = connectome_service
69        .get_cortical_area(src_area)
70        .await
71        .map_err(|e| {
72            ApiError::not_found("Cortical area", &format!("Source area {}: {}", src_area, e))
73        })?;
74
75    // Look for cortical_mapping_dst in properties
76    let mapping_dst = src_area_info
77        .properties
78        .get("cortical_mapping_dst")
79        .and_then(|v| v.as_object());
80
81    if mapping_dst.is_none() {
82        debug!(target: "feagi-api", "No cortical_mapping_dst found for {}", src_area);
83        return Ok(Json(vec![]));
84    }
85
86    // Get connections for this destination
87    let connections = mapping_dst
88        .unwrap()
89        .get(dst_area)
90        .and_then(|v| v.as_array());
91
92    if connections.is_none() {
93        debug!(target: "feagi-api", "No connections found from {} to {}", src_area, dst_area);
94        return Ok(Json(vec![]));
95    }
96
97    // Normalize connections to expected format
98    let mut formatted = Vec::new();
99    for conn in connections.unwrap() {
100        if let Some(arr) = conn.as_array() {
101            // Array format:
102            // [morphology_id, morphology_scalar, psc_multiplier, plasticity_flag,
103            //  plasticity_constant, ltp_multiplier, ltd_multiplier, plasticity_window]
104            if arr.len() < 8 {
105                return Err(ApiError::invalid_input(format!(
106                    "Invalid dstmap rule array (expected 8 elements including plasticity_window), got {}: {:?}",
107                    arr.len(),
108                    arr
109                )));
110            }
111            // Strict parsing (no implicit defaults).
112            let morphology_id = arr[0]
113                .as_str()
114                .ok_or_else(|| ApiError::invalid_input("morphology_id must be a string"))?;
115            let morphology_scalar = arr[1].clone();
116            let psc_multiplier = arr[2].as_i64().ok_or_else(|| {
117                ApiError::invalid_input("postSynapticCurrent_multiplier must be an integer")
118            })?;
119            let plasticity_flag = arr[3]
120                .as_bool()
121                .ok_or_else(|| ApiError::invalid_input("plasticity_flag must be a boolean"))?;
122            let plasticity_constant = arr[4]
123                .as_i64()
124                .ok_or_else(|| ApiError::invalid_input("plasticity_constant must be an integer"))?;
125            let ltp_multiplier = arr[5]
126                .as_i64()
127                .ok_or_else(|| ApiError::invalid_input("ltp_multiplier must be an integer"))?;
128            let ltd_multiplier = arr[6]
129                .as_i64()
130                .ok_or_else(|| ApiError::invalid_input("ltd_multiplier must be an integer"))?;
131            let plasticity_window = arr[7]
132                .as_i64()
133                .ok_or_else(|| ApiError::invalid_input("plasticity_window must be an integer"))?;
134
135            formatted.push(serde_json::json!({
136                "morphology_id": morphology_id,
137                "morphology_scalar": morphology_scalar,
138                "postSynapticCurrent_multiplier": psc_multiplier,
139                "plasticity_flag": plasticity_flag,
140                "plasticity_constant": plasticity_constant,
141                "ltp_multiplier": ltp_multiplier,
142                "ltd_multiplier": ltd_multiplier,
143                "plasticity_window": plasticity_window,
144            }));
145        } else if let Some(obj) = conn.as_object() {
146            // Dict format - strict schema (no implicit defaults)
147            let morphology_id = obj
148                .get("morphology_id")
149                .and_then(|v| v.as_str())
150                .ok_or_else(|| ApiError::invalid_input("morphology_id must be a string"))?;
151            let morphology_scalar = obj
152                .get("morphology_scalar")
153                .cloned()
154                .ok_or_else(|| ApiError::invalid_input("morphology_scalar missing"))?;
155            let psc_multiplier = obj
156                .get("postSynapticCurrent_multiplier")
157                .and_then(|v| v.as_i64())
158                .ok_or_else(|| {
159                    ApiError::invalid_input("postSynapticCurrent_multiplier must be an integer")
160                })?;
161            let plasticity_flag = obj
162                .get("plasticity_flag")
163                .and_then(|v| v.as_bool())
164                .ok_or_else(|| ApiError::invalid_input("plasticity_flag must be a boolean"))?;
165            let plasticity_constant = obj
166                .get("plasticity_constant")
167                .and_then(|v| v.as_i64())
168                .ok_or_else(|| ApiError::invalid_input("plasticity_constant must be an integer"))?;
169            let ltp_multiplier = obj
170                .get("ltp_multiplier")
171                .and_then(|v| v.as_i64())
172                .ok_or_else(|| ApiError::invalid_input("ltp_multiplier must be an integer"))?;
173            let ltd_multiplier = obj
174                .get("ltd_multiplier")
175                .and_then(|v| v.as_i64())
176                .ok_or_else(|| ApiError::invalid_input("ltd_multiplier must be an integer"))?;
177            let plasticity_window = obj
178                .get("plasticity_window")
179                .and_then(|v| v.as_i64())
180                .ok_or_else(|| ApiError::invalid_input("plasticity_window must be an integer"))?;
181
182            formatted.push(serde_json::json!({
183                "morphology_id": morphology_id,
184                "morphology_scalar": morphology_scalar,
185                "postSynapticCurrent_multiplier": psc_multiplier,
186                "plasticity_flag": plasticity_flag,
187                "plasticity_constant": plasticity_constant,
188                "ltp_multiplier": ltp_multiplier,
189                "ltd_multiplier": ltd_multiplier,
190                "plasticity_window": plasticity_window,
191            }));
192        }
193    }
194
195    debug!(target: "feagi-api", "Returning {} mapping connections from {} to {}", formatted.len(), src_area, dst_area);
196    Ok(Json(formatted))
197}
198
199/// PUT /v1/cortical_mapping/mapping_properties
200#[utoipa::path(
201    put,
202    path = "/v1/cortical_mapping/mapping_properties",
203    tag = "cortical_mapping",
204    responses(
205        (status = 200, description = "Cortical mapping updated successfully", body = HashMap<String, serde_json::Value>),
206        (status = 404, description = "Cortical area not found"),
207        (status = 500, description = "Internal server error")
208    )
209)]
210pub async fn put_mapping_properties(
211    State(state): State<ApiState>,
212    Json(req): Json<HashMap<String, serde_json::Value>>,
213) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
214    use tracing::{debug, info};
215
216    let src_area = req
217        .get("src_cortical_area")
218        .and_then(|v| v.as_str())
219        .ok_or_else(|| ApiError::invalid_input("Missing src_cortical_area"))?;
220
221    let dst_area = req
222        .get("dst_cortical_area")
223        .and_then(|v| v.as_str())
224        .ok_or_else(|| ApiError::invalid_input("Missing dst_cortical_area"))?;
225
226    let mapping_string = req
227        .get("mapping_string")
228        .and_then(|v| v.as_array())
229        .ok_or_else(|| ApiError::invalid_input("Missing mapping_string"))?;
230
231    info!(
232        target: "feagi-api",
233        "PUT cortical mapping: {} -> {} with {} connections",
234        src_area,
235        dst_area,
236        mapping_string.len()
237    );
238    debug!(target: "feagi-api", "Mapping data: {:?}", mapping_string);
239
240    let connectome_service = state.connectome_service.as_ref();
241
242    // Update the cortical mapping (this modifies ConnectomeManager and regenerates synapses)
243    let synapse_count = connectome_service
244        .update_cortical_mapping(
245            src_area.to_string(),
246            dst_area.to_string(),
247            mapping_string.clone(),
248        )
249        .await
250        .map_err(|e| match e {
251            feagi_services::types::ServiceError::InvalidInput(msg) => ApiError::invalid_input(msg),
252            _ => ApiError::internal(format!("Failed to update cortical mapping: {}", e)),
253        })?;
254
255    info!(target: "feagi-api", "Cortical mapping updated successfully: {} synapses created", synapse_count);
256
257    // Return success response matching Python format
258    let mut response = HashMap::new();
259    response.insert(
260        "message".to_string(),
261        serde_json::json!(format!(
262            "Cortical mapping properties updated successfully from {} to {}",
263            src_area, dst_area
264        )),
265    );
266    response.insert(
267        "synapse_count".to_string(),
268        serde_json::json!(synapse_count),
269    );
270    response.insert("src_region".to_string(), serde_json::json!(null)); // TODO: Add region context
271    response.insert("dst_region".to_string(), serde_json::json!(null)); // TODO: Add region context
272
273    Ok(Json(response))
274}
275
276/// GET /v1/cortical_mapping/mapping
277/// Get specific cortical mapping between two areas
278#[utoipa::path(
279    get,
280    path = "/v1/cortical_mapping/mapping",
281    tag = "cortical_mapping",
282    params(
283        ("src_cortical_area" = String, Query, description = "Source cortical area ID"),
284        ("dst_cortical_area" = String, Query, description = "Destination cortical area ID")
285    ),
286    responses(
287        (status = 200, description = "Mapping properties", body = HashMap<String, serde_json::Value>)
288    )
289)]
290pub async fn get_mapping(
291    State(state): State<ApiState>,
292    Query(params): Query<HashMap<String, String>>,
293) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
294    let src_area = params
295        .get("src_cortical_area")
296        .ok_or_else(|| ApiError::invalid_input("src_cortical_area required"))?;
297    let dst_area = params
298        .get("dst_cortical_area")
299        .ok_or_else(|| ApiError::invalid_input("dst_cortical_area required"))?;
300
301    // Get mapping properties directly (avoid recursion)
302    let connectome_service = state.connectome_service.as_ref();
303
304    // Get source cortical area
305    let src_area_info = connectome_service
306        .get_cortical_area(src_area)
307        .await
308        .map_err(|e| {
309            ApiError::not_found("Cortical area", &format!("Source area {}: {}", src_area, e))
310        })?;
311
312    // Look for cortical_mapping_dst in properties
313    let mapping_dst = src_area_info
314        .properties
315        .get("cortical_mapping_dst")
316        .and_then(|v| v.as_object());
317
318    if mapping_dst.is_none() {
319        return Ok(Json(HashMap::new()));
320    }
321
322    // Get connections for this destination
323    let connections = mapping_dst
324        .unwrap()
325        .get(dst_area)
326        .and_then(|v| v.as_array());
327
328    let mut response = HashMap::new();
329    response.insert(
330        "connections".to_string(),
331        serde_json::json!(connections.unwrap_or(&vec![])),
332    );
333
334    Ok(Json(response))
335}
336
337/// GET /v1/cortical_mapping/mapping_list
338/// Get list of all cortical mappings
339#[utoipa::path(
340    get,
341    path = "/v1/cortical_mapping/mapping_list",
342    tag = "cortical_mapping",
343    responses(
344        (status = 200, description = "List of all mappings", body = Vec<Vec<String>>)
345    )
346)]
347pub async fn get_mapping_list(State(state): State<ApiState>) -> ApiResult<Json<Vec<Vec<String>>>> {
348    let connectome_service = state.connectome_service.as_ref();
349
350    let areas = connectome_service
351        .list_cortical_areas()
352        .await
353        .map_err(|e| ApiError::internal(format!("Failed to list areas: {}", e)))?;
354
355    let mut mappings = Vec::new();
356
357    // Scan all cortical_mapping_dst properties
358    for area in &areas {
359        if let Ok(area_detail) = connectome_service
360            .get_cortical_area(&area.cortical_id)
361            .await
362        {
363            if let Some(mapping_dst) = area_detail.properties.get("cortical_mapping_dst") {
364                if let Some(dst_map) = mapping_dst.as_object() {
365                    for dst_area_id in dst_map.keys() {
366                        mappings.push(vec![area.cortical_id.clone(), dst_area_id.clone()]);
367                    }
368                }
369            }
370        }
371    }
372
373    Ok(Json(mappings))
374}
375
376/// DELETE /v1/cortical_mapping/mapping
377/// Delete a cortical mapping
378#[utoipa::path(
379    delete,
380    path = "/v1/cortical_mapping/mapping",
381    tag = "cortical_mapping",
382    responses(
383        (status = 200, description = "Mapping deleted", body = HashMap<String, String>)
384    )
385)]
386pub async fn delete_mapping(
387    State(_state): State<ApiState>,
388    Json(_request): Json<HashMap<String, String>>,
389) -> ApiResult<Json<HashMap<String, String>>> {
390    // TODO: Implement mapping deletion
391    Ok(Json(HashMap::from([(
392        "message".to_string(),
393        "Mapping deletion not yet implemented".to_string(),
394    )])))
395}
396
397/// POST /v1/cortical_mapping/batch_update
398/// Batch update multiple cortical mappings
399#[utoipa::path(
400    post,
401    path = "/v1/cortical_mapping/batch_update",
402    tag = "cortical_mapping",
403    responses(
404        (status = 200, description = "Batch update completed", body = HashMap<String, serde_json::Value>)
405    )
406)]
407pub async fn post_batch_update(
408    State(_state): State<ApiState>,
409    Json(_request): Json<Vec<HashMap<String, String>>>,
410) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
411    // TODO: Implement batch update
412    let mut response = HashMap::new();
413    response.insert(
414        "message".to_string(),
415        serde_json::json!("Batch update not yet implemented"),
416    );
417    response.insert("updated_count".to_string(), serde_json::json!(0));
418
419    Ok(Json(response))
420}
421
422// EXACT Python paths:
423/// POST /v1/cortical_mapping/mapping
424#[utoipa::path(post, path = "/v1/cortical_mapping/mapping", tag = "cortical_mapping")]
425pub async fn post_mapping(
426    State(_state): State<ApiState>,
427    Json(_req): Json<HashMap<String, serde_json::Value>>,
428) -> ApiResult<Json<HashMap<String, String>>> {
429    Ok(Json(HashMap::from([(
430        "message".to_string(),
431        "Not yet implemented".to_string(),
432    )])))
433}
434
435/// PUT /v1/cortical_mapping/mapping
436#[utoipa::path(put, path = "/v1/cortical_mapping/mapping", tag = "cortical_mapping")]
437pub async fn put_mapping(
438    State(_state): State<ApiState>,
439    Json(_req): Json<HashMap<String, serde_json::Value>>,
440) -> ApiResult<Json<HashMap<String, String>>> {
441    Ok(Json(HashMap::from([(
442        "message".to_string(),
443        "Not yet implemented".to_string(),
444    )])))
445}