use crate::reconstruction::{
angle_between_vectors, Cluster, Coordinate, Helix, Track, TryTrackFromClusterError,
};
use crate::SpacePoint;
use argmin::core::{CostFunction, Error, Executor};
use argmin::solver::neldermead::NelderMead;
use itertools::Itertools;
use num_complex::Complex;
use std::f64::consts::PI;
use uom::si::angle::radian;
use uom::si::area::square_meter;
use uom::si::f64::{Angle, Area, Length};
use uom::si::length::meter;
use uom::typenum::P2;
pub(crate) fn fit_cluster_to_helix(
cluster: Cluster,
max_num_solver_iter: u64,
nelder_mead_sd_tolerance: f64,
initial_simplex_delta: f64,
max_num_closest_t_iter: usize,
closest_t_tolerance: f64,
) -> Result<Track, TryTrackFromClusterError> {
let sp = cluster.0;
assert!(sp.len() >= 3);
let (first, middle, last) = three_template_points(&sp)?;
let (x0, y0, r) = circle_through_three_points(
(first.x(), first.y()),
(middle.x(), middle.y()),
(last.x(), last.y()),
);
let cm = center_of_mass(&sp);
let phi0 = (cm.y - y0).atan2(cm.x - x0);
let z0 = cm.z;
let theta = angle_between_vectors(
(last.x() - x0, last.y() - y0),
(first.x() - x0, first.y() - y0),
);
let h = if theta.get::<radian>() == 0.0 {
Length::new::<meter>(0.0)
} else {
2.0 * PI * (first.z - last.z) / theta
};
let initial_guess = vec![
x0.get::<meter>(),
y0.get::<meter>(),
z0.get::<meter>(),
r.get::<meter>(),
phi0.get::<radian>(),
h.get::<meter>(),
];
let mut initial_simplex = vec![initial_guess.clone()];
for i in 0..initial_guess.len() {
let mut new_point = initial_guess.clone();
if new_point[i] == 0.0 {
new_point[i] = 0.00025;
} else {
new_point[i] *= 1.0 + initial_simplex_delta;
}
initial_simplex.push(new_point);
}
let problem = Problem {
points: sp,
tolerance: closest_t_tolerance,
max_num_iter: max_num_closest_t_iter,
};
let solver = NelderMead::new(initial_simplex)
.with_sd_tolerance(nelder_mead_sd_tolerance)
.unwrap();
let res = Executor::new(problem, solver)
.configure(|state| state.max_iters(max_num_solver_iter))
.run()
.unwrap();
let best_params = res.state.best_param.unwrap();
let helix = Helix {
x0: Length::new::<meter>(best_params[0]),
y0: Length::new::<meter>(best_params[1]),
z0: Length::new::<meter>(best_params[2]),
r: Length::new::<meter>(best_params[3]),
phi0: Angle::new::<radian>(best_params[4]),
h: Length::new::<meter>(best_params[5]),
};
Ok(Track {
helix,
t_inner: helix.closest_t(first, closest_t_tolerance, max_num_closest_t_iter),
t_outer: helix.closest_t(last, closest_t_tolerance, max_num_closest_t_iter),
})
}
fn three_template_points(
points: &[SpacePoint],
) -> Result<(SpacePoint, SpacePoint, SpacePoint), TryTrackFromClusterError> {
let (&first, &last) = points.iter().minmax_by_key(|p| p.r).into_option().unwrap();
let middle_r = (first.r + last.r) / 2.0;
let middle = points
.iter()
.min_by(|a, b| {
(a.r - middle_r)
.abs()
.partial_cmp(&(b.r - middle_r).abs())
.unwrap()
})
.copied()
.unwrap();
if (last.x() - first.x()) * (middle.y() - first.y())
== (middle.x() - first.x()) * (last.y() - first.y())
{
return Err(TryTrackFromClusterError::NoInitialParameters);
}
Ok((first, middle, last))
}
fn circle_through_three_points(
p1: (Length, Length),
p2: (Length, Length),
p3: (Length, Length),
) -> (Length, Length, Length) {
let z1 = Complex::new(p1.0.get::<meter>(), p1.1.get::<meter>());
let z2 = Complex::new(p2.0.get::<meter>(), p2.1.get::<meter>());
let z3 = Complex::new(p3.0.get::<meter>(), p3.1.get::<meter>());
let w = (z3 - z1) / (z2 - z1);
let c_prime = (w - w.norm_sqr()) / (w - w.conj());
let c = (z2 - z1) * c_prime + z1;
let r = (z1 - c).norm();
(
Length::new::<meter>(c.re),
Length::new::<meter>(c.im),
Length::new::<meter>(r),
)
}
fn center_of_mass(points: &[SpacePoint]) -> Coordinate {
let mut x = Length::new::<meter>(0.0);
let mut y = Length::new::<meter>(0.0);
let mut z = Length::new::<meter>(0.0);
for p in points {
x += p.x();
y += p.y();
z += p.z;
}
let n = points.len() as f64;
Coordinate {
x: x / n,
y: y / n,
z: z / n,
}
}
struct Problem {
points: Vec<SpacePoint>,
tolerance: f64,
max_num_iter: usize,
}
fn norm_sqr(sp: SpacePoint, c: Coordinate) -> Area {
let x = c.x - sp.x();
let y = c.y - sp.y();
let z = c.z - sp.z;
x.powi(P2::new()) + y.powi(P2::new()) + z.powi(P2::new())
}
impl CostFunction for Problem {
type Param = Vec<f64>;
type Output = f64;
fn cost(&self, p: &Self::Param) -> Result<Self::Output, Error> {
let helix = Helix {
x0: Length::new::<meter>(p[0]),
y0: Length::new::<meter>(p[1]),
z0: Length::new::<meter>(p[2]),
r: Length::new::<meter>(p[3]),
phi0: Angle::new::<radian>(p[4]),
h: Length::new::<meter>(p[5]),
};
Ok(self
.points
.iter()
.map(|&p| {
let t = helix.closest_t(p, self.tolerance, self.max_num_iter);
let closest_point = helix.at(t);
let val = norm_sqr(p, closest_point);
assert!(!val.is_nan(), "found NaN in track_fitting::cost_function");
val
})
.sum::<Area>()
.get::<square_meter>())
}
}