use crate::eval::symbols::collect_free_symbols;
use crate::eval::{eval_complex_f64, ComplexF64};
use crate::kernel::{ExprId, ExprPool};
use crate::matrix::Matrix;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt;
fn cadd(a: ComplexF64, b: ComplexF64) -> ComplexF64 {
ComplexF64::new(a.re + b.re, a.im + b.im)
}
fn csub(a: ComplexF64, b: ComplexF64) -> ComplexF64 {
ComplexF64::new(a.re - b.re, a.im - b.im)
}
fn cmul(a: ComplexF64, b: ComplexF64) -> ComplexF64 {
ComplexF64::new(a.re * b.re - a.im * b.im, a.re * b.im + a.im * b.re)
}
fn cdiv(a: ComplexF64, b: ComplexF64) -> ComplexF64 {
let d = b.re * b.re + b.im * b.im;
ComplexF64::new(
(a.re * b.re + a.im * b.im) / d,
(a.im * b.re - a.re * b.im) / d,
)
}
fn cabs(a: ComplexF64) -> f64 {
a.re.hypot(a.im)
}
fn cfinite(a: ComplexF64) -> bool {
a.re.is_finite() && a.im.is_finite()
}
fn complex_det(m: &mut [Vec<ComplexF64>]) -> Option<ComplexF64> {
let n = m.len();
let mut det = ComplexF64::ONE;
for k in 0..n {
let mut piv = k;
for r in (k + 1)..n {
if cabs(m[r][k]) > cabs(m[piv][k]) {
piv = r;
}
}
if cabs(m[piv][k]) < 1e-13 {
return None;
}
if piv != k {
m.swap(piv, k);
det = cmul(det, ComplexF64::new(-1.0, 0.0));
}
det = cmul(det, m[k][k]);
for r in (k + 1)..n {
let f = cdiv(m[r][k], m[k][k]);
let (above, from_r) = m.split_at_mut(r);
let pivot_row = &above[k];
for (dst, &src) in from_r[0].iter_mut().zip(pivot_row.iter()).skip(k) {
*dst = csub(*dst, cmul(f, src));
}
}
}
if cfinite(det) {
Some(det)
} else {
None
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SpectrumRefusal {
lambdas: Vec<String>,
probe: (f64, f64),
product: (f64, f64),
determinant: (f64, f64),
}
impl SpectrumRefusal {
pub fn lambdas(&self) -> &[String] {
&self.lambdas
}
pub fn probe(&self) -> (f64, f64) {
self.probe
}
}
impl fmt::Display for SpectrumRefusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"the closed-form eigenvalues [{}] are not roots of the characteristic \
polynomial when their radicals are read on principal branches: at \
z = {} + {}i, Π(z − λ) = {} + {}i but det(zI − A) = {} + {}i. \
Refusing rather than returning a spectrum whose correctness depends \
on a branch convention nothing in the expression records",
self.lambdas.join(", "),
self.probe.0,
self.probe.1,
self.product.0,
self.product.1,
self.determinant.0,
self.determinant.1,
)
}
}
impl std::error::Error for SpectrumRefusal {}
impl crate::errors::AlkahestError for SpectrumRefusal {
fn code(&self) -> &'static str {
"E-EIGEN-008"
}
fn remediation(&self) -> Option<&'static str> {
Some("substitute concrete numbers for any symbolic entries, or use real_roots / numeric eigenvalues for this matrix")
}
}
thread_local! {
static LAST_REFUSAL: RefCell<Option<SpectrumRefusal>> = const { RefCell::new(None) };
}
pub fn take_spectrum_refusal() -> Option<SpectrumRefusal> {
LAST_REFUSAL.with(|c| c.borrow_mut().take())
}
pub(crate) fn record_refusal(r: SpectrumRefusal) {
LAST_REFUSAL.with(|c| *c.borrow_mut() = Some(r));
}
pub(crate) fn forget_refusal() {
LAST_REFUSAL.with(|c| *c.borrow_mut() = None);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SpectrumCheck {
Confirmed { samples: usize },
Unevaluated,
}
impl SpectrumCheck {
pub(crate) fn is_confirmed(self) -> bool {
matches!(self, SpectrumCheck::Confirmed { .. })
}
}
const PARAM_SETS: [&[f64]; 2] = [&[1.7, 0.6, 2.3, 1.1, 0.4], &[0.37, 1.9, 0.83, 2.7, 1.3]];
const PROBES: [(f64, f64); 3] = [(0.53, 0.29), (-1.17, 0.71), (2.31, -0.43)];
pub(crate) fn confirm_spectrum(
a: &Matrix,
lambdas: &[ExprId],
pool: &ExprPool,
) -> Result<SpectrumCheck, SpectrumRefusal> {
let n = a.rows;
if n != a.cols || lambdas.len() != n {
return Ok(SpectrumCheck::Unevaluated);
}
let mut params: Vec<ExprId> = Vec::new();
for &e in a.entries() {
collect_free_symbols(e, pool, &mut params);
}
for &l in lambdas {
collect_free_symbols(l, pool, &mut params);
}
params.sort_by_key(|&s| pool.display(s).to_string());
let mut checked = 0usize;
for ps in PARAM_SETS {
let mut env: HashMap<ExprId, ComplexF64> = HashMap::new();
for (i, &p) in params.iter().enumerate() {
env.insert(p, ComplexF64::new(ps[i % ps.len()], 0.0));
}
let Some(entries) = a
.entries()
.iter()
.map(|&e| eval_complex_f64(e, pool, &env).ok().filter(|&v| cfinite(v)))
.collect::<Option<Vec<_>>>()
else {
continue;
};
let Some(lam) = lambdas
.iter()
.map(|&l| eval_complex_f64(l, pool, &env).ok().filter(|&v| cfinite(v)))
.collect::<Option<Vec<_>>>()
else {
continue;
};
for (zr, zi) in PROBES {
let z = ComplexF64::new(zr, zi);
let mut m: Vec<Vec<ComplexF64>> = (0..n)
.map(|i| {
(0..n)
.map(|j| {
let e = entries[i * n + j];
let neg = ComplexF64::new(-e.re, -e.im);
if i == j {
cadd(z, neg)
} else {
neg
}
})
.collect()
})
.collect();
let Some(det) = complex_det(&mut m) else {
continue;
};
let prod = lam
.iter()
.fold(ComplexF64::ONE, |acc, &l| cmul(acc, csub(z, l)));
let scale = cabs(det).max(cabs(prod)).max(1.0);
if cabs(csub(det, prod)) > 1e-7 * scale {
return Err(SpectrumRefusal {
lambdas: lambdas
.iter()
.map(|&l| pool.display(l).to_string())
.collect(),
probe: (zr, zi),
product: (prod.re, prod.im),
determinant: (det.re, det.im),
});
}
checked += 1;
}
}
if checked == 0 {
Ok(SpectrumCheck::Unevaluated)
} else {
Ok(SpectrumCheck::Confirmed { samples: checked })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::Domain;
fn mat(rows: usize, entries: Vec<ExprId>, pool: &ExprPool) -> Matrix {
let mut m = Matrix::zeros(rows, rows, pool);
for i in 0..rows {
for j in 0..rows {
m.set(i, j, entries[i * rows + j]);
}
}
m
}
#[test]
fn a_correct_rational_spectrum_is_confirmed() {
let p = ExprPool::new();
let z = p.integer(0_i32);
let m = mat(2, vec![p.integer(2_i32), z, z, p.integer(5_i32)], &p);
let out = confirm_spectrum(&m, &[p.integer(2_i32), p.integer(5_i32)], &p).unwrap();
assert!(out.is_confirmed(), "{out:?}");
}
#[test]
fn a_wrong_spectrum_is_refuted() {
let p = ExprPool::new();
let z = p.integer(0_i32);
let m = mat(2, vec![p.integer(2_i32), z, z, p.integer(5_i32)], &p);
let err = confirm_spectrum(&m, &[p.integer(2_i32), p.integer(6_i32)], &p)
.expect_err("{2, 6} is not the spectrum of diag(2, 5)");
assert_eq!(
<SpectrumRefusal as crate::errors::AlkahestError>::code(&err),
"E-EIGEN-008"
);
assert!(err.lambdas().len() == 2, "{err}");
}
#[test]
fn an_unevaluable_spectrum_is_no_information_not_a_refusal() {
let p = ExprPool::new();
let z = p.integer(0_i32);
let m = mat(2, vec![p.integer(2_i32), z, z, p.integer(5_i32)], &p);
let opaque = p.func("zeta", vec![p.integer(3_i32)]);
let out = confirm_spectrum(&m, &[opaque, opaque], &p).unwrap();
assert_eq!(out, SpectrumCheck::Unevaluated);
}
#[test]
fn pi_is_a_constant_not_a_sampled_parameter() {
let p = ExprPool::new();
let pi = p.symbol("pi", Domain::Real);
let z = p.integer(0_i32);
let m = mat(2, vec![pi, z, z, z], &p);
let out = confirm_spectrum(&m, &[pi, z], &p).unwrap();
assert!(
out.is_confirmed(),
"π must be bound to 3.14159…, not sampled: {out:?}"
);
}
}