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    let (src_shape, dst_shape, location) = if let Some((tx, ty, tz)) = transpose {
87        apply_transpose(src_dims, dst_dims, neuron_location, (tx, ty, tz))
88    } else {
89        (
90            [
91                src_dims.width as usize,
92                src_dims.height as usize,
93                src_dims.depth as usize,
94            ],
95            [
96                dst_dims.width as usize,
97                dst_dims.height as usize,
98                dst_dims.depth as usize,
99            ],
100            [
101                neuron_location.0 as usize,
102                neuron_location.1 as usize,
103                neuron_location.2 as usize,
104            ],
105        )
106    };
107
108    // Calculate destination voxel coordinates for each axis
109    let mut dst_voxels: [Vec<u32>; 3] = [Vec::new(), Vec::new(), Vec::new()];
110
111    for axis in 0..3 {
112        dst_voxels[axis] = calculate_axis_projection(
113            location[axis] as u32,
114            src_shape[axis],
115            dst_shape[axis],
116            project_last_layer_of == Some(axis),
117        )?;
118    }
119
120    // Early exit if any axis has no valid projections
121    if dst_voxels[0].is_empty() || dst_voxels[1].is_empty() || dst_voxels[2].is_empty() {
122        return Ok(Vec::new());
123    }
124
125    // Generate all combinations (Cartesian product)
126    // PERFORMANCE: Pre-allocate exact size
127    let total_combinations = dst_voxels[0].len() * dst_voxels[1].len() * dst_voxels[2].len();
128    let mut candidate_positions = Vec::with_capacity(total_combinations);
129
130    // PERFORMANCE: Vectorized coordinate generation
131    for &x in &dst_voxels[0] {
132        for &y in &dst_voxels[1] {
133            for &z in &dst_voxels[2] {
134                // Bounds check (should always pass if calculate_axis_projection is correct)
135                if x < dst_dims.width && y < dst_dims.height && z < dst_dims.depth {
136                    candidate_positions.push((x, y, z));
137                }
138            }
139        }
140    }
141
142    Ok(candidate_positions)
143}
144
145/// Calculate projection for a single axis.
146///
147/// Handles three cases:
148/// 1. Source > Dest: Scale down (many-to-one)
149/// 2. Source < Dest: Scale up (one-to-many)
150/// 3. Source == Dest: Direct mapping (one-to-one)
151fn calculate_axis_projection(
152    location: u32,
153    src_size: usize,
154    dst_size: usize,
155    force_first_layer: bool,
156) -> BduResult<Vec<u32>> {
157    let mut voxels = Vec::new();
158
159    if force_first_layer {
160        // Special case: project to first layer only
161        voxels.push(0);
162        return Ok(voxels);
163    }
164
165    if src_size > dst_size {
166        // Source is larger: scale down (many-to-one)
167        let ratio = src_size as f32 / dst_size as f32;
168        let target = (location as f32 / ratio) as u32;
169        if (target as usize) < dst_size {
170            voxels.push(target);
171        }
172    } else if src_size < dst_size {
173        // Source is smaller: scale up (one-to-many)
174        // Find all destination voxels that map to this source voxel
175        let ratio = dst_size as f32 / src_size as f32;
176
177        for dst_vox in 0..dst_size {
178            let src_vox = (dst_vox as f32 / ratio) as u32;
179            if src_vox == location {
180                voxels.push(dst_vox as u32);
181            }
182        }
183    } else {
184        // Source and destination are same size: direct mapping
185        if (location as usize) < dst_size {
186            voxels.push(location);
187        }
188    }
189
190    Ok(voxels)
191}
192
193/// Apply axis transposition to dimensions and position.
194fn apply_transpose(
195    src_dims: Dimensions,
196    dst_dims: Dimensions,
197    location: Position,
198    transpose: (usize, usize, usize),
199) -> ([usize; 3], [usize; 3], [usize; 3]) {
200    let src_arr = [
201        src_dims.width as usize,
202        src_dims.height as usize,
203        src_dims.depth as usize,
204    ];
205    let dst_arr = [
206        dst_dims.width as usize,
207        dst_dims.height as usize,
208        dst_dims.depth as usize,
209    ];
210    let loc_arr = [location.0, location.1, location.2];
211
212    let src_transposed = [
213        src_arr[transpose.0],
214        src_arr[transpose.1],
215        src_arr[transpose.2],
216    ];
217    let dst_transposed = [
218        dst_arr[transpose.0],
219        dst_arr[transpose.1],
220        dst_arr[transpose.2],
221    ];
222    let loc_transposed = [
223        loc_arr[transpose.0] as usize,
224        loc_arr[transpose.1] as usize,
225        loc_arr[transpose.2] as usize,
226    ];
227
228    (src_transposed, dst_transposed, loc_transposed)
229}
230
231/// Batch projection for multiple neurons (parallel processing).
232///
233/// PERFORMANCE: Uses rayon for parallel processing of large neuron batches.
234#[allow(clippy::too_many_arguments)]
235pub fn syn_projector_batch(
236    src_area_id: &str,
237    dst_area_id: &str,
238    neuron_ids: &[u64],
239    neuron_locations: &[Position],
240    src_dimensions: (usize, usize, usize),
241    dst_dimensions: (usize, usize, usize),
242    transpose: Option<(usize, usize, usize)>,
243    project_last_layer_of: Option<usize>,
244) -> BduResult<Vec<Vec<Position>>> {
245    // Validate inputs
246    if neuron_ids.len() != neuron_locations.len() {
247        return Err(BduError::Internal(format!(
248            "Neuron ID count {} doesn't match location count {}",
249            neuron_ids.len(),
250            neuron_locations.len()
251        )));
252    }
253
254    // Parallel processing for large batches (sequential fallback for WASM)
255    #[cfg(feature = "parallel")]
256    let results: Vec<BduResult<Vec<Position>>> = neuron_ids
257        .par_iter()
258        .zip(neuron_locations.par_iter())
259        .map(|(id, loc)| {
260            syn_projector(
261                src_area_id,
262                dst_area_id,
263                *id,
264                src_dimensions,
265                dst_dimensions,
266                *loc,
267                transpose,
268                project_last_layer_of,
269            )
270        })
271        .collect();
272
273    #[cfg(not(feature = "parallel"))]
274    let results: Vec<BduResult<Vec<Position>>> = neuron_ids
275        .iter()
276        .zip(neuron_locations.iter())
277        .map(|(id, loc)| {
278            syn_projector(
279                src_area_id,
280                dst_area_id,
281                *id,
282                src_dimensions,
283                dst_dimensions,
284                *loc,
285                transpose,
286                project_last_layer_of,
287            )
288        })
289        .collect();
290
291    // Collect results, failing if any projection failed
292    results.into_iter().collect()
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn test_projection_128x128x3_to_128x128x1() {
301        // This is the actual performance test case: 49,152 source neurons
302        let result = syn_projector(
303            "src",
304            "dst",
305            0,
306            (128, 128, 3),
307            (128, 128, 1),
308            (64, 64, 1),
309            None,
310            None,
311        );
312
313        assert!(result.is_ok());
314        let positions = result.unwrap();
315
316        // Should project to multiple z-layers in destination
317        assert!(!positions.is_empty());
318
319        // All positions should be within bounds
320        for pos in &positions {
321            assert!(pos.0 < 128);
322            assert!(pos.1 < 128);
323            assert!(pos.2 < 1);
324        }
325    }
326
327    #[test]
328    fn test_scale_down() {
329        // 256x256 → 128x128 should map 2 source voxels to 1 dest voxel
330        let result = calculate_axis_projection(64, 256, 128, false);
331        assert!(result.is_ok());
332        let voxels = result.unwrap();
333        assert_eq!(voxels.len(), 1);
334        assert_eq!(voxels[0], 32); // 64 / 2 = 32
335    }
336
337    #[test]
338    fn test_scale_up() {
339        // 128x128 → 256x256 should map 1 source voxel to 2 dest voxels
340        let result = calculate_axis_projection(64, 128, 256, false);
341        assert!(result.is_ok());
342        let voxels = result.unwrap();
343        assert_eq!(voxels.len(), 2); // One-to-many mapping
344    }
345
346    #[test]
347    fn test_same_size() {
348        // 128x128 → 128x128 should be direct mapping
349        let result = calculate_axis_projection(64, 128, 128, false);
350        assert!(result.is_ok());
351        let voxels = result.unwrap();
352        assert_eq!(voxels.len(), 1);
353        assert_eq!(voxels[0], 64);
354    }
355
356    #[test]
357    fn test_force_first_layer() {
358        // Should always return 0 when forcing first layer
359        let result = calculate_axis_projection(99, 128, 20, true);
360        assert!(result.is_ok());
361        let voxels = result.unwrap();
362        assert_eq!(voxels.len(), 1);
363        assert_eq!(voxels[0], 0);
364    }
365
366    #[test]
367    fn test_out_of_bounds() {
368        let result = syn_projector(
369            "src",
370            "dst",
371            0,
372            (128, 128, 3),
373            (128, 128, 1),
374            (200, 0, 0), // Out of bounds
375            None,
376            None,
377        );
378        assert!(result.is_err());
379    }
380}