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 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 cscale(a: ComplexF64, s: f64) -> ComplexF64 {
ComplexF64::new(a.re * s, a.im * s)
}
fn cabs(a: ComplexF64) -> f64 {
a.re.hypot(a.im)
}
fn cfinite(a: ComplexF64) -> bool {
a.re.is_finite() && a.im.is_finite()
}
type Cm = Vec<Vec<ComplexF64>>;
fn cmat_mul(a: &Cm, b: &Cm) -> Cm {
let n = a.len();
let mut out = vec![vec![ComplexF64::new(0.0, 0.0); n]; n];
for (i, row) in out.iter_mut().enumerate() {
for (j, cell) in row.iter_mut().enumerate() {
let mut s = ComplexF64::new(0.0, 0.0);
for k in 0..n {
s = cadd(s, cmul(a[i][k], b[k][j]));
}
*cell = s;
}
}
out
}
fn cmat_norm(a: &Cm) -> f64 {
a.iter()
.map(|r| r.iter().map(|&c| cabs(c)).sum::<f64>())
.fold(0.0_f64, f64::max)
}
fn cmat_exp(a: &Cm) -> Option<Cm> {
let n = a.len();
let norm = cmat_norm(a);
if !norm.is_finite() {
return None;
}
let s = if norm > 0.5 {
(norm / 0.5).log2().ceil().max(0.0) as u32
} else {
0
};
if s > 60 {
return None;
}
let scale = 1.0 / (2.0_f64).powi(s as i32);
let b: Cm = a
.iter()
.map(|r| r.iter().map(|&c| cscale(c, scale)).collect())
.collect();
let mut term: Cm = (0..n)
.map(|i| {
(0..n)
.map(|j| ComplexF64::new(f64::from(u8::from(i == j)), 0.0))
.collect()
})
.collect();
let mut acc = term.clone();
for k in 1..=20u32 {
term = cmat_mul(&term, &b);
let inv = 1.0 / f64::from(k);
for row in term.iter_mut() {
for c in row.iter_mut() {
*c = cscale(*c, inv);
}
}
for (ar, tr) in acc.iter_mut().zip(term.iter()) {
for (a, &t) in ar.iter_mut().zip(tr.iter()) {
*a = cadd(*a, t);
}
}
}
for _ in 0..s {
acc = cmat_mul(&acc, &acc);
}
if acc.iter().flatten().all(|&c| cfinite(c)) {
Some(acc)
} else {
None
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct MatrixExpRefusal {
reason: ExpRefusalReason,
detail: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExpRefusalReason {
SpectrumUnconfirmed,
FailedVerification,
}
impl MatrixExpRefusal {
pub fn reason(&self) -> ExpRefusalReason {
self.reason
}
}
impl fmt::Display for MatrixExpRefusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.detail)
}
}
impl std::error::Error for MatrixExpRefusal {}
impl crate::errors::AlkahestError for MatrixExpRefusal {
fn code(&self) -> &'static str {
"E-LINALG-011"
}
fn remediation(&self) -> Option<&'static str> {
Some(
"substitute concrete numbers for any symbolic entries, or state the assumption \
that makes the eigenvalue gap non-zero",
)
}
}
thread_local! {
static LAST_REFUSAL: RefCell<Option<MatrixExpRefusal>> = const { RefCell::new(None) };
}
pub fn take_matrix_exp_refusal() -> Option<MatrixExpRefusal> {
LAST_REFUSAL.with(|c| c.borrow_mut().take())
}
pub(crate) fn record_refusal(reason: ExpRefusalReason, detail: String) {
crate::matrix::zero_test::forget_refusal();
LAST_REFUSAL.with(|c| *c.borrow_mut() = Some(MatrixExpRefusal { reason, detail }));
}
pub(crate) fn forget_refusal() {
LAST_REFUSAL.with(|c| *c.borrow_mut() = None);
LAST_CONDITIONS.with(|c| c.borrow_mut().clear());
}
thread_local! {
static LAST_CONDITIONS: RefCell<Vec<ExprId>> = const { RefCell::new(Vec::new()) };
}
pub(crate) fn record_side_conditions(gaps: Vec<ExprId>) {
LAST_CONDITIONS.with(|c| *c.borrow_mut() = gaps);
}
pub fn take_matrix_exp_side_conditions() -> Vec<crate::deriv::SideCondition> {
LAST_CONDITIONS.with(|c| {
std::mem::take(&mut *c.borrow_mut())
.into_iter()
.map(crate::deriv::SideCondition::NonZero)
.collect()
})
}
pub(crate) fn spectrum_unconfirmed(n: usize) -> String {
format!(
"the eigenvalues of this {n}×{n} matrix could not be evaluated at any sample, so \
nothing confirms they are its spectrum — and Putzer's expansion of e^A is built \
entirely on them. Refusing rather than returning a matrix whose correctness rests \
on an unchecked eigenvalue list"
)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ExpCheck {
Confirmed { samples: usize },
Unevaluated,
}
impl ExpCheck {
#[cfg(test)]
pub(crate) fn is_confirmed(self) -> bool {
matches!(self, ExpCheck::Confirmed { .. })
}
}
const PARAM_SETS: [&[f64]; 3] = [
&[1.7, 0.6, -2.3, 1.1, 0.4],
&[0.37, -1.9, 0.83, 2.7, 1.3],
&[-0.85, 1.45, 0.21, -0.62, 2.05],
];
const TOL: f64 = 1e-7;
pub(crate) fn confirm_matrix_exponential(
a: &Matrix,
candidate: &Matrix,
pool: &ExprPool,
) -> Result<ExpCheck, MatrixExpRefusal> {
let n = a.rows;
if n != a.cols || candidate.rows != n || candidate.cols != n {
return Ok(ExpCheck::Unevaluated);
}
let mut params: Vec<ExprId> = Vec::new();
for &e in a.entries().iter().chain(candidate.entries().iter()) {
collect_free_symbols(e, pool, &mut params);
}
params.sort_by_key(|&s| pool.display(s).to_string());
params.dedup();
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(av) = eval_grid(a, pool, &env) else {
continue;
};
let Some(cv) = eval_grid(candidate, pool, &env) else {
continue;
};
let Some(truth) = cmat_exp(&av) else {
continue;
};
let scale = cmat_norm(&truth).max(cmat_norm(&cv)).max(1.0);
for i in 0..n {
for j in 0..n {
let diff = cabs(ComplexF64::new(
cv[i][j].re - truth[i][j].re,
cv[i][j].im - truth[i][j].im,
));
if diff > TOL * scale {
return Err(MatrixExpRefusal {
reason: ExpRefusalReason::FailedVerification,
detail: format!(
"the closed-form e^A disagrees with e^A computed by scaling and \
squaring: at entry ({i}, {j}) the closed form is {} + {}i and the \
independent value is {} + {}i (matrix norm {scale:.6}). Refusing \
rather than returning a matrix that is not the exponential it \
claims to be",
cv[i][j].re, cv[i][j].im, truth[i][j].re, truth[i][j].im,
),
});
}
}
}
checked += 1;
}
if checked == 0 {
Ok(ExpCheck::Unevaluated)
} else {
Ok(ExpCheck::Confirmed { samples: checked })
}
}
fn eval_grid(m: &Matrix, pool: &ExprPool, env: &HashMap<ExprId, ComplexF64>) -> Option<Cm> {
let n = m.rows;
let mut out = Vec::with_capacity(n);
for i in 0..n {
let mut row = Vec::with_capacity(n);
for j in 0..n {
let v = eval_complex_f64(m.get(i, j), pool, env).ok()?;
if !cfinite(v) {
return None;
}
row.push(v);
}
out.push(row);
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::Domain;
fn ints(rows: &[&[i32]], pool: &ExprPool) -> Matrix {
Matrix::new(
rows.iter()
.map(|r| r.iter().map(|&v| pool.integer(v)).collect())
.collect(),
)
.expect("square")
}
#[test]
fn the_identity_of_the_zero_matrix_is_confirmed() {
let p = ExprPool::new();
let z = ints(&[&[0, 0], &[0, 0]], &p);
let out = confirm_matrix_exponential(&z, &Matrix::identity(2, &p), &p).unwrap();
assert!(out.is_confirmed(), "{out:?}");
}
#[test]
fn the_old_wrong_nilpotent_answer_is_refuted() {
let p = ExprPool::new();
let n = ints(&[&[0, 1], &[0, 0]], &p);
let err = confirm_matrix_exponential(&n, &Matrix::identity(2, &p), &p)
.expect_err("I is not exp([[0,1],[0,0]])");
assert_eq!(err.reason(), ExpRefusalReason::FailedVerification);
assert_eq!(
<MatrixExpRefusal as crate::errors::AlkahestError>::code(&err),
"E-LINALG-011"
);
}
#[test]
fn the_old_off_by_two_defective_answer_is_refuted() {
let p = ExprPool::new();
let a = ints(&[&[2, 1], &[0, 2]], &p);
let e2 = p.func("exp", vec![p.integer(2_i32)]);
let two_e2 = p.mul(vec![p.integer(2_i32), e2]);
let z = p.integer(0_i32);
let wrong = Matrix::new(vec![vec![e2, two_e2], vec![z, e2]]).unwrap();
let err = confirm_matrix_exponential(&a, &wrong, &p)
.expect_err("the off-diagonal is e², not 2e²");
assert_eq!(err.reason(), ExpRefusalReason::FailedVerification);
}
#[test]
fn an_unevaluable_candidate_is_no_information_not_a_refusal() {
let p = ExprPool::new();
let a = ints(&[&[1, 0], &[0, 2]], &p);
let opaque = p.func("zeta", vec![p.integer(3_i32)]);
let cand = Matrix::new(vec![vec![opaque, opaque], vec![opaque, opaque]]).unwrap();
let out = confirm_matrix_exponential(&a, &cand, &p).unwrap();
assert_eq!(out, ExpCheck::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 one = p.integer(1_i32);
let a = Matrix::new(vec![vec![pi, z], vec![z, z]]).unwrap();
let epi = p.func("exp", vec![pi]);
let cand = Matrix::new(vec![vec![epi, z], vec![z, one]]).unwrap();
let out = confirm_matrix_exponential(&a, &cand, &p).unwrap();
assert!(out.is_confirmed(), "π must be bound to 3.14159…: {out:?}");
}
}