use brepkit_math::nurbs::curve::NurbsCurve;
use brepkit_math::vec::Point3;
pub(crate) struct LipschitzOptimizer {
grid_size: usize,
max_subdivisions: usize,
max_evals: usize,
}
impl Default for LipschitzOptimizer {
fn default() -> Self {
Self::new()
}
}
impl LipschitzOptimizer {
#[must_use]
pub fn new() -> Self {
Self {
grid_size: 16,
max_subdivisions: 50,
max_evals: 500_000,
}
}
#[must_use]
#[allow(clippy::too_many_lines, clippy::cast_precision_loss)]
pub fn minimize_2d<F>(
&self,
f: F,
u_range: (f64, f64),
v_range: (f64, f64),
tolerance: f64,
) -> (f64, f64, f64)
where
F: Fn(f64, f64) -> f64,
{
let (u0, u1) = u_range;
let (v0, v1) = v_range;
let n = self.grid_size;
let grid_len = (n + 1) * (n + 1);
let mut grid_vals = vec![0.0_f64; grid_len];
let mut best = f64::INFINITY;
let mut best_u = (u0 + u1) * 0.5;
let mut best_v = (v0 + v1) * 0.5;
for iu in 0..=n {
let u = u0 + (u1 - u0) * (iu as f64 / n as f64);
for iv in 0..=n {
let v = v0 + (v1 - v0) * (iv as f64 / n as f64);
let val = f(u, v);
grid_vals[iu * (n + 1) + iv] = val;
if val < best {
best = val;
best_u = u;
best_v = v;
}
}
}
{
let step_u = (u1 - u0) / n as f64;
let step_v = (v1 - v0) / n as f64;
let mut lu = (best_u - step_u).max(u0);
let mut hu = (best_u + step_u).min(u1);
let mut lv = (best_v - step_v).max(v0);
let mut hv = (best_v + step_v).min(v1);
for _ in 0..self.max_subdivisions {
let m1 = lu + (hu - lu) / 3.0;
let m2 = hu - (hu - lu) / 3.0;
if f(m1, best_v) < f(m2, best_v) {
hu = m2;
} else {
lu = m1;
}
let cu = (lu + hu) * 0.5;
let fcu = f(cu, best_v);
if fcu < best {
best = fcu;
best_u = cu;
}
let m1v = lv + (hv - lv) / 3.0;
let m2v = hv - (hv - lv) / 3.0;
if f(best_u, m1v) < f(best_u, m2v) {
hv = m2v;
} else {
lv = m1v;
}
let cv = (lv + hv) * 0.5;
let fcv = f(best_u, cv);
if fcv < best {
best = fcv;
best_v = cv;
}
}
}
let du = (u1 - u0) / n as f64;
let dv = (v1 - v0) / n as f64;
let mut lip: f64 = 0.0;
for iu in 0..n {
for iv in 0..n {
let f00 = grid_vals[iu * (n + 1) + iv];
let f10 = grid_vals[(iu + 1) * (n + 1) + iv];
let f01 = grid_vals[iu * (n + 1) + (iv + 1)];
let dfu = (f10 - f00).abs() / du;
let dfv = (f01 - f00).abs() / dv;
let local_lip = dfu.hypot(dfv);
if local_lip > lip {
lip = local_lip;
}
}
}
lip *= 2.0;
if lip < 1e-15 {
return (best_u, best_v, best);
}
let mut stack: Vec<(f64, f64, f64, f64, usize)> = vec![(u0, u1, v0, v1, 0)];
let mut cell_count = 0_usize;
while let Some((cu0, cu1, cv0, cv1, depth)) = stack.pop() {
cell_count += 1;
if cell_count > self.max_evals {
break;
}
let um = (cu0 + cu1) * 0.5;
let vm = (cv0 + cv1) * 0.5;
let fc = f(um, vm);
if fc < best {
best = fc;
best_u = um;
best_v = vm;
}
let du_cell = cu1 - cu0;
let dv_cell = cv1 - cv0;
let radius = (du_cell * du_cell + dv_cell * dv_cell).sqrt() * 0.5;
let lower = fc - lip * radius;
if lower > best {
continue;
}
if radius < tolerance || depth >= self.max_subdivisions {
if fc < best {
best = fc;
best_u = um;
best_v = vm;
}
continue;
}
if du_cell >= dv_cell {
stack.push((cu0, um, cv0, cv1, depth + 1));
stack.push((um, cu1, cv0, cv1, depth + 1));
} else {
stack.push((cu0, cu1, cv0, vm, depth + 1));
stack.push((cu0, cu1, vm, cv1, depth + 1));
}
}
(best_u, best_v, best)
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn estimate_curve_curve_lipschitz(c1: &NurbsCurve, c2: &NurbsCurve) -> f64 {
let n_samples = 20_usize;
let (u0, u1) = c1.domain();
let (v0, v1) = c2.domain();
let mut max_deriv1: f64 = 0.0;
let mut max_deriv2: f64 = 0.0;
let mut max_sep: f64 = 0.0;
for i in 0..=n_samples {
let t = i as f64 / n_samples as f64;
let u = u0 + (u1 - u0) * t;
let d1 = c1.derivatives(u, 1);
max_deriv1 = max_deriv1.max(d1[1].length());
let v = v0 + (v1 - v0) * t;
let d2 = c2.derivatives(v, 1);
max_deriv2 = max_deriv2.max(d2[1].length());
let p1 = Point3::new(d1[0].x(), d1[0].y(), d1[0].z());
let p2 = Point3::new(d2[0].x(), d2[0].y(), d2[0].z());
max_sep = max_sep.max((p1 - p2).length());
}
2.0 * max_sep * max_deriv1.max(max_deriv2)
}
#[must_use]
pub fn nurbs_curve_curve_distance(
curve1: &NurbsCurve,
curve2: &NurbsCurve,
) -> (f64, Point3, Point3) {
let (u0, u1) = curve1.domain();
let (v0, v1) = curve2.domain();
let lip = estimate_curve_curve_lipschitz(curve1, curve2);
if lip < 1e-15 {
let p1 = curve1.evaluate((u0 + u1) * 0.5);
let p2 = curve2.evaluate((v0 + v1) * 0.5);
return ((p1 - p2).length(), p1, p2);
}
let f = |u: f64, v: f64| -> f64 {
let p1 = curve1.evaluate(u);
let p2 = curve2.evaluate(v);
(p1 - p2).length_squared()
};
let opt = LipschitzOptimizer::new();
let (best_u, best_v, _) = opt.minimize_2d(f, (u0, u1), (v0, v1), 1e-4);
let p1 = curve1.evaluate(best_u);
let p2 = curve2.evaluate(best_v);
((p1 - p2).length(), p1, p2)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
#[test]
fn minimize_quadratic_at_origin() {
let f = |u: f64, v: f64| u * u + v * v;
let opt = LipschitzOptimizer::new();
let (u, v, val) = opt.minimize_2d(f, (-1.0, 1.0), (-1.0, 1.0), 1e-6);
assert!(val < 1e-8, "val={val}");
assert!(u.abs() < 1e-4, "u={u}");
assert!(v.abs() < 1e-4, "v={v}");
}
#[test]
fn minimize_quadratic_offset() {
let f = |u: f64, v: f64| (u - 0.3) * (u - 0.3) + (v - 0.7) * (v - 0.7);
let opt = LipschitzOptimizer::new();
let (u, v, val) = opt.minimize_2d(f, (0.0, 1.0), (0.0, 1.0), 1e-6);
assert!(val < 1e-8, "val={val}");
assert!((u - 0.3).abs() < 1e-3, "u={u}");
assert!((v - 0.7).abs() < 1e-3, "v={v}");
}
#[test]
fn minimize_returns_global_not_local() {
let f = |u: f64, v: f64| {
let d1 = (u - 0.1) * (u - 0.1) + (v - 0.5) * (v - 0.5) - 0.01;
let d2 = (u - 0.9) * (u - 0.9) + (v - 0.5) * (v - 0.5);
d1.min(d2)
};
let opt = LipschitzOptimizer::new();
let (_u, _v, val) = opt.minimize_2d(f, (0.0, 1.0), (0.0, 1.0), 1e-5);
assert!(val < -0.005, "val={val}");
}
#[test]
fn nurbs_parallel_lines_distance_one() {
let c1 = NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)],
vec![1.0, 1.0],
)
.unwrap();
let c2 = NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![Point3::new(0.0, 1.0, 0.0), Point3::new(1.0, 1.0, 0.0)],
vec![1.0, 1.0],
)
.unwrap();
let (dist, _, _) = nurbs_curve_curve_distance(&c1, &c2);
assert!((dist - 1.0).abs() < 1e-3, "dist={dist}");
}
}