use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Signed, ToPrimitive, Zero};
use crate::base::arena::Arena;
use crate::base::errors::SymplexError;
use crate::base::node::{ExprId, ExprNode, SymbolId};
use crate::base::numeric::Q;
use crate::prelude::*;
pub(crate) fn z_transform(
arena: &mut Arena,
expr: ExprId,
n_var: ExprId,
z_var: ExprId,
) -> Result<ExprId, SymplexError> {
let n_sym = match arena.node(n_var) {
ExprNode::Symbol(sid) => *sid,
_ => {
return Err(SymplexError::ComputationFailed {
operation: "z_transform",
reason: "n must be a symbol".to_string(),
});
}
};
match arena.node(z_var) {
ExprNode::Symbol(_) => {}
_ => {
return Err(SymplexError::ComputationFailed {
operation: "z_transform",
reason: "z must be a symbol".to_string(),
});
}
};
do_forward(arena, expr, n_var, n_sym, z_var)
}
fn do_forward(
arena: &mut Arena,
expr: ExprId,
n_var: ExprId,
n_sym: SymbolId,
z_var: ExprId,
) -> Result<ExprId, SymplexError> {
let node = arena.node(expr).clone();
if let ExprNode::Add(ref children) = node {
let kids = children.clone();
let mut terms = Vec::with_capacity(kids.len());
for child in kids {
terms.push(do_forward(arena, child, n_var, n_sym, z_var)?);
}
return Ok(arena.add(&terms));
}
if let ExprNode::Neg(inner) = node {
let transformed = do_forward(arena, inner, n_var, n_sym, z_var)?;
return Ok(arena.neg(transformed));
}
let (coeff, body) = split_independent(arena, expr, n_var);
if coeff != arena.one {
let transformed = do_forward(arena, body, n_var, n_sym, z_var)?;
return Ok(arena.mul(&[coeff, transformed]));
}
if let Some(result) = try_table_forward(arena, expr, n_var, n_sym, z_var) {
return Ok(result);
}
if let Some(result) = try_extended_forward(arena, expr, n_var, n_sym, z_var)? {
return Ok(result);
}
if let Some(result) = try_piecewise_sequence(arena, expr, n_var, n_sym, z_var)? {
return Ok(result);
}
if let Some(result) = try_step_shift(arena, expr, n_var, n_sym, z_var)? {
return Ok(result);
}
Err(SymplexError::ComputationFailed {
operation: "z_transform",
reason: format!("cannot transform {}", arena.display(expr)),
})
}
type IntSet = Vec<(i64, Option<i64>)>;
const MAX_WINDOW_TERMS: i64 = 4096;
fn set_normalize(mut s: IntSet) -> IntSet {
s.retain(|&(a, b)| b.is_none_or(|b| b >= a));
s.sort_by_key(|&(a, _)| a);
let mut out: IntSet = Vec::new();
for (a, b) in s {
if let Some(last) = out.last_mut() {
let reach = last.1;
if reach.is_none_or(|r| a <= r.saturating_add(1)) {
last.1 = match (reach, b) {
(None, _) | (_, None) => None,
(Some(r), Some(b)) => Some(r.max(b)),
};
continue;
}
}
out.push((a, b));
}
out
}
fn set_complement(s: &IntSet) -> IntSet {
let mut out = Vec::new();
let mut next = 0i64;
for &(a, b) in s {
if a > next {
out.push((next, Some(a - 1)));
}
match b {
Some(b) => next = b.saturating_add(1),
None => return set_normalize(out),
}
}
out.push((next, None));
set_normalize(out)
}
fn set_intersect(x: &IntSet, y: &IntSet) -> IntSet {
let mut out = Vec::new();
for &(a1, b1) in x {
for &(a2, b2) in y {
let a = a1.max(a2);
let b = match (b1, b2) {
(None, b) | (b, None) => b,
(Some(p), Some(q)) => Some(p.min(q)),
};
out.push((a, b));
}
}
set_normalize(out)
}
fn condition_set(arena: &mut Arena, cond: ExprId, n_var: ExprId) -> Option<IntSet> {
let post = crate::base::walk::post_order_ids(arena, cond);
let mut sets: rustc_hash::FxHashMap<ExprId, IntSet> = rustc_hash::FxHashMap::default();
for id in post {
let s = match arena.node(id).clone() {
ExprNode::BoolTrue => vec![(0, None)],
ExprNode::BoolFalse => vec![],
ExprNode::Gt(a, b) | ExprNode::Ge(a, b) | ExprNode::Eq_(a, b) | ExprNode::Ne(a, b) => {
let kind = arena.node(id).clone();
let d = arena.sub(a, b);
let d = crate::transforms::eval::eval(arena, d);
let (al, be) = linear_in(arena, d, n_var)?;
let al = arena.as_num(al).cloned()?;
let be = arena.as_num(be).cloned()?;
if al.is_zero() {
return None;
}
let theta = -be / al.clone();
let fl = theta.floor().to_integer().to_i64()?;
let ce = theta.ceil().to_integer().to_i64()?;
let integral = theta.is_integer();
let above_strict = vec![(fl.saturating_add(1).max(0), None)]; let above = vec![(ce.max(0), None)]; let below_strict = vec![(0, Some(ce - 1))]; let below = vec![(0, Some(fl))]; let point = if integral && fl >= 0 {
vec![(fl, Some(fl))]
} else {
vec![]
};
let up = al.is_positive();
let raw = match kind {
ExprNode::Gt(..) => {
if up {
above_strict
} else {
below_strict
}
}
ExprNode::Ge(..) => {
if up {
above
} else {
below
}
}
ExprNode::Eq_(..) => point,
_ => set_complement(&set_normalize(point)),
};
set_normalize(raw)
}
ExprNode::And(ch) => {
let mut acc: IntSet = vec![(0, None)];
for c in ch.iter() {
acc = set_intersect(&acc, sets.get(c)?);
}
acc
}
ExprNode::Or(ch) => {
let mut acc: IntSet = Vec::new();
for c in ch.iter() {
acc.extend(sets.get(c)?.iter().copied());
}
set_normalize(acc)
}
ExprNode::Not(c) => set_complement(sets.get(&c)?),
_ => continue,
};
sets.insert(id, s);
}
sets.remove(&cond)
}
fn try_piecewise_sequence(
arena: &mut Arena,
expr: ExprId,
n_var: ExprId,
n_sym: SymbolId,
z_var: ExprId,
) -> Result<Option<ExprId>, SymplexError> {
let factors: Vec<ExprId> = match arena.node(expr) {
ExprNode::Mul(ch) => ch.to_vec(),
_ => vec![expr],
};
let mut pw: Option<smallvec::SmallVec<[(ExprId, ExprId); 3]>> = None;
let mut others = Vec::new();
for f in factors {
match arena.node(f).clone() {
ExprNode::Piecewise(pairs) if pw.is_none() && contains_var(arena, f, n_var) => {
pw = Some(pairs);
}
_ => others.push(f),
}
}
let Some(pairs) = pw else {
return Ok(None);
};
let g = if others.is_empty() {
arena.one
} else {
arena.mul(&others)
};
let mut remaining: IntSet = vec![(0, None)];
let mut terms = Vec::new();
for (val, cond) in pairs {
let Some(cs) = condition_set(arena, cond, n_var) else {
return Ok(None);
};
let here = set_intersect(&remaining, &cs);
remaining = set_intersect(&remaining, &set_complement(&cs));
let gv = arena.mul(&[g, val]);
for (a, b) in here {
match b {
Some(b) => {
if b - a >= MAX_WINDOW_TERMS {
return Ok(None);
}
for j in a..=b {
let jn = arena.int(j);
let term = crate::transforms::subs::subs(arena, gv, n_var, jn);
let term = crate::transforms::eval::eval(arena, term);
let zj = arena.int(-j);
let zp = arena.pow(z_var, zj);
terms.push(arena.mul(&[term, zp]));
}
}
None => {
let h = substitute_shift(arena, gv, n_var, a);
let hz = do_forward(arena, h, n_var, n_sym, z_var)?;
let za = arena.int(-a);
let zp = arena.pow(z_var, za);
terms.push(arena.mul(&[zp, hz]));
}
}
}
}
let total = arena.add(&terms);
Ok(Some(crate::transforms::eval::eval(arena, total)))
}
fn substitute_shift(arena: &mut Arena, g: ExprId, n_var: ExprId, a: i64) -> ExprId {
let an = arena.int(a);
let shifted_n = arena.add(&[n_var, an]);
let h = crate::transforms::subs::subs(arena, g, n_var, shifted_n);
let h = crate::transforms::eval::eval(arena, h);
let post = crate::base::walk::post_order_ids(arena, h);
let mut cache: rustc_hash::FxHashMap<ExprId, ExprId> = rustc_hash::FxHashMap::default();
for id in post {
let rebuilt = crate::base::walk::rebuild_with_cache(arena, id, &cache);
let new = match arena.node(rebuilt).clone() {
ExprNode::Pow(base, exp) if !contains_var(arena, base, n_var) => {
match arena.node(exp).clone() {
ExprNode::Add(terms) => {
let mut c = Q::zero();
let mut rest = Vec::new();
for t in terms.iter() {
match arena.as_num(*t) {
Some(q) if q.is_integer() => c += q.clone(),
_ => rest.push(*t),
}
}
if c.is_zero() || rest.is_empty() {
rebuilt
} else {
let r = arena.add(&rest);
let ce = arena.intern_num(c);
let ce = arena.intern(ExprNode::Num(ce));
let p1 = arena.pow(base, ce);
let p2 = arena.pow(base, r);
arena.mul(&[p1, p2])
}
}
_ => rebuilt,
}
}
_ => rebuilt,
};
cache.insert(id, new);
}
let out = cache.get(&h).copied().unwrap_or(h);
crate::transforms::eval::eval(arena, out)
}
fn try_step_shift(
arena: &mut Arena,
expr: ExprId,
n_var: ExprId,
n_sym: SymbolId,
z_var: ExprId,
) -> Result<Option<ExprId>, SymplexError> {
let ExprNode::Mul(ch) = arena.node(expr).clone() else {
return Ok(None);
};
let mut step: Option<(usize, i64)> = None;
for (i, &f) in ch.iter().enumerate() {
if let ExprNode::Heaviside(arg) = arena.node(f).clone()
&& let Some((a, b)) = linear_in(arena, arg, n_var)
&& arena.as_num(a).is_some_and(|q| q.is_one())
&& let Some(bq) = arena.as_num(b).cloned()
{
let m = (-bq).ceil().to_integer().to_i64();
if let Some(m) = m {
step = Some((i, m.max(0)));
break;
}
}
}
let Some((idx, m)) = step else {
return Ok(None);
};
let rest: Vec<ExprId> = ch
.iter()
.enumerate()
.filter(|&(j, _)| j != idx)
.map(|(_, &f)| f)
.collect();
let g = if rest.is_empty() {
arena.one
} else {
arena.mul(&rest)
};
let h = substitute_shift(arena, g, n_var, m);
if h == expr {
return Ok(None);
}
let hz = do_forward(arena, h, n_var, n_sym, z_var)?;
let zm = arena.int(-m);
let zp = arena.pow(z_var, zm);
Ok(Some(arena.mul(&[zp, hz])))
}
fn negative_integer_exponent(arena: &Arena, e: ExprId) -> Option<num_bigint::BigInt> {
let r = arena.as_num(e)?;
(r.is_integer() && r.is_negative()).then(|| r.to_integer())
}
fn linear_in(arena: &mut Arena, expr: ExprId, n_var: ExprId) -> Option<(ExprId, ExprId)> {
if expr == n_var {
return Some((arena.one, arena.zero));
}
if !contains_var(arena, expr, n_var) {
return Some((arena.zero, expr));
}
match arena.node(expr).clone() {
ExprNode::Neg(inner) => {
let (a, b) = linear_in(arena, inner, n_var)?;
Some((arena.neg(a), arena.neg(b)))
}
ExprNode::Mul(children) => {
let mut coeff = Vec::new();
let mut seen = false;
for &c in &children {
if c == n_var {
if seen {
return None;
}
seen = true;
} else if contains_var(arena, c, n_var) {
return None;
} else {
coeff.push(c);
}
}
if !seen {
return None;
}
let a = if coeff.is_empty() {
arena.one
} else {
arena.mul(&coeff)
};
Some((a, arena.zero))
}
ExprNode::Add(children) => {
let mut a_terms = Vec::new();
let mut b_terms = Vec::new();
for &c in &children {
let (a, b) = linear_in(arena, c, n_var)?;
if !arena.is_zero_structural(a) {
a_terms.push(a);
}
if !arena.is_zero_structural(b) {
b_terms.push(b);
}
}
let a = arena.add(&a_terms);
let b = arena.add(&b_terms);
Some((a, b))
}
_ => None,
}
}
fn shift_of(arena: &mut Arena, arg: ExprId, n_var: ExprId) -> Option<u64> {
let (a, b) = linear_in(arena, arg, n_var)?;
if a != arena.one {
return None;
}
let b = arena.as_num(b)?.clone();
let half = Ratio::new(BigInt::from(1), BigInt::from(2));
let k = if b.is_integer() { -b } else { half - b };
if !k.is_integer() || k.is_negative() {
return None;
}
k.to_integer().try_into().ok()
}
fn kronecker(arena: &mut Arena, n_var: ExprId, k: i64) -> ExprId {
let k_id = arena.int(k);
arena.kronecker_delta(n_var, k_id)
}
fn discrete_step(arena: &mut Arena, n_var: ExprId, k: i64) -> ExprId {
let off = arena.rational(1 - 2 * k, 2);
let arg = arena.add(&[n_var, off]);
arena.heaviside(arg)
}
fn zfail(reason: impl Into<String>) -> SymplexError {
SymplexError::ComputationFailed {
operation: "z_transform",
reason: reason.into(),
}
}
fn try_extended_forward(
arena: &mut Arena,
expr: ExprId,
n_var: ExprId,
n_sym: SymbolId,
z_var: ExprId,
) -> Result<Option<ExprId>, SymplexError> {
let one = arena.one;
match arena.node(expr).clone() {
ExprNode::DiracDelta(arg) => {
let Some(k) = shift_of(arena, arg, n_var) else {
return Ok(None);
};
let neg_k = arena.int(-(k as i64));
Ok(Some(arena.pow(z_var, neg_k)))
}
ExprNode::KroneckerDelta(i, j) => {
let k = if i == n_var {
j
} else if j == n_var {
i
} else {
return Ok(None);
};
let Some(k) = arena.as_num(k).cloned() else {
return Ok(None);
};
if !k.is_integer() || k.is_negative() {
return Ok(None);
}
let neg_k = rational_to_expr(arena, &-k);
Ok(Some(arena.pow(z_var, neg_k)))
}
ExprNode::Heaviside(arg) => {
let Some(k) = shift_of(arena, arg, n_var) else {
return Ok(None);
};
let one_minus_k = arena.int(1 - k as i64);
let zp = arena.pow(z_var, one_minus_k);
let z_minus_1 = arena.sub(z_var, one);
Ok(Some(arena.div(zp, z_minus_1)))
}
ExprNode::Binomial(top, k) if top == n_var && !contains_var(arena, k, n_var) => {
let k_p1 = arena.add(&[k, one]);
let z_minus_1 = arena.sub(z_var, one);
let den = arena.pow(z_minus_1, k_p1);
Ok(Some(arena.div(z_var, den)))
}
ExprNode::Pow(base, e) if e == arena.neg_one => {
if let ExprNode::Factorial(arg) = arena.node(base).clone()
&& arg == n_var
{
let inv_z = arena.div(one, z_var);
return Ok(Some(arena.exp(inv_z)));
}
Ok(None)
}
ExprNode::Pow(base, e) if base == n_var => {
let Some(k) = arena.as_num(e).cloned() else {
return Ok(None);
};
if !k.is_integer() || !k.is_positive() {
return Ok(None);
}
let k: u32 = k
.to_integer()
.try_into()
.map_err(|_| zfail("power too large"))?;
let z_minus_1 = arena.sub(z_var, one);
let two = arena.int(2);
let den = arena.pow(z_minus_1, two);
let mut x = arena.div(z_var, den); for _ in 1..k {
x = neg_z_derivative(arena, x, z_var);
}
Ok(Some(x))
}
ExprNode::Mul(children) => {
let kids: Vec<ExprId> = children.iter().copied().collect();
for (i, &c) in kids.iter().enumerate() {
if let ExprNode::Heaviside(arg) = arena.node(c).clone()
&& let Some(k) = shift_of(arena, arg, n_var)
{
let rest: Vec<ExprId> = kids
.iter()
.enumerate()
.filter(|&(j, _)| j != i)
.map(|(_, &c)| c)
.collect();
let g = if rest.is_empty() {
one
} else {
arena.mul(&rest)
};
let g_shifted = substitute_shift(arena, g, n_var, k as i64);
let gz = do_forward(arena, g_shifted, n_var, n_sym, z_var)?;
let neg_k = arena.int(-(k as i64));
let zk = arena.pow(z_var, neg_k);
return Ok(Some(arena.mul(&[zk, gz])));
}
}
let mut n_power: u32 = 0;
let mut rest: Vec<ExprId> = Vec::new();
for &c in &kids {
if c == n_var {
n_power += 1;
} else if let ExprNode::Pow(base, e) = arena.node(c).clone()
&& base == n_var
&& let Some(k) = arena.as_num(e).cloned()
&& k.is_integer()
&& k.is_positive()
{
let k: u32 = k
.to_integer()
.try_into()
.map_err(|_| zfail("power too large"))?;
n_power += k;
} else {
rest.push(c);
}
}
if n_power > 0 && !rest.is_empty() {
let g = arena.mul(&rest);
let mut x = do_forward(arena, g, n_var, n_sym, z_var)?;
for _ in 0..n_power {
x = neg_z_derivative(arena, x, z_var);
}
return Ok(Some(x));
}
for (i, &c) in kids.iter().enumerate() {
if let ExprNode::Pow(base, e) = arena.node(c).clone()
&& e == n_var
&& !contains_var(arena, base, n_var)
{
let rest: Vec<ExprId> = kids
.iter()
.enumerate()
.filter(|&(j, _)| j != i)
.map(|(_, &c)| c)
.collect();
if rest.is_empty() {
return Ok(None);
}
let g = arena.mul(&rest);
let gz = do_forward(arena, g, n_var, n_sym, z_var)?;
let z_over_a = arena.div(z_var, base);
let scaled = crate::transforms::subs::subs(arena, gz, z_var, z_over_a);
return Ok(Some(scaled));
}
}
Ok(None)
}
_ => Ok(None),
}
}
fn neg_z_derivative(arena: &mut Arena, x: ExprId, z_var: ExprId) -> ExprId {
let (num, den) = crate::poly::polybridge::as_numer_denom(arena, x);
let (base, m) = match arena.node(den).clone() {
ExprNode::Pow(b, e)
if arena
.as_num(e)
.is_some_and(|r| r.is_integer() && r.is_positive()) =>
{
(b, e)
}
_ => (den, arena.one),
};
if den != arena.one && linear_in(arena, base, z_var).is_some() {
let n_prime = crate::transforms::diff::diff(arena, num, z_var);
let b_prime = crate::transforms::diff::diff(arena, base, z_var);
let t1 = arena.mul(&[n_prime, base]);
let t2 = arena.mul(&[m, b_prime, num]);
let diff = arena.sub(t1, t2);
let new_num = arena.mul(&[z_var, diff]);
let new_num = arena.neg(new_num);
let new_num = crate::transforms::expand::expand(arena, new_num);
let new_num = crate::transforms::eval::eval(arena, new_num);
let m_p1 = arena.add(&[m, arena.one]);
let new_den = arena.pow(base, m_p1);
return arena.div(new_num, new_den);
}
let d = crate::transforms::diff::diff(arena, x, z_var);
let zd = arena.mul(&[z_var, d]);
let r = arena.neg(zd);
let r = crate::poly::polybridge::together(arena, r);
let r = crate::poly::polybridge::cancel(arena, r, z_var);
crate::transforms::eval::eval(arena, r)
}
fn contains_var(arena: &Arena, expr: ExprId, var: ExprId) -> bool {
crate::base::walk::has_free_var(arena, expr, var)
}
fn split_independent(arena: &mut Arena, expr: ExprId, var: ExprId) -> (ExprId, ExprId) {
let node = arena.node(expr).clone();
if let ExprNode::Mul(ref children) = node {
let mut indep = Vec::new();
let mut dep = Vec::new();
for &child in children {
if contains_var(arena, child, var) {
dep.push(child);
} else {
indep.push(child);
}
}
if !indep.is_empty() && !dep.is_empty() {
let coeff = if indep.len() == 1 {
indep[0]
} else {
arena.mul(&indep)
};
let body = if dep.len() == 1 {
dep[0]
} else {
arena.mul(&dep)
};
return (coeff, body);
}
}
(arena.one, expr)
}
fn extract_linear_coeff(arena: &mut Arena, expr: ExprId, n_var: ExprId) -> Option<ExprId> {
if expr == n_var {
return Some(arena.one);
}
let node = arena.node(expr).clone();
if let ExprNode::Mul(ref children) = node {
let mut n_count = 0usize;
let mut others = Vec::new();
for &child in children {
if child == n_var {
n_count += 1;
} else if contains_var(arena, child, n_var) {
return None;
} else {
others.push(child);
}
}
if n_count == 1 {
if others.is_empty() {
return Some(arena.one);
} else if others.len() == 1 {
return Some(others[0]);
} else {
return Some(arena.mul(&others));
}
}
}
let poly = crate::poly::polybridge::expr_to_poly(arena, expr, n_var)?;
if poly.degree()? != 1 {
return None;
}
let c0 = poly.coeff(0);
if !c0.is_zero() {
return None;
}
let a = poly.coeff(1);
if a.is_zero() {
return None;
}
let nid = arena.intern_num(a);
Some(arena.intern(ExprNode::Num(nid)))
}
fn rational_to_expr(arena: &mut Arena, r: &Q) -> ExprId {
let nid = arena.intern_num(r.clone());
arena.intern(ExprNode::Num(nid))
}
fn build_quadratic_denom(
arena: &mut Arena,
z_var: ExprId,
two_coeff_z: ExprId,
constant_term: ExprId,
) -> ExprId {
let z_sq = arena.mul(&[z_var, z_var]);
let neg_mid = arena.neg(two_coeff_z);
arena.add(&[z_sq, neg_mid, constant_term])
}
fn try_table_forward(
arena: &mut Arena,
expr: ExprId,
n_var: ExprId,
n_sym: SymbolId,
z_var: ExprId,
) -> Option<ExprId> {
let node = arena.node(expr).clone();
if !contains_var(arena, expr, n_var) {
let numer = arena.mul(&[expr, z_var]);
let z_minus_1 = arena.sub(z_var, arena.one);
return Some(arena.div(numer, z_minus_1));
}
match node {
ExprNode::Symbol(sid) if sid == n_sym => {
let z_minus_1 = arena.sub(z_var, arena.one);
let two = arena.int(2);
let denom = arena.pow(z_minus_1, two);
Some(arena.div(z_var, denom))
}
ExprNode::Pow(base, exp) if exp == n_var && !contains_var(arena, base, n_var) => {
let z_minus_a = arena.sub(z_var, base);
Some(arena.div(z_var, z_minus_a))
}
ExprNode::Sin(arg) => {
if let Some(omega) = extract_linear_coeff(arena, arg, n_var) {
let sin_omega = arena.sin(omega);
let cos_omega = arena.cos(omega);
let two = arena.int(2);
let numer = arena.mul(&[z_var, sin_omega]);
let two_z_cos = arena.mul(&[two, z_var, cos_omega]);
let one = arena.one;
let denom = build_quadratic_denom(arena, z_var, two_z_cos, one);
return Some(arena.div(numer, denom));
}
None
}
ExprNode::Cos(arg) => {
if let Some(omega) = extract_linear_coeff(arena, arg, n_var) {
let cos_omega = arena.cos(omega);
let two = arena.int(2);
let z_minus_cos = arena.sub(z_var, cos_omega);
let numer = arena.mul(&[z_var, z_minus_cos]);
let two_z_cos = arena.mul(&[two, z_var, cos_omega]);
let one = arena.one;
let denom = build_quadratic_denom(arena, z_var, two_z_cos, one);
return Some(arena.div(numer, denom));
}
None
}
ExprNode::Mul(ref children) => {
let kids = children.clone();
try_mul_patterns(arena, &kids, n_var, n_sym, z_var)
}
_ => None,
}
}
fn try_mul_patterns(
arena: &mut Arena,
children: &[ExprId],
n_var: ExprId,
n_sym: SymbolId,
z_var: ExprId,
) -> Option<ExprId> {
let mut pow_base: Option<ExprId> = None;
let mut has_n = false;
let mut trig: Option<(TrigKind, ExprId)> = None; let mut constants: Vec<ExprId> = Vec::new();
let mut unmatched = false;
for &child in children {
let cnode = arena.node(child).clone();
match cnode {
ExprNode::Symbol(sid) if sid == n_sym => {
if has_n {
unmatched = true;
break;
}
has_n = true;
}
ExprNode::Pow(base, exp) if exp == n_var && !contains_var(arena, base, n_var) => {
if pow_base.is_some() {
unmatched = true;
break;
}
pow_base = Some(base);
}
ExprNode::Sin(arg) => {
if let Some(omega) = extract_linear_coeff(arena, arg, n_var) {
if trig.is_some() {
unmatched = true;
break;
}
trig = Some((TrigKind::Sin, omega));
} else {
unmatched = true;
break;
}
}
ExprNode::Cos(arg) => {
if let Some(omega) = extract_linear_coeff(arena, arg, n_var) {
if trig.is_some() {
unmatched = true;
break;
}
trig = Some((TrigKind::Cos, omega));
} else {
unmatched = true;
break;
}
}
_ if !contains_var(arena, child, n_var) => {
constants.push(child);
}
_ => {
unmatched = true;
break;
}
}
}
if unmatched {
return None;
}
let const_factor = if constants.is_empty() {
arena.one
} else if constants.len() == 1 {
constants[0]
} else {
arena.mul(&constants)
};
if has_n
&& let Some(a) = pow_base
&& trig.is_none()
{
let z_minus_a = arena.sub(z_var, a);
let two = arena.int(2);
let denom = arena.pow(z_minus_a, two);
let numer = arena.mul(&[a, z_var]);
let result = arena.div(numer, denom);
if const_factor != arena.one {
return Some(arena.mul(&[const_factor, result]));
}
return Some(result);
}
if has_n && pow_base.is_none() && trig.is_none() {
let z_minus_1 = arena.sub(z_var, arena.one);
let two = arena.int(2);
let denom = arena.pow(z_minus_1, two);
let result = arena.div(z_var, denom);
if const_factor != arena.one {
return Some(arena.mul(&[const_factor, result]));
}
return Some(result);
}
if let Some((TrigKind::Sin, omega)) = trig
&& !has_n
{
let a = pow_base.unwrap_or(arena.one);
let sin_omega = arena.sin(omega);
let cos_omega = arena.cos(omega);
let two = arena.int(2);
let numer = arena.mul(&[a, z_var, sin_omega]);
let two_az_cos = arena.mul(&[two, a, z_var, cos_omega]);
let a_sq = arena.mul(&[a, a]);
let denom = build_quadratic_denom(arena, z_var, two_az_cos, a_sq);
let result = arena.div(numer, denom);
if const_factor != arena.one {
return Some(arena.mul(&[const_factor, result]));
}
return Some(result);
}
if let Some((TrigKind::Cos, omega)) = trig
&& !has_n
{
let a = pow_base.unwrap_or(arena.one);
let cos_omega = arena.cos(omega);
let two = arena.int(2);
let a_cos = arena.mul(&[a, cos_omega]);
let z_minus_a_cos = arena.sub(z_var, a_cos);
let numer = arena.mul(&[z_var, z_minus_a_cos]);
let two_az_cos = arena.mul(&[two, a, z_var, cos_omega]);
let a_sq = arena.mul(&[a, a]);
let denom = build_quadratic_denom(arena, z_var, two_az_cos, a_sq);
let result = arena.div(numer, denom);
if const_factor != arena.one {
return Some(arena.mul(&[const_factor, result]));
}
return Some(result);
}
None
}
#[derive(Clone, Copy)]
enum TrigKind {
Sin,
Cos,
}
pub(crate) fn inverse_z_transform(
arena: &mut Arena,
expr: ExprId,
z_var: ExprId,
n_var: ExprId,
) -> Result<ExprId, SymplexError> {
match arena.node(z_var) {
ExprNode::Symbol(_) => {}
_ => {
return Err(SymplexError::ComputationFailed {
operation: "inverse_z_transform",
reason: "z must be a symbol".to_string(),
});
}
};
match arena.node(n_var) {
ExprNode::Symbol(_) => {}
_ => {
return Err(SymplexError::ComputationFailed {
operation: "inverse_z_transform",
reason: "n must be a symbol".to_string(),
});
}
};
do_inverse(arena, expr, z_var, n_var, 0)
}
fn do_inverse(
arena: &mut Arena,
expr: ExprId,
z_var: ExprId,
n_var: ExprId,
depth: u32,
) -> Result<ExprId, SymplexError> {
let ruled = do_inverse_rules(arena, expr, z_var, n_var, depth);
if ruled.is_ok() {
return ruled;
}
match inverse_rational_exact(arena, expr, z_var, n_var) {
Some(x) => Ok(x),
None => ruled,
}
}
fn inverse_rational_exact(
arena: &mut Arena,
expr: ExprId,
z_var: ExprId,
n_var: ExprId,
) -> Option<ExprId> {
let (numer, denom) = crate::poly::polybridge::as_numer_denom(arena, expr);
let np = crate::poly::polybridge::expr_to_poly(arena, numer, z_var)?;
let dp = crate::poly::polybridge::expr_to_poly(arena, denom, z_var)?;
crate::transforms::rsolve::rational_inverse_into(arena, &np, &dp, n_var, false)
}
fn do_inverse_rules(
arena: &mut Arena,
expr: ExprId,
z_var: ExprId,
n_var: ExprId,
depth: u32,
) -> Result<ExprId, SymplexError> {
if depth > 20 {
return Err(SymplexError::ComputationFailed {
operation: "inverse_z_transform",
reason: "recursion limit reached".to_string(),
});
}
let node = arena.node(expr).clone();
if let ExprNode::Add(ref children) = node {
let kids = children.clone();
let mut terms = Vec::with_capacity(kids.len());
for child in kids {
terms.push(do_inverse(arena, child, z_var, n_var, depth + 1)?);
}
return Ok(arena.add(&terms));
}
if let ExprNode::Neg(inner) = node {
let result = do_inverse(arena, inner, z_var, n_var, depth + 1)?;
return Ok(arena.neg(result));
}
if !contains_var(arena, expr, z_var) {
let d = kronecker(arena, n_var, 0);
return Ok(arena.mul(&[expr, d]));
}
let (coeff, body) = split_independent(arena, expr, z_var);
if coeff != arena.one {
let result = do_inverse(arena, body, z_var, n_var, depth + 1)?;
return Ok(arena.mul(&[coeff, result]));
}
if let Some(result) = try_trig_inverse_general(arena, expr, z_var, n_var) {
return Ok(result);
}
if let Some(result) = try_extended_inverse(arena, expr, z_var, n_var, depth)? {
return Ok(result);
}
if let Some(result) = try_table_inverse(arena, expr, z_var, n_var) {
return Ok(result);
}
if matches!(arena.node(expr), ExprNode::Mul(_)) {
let expanded = crate::transforms::expand::expand(arena, expr);
if expanded != expr && matches!(arena.node(expanded), ExprNode::Add(_)) {
return do_inverse(arena, expanded, z_var, n_var, depth + 1);
}
}
let decomposed = crate::transforms::apart::apart(arena, expr, z_var);
if decomposed != expr {
return do_inverse(arena, decomposed, z_var, n_var, depth + 1);
}
Err(SymplexError::ComputationFailed {
operation: "inverse_z_transform",
reason: format!("cannot invert {}", arena.display(expr)),
})
}
fn try_trig_inverse_general(
arena: &mut Arena,
expr: ExprId,
z_var: ExprId,
n_var: ExprId,
) -> Option<ExprId> {
let (numer, denom) = crate::poly::polybridge::as_numer_denom(arena, expr);
let one = arena.one;
let two = arena.int(2);
let z2 = arena.pow(z_var, two);
let ExprNode::Add(terms) = arena.node(denom).clone() else {
return None;
};
if terms.len() != 3 || !terms.contains(&z2) {
return None;
}
let mut lin: Option<ExprId> = None;
let mut q: Option<ExprId> = None;
for &t in &terms {
if t == z2 {
continue;
}
if !contains_var(arena, t, z_var) {
q = Some(t);
} else if let Some((c, b)) = linear_in(arena, t, z_var)
&& arena.is_zero_structural(b)
{
lin = Some(c);
} else {
return None;
}
}
let (c, q) = (lin?, q?);
let factors: Vec<ExprId> = match arena.node(c).clone() {
ExprNode::Mul(ch) => ch.iter().copied().collect(),
ExprNode::Neg(inner) => {
let mut v = vec![arena.neg_one];
match arena.node(inner).clone() {
ExprNode::Mul(ch) => v.extend(ch.iter().copied()),
_ => v.push(inner),
}
v
}
_ => return None,
};
let mut omega: Option<ExprId> = None;
let mut rest: Vec<ExprId> = Vec::new();
for &f in &factors {
if let ExprNode::Cos(w) = arena.node(f).clone()
&& omega.is_none()
{
omega = Some(w);
} else {
rest.push(f);
}
}
let omega = omega?;
let rest_prod = arena.mul(&rest);
let neg_two = arena.int(-2);
let a = arena.div(rest_prod, neg_two);
let a = crate::transforms::eval::eval(arena, a);
let a2 = arena.pow(a, two);
let a2 = crate::transforms::eval::eval(arena, a2);
let q_e = crate::transforms::eval::eval(arena, q);
if a2 != q_e {
let d = arena.sub(a2, q_e);
let d = crate::transforms::expand::expand(arena, d);
let d = crate::transforms::eval::eval(arena, d);
if !arena.is_zero_structural(d) {
return None;
}
}
let numer_x = crate::transforms::expand::expand(arena, numer);
let numer_terms: Vec<ExprId> = match arena.node(numer_x).clone() {
ExprNode::Add(ch) => ch.iter().copied().collect(),
_ => vec![numer_x],
};
let mut alpha = arena.zero;
let mut beta = arena.zero;
for &t in &numer_terms {
if !contains_var(arena, t, z_var) {
return None;
}
let (k, is_sq) = match arena.node(t).clone() {
_ if t == z_var => (one, false),
_ if t == z2 => (one, true),
ExprNode::Mul(ch) => {
let mut coeff = Vec::new();
let mut kind: Option<bool> = None;
for &f in &ch {
if f == z_var && kind.is_none() {
kind = Some(false);
} else if f == z2 && kind.is_none() {
kind = Some(true);
} else if contains_var(arena, f, z_var) {
return None;
} else {
coeff.push(f);
}
}
(arena.mul(&coeff), kind?)
}
_ => return None,
};
if is_sq {
alpha = arena.add(&[alpha, k]);
} else {
beta = arena.add(&[beta, k]);
}
}
let wn = arena.mul(&[omega, n_var]);
let cos_wn = arena.cos(wn);
let sin_wn = arena.sin(wn);
let an = arena.pow(a, n_var);
let cos_w = arena.cos(omega);
let sin_w = arena.sin(omega);
let acos = arena.mul(&[alpha, a, cos_w]);
let sin_coeff_num = arena.add(&[beta, acos]);
let a_sin = arena.mul(&[a, sin_w]);
let sin_coeff = arena.div(sin_coeff_num, a_sin);
let cos_term = arena.mul(&[alpha, an, cos_wn]);
let sin_term = arena.mul(&[sin_coeff, an, sin_wn]);
let r = arena.add(&[cos_term, sin_term]);
Some(crate::transforms::eval::eval(arena, r))
}
fn try_extended_inverse(
arena: &mut Arena,
expr: ExprId,
z_var: ExprId,
n_var: ExprId,
depth: u32,
) -> Result<Option<ExprId>, SymplexError> {
let one = arena.one;
let kids: Vec<ExprId> = match arena.node(expr).clone() {
ExprNode::Mul(ch) => ch.iter().copied().collect(),
_ => vec![expr],
};
let mut z_power: i64 = 0; let mut pole: Option<(ExprId, u64)> = None; let mut others: Vec<ExprId> = Vec::new();
for &k in &kids {
if k == z_var {
z_power += 1;
continue;
}
match arena.node(k).clone() {
ExprNode::Pow(base, e) if base == z_var => {
if let Some(r) = arena.as_num(e)
&& r.is_integer()
{
z_power += i64::try_from(r.to_integer()).map_err(|_| {
SymplexError::ComputationFailed {
operation: "inverse_z_transform",
reason: "power too large".into(),
}
})?;
continue;
}
others.push(k);
}
ExprNode::Pow(base, e) if pole.is_none() => {
let Some(neg_m) = negative_integer_exponent(arena, e) else {
others.push(k);
continue;
};
match linear_in(arena, base, z_var) {
Some((c1, c0)) if c1 == one => {
let a = arena.neg(c0);
let a = crate::transforms::eval::eval(arena, a);
let m: u64 =
(-neg_m)
.try_into()
.map_err(|_| SymplexError::ComputationFailed {
operation: "inverse_z_transform",
reason: "power too large".into(),
})?;
pole = Some((a, m));
}
_ => others.push(k),
}
}
ExprNode::Exp(arg) if others.is_empty() && pole.is_none() => {
let inv_z = arena.pow(z_var, arena.neg_one);
if arg == inv_z && kids.len() == 1 {
let f = arena.factorial(n_var);
return Ok(Some(arena.div(one, f)));
}
others.push(k);
}
_ => others.push(k),
}
}
if pole.is_none() && others.is_empty() {
if z_power <= 0 {
return Ok(Some(kronecker(arena, n_var, -z_power)));
}
return Err(SymplexError::ComputationFailed {
operation: "inverse_z_transform",
reason: "positive powers of z correspond to non-causal sequences".into(),
});
}
if let Some((a, m)) = pole
&& others.is_empty()
{
let delay = 1 - z_power; if delay < 0 {
return Ok(None); }
let m_minus_1 = arena.int(m as i64 - 1);
let exp = arena.sub(n_var, m_minus_1);
let a_pow = arena.pow(a, exp);
let base = if m == 1 {
arena.pow(a, n_var)
} else {
let mut factors = Vec::with_capacity(m as usize);
for j in 0..(m - 1) {
let jj = arena.int(j as i64);
factors.push(arena.sub(n_var, jj));
}
let fact: i64 = (1..m as i64).product();
let inv_fact = arena.rational(1, fact);
factors.push(inv_fact);
factors.push(a_pow);
arena.mul(&factors)
};
let base = crate::transforms::eval::eval(arena, base);
if delay == 0 {
return Ok(Some(base));
}
let k = arena.int(delay);
let n_minus_k = arena.sub(n_var, k);
let shifted = crate::transforms::subs::subs(arena, base, n_var, n_minus_k);
let h = discrete_step(arena, n_var, delay);
return Ok(Some(arena.mul(&[shifted, h])));
}
if z_power < 0 {
let mut rest = others.clone();
if let Some((a, m)) = pole {
let base = arena.sub(z_var, a);
let neg_m = arena.int(-(m as i64));
rest.push(arena.pow(base, neg_m));
}
let x = arena.mul(&rest);
let xz = arena.mul(&[z_var, x]);
let (k, inner) = if let Ok(r) = do_inverse(arena, xz, z_var, n_var, depth + 1) {
(-z_power + 1, r)
} else {
(-z_power, do_inverse(arena, x, z_var, n_var, depth + 1)?)
};
let k_id = arena.int(k);
let n_minus_k = arena.sub(n_var, k_id);
let shifted = crate::transforms::subs::subs(arena, inner, n_var, n_minus_k);
let h = discrete_step(arena, n_var, k);
return Ok(Some(arena.mul(&[shifted, h])));
}
Ok(None)
}
fn try_table_inverse(
arena: &mut Arena,
expr: ExprId,
z_var: ExprId,
n_var: ExprId,
) -> Option<ExprId> {
let (numer, denom) = crate::poly::polybridge::as_numer_denom(arena, expr);
if denom == arena.one {
return None;
}
let denom_poly = crate::poly::polybridge::expr_to_poly(arena, denom, z_var)?;
let deg = denom_poly.degree()?;
if deg == 1 {
return inverse_degree1(arena, numer, &denom_poly, z_var, n_var);
}
if deg == 2 {
return inverse_degree2(arena, numer, &denom_poly, z_var, n_var);
}
None
}
fn inverse_degree1(
arena: &mut Arena,
numer: ExprId,
denom_poly: &crate::poly::Poly,
z_var: ExprId,
n_var: ExprId,
) -> Option<ExprId> {
let c0 = denom_poly.coeff(0);
let c1 = denom_poly.coeff(1);
if c1.is_zero() {
return None;
}
let a_rat = -&c0 / &c1;
let numer_poly = crate::poly::polybridge::expr_to_poly(arena, numer, z_var)?;
let numer_deg = numer_poly.degree()?;
if numer_deg == 1 {
let n0 = numer_poly.coeff(0);
let n1 = numer_poly.coeff(1);
if n0.is_zero() {
let scale = &n1 / &c1;
let a_id = rational_to_expr(arena, &a_rat);
let result = arena.pow(a_id, n_var);
if scale.is_one() {
return Some(result);
}
let scale_id = rational_to_expr(arena, &scale);
return Some(arena.mul(&[scale_id, result]));
}
}
if numer_deg == 0 {
let _n0 = numer_poly.coeff(0);
return None;
}
None
}
fn inverse_degree2(
arena: &mut Arena,
numer: ExprId,
denom_poly: &crate::poly::Poly,
z_var: ExprId,
n_var: ExprId,
) -> Option<ExprId> {
let c0 = denom_poly.coeff(0);
let c1 = denom_poly.coeff(1);
let c2 = denom_poly.coeff(2);
if c2.is_zero() {
return None;
}
let p = &c1 / &c2; let q = &c0 / &c2;
let two = Ratio::from_integer(BigInt::from(2));
let candidate_a = -&p / &two;
let a_sq = &candidate_a * &candidate_a;
if a_sq == q {
let numer_poly = crate::poly::polybridge::expr_to_poly(arena, numer, z_var)?;
let numer_deg = numer_poly.degree()?;
if numer_deg == 1 {
let n0 = numer_poly.coeff(0);
let n1 = numer_poly.coeff(1);
if n0.is_zero() {
let scale = &n1 / &c2;
let a_id = rational_to_expr(arena, &candidate_a);
if candidate_a.is_zero() {
return None;
}
let n_minus_1 = arena.sub(n_var, arena.one);
let a_pow_nm1 = arena.pow(a_id, n_minus_1);
let n_times_pow = arena.mul(&[n_var, a_pow_nm1]);
if scale.is_one() {
return Some(n_times_pow);
}
let scale_id = rational_to_expr(arena, &scale);
return Some(arena.mul(&[scale_id, n_times_pow]));
}
}
}
let four = Ratio::from_integer(BigInt::from(4));
let discriminant = &p * &p - &four * &q;
if discriminant.is_negative() && q.is_positive() {
if q.is_one() {
return try_trig_inverse_structural(arena, numer, denom_poly, z_var, n_var);
}
}
None
}
fn try_trig_inverse_structural(
arena: &mut Arena,
numer: ExprId,
_denom_poly: &crate::poly::Poly,
z_var: ExprId,
n_var: ExprId,
) -> Option<ExprId> {
let numer_node = arena.node(numer).clone();
match numer_node {
ExprNode::Mul(ref children) => {
let kids = children.clone();
let mut has_z = false;
let mut sin_arg: Option<ExprId> = None;
let mut constants: Vec<ExprId> = Vec::new();
for &child in &kids {
if child == z_var {
has_z = true;
continue;
}
let cnode = arena.node(child).clone();
match cnode {
ExprNode::Sin(arg) if !contains_var(arena, arg, z_var) => {
sin_arg = Some(arg);
}
_ if !contains_var(arena, child, z_var) => {
constants.push(child);
}
_ => {
return None;
}
}
}
if has_z && let Some(omega) = sin_arg {
let omega_n = arena.mul(&[omega, n_var]);
let result = arena.sin(omega_n);
if constants.is_empty() {
return Some(result);
}
let scale = if constants.len() == 1 {
constants[0]
} else {
arena.mul(&constants)
};
return Some(arena.mul(&[scale, result]));
}
None
}
_ => None,
}
}
impl Ex {
#[must_use = "returns the z-transform; does not modify in place"]
pub fn z_transform(&self, n: &Ex, z: &Ex) -> Result<Ex, SymplexError> {
let n_id = self.checked_id(n);
let z_id = self.checked_id(z);
let id = {
let mut guard = self.inner.write();
z_transform(&mut guard.arena, self.raw_id(), n_id, z_id)?
};
Ok(self.wrap(id))
}
#[must_use = "returns the inverse z-transform; does not modify in place"]
pub fn inverse_z_transform(&self, z: &Ex, n: &Ex) -> Result<Ex, SymplexError> {
let z_id = self.checked_id(z);
let n_id = self.checked_id(n);
let id = {
let mut guard = self.inner.write();
inverse_z_transform(&mut guard.arena, self.raw_id(), z_id, n_id)?
};
Ok(self.wrap(id))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::arena::Arena;
fn sym(a: &mut Arena, name: &str) -> ExprId {
a.symbol(name)
}
fn display(a: &Arena, id: ExprId) -> String {
a.display(id).to_string()
}
#[test]
fn forward_constant() {
let mut a = Arena::new();
let n = sym(&mut a, "n");
let z = sym(&mut a, "z");
let five = a.int(5);
let result = z_transform(&mut a, five, n, z).unwrap();
let d = display(&a, result);
assert!(
d.contains("5") && d.contains("z"),
"Z{{5}} should be 5z/(z-1), got: {d}"
);
}
#[test]
fn forward_exponential() {
let mut a = Arena::new();
let n = sym(&mut a, "n");
let z = sym(&mut a, "z");
let half = a.rational(1, 2);
let expr = a.pow(half, n); let result = z_transform(&mut a, expr, n, z).unwrap();
let d = display(&a, result);
assert!(d.contains("z"), "Z{{(1/2)^n}} should involve z, got: {d}");
}
#[test]
fn forward_sin() {
let mut a = Arena::new();
let n = sym(&mut a, "n");
let z = sym(&mut a, "z");
let three = a.int(3);
let three_n = a.mul(&[three, n]);
let sin_3n = a.sin(three_n);
let result = z_transform(&mut a, sin_3n, n, z).unwrap();
let d = display(&a, result);
assert!(
d.contains("sin") && d.contains("z"),
"Z{{sin(3n)}} should involve sin and z, got: {d}"
);
}
#[test]
fn forward_cos() {
let mut a = Arena::new();
let n = sym(&mut a, "n");
let z = sym(&mut a, "z");
let cos_n = a.cos(n);
let result = z_transform(&mut a, cos_n, n, z).unwrap();
let d = display(&a, result);
assert!(
d.contains("cos") && d.contains("z"),
"Z{{cos(n)}} should involve cos and z, got: {d}"
);
}
#[test]
fn forward_linearity() {
let mut a = Arena::new();
let n = sym(&mut a, "n");
let z = sym(&mut a, "z");
let half = a.rational(1, 2);
let exp_term = a.pow(half, n);
let sin_n = a.sin(n);
let sum = a.add(&[exp_term, sin_n]);
let result = z_transform(&mut a, sum, n, z);
assert!(result.is_ok(), "linearity should work");
}
#[test]
fn forward_n_must_be_symbol() {
let mut a = Arena::new();
let n = a.int(5); let z = a.symbol("z");
let one = a.one;
let result = z_transform(&mut a, one, n, z);
assert!(result.is_err());
}
#[test]
fn forward_n_var() {
let mut a = Arena::new();
let n = sym(&mut a, "n");
let z = sym(&mut a, "z");
let result = z_transform(&mut a, n, n, z).unwrap();
let d = display(&a, result);
assert!(d.contains("z"), "Z{{n}} should involve z, got: {d}");
}
#[test]
fn forward_n_times_a_n() {
let mut a = Arena::new();
let n = sym(&mut a, "n");
let z = sym(&mut a, "z");
let two = a.int(2);
let two_n = a.pow(two, n); let n_times_2n = a.mul(&[n, two_n]); let result = z_transform(&mut a, n_times_2n, n, z).unwrap();
let d = display(&a, result);
assert!(
d.contains("z") && d.contains("2"),
"Z{{n·2^n}} should involve z and 2, got: {d}"
);
}
}