use crate::kernel::{ExprId, ExprPool};
use crate::matrix::zero_test::{settled_nonzero, zero_status, ZeroStatus};
use crate::matrix::Matrix;
use crate::simplify::engine::{expand_powers, simplify_expanded};
fn simp(e: ExprId, pool: &ExprPool) -> ExprId {
simplify_expanded(e, pool).value
}
fn div(a: ExprId, b: ExprId, pool: &ExprPool) -> ExprId {
let inv_b = pool.pow(b, pool.integer(-1_i32));
simp(pool.mul(vec![a, inv_b]), pool)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Gap {
Confluent,
Distinct(ExprId),
}
pub(crate) trait GapPolicy {
fn classify(&mut self, lambda: ExprId, mu: ExprId, pool: &ExprPool) -> Gap;
}
#[derive(Default)]
pub(crate) struct RecordedGaps {
pub(crate) assumed_nonzero: Vec<ExprId>,
}
impl GapPolicy for RecordedGaps {
fn classify(&mut self, lambda: ExprId, mu: ExprId, pool: &ExprPool) -> Gap {
let d = normalised_gap(lambda, mu, pool);
if lambda == mu || matches!(zero_status(pool, d), ZeroStatus::Zero) {
return Gap::Confluent;
}
if !settled_nonzero(d, pool) && !self.assumed_nonzero.contains(&d) {
self.assumed_nonzero.push(d);
}
Gap::Distinct(d)
}
}
pub(crate) fn normalised_gap(lambda: ExprId, mu: ExprId, pool: &ExprPool) -> ExprId {
let neg_mu = pool.mul(vec![pool.integer(-1_i32), mu]);
expand_powers(simp(pool.add(vec![lambda, neg_mu]), pool), pool)
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct ExpTerm {
coeff: ExprId,
power: usize,
lambda: ExprId,
}
pub(crate) fn putzer_r(
lambdas: &[ExprId],
policy: &mut dyn GapPolicy,
pool: &ExprPool,
) -> Option<Vec<Vec<ExpTerm>>> {
let mut rs: Vec<Vec<ExpTerm>> = Vec::with_capacity(lambdas.len());
rs.push(vec![ExpTerm {
coeff: pool.integer(1_i32),
power: 0,
lambda: *lambdas.first()?,
}]);
for &mu in &lambdas[1..] {
let prev = rs.last().expect("r_1 was pushed before the loop").clone();
let mut next: Vec<ExpTerm> = Vec::new();
for term in prev {
let d = match policy.classify(term.lambda, mu, pool) {
Gap::Confluent => {
next.push(ExpTerm {
coeff: div(term.coeff, pool.integer((term.power + 1) as i64), pool),
power: term.power + 1,
lambda: mu,
});
continue;
}
Gap::Distinct(d) => d,
};
let j = term.power;
let mut falling = 1_i64; for i in 0..=j {
if i > 0 {
falling *= (j - i + 1) as i64;
}
let sign = if i % 2 == 0 { 1_i64 } else { -1_i64 };
let denom = pool.pow(d, pool.integer((i + 1) as i64));
let coeff = div(
pool.mul(vec![term.coeff, pool.integer(sign * falling)]),
denom,
pool,
);
next.push(ExpTerm {
coeff,
power: j - i,
lambda: term.lambda,
});
}
let jfact: i64 = (1..=(j as i64)).product::<i64>().max(1);
let sign = if j % 2 == 0 { -1_i64 } else { 1_i64 };
let denom = pool.pow(d, pool.integer((j + 1) as i64));
next.push(ExpTerm {
coeff: div(
pool.mul(vec![term.coeff, pool.integer(sign * jfact)]),
denom,
pool,
),
power: 0,
lambda: mu,
});
}
rs.push(merge(next, pool));
}
Some(rs)
}
fn merge(terms: Vec<ExpTerm>, pool: &ExprPool) -> Vec<ExpTerm> {
let mut out: Vec<ExpTerm> = Vec::new();
for t in terms {
if let Some(slot) = out
.iter_mut()
.find(|o| o.power == t.power && o.lambda == t.lambda)
{
slot.coeff = simp(pool.add(vec![slot.coeff, t.coeff]), pool);
} else {
out.push(t);
}
}
out.retain(|t| !matches!(zero_status(pool, t.coeff), ZeroStatus::Zero));
out
}
pub(crate) fn exp_poly_to_expr(terms: &[ExpTerm], t: ExprId, pool: &ExprPool) -> ExprId {
let mut sum = Vec::with_capacity(terms.len());
for term in terms {
let mut factors = vec![term.coeff];
if term.power > 0 {
factors.push(pool.pow(t, pool.integer(term.power as i64)));
}
if !matches!(zero_status(pool, term.lambda), ZeroStatus::Zero) {
let lt = simp(pool.mul(vec![term.lambda, t]), pool);
factors.push(pool.func("exp", vec![lt]));
}
sum.push(pool.mul(factors));
}
simp(pool.add(sum), pool)
}
pub(crate) fn matrix_exponential(
a: &Matrix,
lambdas: &[ExprId],
t: ExprId,
policy: &mut dyn GapPolicy,
pool: &ExprPool,
) -> Option<Matrix> {
let n = a.rows;
let rs = putzer_r(lambdas, policy, pool)?;
let mut p = Matrix::identity(n, pool);
let mut acc = Matrix::zeros(n, n, pool);
for (k, r) in rs.iter().enumerate() {
let r_expr = exp_poly_to_expr(r, t, pool);
for i in 0..n {
for j in 0..n {
acc.set(
i,
j,
pool.add(vec![acc.get(i, j), pool.mul(vec![r_expr, p.get(i, j)])]),
);
}
}
if k + 1 < n {
p = mat_mul_shift(a, &p, lambdas[k], pool);
}
}
for i in 0..n {
for j in 0..n {
acc.set(i, j, simp(acc.get(i, j), pool));
}
}
Some(acc)
}
fn mat_mul_shift(a: &Matrix, p: &Matrix, lam: ExprId, pool: &ExprPool) -> Matrix {
let n = a.rows;
let mut out = Matrix::zeros(n, n, pool);
for i in 0..n {
for j in 0..n {
let mut terms = Vec::with_capacity(n + 1);
for k in 0..n {
let mut aik = a.get(i, k);
if i == k {
aik = pool.add(vec![aik, pool.mul(vec![pool.integer(-1_i32), lam])]);
}
terms.push(pool.mul(vec![aik, p.get(k, j)]));
}
out.set(i, j, simp(pool.add(terms), pool));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::eval::{eval_complex_f64, ComplexF64};
use std::collections::HashMap;
fn num(m: &Matrix, i: usize, j: usize, pool: &ExprPool) -> ComplexF64 {
eval_complex_f64(m.get(i, j), pool, &HashMap::new()).expect("numeric entry")
}
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 defective_block_gets_the_t_e_lambda_t_entry() {
let p = ExprPool::new();
let a = ints(&[&[2, 1], &[0, 2]], &p);
let two = p.integer(2_i32);
let mut policy = RecordedGaps::default();
let e = matrix_exponential(&a, &[two, two], p.integer(1_i32), &mut policy, &p)
.expect("rational spectrum");
let e2 = std::f64::consts::E * std::f64::consts::E;
assert!((num(&e, 0, 0, &p).re - e2).abs() < 1e-12);
assert!(
(num(&e, 0, 1, &p).re - e2).abs() < 1e-12,
"{:?}",
num(&e, 0, 1, &p)
);
assert!(num(&e, 1, 0, &p).re.abs() < 1e-12);
assert!((num(&e, 1, 1, &p).re - e2).abs() < 1e-12);
assert!(
policy.assumed_nonzero.is_empty(),
"a repeated rational eigenvalue is the confluent branch; nothing is divided by"
);
}
#[test]
fn a_settled_gap_records_no_hypothesis() {
let p = ExprPool::new();
let a = ints(&[&[1, 0], &[0, 2]], &p);
let mut policy = RecordedGaps::default();
matrix_exponential(
&a,
&[p.integer(1_i32), p.integer(2_i32)],
p.integer(1_i32),
&mut policy,
&p,
)
.expect("distinct rational spectrum");
assert!(policy.assumed_nonzero.is_empty());
}
#[test]
fn an_undecided_gap_is_recorded_rather_than_assumed_silently() {
let p = ExprPool::new();
let a = p.symbol("a", crate::kernel::Domain::Real);
let b = p.symbol("b", crate::kernel::Domain::Real);
let z = p.integer(0_i32);
let m = Matrix::new(vec![vec![a, p.integer(1_i32)], vec![z, b]]).unwrap();
let mut policy = RecordedGaps::default();
matrix_exponential(&m, &[a, b], p.integer(1_i32), &mut policy, &p)
.expect("the generic branch");
assert_eq!(
policy.assumed_nonzero.len(),
1,
"a − b was divided by and is not settled non-zero"
);
}
}