1use 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#[derive(Debug, Clone, Default)]
21pub struct ProjectorParams {
22 pub transpose: Option<(usize, usize, usize)>,
24 pub project_last_layer_of: Option<usize>,
26}
27
28#[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 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 if !src_dims.contains(neuron_location) {
79 return Err(BduError::OutOfBounds {
80 pos: neuron_location,
81 dims: src_dimensions,
82 });
83 }
84
85 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 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 if dst_voxels[0].is_empty() || dst_voxels[1].is_empty() || dst_voxels[2].is_empty() {
124 return Ok(Vec::new());
125 }
126
127 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 for &x in &dst_voxels[0] {
134 for &y in &dst_voxels[1] {
135 for &z in &dst_voxels[2] {
136 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
147fn 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 voxels.push(0);
164 return Ok(voxels);
165 }
166
167 if src_size > dst_size {
168 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 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 if (location as usize) < dst_size {
188 voxels.push(location);
189 }
190 }
191
192 Ok(voxels)
193}
194
195fn 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#[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 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 #[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 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 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 assert!(!positions.is_empty());
309
310 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 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); }
327
328 #[test]
329 fn test_scale_up() {
330 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); }
336
337 #[test]
338 fn test_same_size() {
339 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 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), 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 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)), None,
385 )
386 .expect("transpose_xz projection should succeed");
387
388 assert_eq!(result, vec![(0, 0, 7)]);
389 }
390}