use crate::api::expr::Ex;
use crate::api::expr_solve_ext::{LinearSolution, linsolve};
use crate::base::errors::SymplexError;
use crate::base::node::ExprNode;
use crate::base::numeric::Q;
struct ForcingTerm {
coeff: Ex,
degree: usize,
base: Ex,
}
fn constant(ctx: &crate::api::context::Context, k: usize) -> Ex {
ctx.symbol(&format!("C{k}"))
}
fn as_rational(e: &Ex) -> Option<Q> {
e.inner.read().arena.as_num(e.raw_id()).cloned()
}
fn parse_forcing_term(term: &Ex, n: &Ex) -> Option<ForcingTerm> {
let ctx = term.context();
let factors: Vec<Ex> = if term.expr_type() == crate::api::expr::ExprType::Mul {
term.args()
} else {
vec![term.clone()]
};
let mut coeff = ctx.one();
let mut degree = 0usize;
let mut base: Option<Ex> = None;
for f in factors {
if &f == n {
degree += 1;
continue;
}
if !f.contains(n) {
coeff = &coeff * &f;
continue;
}
let node = {
let inner = f.inner.read();
inner.arena.node(f.raw_id()).clone()
};
match node {
ExprNode::Pow(b, e) => {
let b = f.wrap(b);
let e = f.wrap(e);
if &b == n && !e.contains(n) {
let d = as_rational(&e)?;
if !d.is_integer() || num_traits::Signed::is_negative(&d) {
return None;
}
degree += usize::try_from(d.to_integer()).ok()?;
} else if !b.contains(n) {
if base.is_some() {
return None;
}
let coeffs = {
let mut inner = f.inner.write();
crate::transforms::solve::symbolic_poly_coeffs(
&mut inner.arena,
e.raw_id(),
n.raw_id(),
)
}?;
if coeffs.len() != 2 {
return None;
}
let m = f.wrap(coeffs[0]);
let k = f.wrap(coeffs[1]);
let bk = if k.is_one_structural() {
b.clone()
} else {
b.pow(&k)
};
let bm = b.pow(&m).eval();
coeff = &coeff * &bm;
base = Some(bk.eval());
} else {
return None;
}
}
ExprNode::Exp(arg) => {
if base.is_some() {
return None;
}
let arg = f.wrap(arg);
let coeffs = {
let mut inner = f.inner.write();
crate::transforms::solve::symbolic_poly_coeffs(
&mut inner.arena,
arg.raw_id(),
n.raw_id(),
)
}?;
if coeffs.len() != 2 {
return None;
}
let m = f.wrap(coeffs[0]);
let k = f.wrap(coeffs[1]);
coeff = &coeff * &m.exp().eval();
base = Some(k.exp().eval());
}
_ => return None,
}
}
Some(ForcingTerm {
coeff: coeff.eval(),
degree,
base: base.unwrap_or_else(|| ctx.one()),
})
}
fn n_pow(n: &Ex, j: usize) -> Ex {
match j {
0 => n.context().one(),
1 => n.clone(),
_ => n.powi(j as i64),
}
}
const RSOLVE_BUDGET: usize = crate::base::config::EXPRESSION_BUDGET / 4;
fn fit_vandermonde(roots: &[(Ex, usize)], n: &Ex, ics: &[Ex]) -> Option<Ex> {
let ctx = n.context();
if roots.len() != ics.len() || roots.is_empty() {
return None;
}
let i_unit = ctx.i_unit();
if roots
.iter()
.any(|(r, m)| *m != 1 || r.is_zero_structural() || r.contains(&i_unit))
{
return None;
}
let rs: Vec<Ex> = roots.iter().map(|(r, _)| r.clone()).collect();
let m = rs.len();
let mut closed = ctx.zero();
for k in 0..m {
let mut num: Vec<Ex> = vec![ctx.one()];
let mut denom = ctx.one();
for (i, ri) in rs.iter().enumerate() {
if i == k {
continue;
}
let mut next: Vec<Ex> = vec![ctx.zero(); num.len() + 1];
for (j, c) in num.iter().enumerate() {
next[j + 1] = (&next[j + 1] + c).eval();
next[j] = (&next[j] - &(c * ri)).eval();
}
num = next;
let d = (&rs[k] - ri).eval();
if d.is_zero_structural() {
return None; }
denom = (&denom * &d).eval();
}
let mut ck = ctx.zero();
for (j, a) in ics.iter().enumerate() {
if a.is_zero_structural() {
continue;
}
ck = (&ck + &(&num[j] * a)).eval();
}
let ck = (&ck / &denom).eval();
if ck.is_zero_structural() {
continue;
}
let term = if rs[k].is_one_structural() {
ck
} else {
&ck * &rs[k].pow(n)
};
closed = &closed + &term;
}
Some(closed.eval())
}
fn tree_size_capped(e: &Ex, cap: usize) -> usize {
let inner = e.inner.read();
crate::transforms::pattern::tree_size_capped(&inner.arena, e.raw_id(), cap.saturating_add(1))
}
fn swell_check(exprs: &[&Ex]) -> Result<(), SymplexError> {
let mut total = 0usize;
for e in exprs {
total += tree_size_capped(e, RSOLVE_BUDGET - total.min(RSOLVE_BUDGET));
if total > RSOLVE_BUDGET {
return Err(SymplexError::ComputationFailed {
operation: "rsolve_linear",
reason: format!(
"expression swell: the closed form exceeds the budget of {RSOLVE_BUDGET} \
expression nodes"
),
});
}
}
Ok(())
}
struct RootGroup {
factor: crate::poly::Poly,
power: crate::poly::Poly,
mult: usize,
roots: Vec<Ex>,
}
fn flat_roots(groups: &[RootGroup]) -> Vec<(Ex, usize)> {
groups
.iter()
.flat_map(|g| g.roots.iter().map(move |r| (r.clone(), g.mult)))
.collect()
}
fn characteristic_roots(coeffs: &[Ex], n: &Ex) -> Result<Vec<RootGroup>, SymplexError> {
let ctx = n.context();
let r = if format!("{n}") == "r" {
ctx.symbol("_r")
} else {
ctx.symbol("r")
};
let mut rat_coeffs = Vec::with_capacity(coeffs.len());
for c in coeffs {
match as_rational(&c.eval()) {
Some(v) => rat_coeffs.push(v),
None => {
return Err(SymplexError::InvalidArgument {
operation: "rsolve_linear",
reason: format!("coefficient {c} is not a rational number"),
});
}
}
}
let poly = crate::poly::Poly::from_coeffs(rat_coeffs);
let (_content, factors) = poly.factor_over_z();
let mut groups: Vec<RootGroup> = Vec::new();
for (factor, mult) in factors {
let mult = mult as usize;
let degree = factor.degree().unwrap_or(0);
if degree == 0 {
continue;
}
let f_expr = {
let mut inner = ctx.inner.write();
crate::poly::polybridge::poly_to_expr(&mut inner.arena, &factor, r.raw_id())
};
let mut roots: Vec<Ex> = Vec::new();
if degree >= 3 {
for k in 0..degree {
let root = {
let mut inner = ctx.inner.write();
let idx = inner.arena.int(k as i64);
inner
.arena
.intern(ExprNode::RootOf(f_expr, r.raw_id(), idx))
};
roots.push(r.wrap(root));
}
groups.push(RootGroup {
power: factor.pow(mult),
factor,
mult,
roots,
});
continue;
}
let sols = {
let mut inner = ctx.inner.write();
crate::transforms::solve::solve(&mut inner.arena, f_expr, r.raw_id())
};
if sols.len() != degree {
return Err(SymplexError::ComputationFailed {
operation: "rsolve_linear",
reason: "could not find the characteristic roots".into(),
});
}
for s in sols {
let root = r.wrap(s.value).eval();
if root.has_unevaluated() {
return Err(SymplexError::ComputationFailed {
operation: "rsolve_linear",
reason: format!("characteristic root is not in closed form: {root}"),
});
}
roots.push(root);
}
groups.push(RootGroup {
power: factor.pow(mult),
factor,
mult,
roots,
});
}
Ok(groups)
}
fn power_sums(f: &crate::poly::Poly, count: usize) -> Option<Vec<Q>> {
use num_traits::Zero;
let d = f.degree()?;
let lead = f.leading_coeff()?.clone();
let a: Vec<Q> = (0..=d).map(|j| f.coeff(j) / &lead).collect();
let mut p: Vec<Q> = Vec::with_capacity(count);
for m in 0..count {
let v = if m == 0 {
Q::from_integer(num_bigint::BigInt::from(d))
} else if m <= d {
let mut acc = Q::from_integer(num_bigint::BigInt::from(m)) * &a[d - m];
for i in 1..m {
acc += &a[d - i] * &p[m - i];
}
-acc
} else {
let mut acc = Q::zero();
for i in 1..=d {
acc += &a[d - i] * &p[m - i];
}
-acc
};
p.push(v);
}
Some(p)
}
fn solve_rational(mut m: Vec<Vec<Q>>, mut rhs: Vec<Q>) -> Option<Vec<Q>> {
use num_traits::Zero;
let n = m.len();
for col in 0..n {
let p = (col..n).find(|&i| !m[i][col].is_zero())?;
m.swap(col, p);
rhs.swap(col, p);
let pivot_row = m[col].clone();
let pivot_rhs = rhs[col].clone();
for (i, (row, r)) in m.iter_mut().zip(rhs.iter_mut()).enumerate() {
if i != col && !row[col].is_zero() {
let f = &row[col] / &pivot_row[col];
for (x, p) in row.iter_mut().zip(&pivot_row).skip(col) {
*x -= p * &f;
}
*r -= &pivot_rhs * &f;
}
}
}
Some((0..n).map(|i| &rhs[i] / &m[i][i]).collect())
}
fn fit_root_of_component(
g: &RootGroup,
members: &[usize],
basis: &[BasisSeq],
data: &[Q],
continuous: bool,
ctx: &crate::api::context::Context,
) -> Option<Vec<Ex>> {
use num_traits::Zero;
let d = g.factor.degree()?;
let m = g.mult;
let dim = d * m;
let p = power_sums(&g.factor, 2 * dim + d + 1)?;
let mut mat = vec![vec![Q::zero(); dim]; dim];
for (k, row) in mat.iter_mut().enumerate() {
for j in 0..m {
for l in 0..d {
row[j * d + l] = if continuous {
if k < j {
Q::zero()
} else {
let mut falling = num_bigint::BigInt::from(1);
for i in (k - j + 1)..=k {
falling *= num_bigint::BigInt::from(i);
}
Q::from_integer(falling) * &p[k - j + l]
}
} else {
let kj = num_bigint::BigInt::from(k).pow(j as u32);
Q::from_integer(kj) * &p[k + l]
};
}
}
}
let c = solve_rational(mat, data[..dim].to_vec())?;
let mut out = Vec::with_capacity(members.len());
for &i in members {
let BasisKind::Power { root, j } = &basis[i].kind else {
return None;
};
let mut acc = ctx.zero();
for l in 0..d {
let cl = &c[*j * d + l];
if cl.is_zero() {
continue;
}
let term = &ctx.from_ratio(cl.clone()) * &root.powi(l as i64);
acc = &acc + &term;
}
out.push(acc);
}
Some(out)
}
enum BasisKind {
Power { root: Ex, j: usize },
Delta(usize),
Cos { re: Ex, im: Ex, j: usize },
Sin { re: Ex, im: Ex, j: usize },
}
struct BasisSeq {
expr: Ex,
kind: BasisKind,
group: usize,
continuous: bool,
}
fn exact_pow(x: &Ex, k: usize) -> Ex {
let ctx = x.context();
let mut acc = ctx.one();
for _ in 0..k {
acc = (&acc * x).expand().eval();
}
acc
}
fn exact_complex_pow(re: &Ex, im: &Ex, k: usize) -> (Ex, Ex) {
let ctx = re.context();
let (mut x, mut y) = (ctx.one(), ctx.zero());
for _ in 0..k {
let nx = (&(&x * re) - &(&y * im)).expand().eval();
let ny = (&(&x * im) + &(&y * re)).expand().eval();
x = nx;
y = ny;
}
(x, y)
}
impl BasisSeq {
fn value_at(&self, k: usize, ctx: &crate::api::context::Context) -> Ex {
if self.continuous {
return self.derivative_at_zero(k, ctx);
}
let k_pow = |j: usize| -> Ex {
let mut acc = ctx.one();
for _ in 0..j {
acc = (&acc * k as i64).eval();
}
acc
};
match &self.kind {
BasisKind::Delta(i) => {
if *i == k {
ctx.one()
} else {
ctx.zero()
}
}
BasisKind::Power { root, j } => {
let rk = if contains_root_of(root) {
root.powi(k as i64).eval()
} else {
exact_pow(root, k)
};
(&k_pow(*j) * &rk).eval()
}
BasisKind::Cos { re, im, j } => {
let (x, _) = exact_complex_pow(re, im, k);
(&k_pow(*j) * &x).eval()
}
BasisKind::Sin { re, im, j } => {
let (_, y) = exact_complex_pow(re, im, k);
(&k_pow(*j) * &y).eval()
}
}
}
fn derivative_at_zero(&self, k: usize, ctx: &crate::api::context::Context) -> Ex {
let j = match &self.kind {
BasisKind::Power { j, .. } | BasisKind::Cos { j, .. } | BasisKind::Sin { j, .. } => *j,
BasisKind::Delta(_) => return ctx.zero(),
};
if k < j {
return ctx.zero();
}
let mut falling = num_bigint::BigInt::from(1);
for i in (k - j + 1)..=k {
falling *= num_bigint::BigInt::from(i);
}
let falling = ctx.from_ratio(Q::from_integer(falling));
let v = match &self.kind {
BasisKind::Power { root, .. } => {
if contains_root_of(root) {
root.powi((k - j) as i64).eval()
} else {
exact_pow(root, k - j)
}
}
BasisKind::Cos { re, im, .. } => exact_complex_pow(re, im, k - j).0,
BasisKind::Sin { re, im, .. } => exact_complex_pow(re, im, k - j).1,
BasisKind::Delta(_) => ctx.zero(),
};
(&falling * &v).eval()
}
}
fn contains_root_of(e: &Ex) -> bool {
let inner = e.inner.read();
let mut stack = vec![e.raw_id()];
let mut seen = rustc_hash::FxHashSet::default();
while let Some(id) = stack.pop() {
if !seen.insert(id) {
continue;
}
let node = inner.arena.node(id);
if matches!(node, ExprNode::RootOf(..)) {
return true;
}
node.for_each_child(|c| stack.push(c));
}
false
}
fn pair_angle(re: &Ex, im: &Ex, rho: &Ex) -> Ex {
let ctx = re.context();
let cos2 = (&re.powi(2) / &(&re.powi(2) + &im.powi(2))).expand().eval();
if let (Some(c2), Some(a)) = (as_rational(&cos2), as_rational(&re.eval())) {
let neg = num_traits::Signed::is_negative(&a);
let table: [(i64, i64, i64, i64); 4] = [
(0, 1, 1, 2),
(1, 4, 1, 3),
(1, 2, 1, 4),
(3, 4, 1, 6),
];
for (cn, cd, tn, td) in table {
if c2 == Q::new(cn.into(), cd.into()) {
let frac = if neg && cn != 0 {
ctx.rational(td - tn, td)
} else {
ctx.rational(tn, td)
};
return (&frac * &ctx.pi()).eval();
}
}
}
(re / rho).acos().eval().simplify()
}
fn homogeneous_basis(groups: &[RootGroup], n: &Ex, continuous: bool) -> Vec<BasisSeq> {
let mut basis = Vec::new();
for (gi, g) in groups.iter().enumerate() {
let roots: Vec<(Ex, usize)> = g.roots.iter().map(|r| (r.clone(), g.mult)).collect();
group_basis(&roots, n, gi, continuous, &mut basis);
}
basis
}
fn group_basis(
roots: &[(Ex, usize)],
n: &Ex,
group: usize,
continuous: bool,
basis: &mut Vec<BasisSeq>,
) {
let ctx = n.context();
let i_unit = ctx.i_unit();
let mut used = vec![false; roots.len()];
for i in 0..roots.len() {
if used[i] {
continue;
}
used[i] = true;
let (root, mult) = &roots[i];
if !root.contains(&i_unit) {
for j in 0..*mult {
if root.is_zero_structural() && !continuous {
basis.push(BasisSeq {
expr: n.kronecker_delta(&ctx.int(j as i64)),
kind: BasisKind::Delta(j),
group,
continuous,
});
continue;
}
let term = if continuous {
if root.is_zero_structural() {
n_pow(n, j)
} else {
&n_pow(n, j) * &(root * n).exp()
}
} else if root.is_one_structural() {
n_pow(n, j)
} else {
&n_pow(n, j) * &root.pow(n)
};
basis.push(BasisSeq {
expr: term.eval(),
kind: BasisKind::Power {
root: root.clone(),
j,
},
group,
continuous,
});
}
continue;
}
let re = root.re().eval().simplify();
let im = root.im().eval().simplify();
let neg_im = (-&im).eval().simplify();
for k in (i + 1)..roots.len() {
if used[k] {
continue;
}
let re2 = roots[k].0.re().eval().simplify();
let im2 = roots[k].0.im().eval().simplify();
if re2 == re && im2 == neg_im {
used[k] = true;
break;
}
}
let im = im.powi(2).sqrt().eval().simplify();
let (envelope_base, angle) = if continuous {
let env = if re.is_zero_structural() {
None
} else {
Some((&re * n).exp())
};
(env, im.clone())
} else {
let rho = (&re.powi(2) + &im.powi(2)).sqrt().eval().simplify();
let env = if rho.is_one_structural() {
None
} else {
Some(rho.pow(n))
};
(env, pair_angle(&re, &im, &rho))
};
for j in 0..*mult {
let envelope = match &envelope_base {
None => n_pow(n, j),
Some(e) => &n_pow(n, j) * e,
};
let n_theta = n * ∠
basis.push(BasisSeq {
expr: (&envelope * &n_theta.cos()).eval(),
kind: BasisKind::Cos {
re: re.clone(),
im: im.clone(),
j,
},
group,
continuous,
});
basis.push(BasisSeq {
expr: (&envelope * &n_theta.sin()).eval(),
kind: BasisKind::Sin {
re: re.clone(),
im: im.clone(),
j,
},
group,
continuous,
});
}
}
}
fn fit_by_components(
coeffs: &[Q],
groups: &[RootGroup],
basis: &[BasisSeq],
constants: &[Ex],
initial: &[Q],
ctx: &crate::api::context::Context,
) -> Option<Vec<(Ex, Ex)>> {
use num_traits::Zero;
let order = coeffs.len() - 1;
if initial.len() != order {
return None;
}
let mut h: Vec<Q> = initial.to_vec();
let lead = coeffs[order].clone();
if lead.is_zero() {
return None;
}
for m in order..2 * order {
let mut acc = Q::zero();
for (j, c) in coeffs.iter().enumerate().take(order) {
acc += c * &h[m - order + j];
}
h.push(-acc / &lead);
}
let chi = crate::poly::Poly::from_coeffs(coeffs.to_vec());
let mut pairs: Vec<(Ex, Ex)> = Vec::new();
for (gi, g) in groups.iter().enumerate() {
let (cofactor, rem) = chi.div_rem(&g.power);
if !rem.is_zero() {
return None;
}
let eg = crate::poly::Poly::extended_gcd(&cofactor, &g.power);
if eg.gcd.degree() != Some(0) {
return None;
}
let w = eg.x.mul(&cofactor).rem(&chi);
let dim = g.power.degree()?;
let members: Vec<usize> = (0..basis.len()).filter(|&i| basis[i].group == gi).collect();
if members.len() != dim {
return None;
}
let unknowns: Vec<Ex> = members.iter().map(|&i| constants[i].clone()).collect();
let mut hi: Vec<Q> = Vec::with_capacity(dim);
for k in 0..dim {
let mut hk = Q::zero();
for (j, wj) in w.coeffs().iter().enumerate() {
hk += wj * h.get(k + j)?;
}
hi.push(hk);
}
if g.factor.degree().is_some_and(|d| d >= 3) {
let continuous = basis[members[0]].continuous;
let values = fit_root_of_component(g, &members, basis, &hi, continuous, ctx)?;
pairs.extend(unknowns.iter().cloned().zip(values));
continue;
}
let mut eqs: Vec<Ex> = Vec::with_capacity(dim);
for (k, hk) in hi.into_iter().enumerate() {
let mut acc = -ctx.from_ratio(hk);
for &i in &members {
let bk = basis[i].value_at(k, ctx);
if !bk.is_zero_structural() {
acc = &acc + &(&constants[i] * &bk);
}
}
eqs.push(acc.eval());
}
match linsolve(&eqs, &unknowns).ok()? {
LinearSolution::Unique(sol) => pairs.extend(sol),
_ => return None,
}
}
Some(pairs)
}
#[allow(clippy::too_many_arguments)]
fn fit_partial_by_components(
coeffs: &[Q],
groups: &[RootGroup],
basis: &[BasisSeq],
constants: &[Ex],
general: &Ex,
particular: &Ex,
ics: &[Ex],
n: &Ex,
) -> Option<Ex> {
use num_traits::Zero;
let ctx = n.context();
let order = coeffs.len() - 1;
let known = ics.len();
let mut base: Vec<Q> = Vec::with_capacity(order);
for (k, v) in ics.iter().enumerate() {
base.push(as_rational(
&(v - &particular.subs_i64(n, k as i64).eval()).eval(),
)?);
}
let mut yp_tail: Vec<Q> = Vec::new();
for k in known..order {
yp_tail.push(as_rational(&particular.subs_i64(n, k as i64).eval())?);
}
let mut data0 = base.clone();
data0.extend(yp_tail.iter().map(|y| -y.clone()));
let fit0 = fit_by_components(coeffs, groups, basis, constants, &data0, &ctx)?;
let params: Vec<Ex> = (0..order - known)
.map(|i| ctx.symbol(&format!("__rsolve_P{i}")))
.collect();
let mut value: Vec<Ex> = constants
.iter()
.map(|c| fit0.iter().find(|(k, _)| k == c).map(|(_, v)| v.clone()))
.collect::<Option<Vec<_>>>()?;
for (i, p) in params.iter().enumerate() {
let mut unit = vec![Q::zero(); order];
unit[known + i] = Q::from_integer(1.into());
let fit = fit_by_components(coeffs, groups, basis, constants, &unit, &ctx)?;
for (slot, c) in value.iter_mut().zip(constants) {
let vi = fit.iter().find(|(k, _)| k == c).map(|(_, v)| v.clone())?;
if !vi.is_zero_structural() {
*slot = &*slot + &(p * &vi);
}
}
}
let pairs: Vec<(&Ex, &Ex)> = constants.iter().zip(value.iter()).collect();
let mut sol = general.subs_map(&pairs);
for (i, p) in params.iter().enumerate() {
sol = sol.subs(p, &constant(&ctx, i + 1));
}
swell_check(&[&sol]).ok()?;
let sol = sol.eval();
swell_check(&[&sol]).ok()?;
Some(sol.simplify())
}
fn particular_solution(
coeffs: &[Ex],
q: &[Ex],
base: &Ex,
roots: &[(Ex, usize)],
n: &Ex,
) -> Result<Ex, SymplexError> {
let ctx = n.context();
let s = roots
.iter()
.find(|(r, _)| (r - base).eval().simplify().is_zero_structural())
.map(|(_, m)| *m)
.unwrap_or(0);
let d = q.len() - 1;
let unknowns: Vec<Ex> = (0..=d).map(|j| ctx.symbol(&format!("__A{j}"))).collect();
let trial_poly = |shift: i64| -> Ex {
let m = n + shift;
let mut acc = ctx.zero();
for (j, a) in unknowns.iter().enumerate() {
acc = &acc + &(a * &n_pow(&m, j));
}
&acc * &n_pow(&m, s)
};
let mut lhs = ctx.zero();
for (k, c) in coeffs.iter().enumerate() {
if c.is_zero_structural() {
continue;
}
let bk = base.powi(k as i64).eval();
lhs = &lhs + &(&(c * &bk) * &trial_poly(k as i64));
}
let mut rhs = ctx.zero();
for (j, qj) in q.iter().enumerate() {
rhs = &rhs + &(qj * &n_pow(n, j));
}
let residual = (&lhs - &rhs).expand().eval();
let eqs: Vec<Ex> = {
let mut inner = ctx.inner.write();
let coeffs = crate::transforms::solve::symbolic_poly_coeffs(
&mut inner.arena,
residual.raw_id(),
n.raw_id(),
)
.ok_or_else(|| SymplexError::ComputationFailed {
operation: "rsolve_linear",
reason: "forcing residual is not polynomial in n".into(),
})?;
coeffs.into_iter().map(|id| residual.wrap(id)).collect()
};
let eqs: Vec<Ex> = eqs
.into_iter()
.filter(|e| !e.is_zero_structural())
.collect();
if eqs.is_empty() {
return Ok(ctx.zero());
}
match linsolve(&eqs, &unknowns)? {
LinearSolution::Unique(pairs) => {
let mut sol = trial_poly(0);
for (a, v) in &pairs {
sol = sol.subs(a, v);
}
let sol = if base.is_one_structural() {
sol
} else {
&sol * &base.pow(n)
};
Ok(sol.eval().simplify())
}
LinearSolution::Parametric { solution, free } => {
let mut sol = trial_poly(0);
for (a, v) in &solution {
let v = if free.contains(a) {
ctx.zero()
} else {
v.clone()
};
sol = sol.subs(a, &v);
}
for f in &free {
sol = sol.subs(f, &ctx.zero());
}
let sol = if base.is_one_structural() {
sol
} else {
&sol * &base.pow(n)
};
Ok(sol.eval().simplify())
}
LinearSolution::Inconsistent => Err(SymplexError::ComputationFailed {
operation: "rsolve_linear",
reason: "undetermined coefficients failed for the forcing term".into(),
}),
}
}
pub fn rsolve_linear(
coeffs: &[Ex],
forcing: Option<&Ex>,
n: &Ex,
ics: &[Ex],
) -> Result<Ex, SymplexError> {
if coeffs.len() < 2 {
return Err(SymplexError::InvalidArgument {
operation: "rsolve_linear",
reason: "need at least two coefficients (order ≥ 1)".into(),
});
}
for c in coeffs {
let _ = n.checked_id(c);
}
for v in ics {
let _ = n.checked_id(v);
}
let order = coeffs.len() - 1;
if coeffs[order].eval().is_zero_structural() {
return Err(SymplexError::InvalidArgument {
operation: "rsolve_linear",
reason: "leading coefficient must be nonzero".into(),
});
}
if ics.len() > order {
return Err(SymplexError::InvalidArgument {
operation: "rsolve_linear",
reason: format!("at most {order} initial values allowed, got {}", ics.len()),
});
}
let ctx = n.context();
let coeffs: Vec<Ex> = coeffs.iter().map(|c| c.eval()).collect();
let groups = characteristic_roots(&coeffs, n)?;
let roots = flat_roots(&groups);
let basis = homogeneous_basis(&groups, n, false);
let constants: Vec<Ex> = (1..=basis.len()).map(|k| constant(&ctx, k)).collect();
let mut general = ctx.zero();
for (c, b) in constants.iter().zip(&basis) {
general = &general + &(c * &b.expr);
}
let mut particular = ctx.zero();
if let Some(f) = forcing {
let _ = n.checked_id(f);
let f = f.expand().eval();
if !f.is_zero_structural() {
let terms: Vec<Ex> = if f.expr_type() == crate::api::expr::ExprType::Add {
f.args()
} else {
vec![f.clone()]
};
let mut groups: Vec<(Ex, Vec<Ex>)> = Vec::new();
for t in &terms {
let ft =
parse_forcing_term(t, n).ok_or_else(|| SymplexError::ComputationFailed {
operation: "rsolve_linear",
reason: format!("unsupported forcing term: {t}"),
})?;
let entry = match groups.iter_mut().find(|(b, _)| *b == ft.base) {
Some(e) => e,
None => {
groups.push((ft.base.clone(), Vec::new()));
groups
.last_mut()
.ok_or_else(|| SymplexError::ComputationFailed {
operation: "rsolve_linear",
reason: "internal grouping error".into(),
})?
}
};
if entry.1.len() <= ft.degree {
entry.1.resize(ft.degree + 1, ctx.zero());
}
entry.1[ft.degree] = (&entry.1[ft.degree] + &ft.coeff).eval();
}
for (base, q) in &groups {
let yp = particular_solution(&coeffs, q, base, &roots, n)?;
general = &general + &yp;
particular = &particular + &yp;
}
}
}
let general = general.eval();
swell_check(&[&general])?;
if ics.is_empty() || constants.is_empty() {
return Ok(general.simplify());
}
if forcing.is_none_or(|f| f.eval().is_zero_structural())
&& let Some(fitted) = fit_vandermonde(&roots, n, ics)
{
swell_check(&[&fitted])?;
return Ok(fitted.simplify());
}
let rat_coeffs: Option<Vec<Q>> = coeffs.iter().map(as_rational).collect();
let initial: Option<Vec<Q>> = ics
.iter()
.enumerate()
.map(|(k, v)| as_rational(&(v - &particular.subs_i64(n, k as i64).eval()).eval()))
.collect();
if let (Some(rc), Some(initial)) = (rat_coeffs, initial)
&& let Some(pairs) = fit_by_components(&rc, &groups, &basis, &constants, &initial, &ctx)
{
let mut sol = general.clone();
for (c, v) in &pairs {
sol = sol.subs(c, v);
}
swell_check(&[&sol])?;
let sol = sol.eval();
swell_check(&[&sol])?;
return Ok(sol.simplify());
}
let quadratic_fields = groups
.iter()
.filter(|g| g.factor.degree() == Some(2))
.count();
let risky = groups
.iter()
.any(|g| g.factor.degree().is_some_and(|d| d >= 3))
|| quadratic_fields >= 2;
if risky
&& ics.len() < coeffs.len() - 1
&& let Some(rc) = coeffs.iter().map(as_rational).collect::<Option<Vec<Q>>>()
&& let Some(sol) = fit_partial_by_components(
&rc,
&groups,
&basis,
&constants,
&general,
&particular,
ics,
n,
)
{
return Ok(sol);
}
let eqs: Vec<Ex> = ics
.iter()
.enumerate()
.map(|(k, v)| {
let mut acc = (particular.subs_i64(n, k as i64).eval() - v).eval();
for (c, b) in constants.iter().zip(&basis) {
let bk = b.value_at(k, &ctx);
if !bk.is_zero_structural() {
acc = &acc + &(c * &bk);
}
}
acc.eval()
})
.collect();
let fitted =
crate::api::expr_solve_ext::fit_constants(&general, &constants, &eqs, "rsolve_linear")?;
swell_check(&[&fitted])?;
Ok(fitted)
}
pub(crate) fn solve_constant_coefficient_ivp(
coeffs: &[Q],
initial: &[Q],
var: &Ex,
continuous: bool,
) -> Result<Ex, SymplexError> {
let op = if continuous {
"linear ODE initial value problem"
} else {
"linear recurrence initial value problem"
};
let order = coeffs.len().saturating_sub(1);
if order == 0 || num_traits::Zero::is_zero(&coeffs[order]) || initial.len() != order {
return Err(SymplexError::InvalidArgument {
operation: "solve_constant_coefficient_ivp",
reason: format!("{op}: need a nonzero leading coefficient and one datum per order"),
});
}
let ctx = var.context();
let coeff_ex: Vec<Ex> = coeffs.iter().map(|c| ctx.from_ratio(c.clone())).collect();
let groups = characteristic_roots(&coeff_ex, var)?;
let basis = homogeneous_basis(&groups, var, continuous);
let constants: Vec<Ex> = (1..=basis.len())
.map(|k| ctx.symbol(&format!("__ivp_C{k}")))
.collect();
let pairs =
fit_by_components(coeffs, &groups, &basis, &constants, initial, &ctx).ok_or_else(|| {
SymplexError::ComputationFailed {
operation: "solve_constant_coefficient_ivp",
reason: format!("{op}: could not fit the initial data"),
}
})?;
let mut sol = ctx.zero();
for (c, b) in constants.iter().zip(&basis) {
let v = pairs
.iter()
.find(|(k, _)| k == c)
.map(|(_, v)| v.clone())
.ok_or_else(|| SymplexError::ComputationFailed {
operation: "solve_constant_coefficient_ivp",
reason: format!("{op}: a constant was not determined"),
})?;
if !v.is_zero_structural() {
sol = &sol + &(&v * &b.expr);
}
}
swell_check(&[&sol])?;
let sol = sol.eval();
swell_check(&[&sol])?;
Ok(sol.simplify())
}
pub(crate) fn series_at_infinity(
num: &crate::poly::Poly,
den: &crate::poly::Poly,
count: usize,
) -> Option<Vec<Q>> {
use num_traits::Zero;
let d = den.degree()?;
if num.degree().is_some_and(|k| k > d) {
return None;
}
let at =
|p: &crate::poly::Poly, m: usize| -> Q { if m > d { Q::zero() } else { p.coeff(d - m) } };
let d0 = at(den, 0);
if d0.is_zero() {
return None;
}
let mut e: Vec<Q> = Vec::with_capacity(count);
for m in 0..count {
let mut acc = at(num, m);
for j in 1..=m.min(d) {
acc -= at(den, j) * &e[m - j];
}
e.push(acc / &d0);
}
Some(e)
}
pub(crate) fn rational_inverse_into(
arena: &mut crate::base::arena::Arena,
num: &crate::poly::Poly,
den: &crate::poly::Poly,
var: crate::base::node::ExprId,
continuous: bool,
) -> Option<crate::base::node::ExprId> {
let d = den.degree()?;
if d == 0 || num.is_zero() {
return None;
}
let (coeffs, data) = if continuous {
if num.degree()? >= d {
return None;
}
let e = series_at_infinity(num, den, d + 1)?;
(den.coeffs().to_vec(), e[1..].to_vec())
} else {
let e = series_at_infinity(num, den, d + 1)?;
let mut c = vec![<Q as num_traits::Zero>::zero()];
c.extend(den.coeffs().iter().cloned());
(c, e)
};
let scratch = crate::api::context::Context::new();
let v = scratch.symbol("__rational_inverse_var");
let sol = solve_constant_coefficient_ivp(&coeffs, &data, &v, continuous).ok()?;
if sol.has_unevaluated() {
return None;
}
let id = {
let inner = scratch.inner.read();
let saved = arena.last_compact_size;
let mut map = rustc_hash::FxHashMap::default();
let id =
crate::base::compact::transfer_subtree(&inner.arena, arena, sol.raw_id(), &mut map);
arena.last_compact_size = saved;
id
};
let tmp = arena.symbol("__rational_inverse_var");
let id = crate::transforms::subs::subs(arena, id, tmp, var);
Some(crate::transforms::eval::eval(arena, id))
}
fn first_index_singularity(p: &Ex, n: &Ex) -> Option<(i64, bool)> {
let (num, den) = p.eval().as_numer_denom();
let mut best: Option<(i64, bool)> = None;
for (part, is_pole) in [(num, false), (den, true)] {
if !part.contains(n) || !part.is_polynomial(n) {
continue;
}
let coeffs = {
let mut inner = part.inner.write();
crate::transforms::solve::symbolic_poly_coeffs(
&mut inner.arena,
part.raw_id(),
n.raw_id(),
)
};
let Some(coeffs) = coeffs else { continue };
let rat: Option<Vec<Q>> = coeffs.iter().map(|&c| as_rational(&part.wrap(c))).collect();
let Some(rat) = rat else { continue };
let poly = crate::poly::Poly::from_coeffs(rat);
let Some(lead) = poly.leading_coeff().cloned() else {
continue;
};
if poly.degree() == Some(1) {
let root = -poly.coeff(0) / &lead;
if root.is_integer()
&& !num_traits::Signed::is_negative(&root)
&& let Ok(k0) = i64::try_from(root.to_integer())
&& best.is_none_or(|(b, _)| k0 < b)
{
best = Some((k0, is_pole));
}
continue;
}
let mut bound = Q::from_integer(1.into());
for c in poly.coeffs() {
let r = num_traits::Signed::abs(&(c / &lead));
bound += r;
}
let bound = bound.to_integer();
let bound: i64 = bound.try_into().unwrap_or(i64::MAX).min(1_000_000);
let mut kk = 0i64;
while kk <= bound {
if best.is_some_and(|(b, _)| b <= kk) {
break;
}
if num_traits::Zero::is_zero(&poly.eval(&Q::from_integer(kk.into()))) {
best = Some((kk, is_pole));
break;
}
kk += 1;
}
}
best
}
fn product_closed_form(p: &Ex, k: &Ex, n: &Ex) -> Ex {
let ctx = n.context();
if !p.contains(k) {
return p.pow(n);
}
let coeffs = {
let mut inner = ctx.inner.write();
crate::transforms::solve::symbolic_poly_coeffs(&mut inner.arena, p.raw_id(), k.raw_id())
};
if let Some(c) = coeffs
&& c.len() == 2
{
let beta = p.wrap(c[0]);
let alpha = p.wrap(c[1]);
let shift = (&beta / &alpha).eval();
let alpha_pow = if alpha.is_one_structural() {
ctx.one()
} else {
alpha.pow(n)
};
if let Some(r) = as_rational(&shift)
&& r.is_integer()
&& num_traits::Signed::is_positive(&r)
{
let m: i64 = r.to_integer().try_into().unwrap_or(1);
let numer = (n + (m - 1)).factorial();
let denom = ctx.int(m - 1).factorial().eval();
return (&alpha_pow * &(&numer / &denom)).eval();
}
let g_num = (n + &shift).gamma();
let g_den = shift.gamma();
return (&alpha_pow * &(&g_num / &g_den)).eval();
}
let upper = n - 1;
let closed = p.product_over(k, &ctx.int(0), &upper);
if !closed.has_unevaluated() {
return closed;
}
Ex::symbolic_product(p, k, &ctx.int(0), &upper)
}
pub fn rsolve_first_order(p: &Ex, q: &Ex, n: &Ex, a0: Option<&Ex>) -> Result<Ex, SymplexError> {
let _ = n.checked_id(p);
let _ = n.checked_id(q);
let ctx = n.context();
if p.eval().is_zero_structural() {
return Err(SymplexError::InvalidArgument {
operation: "rsolve_first_order",
reason: "p(n) must be nonzero".into(),
});
}
let start = match a0 {
Some(v) => {
let _ = n.checked_id(v);
v.clone()
}
None => constant(&ctx, 1),
};
let q_eval = q.eval();
if let Some((k0, is_pole)) = first_index_singularity(p, n) {
if is_pole {
return Err(SymplexError::InvalidArgument {
operation: "rsolve_first_order",
reason: format!(
"p(n) = {p} has a pole at n = {k0}: a({}) is undefined",
k0 + 1
),
});
}
if !q_eval.is_zero_structural() {
return Err(SymplexError::ComputationFailed {
operation: "rsolve_first_order",
reason: format!(
"p(n) = {p} vanishes at n = {k0}; the product formula does not apply"
),
});
}
let mut value = start.clone();
let mut total = ctx.zero();
for i in 0..=k0 {
total = &total + &(&value * &n.kronecker_delta(&ctx.int(i)));
value = (&value * &p.subs_i64(n, i)).eval();
}
return Ok(total.eval());
}
let k = ctx.symbol("__k_rsolve");
let p_k = p.subs(n, &k);
let big_p = product_closed_form(&p_k, &k, n);
if q_eval.is_zero_structural() {
return Ok((&big_p * &start).eval().simplify());
}
let q_k = q.subs(n, &k);
let p_k1 = big_p.subs(n, &(&k + 1)).eval();
let body = (&q_k / &p_k1).eval();
let upper = n - 1;
let sum = Ex::symbolic_sum(&body, &k, &ctx.int(0), &upper).closed_form_sum();
let total = (&big_p * &(&start + &sum)).eval();
let s = total.simplify();
Ok(if s.count_ops() <= total.count_ops() {
s
} else {
total
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::context::Context;
fn at(f: &Ex, n: &Ex, k: i64) -> f64 {
f.subs_i64(n, k).eval_f64().unwrap()
}
#[test]
fn hanoi() {
let ctx = Context::new();
let n = ctx.symbol("n");
let sol = rsolve_linear(
&[ctx.int(-2), ctx.int(1)],
Some(&ctx.int(1)),
&n,
&[ctx.int(0)],
)
.unwrap();
for k in 0..8 {
assert!((at(&sol, &n, k) - (2f64.powi(k as i32) - 1.0)).abs() < 1e-9);
}
}
#[test]
fn fibonacci_binet() {
let ctx = Context::new();
let n = ctx.symbol("n");
let sol = rsolve_linear(
&[ctx.int(-1), ctx.int(-1), ctx.int(1)],
None,
&n,
&[ctx.int(0), ctx.int(1)],
)
.unwrap();
let (mut a, mut b) = (0.0, 1.0);
for k in 0..12 {
assert!(
(at(&sol, &n, k) - a).abs() < 1e-8,
"a({k}) = {}",
at(&sol, &n, k)
);
let c = a + b;
a = b;
b = c;
}
assert!(format!("{sol}").contains("sqrt(5)"));
}
#[test]
fn repeated_root() {
let ctx = Context::new();
let n = ctx.symbol("n");
let sol = rsolve_linear(&[ctx.int(4), ctx.int(-4), ctx.int(1)], None, &n, &[]).unwrap();
let s = format!("{sol}");
assert!(
s.contains("C1") && s.contains("C2") && s.contains("2^n"),
"{s}"
);
}
#[test]
fn complex_roots_real_form() {
let ctx = Context::new();
let n = ctx.symbol("n");
let sol = rsolve_linear(
&[ctx.int(1), ctx.int(0), ctx.int(1)],
None,
&n,
&[ctx.int(1), ctx.int(0)],
)
.unwrap();
let expected = [1.0, 0.0, -1.0, 0.0, 1.0, 0.0];
for (k, e) in expected.iter().enumerate() {
assert!((at(&sol, &n, k as i64) - e).abs() < 1e-9, "a({k})");
}
}
#[test]
fn polynomial_forcing_with_resonance() {
let ctx = Context::new();
let n = ctx.symbol("n");
let sol = rsolve_linear(&[ctx.int(-1), ctx.int(1)], Some(&n), &n, &[ctx.int(0)]).unwrap();
for k in 0..8 {
let kf = k as f64;
assert!((at(&sol, &n, k) - kf * (kf - 1.0) / 2.0).abs() < 1e-9);
}
}
#[test]
fn exponential_forcing() {
let ctx = Context::new();
let n = ctx.symbol("n");
let three_n = ctx.int(3).pow(&n);
let sol = rsolve_linear(
&[ctx.int(-2), ctx.int(1)],
Some(&three_n),
&n,
&[ctx.int(0)],
)
.unwrap();
for k in 0..8 {
let e = 3f64.powi(k as i32) - 2f64.powi(k as i32);
assert!((at(&sol, &n, k) - e).abs() < 1e-8);
}
}
#[test]
fn factorial_first_order() {
let ctx = Context::new();
let n = ctx.symbol("n");
let sol = rsolve_first_order(&(&n + 1), &ctx.int(0), &n, Some(&ctx.int(1))).unwrap();
assert_eq!(format!("{sol}"), "n!");
let mut f = 1.0;
for k in 0..7 {
if k > 0 {
f *= k as f64;
}
assert!((at(&sol, &n, k) - f).abs() < 1e-9);
}
}
#[test]
fn first_order_constant_with_forcing() {
let ctx = Context::new();
let n = ctx.symbol("n");
let sol = rsolve_first_order(&ctx.int(2), &ctx.int(1), &n, Some(&ctx.int(0))).unwrap();
for k in 0..8 {
assert!(
(at(&sol, &n, k) - (2f64.powi(k as i32) - 1.0)).abs() < 1e-9,
"a({k}) = {} from {sol}",
at(&sol, &n, k)
);
}
}
#[test]
fn rejects_bad_input() {
let ctx = Context::new();
let n = ctx.symbol("n");
assert!(rsolve_linear(&[ctx.int(1)], None, &n, &[]).is_err());
assert!(rsolve_linear(&[ctx.int(1), ctx.int(0)], None, &n, &[]).is_err());
assert!(
rsolve_linear(
&[ctx.int(1), ctx.int(1)],
None,
&n,
&[ctx.int(0), ctx.int(1)]
)
.is_err()
);
}
}