use crate::error::{Result, VisionError};
use crate::pointnet::sampling::{euclidean_dist_sq, knn_query};
use crate::pointnet::set_abstraction::mlp_forward;
use scirs2_core::ndarray::{Array1, Array2};
#[derive(Debug, Clone)]
pub struct FPConfig {
pub mlp_channels: Vec<usize>,
pub k_neighbors: usize,
}
impl Default for FPConfig {
fn default() -> Self {
Self {
mlp_channels: vec![128, 128],
k_neighbors: 3,
}
}
}
fn xavier_init(fan_in: usize, fan_out: usize) -> Array2<f64> {
let limit = (6.0_f64 / (fan_in + fan_out) as f64).sqrt();
let mut state: u64 = 0x_feed_face_dead_beef;
let mut w = Array2::zeros((fan_in, fan_out));
for i in 0..fan_in {
for j in 0..fan_out {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
let t = (state as f64) / (u64::MAX as f64);
w[[i, j]] = (2.0 * t - 1.0) * limit;
}
}
w
}
pub struct FeaturePropagation {
config: FPConfig,
weights: Vec<Array2<f64>>,
biases: Vec<Array1<f64>>,
}
impl FeaturePropagation {
pub fn new(config: FPConfig, in_channels: usize) -> Result<Self> {
if config.mlp_channels.is_empty() {
return Err(VisionError::InvalidParameter(
"FeaturePropagation: mlp_channels must not be empty".to_string(),
));
}
if config.k_neighbors == 0 {
return Err(VisionError::InvalidParameter(
"FeaturePropagation: k_neighbors must be > 0".to_string(),
));
}
let mut dims = vec![in_channels];
dims.extend_from_slice(&config.mlp_channels);
let mut weights = Vec::with_capacity(dims.len() - 1);
let mut biases = Vec::with_capacity(dims.len() - 1);
for pair in dims.windows(2) {
weights.push(xavier_init(pair[0], pair[1]));
biases.push(Array1::zeros(pair[1]));
}
Ok(Self {
config,
weights,
biases,
})
}
pub fn out_channels(&self) -> usize {
self.config.mlp_channels.last().copied().unwrap_or(0)
}
pub fn forward(
&self,
xyz1: &Array2<f64>,
xyz2: &Array2<f64>,
features1: Option<&Array2<f64>>,
features2: &Array2<f64>,
) -> Result<Array2<f64>> {
let n1 = xyz1.nrows();
let n2 = xyz2.nrows();
let c2 = features2.ncols();
if xyz1.ncols() != 3 || xyz2.ncols() != 3 {
return Err(VisionError::InvalidParameter(
"FeaturePropagation: xyz arrays must have 3 columns".to_string(),
));
}
if features2.nrows() != n2 {
return Err(VisionError::DimensionMismatch(format!(
"FeaturePropagation: features2.nrows ({}) != xyz2.nrows ({})",
features2.nrows(),
n2
)));
}
if let Some(f1) = features1 {
if f1.nrows() != n1 {
return Err(VisionError::DimensionMismatch(format!(
"FeaturePropagation: features1.nrows ({}) != xyz1.nrows ({})",
f1.nrows(),
n1
)));
}
}
let k = self.config.k_neighbors.min(n2);
let knn = knn_query(xyz2, xyz1, k);
let interpolated = self.idw_interpolate(xyz1, xyz2, features2, &knn, k, c2)?;
let c_out_mlp = self.out_channels();
let mut out = Array2::zeros((n1, c_out_mlp));
for i in 0..n1 {
let interp_row = interpolated.row(i);
let concat: Vec<f64> = match features1 {
None => interp_row.iter().copied().collect(),
Some(f1) => {
let skip_row = f1.row(i);
interp_row
.iter()
.copied()
.chain(skip_row.iter().copied())
.collect()
}
};
let h = Array1::from_vec(concat);
let result = mlp_forward(&self.weights, &self.biases, &h);
for d in 0..c_out_mlp {
out[[i, d]] = result[d];
}
}
Ok(out)
}
fn idw_interpolate(
&self,
xyz1: &Array2<f64>,
xyz2: &Array2<f64>,
features2: &Array2<f64>,
knn: &[Vec<usize>],
k: usize,
c2: usize,
) -> Result<Array2<f64>> {
let n1 = xyz1.nrows();
let dim = xyz1.ncols().min(xyz2.ncols());
let mut interp = Array2::zeros((n1, c2));
for i in 0..n1 {
let px: Vec<f64> = (0..dim).map(|d| xyz1[[i, d]]).collect();
let neighbors = &knn[i];
let mut dists: Vec<f64> = neighbors
.iter()
.map(|&j| {
let qx: Vec<f64> = (0..dim).map(|d| xyz2[[j, d]]).collect();
euclidean_dist_sq(&px, &qx).sqrt()
})
.collect();
let exact_match: Option<usize> = dists
.iter()
.enumerate()
.find(|(_, &d)| d < 1e-15)
.map(|(idx, _)| neighbors[idx]);
if let Some(src) = exact_match {
for d in 0..c2 {
interp[[i, d]] = features2[[src, d]];
}
continue;
}
let inv_dists: Vec<f64> = dists.iter_mut().map(|d| 1.0 / *d).collect();
let weight_sum: f64 = inv_dists.iter().sum();
if weight_sum < 1e-30 {
let uniform = 1.0 / k as f64;
for &j in neighbors.iter().take(k) {
for d in 0..c2 {
interp[[i, d]] += uniform * features2[[j, d]];
}
}
} else {
for (idx, &j) in neighbors.iter().take(k).enumerate() {
let w = inv_dists[idx] / weight_sum;
for d in 0..c2 {
interp[[i, d]] += w * features2[[j, d]];
}
}
}
}
Ok(interp)
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
fn make_xyz(coords: &[[f64; 3]]) -> Array2<f64> {
let n = coords.len();
let mut a = Array2::zeros((n, 3));
for (i, c) in coords.iter().enumerate() {
a[[i, 0]] = c[0];
a[[i, 1]] = c[1];
a[[i, 2]] = c[2];
}
a
}
#[test]
fn test_fp_output_shape_matches_finer_level() {
let xyz1 = make_xyz(&[
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[3.0, 0.0, 0.0],
]);
let xyz2 = make_xyz(&[[0.5, 0.0, 0.0], [2.5, 0.0, 0.0]]);
let c2 = 8usize;
let mut feats2 = Array2::zeros((2, c2));
feats2[[0, 0]] = 1.0;
feats2[[1, 0]] = 2.0;
let cfg = FPConfig {
mlp_channels: vec![32, 64],
k_neighbors: 2,
};
let fp = FeaturePropagation::new(cfg, c2).expect("FP construction failed");
let out = fp
.forward(&xyz1, &xyz2, None, &feats2)
.expect("FP forward failed");
assert_eq!(out.nrows(), 4, "output rows should match xyz1");
assert_eq!(out.ncols(), 64);
}
#[test]
fn test_fp_interpolation_weights_sum_to_one() {
let xyz1 = make_xyz(&[[0.0, 0.0, 0.0], [1.0, 1.0, 1.0], [2.0, 0.0, 0.0]]);
let xyz2 = make_xyz(&[[0.5, 0.5, 0.5]]);
let c2 = 4usize;
let mut feats2 = Array2::zeros((1, c2));
for d in 0..c2 {
feats2[[0, d]] = (d + 1) as f64;
}
let cfg = FPConfig {
mlp_channels: vec![16],
k_neighbors: 1,
};
let fp = FeaturePropagation::new(cfg, c2).expect("FP construction failed");
let out = fp
.forward(&xyz1, &xyz2, None, &feats2)
.expect("FP forward failed");
for i in 0..3 {
for d in 0..16 {
assert_eq!(
out[[i, d]],
out[[0, d]],
"all rows should be identical with single coarse source"
);
}
}
}
#[test]
fn test_fp_single_source_copies_features() {
let xyz1 = make_xyz(&[[0.0, 0.0, 0.0]]);
let xyz2 = make_xyz(&[[0.0, 0.0, 0.0]]);
let c2 = 3usize;
let mut feats2 = Array2::zeros((1, c2));
feats2[[0, 0]] = 5.0;
feats2[[0, 1]] = 7.0;
feats2[[0, 2]] = 11.0;
let cfg = FPConfig {
mlp_channels: vec![8],
k_neighbors: 1,
};
let fp = FeaturePropagation::new(cfg, c2).expect("FP construction failed");
let out = fp
.forward(&xyz1, &xyz2, None, &feats2)
.expect("FP forward failed");
assert_eq!(out.nrows(), 1);
let any_nonzero = (0..8).any(|d| out[[0, d]] != 0.0);
assert!(
any_nonzero,
"FP output should not be all zeros when input features are non-zero"
);
}
#[test]
fn test_fp_with_skip_connection() {
let xyz1 = make_xyz(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
let xyz2 = make_xyz(&[[0.5, 0.0, 0.0]]);
let c2 = 4usize;
let c_skip = 4usize;
let mut feats2 = Array2::zeros((1, c2));
feats2[[0, 0]] = 1.0;
let mut skip = Array2::zeros((2, c_skip));
skip[[0, 1]] = 2.0;
skip[[1, 2]] = 3.0;
let cfg = FPConfig {
mlp_channels: vec![16, 32],
k_neighbors: 1,
};
let fp = FeaturePropagation::new(cfg, c2 + c_skip).expect("FP construction failed");
let out = fp
.forward(&xyz1, &xyz2, Some(&skip), &feats2)
.expect("FP forward failed");
assert_eq!(out.nrows(), 2);
assert_eq!(out.ncols(), 32);
}
}