use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::Zero;
use symplex::multipoly::{GrevLex, MultiPoly};
use symplex::polysys::solve_polynomial_system;
use symplex::robotics::inverse_kinematics_2dof;
fn rat(n: i64) -> Ratio<BigInt> {
Ratio::from_integer(BigInt::from(n))
}
fn ratio(p: i64, q: i64) -> Ratio<BigInt> {
Ratio::new(BigInt::from(p), BigInt::from(q))
}
fn sorted(mut sols: Vec<Vec<Ratio<BigInt>>>) -> Vec<Vec<Ratio<BigInt>>> {
sols.sort_by(|a, b| {
for (ai, bi) in a.iter().zip(b.iter()) {
match ai.cmp(bi) {
std::cmp::Ordering::Equal => continue,
other => return other,
}
}
a.len().cmp(&b.len())
});
sols
}
fn verify_solution(polys: &[MultiPoly<GrevLex>], point: &[Ratio<BigInt>]) {
for (i, p) in polys.iter().enumerate() {
let val = p.eval(point);
assert!(
val.is_zero(),
"polynomial {} evaluated to {} at {:?} (expected 0)",
i,
val,
point
);
}
}
fn verify_fk_2dof(l1: f64, l2: f64, theta1: f64, theta2: f64, tx: f64, ty: f64) -> bool {
let x = l1 * theta1.cos() + l2 * (theta1 + theta2).cos();
let y = l1 * theta1.sin() + l2 * (theta1 + theta2).sin();
(x - tx).abs() < 0.01 && (y - ty).abs() < 0.01
}
#[test]
fn solve_single_variable_quadratic() {
let x = MultiPoly::<GrevLex>::var(1, 0);
let p = &x * &x - MultiPoly::from_int(1, 4);
let sols = solve_polynomial_system(&[p]).unwrap();
let sols = sorted(sols);
assert_eq!(sols.len(), 2, "expected 2 solutions, got {:?}", sols);
assert!(sols.contains(&vec![rat(-2)]));
assert!(sols.contains(&vec![rat(2)]));
}
#[test]
fn solve_linear_2var() {
let nv = 2;
let x = MultiPoly::<GrevLex>::var(nv, 0);
let y = MultiPoly::<GrevLex>::var(nv, 1);
let one = MultiPoly::<GrevLex>::from_int(nv, 1);
let p1 = &x + &y - one; let p2 = &x - &y;
let sols = solve_polynomial_system(&[p1, p2]).unwrap();
assert_eq!(sols.len(), 1, "expected 1 solution, got {:?}", sols);
assert_eq!(sols[0].len(), 2);
assert_eq!(sols[0][0], ratio(1, 2), "x should be 1/2");
assert_eq!(sols[0][1], ratio(1, 2), "y should be 1/2");
}
#[test]
fn solve_circle_line() {
let nv = 2;
let x = MultiPoly::<GrevLex>::var(nv, 0);
let y = MultiPoly::<GrevLex>::var(nv, 1);
let one = MultiPoly::<GrevLex>::from_int(nv, 1);
let circle = &(&x * &x) + &(&y * &y) - one.clone();
let line = &x + &y - one;
let sols = solve_polynomial_system(&[circle, line]).unwrap();
let sols = sorted(sols);
assert_eq!(sols.len(), 2, "expected 2 solutions, got {:?}", sols);
assert!(
sols.contains(&vec![rat(0), rat(1)]),
"missing solution (0, 1): {:?}",
sols
);
assert!(
sols.contains(&vec![rat(1), rat(0)]),
"missing solution (1, 0): {:?}",
sols
);
}
#[test]
fn solve_two_conics() {
let nv = 2;
let x = MultiPoly::<GrevLex>::var(nv, 0);
let y = MultiPoly::<GrevLex>::var(nv, 1);
let p1 = &(&x * &x) + &(&y * &y) - MultiPoly::from_int(nv, 5);
let p2 = &x * &y - MultiPoly::from_int(nv, 2);
let sols = solve_polynomial_system(&[p1, p2]).unwrap();
let sols = sorted(sols);
assert_eq!(sols.len(), 4, "expected 4 solutions, got {:?}", sols);
assert!(sols.contains(&vec![rat(1), rat(2)]));
assert!(sols.contains(&vec![rat(2), rat(1)]));
assert!(sols.contains(&vec![rat(-1), rat(-2)]));
assert!(sols.contains(&vec![rat(-2), rat(-1)]));
}
#[test]
fn solve_inconsistent_no_real_solutions() {
let nv = 2;
let x = MultiPoly::<GrevLex>::var(nv, 0);
let y = MultiPoly::<GrevLex>::var(nv, 1);
let p = &(&x * &x) + &(&y * &y) + MultiPoly::from_int(nv, 1);
let result = solve_polynomial_system(&[p]);
match result {
Ok(sols) => {
assert!(
sols.is_empty(),
"x²+y²+1=0 should have no rational solutions, got {:?}",
sols
);
}
Err(_) => {
}
}
}
#[test]
fn solve_verify_solutions_circle_line() {
let nv = 2;
let x = MultiPoly::<GrevLex>::var(nv, 0);
let y = MultiPoly::<GrevLex>::var(nv, 1);
let one = MultiPoly::<GrevLex>::from_int(nv, 1);
let circle = &(&x * &x) + &(&y * &y) - one.clone();
let line = &x + &y - one;
let system = vec![circle, line];
let sols = solve_polynomial_system(&system).unwrap();
assert!(!sols.is_empty(), "should have at least one solution");
for sol in &sols {
verify_solution(&system, sol);
}
}
#[test]
fn solve_verify_solutions_two_conics() {
let nv = 2;
let x = MultiPoly::<GrevLex>::var(nv, 0);
let y = MultiPoly::<GrevLex>::var(nv, 1);
let p1 = &(&x * &x) + &(&y * &y) - MultiPoly::from_int(nv, 5);
let p2 = &x * &y - MultiPoly::from_int(nv, 2);
let system = vec![p1, p2];
let sols = solve_polynomial_system(&system).unwrap();
assert_eq!(sols.len(), 4);
for sol in &sols {
verify_solution(&system, sol);
}
}
#[test]
fn solve_linear_3var() {
let nv = 3;
let x = MultiPoly::<GrevLex>::var(nv, 0);
let y = MultiPoly::<GrevLex>::var(nv, 1);
let z = MultiPoly::<GrevLex>::var(nv, 2);
let p1 = &(&x + &y) + &z - MultiPoly::from_int(nv, 6);
let p2 = &x - &y;
let p3 = &y - &z;
let system = vec![p1, p2, p3];
let sols = solve_polynomial_system(&system).unwrap();
assert_eq!(sols.len(), 1, "expected 1 solution, got {:?}", sols);
for sol in &sols {
verify_solution(&system, sol);
}
assert_eq!(sols[0][0], rat(2));
assert_eq!(sols[0][1], rat(2));
assert_eq!(sols[0][2], rat(2));
}
#[test]
fn solve_single_linear() {
let x = MultiPoly::<GrevLex>::var(1, 0);
let p = x.scale(&rat(3)).sub(&MultiPoly::from_int(1, 6));
let sols = solve_polynomial_system(&[p]).unwrap();
assert_eq!(sols.len(), 1);
assert_eq!(sols[0], vec![rat(2)]);
}
#[test]
fn solve_two_linear_unique() {
let nv = 2;
let x = MultiPoly::<GrevLex>::var(nv, 0);
let y = MultiPoly::<GrevLex>::var(nv, 1);
let p1 = x
.scale(&rat(2))
.add(&y.scale(&rat(3)))
.sub(&MultiPoly::from_int(nv, 8));
let p2 = x.sub(&y).sub(&MultiPoly::from_int(nv, 1));
let system = vec![p1, p2];
let sols = solve_polynomial_system(&system).unwrap();
assert_eq!(sols.len(), 1, "expected unique solution, got {:?}", sols);
for sol in &sols {
verify_solution(&system, sol);
}
assert_eq!(sols[0][0], ratio(11, 5));
assert_eq!(sols[0][1], ratio(6, 5));
}
#[test]
fn solve_quadratic_single_var_no_rational_roots() {
let x = MultiPoly::<GrevLex>::var(1, 0);
let p = &x * &x + MultiPoly::from_int(1, 1);
let sols = solve_polynomial_system(&[p]).unwrap();
assert!(
sols.is_empty(),
"x²+1 has no rational roots, got {:?}",
sols
);
}
#[test]
fn solve_cubic_single_var() {
let x = MultiPoly::<GrevLex>::var(1, 0);
let x2 = &x * &x;
let x3 = &x2 * &x;
let six = MultiPoly::from_int(1, 6);
let p = &(&x3 - &x2.scale(&rat(6))) + &(&x.scale(&rat(11)) - &six);
let sols = solve_polynomial_system(&[p]).unwrap();
let sols = sorted(sols);
assert_eq!(sols.len(), 3, "expected 3 roots, got {:?}", sols);
assert!(sols.contains(&vec![rat(1)]));
assert!(sols.contains(&vec![rat(2)]));
assert!(sols.contains(&vec![rat(3)]));
}
#[test]
fn ik_2dof_extended_arm() {
let solutions = inverse_kinematics_2dof(1.0, 1.0, 2.0, 0.0);
assert!(
!solutions.is_empty(),
"fully extended arm should have at least one IK solution"
);
for &(t1, t2) in &solutions {
assert!(
verify_fk_2dof(1.0, 1.0, t1, t2, 2.0, 0.0),
"FK verification failed for θ₁={t1}, θ₂={t2}"
);
}
}
#[test]
fn ik_2dof_basic() {
let solutions = inverse_kinematics_2dof(1.0, 1.0, 1.0, 1.0);
for &(t1, t2) in &solutions {
assert!(
verify_fk_2dof(1.0, 1.0, t1, t2, 1.0, 1.0),
"FK verification failed for θ₁={t1:.4}, θ₂={t2:.4}: \
got ({:.4}, {:.4}), expected (1, 1)",
1.0 * t1.cos() + 1.0 * (t1 + t2).cos(),
1.0 * t1.sin() + 1.0 * (t1 + t2).sin(),
);
}
if solutions.len() == 2 {
} else if solutions.is_empty() {
panic!("IK returned no solutions for a clearly reachable point (1,1)");
}
}
#[test]
fn ik_2dof_verify_fk() {
let cases: Vec<(f64, f64, f64, f64)> = vec![
(1.0, 1.0, 2.0, 0.0), (1.0, 1.0, 0.0, 2.0), (1.0, 1.0, -2.0, 0.0), (2.0, 1.0, 3.0, 0.0), (1.0, 1.0, 0.0, 0.0), ];
for (l1, l2, tx, ty) in cases {
let solutions = inverse_kinematics_2dof(l1, l2, tx, ty);
for &(t1, t2) in &solutions {
assert!(
verify_fk_2dof(l1, l2, t1, t2, tx, ty),
"FK verification failed: l1={l1}, l2={l2}, target=({tx},{ty}), \
θ₁={t1:.4}, θ₂={t2:.4}"
);
}
}
}
#[test]
fn ik_2dof_unreachable() {
let solutions = inverse_kinematics_2dof(1.0, 1.0, 5.0, 0.0);
assert!(
solutions.is_empty(),
"unreachable target should yield no solutions, got {:?}",
solutions
);
}