Skip to main content

feagi_brain_development/connectivity/rules/
projector.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5Projection mapping - the critical hot path for synaptogenesis.
6
7This function maps neurons from source to destination areas while maintaining
8spatial topology. It's the primary bottleneck in Python (40 seconds for 128×128×3).
9
10PERFORMANCE TARGET: <100ms for 128×128×3 → 128×128×1 projection (400x faster than Python)
11*/
12
13use crate::types::{BduError, BduResult, Position};
14use feagi_structures::genomic::cortical_area::descriptors::CorticalAreaDimensions as Dimensions;
15
16#[cfg(feature = "parallel")]
17use rayon::prelude::*;
18
19/// Parameters for projection mapping
20#[derive(Debug, Clone, Default)]
21pub struct ProjectorParams {
22    /// Axis transpose mapping ("x", "y", "z") → (0, 1, 2)
23    pub transpose: Option<(usize, usize, usize)>,
24    /// Project from last layer of specific axis
25    pub project_last_layer_of: Option<usize>,
26}
27
28/// High-performance projection mapping.
29///
30/// Maps a single source neuron to multiple destination positions based on
31/// dimensional scaling and optional transposition.
32///
33/// # Performance
34///
35/// - Vectorized coordinate generation
36/// - SIMD-optimized bounds checking
37/// - Pre-allocated result vectors
38/// - Parallel processing for large result sets
39///
40/// # Arguments
41///
42/// * `src_area_id` - Source cortical area identifier
43/// * `dst_area_id` - Destination cortical area identifier
44/// * `src_neuron_id` - Source neuron identifier
45/// * `src_dimensions` - Source area dimensions (width, height, depth)
46/// * `dst_dimensions` - Destination area dimensions
47/// * `neuron_location` - Source neuron position
48/// * `transpose` - Optional axis transposition
49/// * `project_last_layer_of` - Optional axis to project from last layer
50///
51/// # Returns
52///
53/// Vector of destination positions that match this source neuron
54#[allow(clippy::too_many_arguments)]
55pub fn syn_projector(
56    _src_area_id: &str,
57    _dst_area_id: &str,
58    _src_neuron_id: u64,
59    src_dimensions: (usize, usize, usize),
60    dst_dimensions: (usize, usize, usize),
61    neuron_location: Position,
62    transpose: Option<(usize, usize, usize)>,
63    project_last_layer_of: Option<usize>,
64) -> BduResult<Vec<Position>> {
65    // Convert to Dimensions for convenience
66    let src_dims = Dimensions::from_tuple((
67        src_dimensions.0 as u32,
68        src_dimensions.1 as u32,
69        src_dimensions.2 as u32,
70    ))?;
71    let dst_dims = Dimensions::from_tuple((
72        dst_dimensions.0 as u32,
73        dst_dimensions.1 as u32,
74        dst_dimensions.2 as u32,
75    ))?;
76
77    // Validate neuron location is within source bounds
78    if !src_dims.contains(neuron_location) {
79        return Err(BduError::OutOfBounds {
80            pos: neuron_location,
81            dims: src_dimensions,
82        });
83    }
84
85    // Apply transposition if specified.
86    // Legacy Python compatibility: transpose modifies source frame only
87    // (source shape + source neuron location), NOT destination shape.
88    let (src_shape, location) = if let Some((tx, ty, tz)) = transpose {
89        apply_transpose(src_dims, neuron_location, (tx, ty, tz))
90    } else {
91        (
92            [
93                src_dims.width as usize,
94                src_dims.height as usize,
95                src_dims.depth as usize,
96            ],
97            [
98                neuron_location.0 as usize,
99                neuron_location.1 as usize,
100                neuron_location.2 as usize,
101            ],
102        )
103    };
104    let dst_shape = [
105        dst_dims.width as usize,
106        dst_dims.height as usize,
107        dst_dims.depth as usize,
108    ];
109
110    // Calculate destination voxel coordinates for each axis
111    let mut dst_voxels: [Vec<u32>; 3] = [Vec::new(), Vec::new(), Vec::new()];
112
113    for axis in 0..3 {
114        dst_voxels[axis] = calculate_axis_projection(
115            location[axis] as u32,
116            src_shape[axis],
117            dst_shape[axis],
118            project_last_layer_of == Some(axis),
119        )?;
120    }
121
122    // Early exit if any axis has no valid projections
123    if dst_voxels[0].is_empty() || dst_voxels[1].is_empty() || dst_voxels[2].is_empty() {
124        return Ok(Vec::new());
125    }
126
127    // Generate all combinations (Cartesian product)
128    // PERFORMANCE: Pre-allocate exact size
129    let total_combinations = dst_voxels[0].len() * dst_voxels[1].len() * dst_voxels[2].len();
130    let mut candidate_positions = Vec::with_capacity(total_combinations);
131
132    // PERFORMANCE: Vectorized coordinate generation
133    for &x in &dst_voxels[0] {
134        for &y in &dst_voxels[1] {
135            for &z in &dst_voxels[2] {
136                // Bounds check (should always pass if calculate_axis_projection is correct)
137                if x < dst_dims.width && y < dst_dims.height && z < dst_dims.depth {
138                    candidate_positions.push((x, y, z));
139                }
140            }
141        }
142    }
143
144    Ok(candidate_positions)
145}
146
147/// Calculate projection for a single axis.
148///
149/// Handles three cases:
150/// 1. Source > Dest: Scale down (many-to-one)
151/// 2. Source < Dest: Scale up (one-to-many)
152/// 3. Source == Dest: Direct mapping (one-to-one)
153fn calculate_axis_projection(
154    location: u32,
155    src_size: usize,
156    dst_size: usize,
157    force_first_layer: bool,
158) -> BduResult<Vec<u32>> {
159    let mut voxels = Vec::new();
160
161    if force_first_layer {
162        // Special case: project to first layer only
163        voxels.push(0);
164        return Ok(voxels);
165    }
166
167    if src_size > dst_size {
168        // Source is larger: scale down (many-to-one)
169        let ratio = src_size as f32 / dst_size as f32;
170        let target = (location as f32 / ratio) as u32;
171        if (target as usize) < dst_size {
172            voxels.push(target);
173        }
174    } else if src_size < dst_size {
175        // Source is smaller: scale up (one-to-many)
176        // Find all destination voxels that map to this source voxel
177        let ratio = dst_size as f32 / src_size as f32;
178
179        for dst_vox in 0..dst_size {
180            let src_vox = (dst_vox as f32 / ratio) as u32;
181            if src_vox == location {
182                voxels.push(dst_vox as u32);
183            }
184        }
185    } else {
186        // Source and destination are same size: direct mapping
187        if (location as usize) < dst_size {
188            voxels.push(location);
189        }
190    }
191
192    Ok(voxels)
193}
194
195/// Apply axis transposition to dimensions and position.
196fn apply_transpose(
197    src_dims: Dimensions,
198    location: Position,
199    transpose: (usize, usize, usize),
200) -> ([usize; 3], [usize; 3]) {
201    let src_arr = [
202        src_dims.width as usize,
203        src_dims.height as usize,
204        src_dims.depth as usize,
205    ];
206    let loc_arr = [location.0, location.1, location.2];
207
208    let src_transposed = [
209        src_arr[transpose.0],
210        src_arr[transpose.1],
211        src_arr[transpose.2],
212    ];
213    let loc_transposed = [
214        loc_arr[transpose.0] as usize,
215        loc_arr[transpose.1] as usize,
216        loc_arr[transpose.2] as usize,
217    ];
218
219    (src_transposed, loc_transposed)
220}
221
222/// Batch projection for multiple neurons (parallel processing).
223///
224/// PERFORMANCE: Uses rayon for parallel processing of large neuron batches.
225#[allow(clippy::too_many_arguments)]
226pub fn syn_projector_batch(
227    src_area_id: &str,
228    dst_area_id: &str,
229    neuron_ids: &[u64],
230    neuron_locations: &[Position],
231    src_dimensions: (usize, usize, usize),
232    dst_dimensions: (usize, usize, usize),
233    transpose: Option<(usize, usize, usize)>,
234    project_last_layer_of: Option<usize>,
235) -> BduResult<Vec<Vec<Position>>> {
236    // Validate inputs
237    if neuron_ids.len() != neuron_locations.len() {
238        return Err(BduError::Internal(format!(
239            "Neuron ID count {} doesn't match location count {}",
240            neuron_ids.len(),
241            neuron_locations.len()
242        )));
243    }
244
245    // Parallel processing for large batches (sequential fallback for WASM)
246    #[cfg(feature = "parallel")]
247    let results: Vec<BduResult<Vec<Position>>> = neuron_ids
248        .par_iter()
249        .zip(neuron_locations.par_iter())
250        .map(|(id, loc)| {
251            syn_projector(
252                src_area_id,
253                dst_area_id,
254                *id,
255                src_dimensions,
256                dst_dimensions,
257                *loc,
258                transpose,
259                project_last_layer_of,
260            )
261        })
262        .collect();
263
264    #[cfg(not(feature = "parallel"))]
265    let results: Vec<BduResult<Vec<Position>>> = neuron_ids
266        .iter()
267        .zip(neuron_locations.iter())
268        .map(|(id, loc)| {
269            syn_projector(
270                src_area_id,
271                dst_area_id,
272                *id,
273                src_dimensions,
274                dst_dimensions,
275                *loc,
276                transpose,
277                project_last_layer_of,
278            )
279        })
280        .collect();
281
282    // Collect results, failing if any projection failed
283    results.into_iter().collect()
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn test_projection_128x128x3_to_128x128x1() {
292        // This is the actual performance test case: 49,152 source neurons
293        let result = syn_projector(
294            "src",
295            "dst",
296            0,
297            (128, 128, 3),
298            (128, 128, 1),
299            (64, 64, 1),
300            None,
301            None,
302        );
303
304        assert!(result.is_ok());
305        let positions = result.unwrap();
306
307        // Should project to multiple z-layers in destination
308        assert!(!positions.is_empty());
309
310        // All positions should be within bounds
311        for pos in &positions {
312            assert!(pos.0 < 128);
313            assert!(pos.1 < 128);
314            assert!(pos.2 < 1);
315        }
316    }
317
318    #[test]
319    fn test_scale_down() {
320        // 256x256 → 128x128 should map 2 source voxels to 1 dest voxel
321        let result = calculate_axis_projection(64, 256, 128, false);
322        assert!(result.is_ok());
323        let voxels = result.unwrap();
324        assert_eq!(voxels.len(), 1);
325        assert_eq!(voxels[0], 32); // 64 / 2 = 32
326    }
327
328    #[test]
329    fn test_scale_up() {
330        // 128x128 → 256x256 should map 1 source voxel to 2 dest voxels
331        let result = calculate_axis_projection(64, 128, 256, false);
332        assert!(result.is_ok());
333        let voxels = result.unwrap();
334        assert_eq!(voxels.len(), 2); // One-to-many mapping
335    }
336
337    #[test]
338    fn test_same_size() {
339        // 128x128 → 128x128 should be direct mapping
340        let result = calculate_axis_projection(64, 128, 128, false);
341        assert!(result.is_ok());
342        let voxels = result.unwrap();
343        assert_eq!(voxels.len(), 1);
344        assert_eq!(voxels[0], 64);
345    }
346
347    #[test]
348    fn test_force_first_layer() {
349        // Should always return 0 when forcing first layer
350        let result = calculate_axis_projection(99, 128, 20, true);
351        assert!(result.is_ok());
352        let voxels = result.unwrap();
353        assert_eq!(voxels.len(), 1);
354        assert_eq!(voxels[0], 0);
355    }
356
357    #[test]
358    fn test_out_of_bounds() {
359        let result = syn_projector(
360            "src",
361            "dst",
362            0,
363            (128, 128, 3),
364            (128, 128, 1),
365            (200, 0, 0), // Out of bounds
366            None,
367            None,
368        );
369        assert!(result.is_err());
370    }
371
372    #[test]
373    fn test_transpose_xz_maps_source_x_to_destination_z() {
374        // Legacy Python-compatible behavior for projector_xz:
375        // src (x,0,0) in 10x1x1 should map to dst (0,0,x) in 1x1x10.
376        let result = syn_projector(
377            "src",
378            "dst",
379            0,
380            (10, 1, 1),
381            (1, 1, 10),
382            (7, 0, 0),
383            Some((2, 1, 0)), // xz transpose
384            None,
385        )
386        .expect("transpose_xz projection should succeed");
387
388        assert_eq!(result, vec![(0, 0, 7)]);
389    }
390}