use super::poly::{RealRoot, real_roots_touching};
use crate::{InlineList, Tolerance};
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum QuadraticSolution {
Two(f64, f64),
One(f64),
Linear(f64),
Degenerate,
Empty,
}
impl QuadraticSolution {
pub(crate) fn shifted(self, t0: f64) -> Self {
match self {
Self::Two(t1, t2) => Self::Two(t0 + t1, t0 + t2),
Self::One(t) => Self::One(t0 + t),
Self::Linear(t) => Self::Linear(t0 + t),
other => other,
}
}
pub(crate) fn touching_at(self, vertex: f64, touches: impl Fn(f64) -> bool) -> Self {
match self {
Self::Two(..) | Self::Empty if vertex.is_finite() && touches(vertex) => {
Self::One(vertex)
}
other => other,
}
}
}
pub(crate) fn roots_with_tangency(
poly: &[f64],
tol: Tolerance,
touches: impl Fn(f64) -> bool,
) -> InlineList<RealRoot, 4> {
real_roots_touching(poly, tol.confusion, touches)
}
pub(crate) fn quadratic_centre(a: f64, b: f64, tol: Tolerance) -> f64 {
let t0 = -b / (2.0 * a);
if a.abs() > tol.angular && t0.is_finite() {
t0
} else {
0.0
}
}
pub(crate) fn solve_quadratic(a: f64, b: f64, c: f64, tol: Tolerance) -> QuadraticSolution {
if a.abs() <= tol.angular {
if b.abs() <= tol.confusion {
return QuadraticSolution::Degenerate;
}
return QuadraticSolution::Linear(-c / b);
}
let disc = b * b - 4.0 * a * c;
let mid = -b / (2.0 * a);
let sep = disc.abs().sqrt() / (2.0 * a.abs());
let noise = (4.0 * f64::EPSILON * (b * b + 4.0 * (a * c).abs())).sqrt() / (2.0 * a.abs());
if sep <= tol.confusion * (1.0 + mid.abs()) + noise {
QuadraticSolution::One(mid)
} else if disc < 0.0 {
QuadraticSolution::Empty
} else {
let q = -0.5 * (b + disc.sqrt().copysign(b));
let (r1, r2) = (q / a, c / q);
QuadraticSolution::Two(r1.min(r2), r1.max(r2))
}
}
pub(crate) fn solve_trig_linear(
c: f64,
d: f64,
e: f64,
scale: f64,
tol: Tolerance,
) -> InlineList<(f64, u32), 4> {
let mut out = InlineList::new();
let amp = c.hypot(d);
let band = tol.confusion * scale.max(f64::MIN_POSITIVE);
if amp <= band || e.abs() > amp + band {
return out;
}
let phi = d.atan2(c);
let t0 = (-e / amp).clamp(-1.0, 1.0).acos();
if e.abs() >= amp - band {
out.push((phi + t0, 2));
} else {
out.push((phi + t0, 1));
out.push((phi - t0, 1));
}
out
}
pub(crate) fn solve_trig(
a2: f64,
b2: f64,
c1: f64,
d1: f64,
e: f64,
tol: Tolerance,
touches: impl Fn(f64) -> bool,
) -> InlineList<(f64, u32), 4> {
use std::f64::consts::{FRAC_1_SQRT_2 as H, FRAC_PI_4, PI};
const UNIT: [(f64, f64); 8] = [
(1.0, 0.0),
(H, H),
(0.0, 1.0),
(-H, H),
(-1.0, 0.0),
(-H, -H),
(0.0, -1.0),
(H, -H),
];
let f_at = |k: usize| {
let ((cos1, sin1), (cos2, sin2)) = (UNIT[k % 8], UNIT[2 * k % 8]);
a2 * cos2 + b2 * sin2 + c1 * cos1 + d1 * sin1 + e
};
let (mut k, mut largest) = (0, f_at(4).abs());
for j in 1..8 {
let v = f_at(j + 4).abs();
if v.total_cmp(&largest).is_ge() {
(k, largest) = (j, v);
}
}
let t0 = k as f64 * FRAC_PI_4;
let ((cos1, sin1), (cos2, sin2)) = (UNIT[k], UNIT[2 * k % 8]);
let (a2, b2) = (a2 * cos2 + b2 * sin2, b2 * cos2 - a2 * sin2);
let (c1, d1) = (c1 * cos1 + d1 * sin1, d1 * cos1 - c1 * sin1);
let q = [
a2 + c1 + e,
4.0 * b2 + 2.0 * d1,
-6.0 * a2 + 2.0 * e,
-4.0 * b2 + 2.0 * d1,
a2 - c1 + e,
];
let to_t = |u: f64| {
let t = t0 + 2.0 * u.atan();
if t > PI { t - 2.0 * PI } else { t }
};
let mut out = InlineList::new();
for r in &roots_with_tangency(&q, tol, |u| touches(to_t(u))) {
out.push((to_t(r.value), r.multiplicity));
}
out.sort_unstable_by(|a, b| a.0.total_cmp(&b.0));
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_solve_quadratic_two_roots_ordered() {
assert_eq!(
solve_quadratic(1.0, -5.0, 6.0, Tolerance::DEFAULT),
QuadraticSolution::Two(2.0, 3.0)
);
}
#[test]
fn test_solve_quadratic_tangent_band() {
assert_eq!(
solve_quadratic(1.0, -4.0, 4.0, Tolerance::DEFAULT),
QuadraticSolution::One(2.0)
);
assert_eq!(
solve_quadratic(1.0, 0.0, 1e-14, Tolerance::DEFAULT),
QuadraticSolution::One(0.0)
);
assert_eq!(
solve_quadratic(1.0, 0.0, 1e-9, Tolerance::DEFAULT),
QuadraticSolution::Empty
);
assert_eq!(
solve_quadratic(1.0, 0.0, 1.0, Tolerance::DEFAULT),
QuadraticSolution::Empty
);
}
#[test]
fn test_solve_quadratic_rounded_double_root_is_tangent() {
let tight = Tolerance {
confusion: 1e-12,
..Tolerance::DEFAULT
};
for (a, r) in [(7.0, 2.3), (11.0, 0.7)] {
match solve_quadratic(a, -2.0 * a * r, a * r * r, tight) {
QuadraticSolution::One(t) => assert!((t - r).abs() < 1e-14),
other => panic!("expected a double root near {r}, got {other:?}"),
}
}
match solve_quadratic(1.0, -2.0, 1.0 - 1e-12, tight) {
QuadraticSolution::Two(t1, t2) => {
assert!((t1 - (1.0 - 1e-6)).abs() < 1e-9);
assert!((t2 - (1.0 + 1e-6)).abs() < 1e-9);
}
other => panic!("expected two roots, got {other:?}"),
}
}
#[test]
fn test_solve_quadratic_widely_separated_roots_are_accurate() {
match solve_quadratic(1.0, -1e8, 1.0, Tolerance::DEFAULT) {
QuadraticSolution::Two(t1, t2) => {
assert!((t1 - 1e-8).abs() < 1e-22);
assert!((t2 - 1e8).abs() < 1e-7);
}
other => panic!("expected two roots, got {other:?}"),
}
}
#[test]
fn test_solve_quadratic_degenerate_arms() {
assert_eq!(
solve_quadratic(0.0, 2.0, 4.0, Tolerance::DEFAULT),
QuadraticSolution::Linear(-2.0)
);
assert_eq!(
solve_quadratic(0.0, 0.0, 1.0, Tolerance::DEFAULT),
QuadraticSolution::Degenerate
);
}
}