use crate::types::{BduError, BduResult, Position};
use feagi_structures::genomic::cortical_area::descriptors::CorticalAreaDimensions as Dimensions;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
#[derive(Debug, Clone, Default)]
pub struct ProjectorParams {
pub transpose: Option<(usize, usize, usize)>,
pub project_last_layer_of: Option<usize>,
}
#[allow(clippy::too_many_arguments)]
pub fn syn_projector(
_src_area_id: &str,
_dst_area_id: &str,
_src_neuron_id: u64,
src_dimensions: (usize, usize, usize),
dst_dimensions: (usize, usize, usize),
neuron_location: Position,
transpose: Option<(usize, usize, usize)>,
project_last_layer_of: Option<usize>,
) -> BduResult<Vec<Position>> {
let src_dims = Dimensions::from_tuple((
src_dimensions.0 as u32,
src_dimensions.1 as u32,
src_dimensions.2 as u32,
))?;
let dst_dims = Dimensions::from_tuple((
dst_dimensions.0 as u32,
dst_dimensions.1 as u32,
dst_dimensions.2 as u32,
))?;
if !src_dims.contains(neuron_location) {
return Err(BduError::OutOfBounds {
pos: neuron_location,
dims: src_dimensions,
});
}
let (src_shape, location) = if let Some((tx, ty, tz)) = transpose {
apply_transpose(src_dims, neuron_location, (tx, ty, tz))
} else {
(
[
src_dims.width as usize,
src_dims.height as usize,
src_dims.depth as usize,
],
[
neuron_location.0 as usize,
neuron_location.1 as usize,
neuron_location.2 as usize,
],
)
};
let dst_shape = [
dst_dims.width as usize,
dst_dims.height as usize,
dst_dims.depth as usize,
];
let mut dst_voxels: [Vec<u32>; 3] = [Vec::new(), Vec::new(), Vec::new()];
for axis in 0..3 {
dst_voxels[axis] = calculate_axis_projection(
location[axis] as u32,
src_shape[axis],
dst_shape[axis],
project_last_layer_of == Some(axis),
)?;
}
if dst_voxels[0].is_empty() || dst_voxels[1].is_empty() || dst_voxels[2].is_empty() {
return Ok(Vec::new());
}
let total_combinations = dst_voxels[0].len() * dst_voxels[1].len() * dst_voxels[2].len();
let mut candidate_positions = Vec::with_capacity(total_combinations);
for &x in &dst_voxels[0] {
for &y in &dst_voxels[1] {
for &z in &dst_voxels[2] {
if x < dst_dims.width && y < dst_dims.height && z < dst_dims.depth {
candidate_positions.push((x, y, z));
}
}
}
}
Ok(candidate_positions)
}
fn calculate_axis_projection(
location: u32,
src_size: usize,
dst_size: usize,
force_first_layer: bool,
) -> BduResult<Vec<u32>> {
let mut voxels = Vec::new();
if force_first_layer {
voxels.push(0);
return Ok(voxels);
}
if src_size > dst_size {
let ratio = src_size as f32 / dst_size as f32;
let target = (location as f32 / ratio) as u32;
if (target as usize) < dst_size {
voxels.push(target);
}
} else if src_size < dst_size {
let ratio = dst_size as f32 / src_size as f32;
for dst_vox in 0..dst_size {
let src_vox = (dst_vox as f32 / ratio) as u32;
if src_vox == location {
voxels.push(dst_vox as u32);
}
}
} else {
if (location as usize) < dst_size {
voxels.push(location);
}
}
Ok(voxels)
}
fn apply_transpose(
src_dims: Dimensions,
location: Position,
transpose: (usize, usize, usize),
) -> ([usize; 3], [usize; 3]) {
let src_arr = [
src_dims.width as usize,
src_dims.height as usize,
src_dims.depth as usize,
];
let loc_arr = [location.0, location.1, location.2];
let src_transposed = [
src_arr[transpose.0],
src_arr[transpose.1],
src_arr[transpose.2],
];
let loc_transposed = [
loc_arr[transpose.0] as usize,
loc_arr[transpose.1] as usize,
loc_arr[transpose.2] as usize,
];
(src_transposed, loc_transposed)
}
#[allow(clippy::too_many_arguments)]
pub fn syn_projector_batch(
src_area_id: &str,
dst_area_id: &str,
neuron_ids: &[u64],
neuron_locations: &[Position],
src_dimensions: (usize, usize, usize),
dst_dimensions: (usize, usize, usize),
transpose: Option<(usize, usize, usize)>,
project_last_layer_of: Option<usize>,
) -> BduResult<Vec<Vec<Position>>> {
if neuron_ids.len() != neuron_locations.len() {
return Err(BduError::Internal(format!(
"Neuron ID count {} doesn't match location count {}",
neuron_ids.len(),
neuron_locations.len()
)));
}
#[cfg(feature = "parallel")]
let results: Vec<BduResult<Vec<Position>>> = neuron_ids
.par_iter()
.zip(neuron_locations.par_iter())
.map(|(id, loc)| {
syn_projector(
src_area_id,
dst_area_id,
*id,
src_dimensions,
dst_dimensions,
*loc,
transpose,
project_last_layer_of,
)
})
.collect();
#[cfg(not(feature = "parallel"))]
let results: Vec<BduResult<Vec<Position>>> = neuron_ids
.iter()
.zip(neuron_locations.iter())
.map(|(id, loc)| {
syn_projector(
src_area_id,
dst_area_id,
*id,
src_dimensions,
dst_dimensions,
*loc,
transpose,
project_last_layer_of,
)
})
.collect();
results.into_iter().collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_projection_128x128x3_to_128x128x1() {
let result = syn_projector(
"src",
"dst",
0,
(128, 128, 3),
(128, 128, 1),
(64, 64, 1),
None,
None,
);
assert!(result.is_ok());
let positions = result.unwrap();
assert!(!positions.is_empty());
for pos in &positions {
assert!(pos.0 < 128);
assert!(pos.1 < 128);
assert!(pos.2 < 1);
}
}
#[test]
fn test_scale_down() {
let result = calculate_axis_projection(64, 256, 128, false);
assert!(result.is_ok());
let voxels = result.unwrap();
assert_eq!(voxels.len(), 1);
assert_eq!(voxels[0], 32); }
#[test]
fn test_scale_up() {
let result = calculate_axis_projection(64, 128, 256, false);
assert!(result.is_ok());
let voxels = result.unwrap();
assert_eq!(voxels.len(), 2); }
#[test]
fn test_same_size() {
let result = calculate_axis_projection(64, 128, 128, false);
assert!(result.is_ok());
let voxels = result.unwrap();
assert_eq!(voxels.len(), 1);
assert_eq!(voxels[0], 64);
}
#[test]
fn test_force_first_layer() {
let result = calculate_axis_projection(99, 128, 20, true);
assert!(result.is_ok());
let voxels = result.unwrap();
assert_eq!(voxels.len(), 1);
assert_eq!(voxels[0], 0);
}
#[test]
fn test_out_of_bounds() {
let result = syn_projector(
"src",
"dst",
0,
(128, 128, 3),
(128, 128, 1),
(200, 0, 0), None,
None,
);
assert!(result.is_err());
}
#[test]
fn test_transpose_xz_maps_source_x_to_destination_z() {
let result = syn_projector(
"src",
"dst",
0,
(10, 1, 1),
(1, 1, 10),
(7, 0, 0),
Some((2, 1, 0)), None,
)
.expect("transpose_xz projection should succeed");
assert_eq!(result, vec![(0, 0, 7)]);
}
}