use std::sync::Arc;
use scirs2_symbolic::eml::eval::{eval_real, EvalCtx};
use scirs2_symbolic::eml::{grad, hessian, LoweredOp};
#[derive(Debug, Clone)]
pub struct SymbolicNewtonResult {
pub x: Vec<f64>,
pub f_final: f64,
pub iters: usize,
pub converged: bool,
}
#[derive(Debug, thiserror::Error)]
pub enum SymbolicNewtonError {
#[error("evaluation error: {0}")]
EvalError(String),
#[error("singular Hessian — cannot invert")]
SingularHessian,
#[error("dimension mismatch: x0 has {x0_len}, cost expects {n_vars}")]
DimMismatch {
x0_len: usize,
n_vars: usize,
},
}
pub fn newton(
cost: &LoweredOp,
x0: &[f64],
max_iter: usize,
tol: f64,
) -> Result<SymbolicNewtonResult, SymbolicNewtonError> {
let n_vars = cost.count_vars();
if x0.len() < n_vars {
return Err(SymbolicNewtonError::DimMismatch {
x0_len: x0.len(),
n_vars,
});
}
let grad_ops: Vec<LoweredOp> = (0..n_vars).map(|i| grad(cost, i)).collect();
let hess_ops: Vec<Vec<LoweredOp>> = hessian(cost, n_vars);
let mut x = x0.to_vec();
let mut converged = false;
let mut iters = 0;
for k in 0..max_iter {
iters = k + 1;
let ctx = EvalCtx::new(&x);
let mut grad_vec: Vec<f64> = Vec::with_capacity(n_vars);
for g_op in &grad_ops {
let v =
eval_real(g_op, &ctx).map_err(|e| SymbolicNewtonError::EvalError(e.to_string()))?;
grad_vec.push(v);
}
let grad_norm: f64 = grad_vec.iter().map(|g| g * g).sum::<f64>().sqrt();
if grad_norm < tol {
converged = true;
break;
}
let mut hess_mat: Vec<Vec<f64>> = Vec::with_capacity(n_vars);
for row in &hess_ops {
let mut hess_row: Vec<f64> = Vec::with_capacity(n_vars);
for h_op in row {
let v = eval_real(h_op, &ctx)
.map_err(|e| SymbolicNewtonError::EvalError(e.to_string()))?;
hess_row.push(v);
}
hess_mat.push(hess_row);
}
let dx = solve_linear(&hess_mat, &grad_vec)?;
for (xi, dxi) in x.iter_mut().zip(dx.iter()) {
*xi -= *dxi;
}
}
let final_ctx = EvalCtx::new(&x);
let f_final =
eval_real(cost, &final_ctx).map_err(|e| SymbolicNewtonError::EvalError(e.to_string()))?;
Ok(SymbolicNewtonResult {
x,
f_final,
iters,
converged,
})
}
#[derive(Debug, Clone)]
pub struct SymbolicOptResult {
pub x: scirs2_core::ndarray::Array1<f64>,
pub f_val: f64,
pub grad_norm: f64,
pub iters: usize,
pub converged: bool,
}
#[derive(Debug, thiserror::Error)]
pub enum SymbolicOptError {
#[error("eval error: {0}")]
EvalError(String),
#[error("gradient eval error: {0}")]
GradEvalError(String),
#[error("Hessian eval error: {0}")]
HessEvalError(String),
#[error("dimension mismatch: expected {expected} variables, got {got}")]
DimMismatch {
expected: usize,
got: usize,
},
#[error("not converged after {iters} iters (grad_norm = {grad_norm:.6e})")]
NotConverged {
iters: usize,
grad_norm: f64,
},
#[error("line search failed")]
LineSearchFailed,
}
fn eval_gradient(grad_ops: &[LoweredOp], x: &[f64]) -> Result<Vec<f64>, SymbolicOptError> {
let ctx = EvalCtx::new(x);
let mut g = Vec::with_capacity(grad_ops.len());
for op in grad_ops {
let v = eval_real(op, &ctx).map_err(|e| SymbolicOptError::GradEvalError(e.to_string()))?;
g.push(v);
}
Ok(g)
}
fn eval_objective(obj: &LoweredOp, x: &[f64]) -> Result<f64, SymbolicOptError> {
let ctx = EvalCtx::new(x);
eval_real(obj, &ctx).map_err(|e| SymbolicOptError::EvalError(e.to_string()))
}
fn l2_norm(v: &[f64]) -> f64 {
v.iter().map(|x| x * x).sum::<f64>().sqrt()
}
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(ai, bi)| ai * bi).sum()
}
pub fn lbfgs_symbolic(
objective: &Arc<LoweredOp>,
x0: scirs2_core::ndarray::ArrayView1<f64>,
max_iter: usize,
tol: f64,
memory: usize,
) -> Result<SymbolicOptResult, SymbolicOptError> {
use scirs2_core::ndarray::Array1;
let n_vars = objective.count_vars();
if x0.len() != n_vars {
return Err(SymbolicOptError::DimMismatch {
expected: n_vars,
got: x0.len(),
});
}
let grad_ops: Vec<LoweredOp> = (0..n_vars).map(|i| grad(objective.as_ref(), i)).collect();
let mut x: Vec<f64> = x0.iter().copied().collect();
let mut g = eval_gradient(&grad_ops, &x)?;
let mut grad_norm = l2_norm(&g);
if max_iter == 0 {
return Err(SymbolicOptError::NotConverged {
iters: 0,
grad_norm,
});
}
if grad_norm < tol {
let f_val = eval_objective(objective.as_ref(), &x)?;
return Ok(SymbolicOptResult {
x: Array1::from_vec(x),
f_val,
grad_norm,
iters: 0,
converged: true,
});
}
let mem = memory.max(1);
let mut history: std::collections::VecDeque<(Vec<f64>, Vec<f64>, f64)> =
std::collections::VecDeque::with_capacity(mem);
let mut converged = false;
let mut iters = 0;
for k in 0..max_iter {
iters = k + 1;
let direction = lbfgs_direction(&g, &history);
let f_k = eval_objective(objective.as_ref(), &x)?;
let dg = dot(&g, &direction);
const C1: f64 = 1e-4; const C2: f64 = 0.9; const MAX_LS: usize = 20;
let mut alpha = 1.0_f64;
let mut x_new: Vec<f64> = Vec::with_capacity(n_vars);
let mut g_new: Vec<f64>;
let mut armijo_ok = false;
let mut ls_iter = 0;
loop {
x_new = x
.iter()
.zip(&direction)
.map(|(xi, di)| xi + alpha * di)
.collect();
let f_new = eval_objective(objective.as_ref(), &x_new)?;
let suff_dec = f_new <= f_k + C1 * alpha * dg;
if suff_dec {
g_new = eval_gradient(&grad_ops, &x_new)?;
let curvature = dot(&g_new, &direction).abs() <= C2 * dg.abs();
if curvature {
armijo_ok = true;
break;
}
armijo_ok = true;
} else {
g_new = Vec::new(); }
ls_iter += 1;
if ls_iter >= MAX_LS {
break;
}
alpha *= 0.5;
}
if !armijo_ok {
x_new = x
.iter()
.zip(&direction)
.map(|(xi, di)| xi + alpha * di)
.collect();
let f_new = eval_objective(objective.as_ref(), &x_new)?;
if f_new > f_k + C1 * alpha * dg {
return Err(SymbolicOptError::LineSearchFailed);
}
g_new = eval_gradient(&grad_ops, &x_new)?;
} else if g_new.is_empty() {
g_new = eval_gradient(&grad_ops, &x_new)?;
}
let s_k: Vec<f64> = x_new.iter().zip(&x).map(|(xn, xo)| xn - xo).collect();
let y_k: Vec<f64> = g_new.iter().zip(&g).map(|(gn, go)| gn - go).collect();
let sy = dot(&s_k, &y_k);
if sy.abs() > 1e-20 {
let rho = 1.0 / sy;
if history.len() == mem {
history.pop_front();
}
history.push_back((s_k, y_k, rho));
}
x = x_new;
g = g_new;
grad_norm = l2_norm(&g);
if grad_norm < tol {
converged = true;
break;
}
}
let f_val = eval_objective(objective.as_ref(), &x)?;
if converged {
Ok(SymbolicOptResult {
x: Array1::from_vec(x),
f_val,
grad_norm,
iters,
converged: true,
})
} else {
Err(SymbolicOptError::NotConverged { iters, grad_norm })
}
}
fn lbfgs_direction(
g: &[f64],
history: &std::collections::VecDeque<(Vec<f64>, Vec<f64>, f64)>,
) -> Vec<f64> {
let n = g.len();
let m = history.len();
let mut q: Vec<f64> = g.to_vec();
let mut alphas: Vec<f64> = vec![0.0; m];
for (idx, (s, y, rho)) in history.iter().rev().enumerate() {
let a = rho * dot(s, &q);
alphas[m - 1 - idx] = a;
for j in 0..n {
q[j] -= a * y[j];
}
}
let mut r: Vec<f64> = if let Some((s, y, _)) = history.back() {
let sy = dot(s, y);
let yy = dot(y, y);
let gamma = if yy > 1e-20 { sy / yy } else { 1.0 };
q.iter().map(|qi| gamma * qi).collect()
} else {
q.clone() };
for (idx, (s, y, rho)) in history.iter().enumerate() {
let beta = rho * dot(y, &r);
let a = alphas[idx];
for j in 0..n {
r[j] += s[j] * (a - beta);
}
}
r.iter().map(|ri| -ri).collect()
}
pub fn trust_region_symbolic(
objective: &Arc<LoweredOp>,
x0: scirs2_core::ndarray::ArrayView1<f64>,
max_iter: usize,
tol: f64,
initial_radius: f64,
) -> Result<SymbolicOptResult, SymbolicOptError> {
use scirs2_core::ndarray::Array1;
let n_vars = objective.count_vars();
if x0.len() != n_vars {
return Err(SymbolicOptError::DimMismatch {
expected: n_vars,
got: x0.len(),
});
}
let grad_ops: Vec<LoweredOp> = (0..n_vars).map(|i| grad(objective.as_ref(), i)).collect();
let hess_ops: Vec<Vec<LoweredOp>> = hessian(objective.as_ref(), n_vars);
let mut x: Vec<f64> = x0.iter().copied().collect();
let mut g = eval_gradient(&grad_ops, &x)?;
let mut grad_norm = l2_norm(&g);
if max_iter == 0 {
return Err(SymbolicOptError::NotConverged {
iters: 0,
grad_norm,
});
}
if grad_norm < tol {
let f_val = eval_objective(objective.as_ref(), &x)?;
return Ok(SymbolicOptResult {
x: Array1::from_vec(x),
f_val,
grad_norm,
iters: 0,
converged: true,
});
}
let mut delta = initial_radius.max(1e-8);
let mut converged = false;
let mut iters = 0;
for k in 0..max_iter {
iters = k + 1;
let f_k = eval_objective(objective.as_ref(), &x)?;
let ctx = EvalCtx::new(&x);
let mut h_mat: Vec<Vec<f64>> = Vec::with_capacity(n_vars);
for row in &hess_ops {
let mut h_row: Vec<f64> = Vec::with_capacity(n_vars);
for h_op in row {
let v = eval_real(h_op, &ctx)
.map_err(|e| SymbolicOptError::HessEvalError(e.to_string()))?;
h_row.push(v);
}
h_mat.push(h_row);
}
let p = dogleg_step_sym(&g, &h_mat, delta);
let x_new: Vec<f64> = x.iter().zip(&p).map(|(xi, pi)| xi + pi).collect();
let f_new = eval_objective(objective.as_ref(), &x_new)?;
let actual_red = f_k - f_new;
let gtp = dot(&g, &p);
let htp: Vec<f64> = matvec(&h_mat, &p);
let phtp = dot(&p, &htp);
let pred_red = -(gtp + 0.5 * phtp);
let rho = if pred_red.abs() < 1e-20 {
1.0
} else {
actual_red / pred_red
};
if rho >= 0.1 {
x = x_new;
g = eval_gradient(&grad_ops, &x)?;
grad_norm = l2_norm(&g);
if grad_norm < tol {
converged = true;
if rho >= 0.75 {
let p_norm = l2_norm(&p);
if p_norm >= 0.8 * delta {
delta = (2.0 * delta).min(100.0);
}
}
break;
}
}
if rho >= 0.75 {
let p_norm = l2_norm(&p);
if p_norm >= 0.8 * delta {
delta = (2.0 * delta).min(100.0);
}
} else if rho < 0.25 {
delta *= 0.25;
}
if delta < 1e-14 {
break;
}
}
let f_val = eval_objective(objective.as_ref(), &x)?;
if converged {
Ok(SymbolicOptResult {
x: Array1::from_vec(x),
f_val,
grad_norm,
iters,
converged: true,
})
} else {
Err(SymbolicOptError::NotConverged { iters, grad_norm })
}
}
fn dogleg_step_sym(g: &[f64], h: &[Vec<f64>], delta: f64) -> Vec<f64> {
let n = g.len();
let neg_g: Vec<f64> = g.iter().map(|gi| -gi).collect();
let p_n_opt = solve_linear_opt(h, &neg_g);
let gtg = dot(g, g);
let hg = matvec(h, g);
let gthg = dot(g, &hg);
let p_sd: Vec<f64> = if gthg > 1e-20 {
let scale = gtg / gthg;
g.iter().map(|gi| -scale * gi).collect()
} else {
let gn = gtg.sqrt().max(1e-20);
g.iter().map(|gi| -gi / gn).collect()
};
let p_sd_norm = l2_norm(&p_sd);
if let Some(p_n) = p_n_opt {
let p_n_norm = l2_norm(&p_n);
if p_n_norm <= delta {
return p_n;
}
if p_sd_norm >= delta {
return p_sd.iter().map(|pi| delta / p_sd_norm * pi).collect();
}
let d: Vec<f64> = p_n.iter().zip(&p_sd).map(|(pni, psi)| pni - psi).collect();
let dd = dot(&d, &d);
let psd_d = dot(&p_sd, &d);
let psd2 = p_sd_norm * p_sd_norm;
let discriminant = psd_d * psd_d - dd * (psd2 - delta * delta);
let tau = if dd < 1e-20 || discriminant < 0.0 {
1.0
} else {
(-psd_d + discriminant.sqrt()) / dd
};
let tau = tau.clamp(0.0, 1.0);
p_sd.iter()
.zip(&d)
.map(|(psi, di)| psi + tau * di)
.collect()
} else {
if p_sd_norm <= delta {
p_sd
} else {
p_sd.iter().map(|pi| delta / p_sd_norm * pi).collect()
}
}
}
fn matvec(h: &[Vec<f64>], v: &[f64]) -> Vec<f64> {
h.iter().map(|row| dot(row, v)).collect()
}
fn solve_linear_opt(a: &[Vec<f64>], b: &[f64]) -> Option<Vec<f64>> {
let n = b.len();
if n == 0 {
return Some(Vec::new());
}
let mut mat: Vec<Vec<f64>> = a
.iter()
.zip(b.iter())
.map(|(row, &bi)| {
let mut r = row.clone();
r.push(bi);
r
})
.collect();
for k in 0..n {
let mut max_idx = k;
let mut max_val = mat[k][k].abs();
for i in (k + 1)..n {
let v = mat[i][k].abs();
if v > max_val {
max_val = v;
max_idx = i;
}
}
if max_val < 1e-12 {
return None; }
mat.swap(k, max_idx);
for i in (k + 1)..n {
let factor = mat[i][k] / mat[k][k];
for j in k..(n + 1) {
mat[i][j] -= factor * mat[k][j];
}
}
}
let mut x = vec![0.0; n];
for i in (0..n).rev() {
let mut sum = mat[i][n];
for j in (i + 1)..n {
sum -= mat[i][j] * x[j];
}
x[i] = sum / mat[i][i];
}
Some(x)
}
fn solve_linear(a: &[Vec<f64>], b: &[f64]) -> Result<Vec<f64>, SymbolicNewtonError> {
let n = b.len();
if n == 0 {
return Ok(Vec::new());
}
let mut mat: Vec<Vec<f64>> = a
.iter()
.zip(b.iter())
.map(|(row, &bi)| {
let mut r = row.clone();
r.push(bi);
r
})
.collect();
for k in 0..n {
let mut max_idx = k;
let mut max_val = mat[k][k].abs();
for i in (k + 1)..n {
let v = mat[i][k].abs();
if v > max_val {
max_val = v;
max_idx = i;
}
}
if max_val < 1e-12 {
return Err(SymbolicNewtonError::SingularHessian);
}
mat.swap(k, max_idx);
for i in (k + 1)..n {
let factor = mat[i][k] / mat[k][k];
for j in k..(n + 1) {
mat[i][j] -= factor * mat[k][j];
}
}
}
let mut x = vec![0.0; n];
for i in (0..n).rev() {
let mut sum = mat[i][n];
for j in (i + 1)..n {
sum -= mat[i][j] * x[j];
}
x[i] = sum / mat[i][i];
}
Ok(x)
}
pub struct KktSystem {
pub lagrangian: LoweredOp,
pub stationarity: Vec<LoweredOp>,
pub constraint_residuals: Vec<LoweredOp>,
pub n_vars: usize,
pub n_constraints: usize,
}
#[derive(Debug)]
pub enum LagrangianError {
DimMismatch {
expected: usize,
got: usize,
},
GradError(String),
}
pub fn build_kkt(
objective: &Arc<LoweredOp>,
constraints: &[Arc<LoweredOp>],
n_vars: usize,
) -> Result<KktSystem, LagrangianError> {
let m = constraints.len();
let obj_vars = objective.count_vars();
if obj_vars > n_vars {
return Err(LagrangianError::DimMismatch {
expected: n_vars,
got: obj_vars,
});
}
let mut lagrangian: LoweredOp = objective.as_ref().clone();
for (i, g) in constraints.iter().enumerate() {
let lam_i = LoweredOp::Var(n_vars + i);
let term = LoweredOp::Mul(Box::new(lam_i), Box::new(g.as_ref().clone()));
lagrangian = LoweredOp::Add(Box::new(lagrangian), Box::new(term));
}
let stationarity: Vec<LoweredOp> = (0..n_vars).map(|j| grad(&lagrangian, j)).collect();
let constraint_residuals: Vec<LoweredOp> =
constraints.iter().map(|g| g.as_ref().clone()).collect();
Ok(KktSystem {
lagrangian,
stationarity,
constraint_residuals,
n_vars,
n_constraints: m,
})
}
pub fn solve_lagrangian_symbolic(
objective: &Arc<LoweredOp>,
constraints: &[Arc<LoweredOp>],
x0: scirs2_core::ndarray::ArrayView1<f64>,
lambda0: scirs2_core::ndarray::ArrayView1<f64>,
max_iter: usize,
tol: f64,
) -> Result<SymbolicOptResult, SymbolicOptError> {
use scirs2_core::ndarray::Array1;
let m = constraints.len();
let n_vars = std::iter::once(objective.count_vars())
.chain(constraints.iter().map(|c| c.count_vars()))
.max()
.unwrap_or(0);
if x0.len() != n_vars {
return Err(SymbolicOptError::DimMismatch {
expected: n_vars,
got: x0.len(),
});
}
if lambda0.len() != m {
return Err(SymbolicOptError::DimMismatch {
expected: m,
got: lambda0.len(),
});
}
let kkt = build_kkt(objective, constraints, n_vars).map_err(|e| match e {
LagrangianError::DimMismatch { expected, got } => {
SymbolicOptError::DimMismatch { expected, got }
}
LagrangianError::GradError(s) => SymbolicOptError::GradEvalError(s),
})?;
let big_n = n_vars + m;
let mut z: Vec<f64> = x0.iter().copied().chain(lambda0.iter().copied()).collect();
let eqs: Vec<&LoweredOp> = kkt
.stationarity
.iter()
.chain(kkt.constraint_residuals.iter())
.collect();
let jac_ops: Vec<Vec<LoweredOp>> = eqs
.iter()
.map(|eq| (0..big_n).map(|j| grad(eq, j)).collect())
.collect();
let initial_residual = {
let ctx = EvalCtx::new(&z);
let mut f_vec = Vec::with_capacity(big_n);
for eq in &eqs {
let v = eval_real(eq, &ctx).map_err(|e| SymbolicOptError::EvalError(e.to_string()))?;
f_vec.push(v);
}
f_vec
};
let mut residual_norm = l2_norm(&initial_residual);
if max_iter == 0 {
return Err(SymbolicOptError::NotConverged {
iters: 0,
grad_norm: residual_norm,
});
}
if residual_norm < tol {
let f_val = eval_objective(objective.as_ref(), &z[..n_vars])?;
return Ok(SymbolicOptResult {
x: Array1::from_vec(z[..n_vars].to_vec()),
f_val,
grad_norm: residual_norm,
iters: 0,
converged: true,
});
}
let mut converged = false;
let mut iters = 0;
for k in 0..max_iter {
iters = k + 1;
let ctx = EvalCtx::new(&z);
let mut f_vec: Vec<f64> = Vec::with_capacity(big_n);
for eq in &eqs {
let v = eval_real(eq, &ctx).map_err(|e| SymbolicOptError::EvalError(e.to_string()))?;
f_vec.push(v);
}
residual_norm = l2_norm(&f_vec);
if residual_norm < tol {
converged = true;
break;
}
let mut jac_mat: Vec<Vec<f64>> = Vec::with_capacity(big_n);
for row_ops in &jac_ops {
let mut row: Vec<f64> = Vec::with_capacity(big_n);
for op in row_ops {
let v =
eval_real(op, &ctx).map_err(|e| SymbolicOptError::EvalError(e.to_string()))?;
row.push(v);
}
jac_mat.push(row);
}
let neg_f: Vec<f64> = f_vec.iter().map(|v| -v).collect();
let dz = match solve_linear_opt(&jac_mat, &neg_f) {
Some(d) => d,
None => {
neg_f
}
};
for (zi, dzi) in z.iter_mut().zip(dz.iter()) {
*zi += dzi;
}
}
let f_val = eval_objective(objective.as_ref(), &z[..n_vars])?;
if converged {
Ok(SymbolicOptResult {
x: Array1::from_vec(z[..n_vars].to_vec()),
f_val,
grad_norm: residual_norm,
iters,
converged: true,
})
} else {
Err(SymbolicOptError::NotConverged {
iters,
grad_norm: residual_norm,
})
}
}
pub mod line_search;
pub use line_search::{OptLineSearchError, SymbolicLineSearch};
#[cfg(test)]
mod tests {
use super::*;
fn var(i: usize) -> LoweredOp {
LoweredOp::Var(i)
}
fn c(v: f64) -> LoweredOp {
LoweredOp::Const(v)
}
#[test]
fn newton_x_squared_one_step() {
let cost = LoweredOp::Mul(Box::new(var(0)), Box::new(var(0)));
let result = newton(&cost, &[5.0], 10, 1e-8).expect("converge");
assert!(result.converged);
assert!(result.x[0].abs() < 1e-6, "x = {}", result.x[0]);
assert!(result.iters <= 2);
}
#[test]
fn newton_quadratic_2d() {
let cost = LoweredOp::Add(
Box::new(LoweredOp::Mul(Box::new(var(0)), Box::new(var(0)))),
Box::new(LoweredOp::Mul(Box::new(var(1)), Box::new(var(1)))),
);
let result = newton(&cost, &[3.0, -4.0], 10, 1e-8).expect("converge");
assert!(result.converged);
assert!(result.x[0].abs() < 1e-6);
assert!(result.x[1].abs() < 1e-6);
}
#[test]
fn newton_shifted_quadratic() {
let inner = LoweredOp::Sub(Box::new(var(0)), Box::new(c(3.0)));
let cost = LoweredOp::Mul(Box::new(inner.clone()), Box::new(inner));
let result = newton(&cost, &[0.0], 10, 1e-8).expect("converge");
assert!(result.converged);
assert!((result.x[0] - 3.0).abs() < 1e-6, "x = {}", result.x[0]);
}
#[test]
fn newton_dim_mismatch_returns_err() {
let cost = LoweredOp::Mul(Box::new(var(0)), Box::new(var(0)));
let result = newton(&cost, &[], 10, 1e-8);
assert!(matches!(
result,
Err(SymbolicNewtonError::DimMismatch { .. })
));
}
#[test]
fn solve_linear_2x2() {
let a = vec![vec![2.0, 1.0], vec![1.0, 3.0]];
let b = vec![5.0, 10.0];
let x = solve_linear(&a, &b).expect("solve");
assert!((x[0] - 1.0).abs() < 1e-10);
assert!((x[1] - 3.0).abs() < 1e-10);
}
#[test]
fn solve_linear_singular_returns_err() {
let a = vec![vec![1.0, 2.0], vec![2.0, 4.0]]; let b = vec![3.0, 6.0];
assert!(matches!(
solve_linear(&a, &b),
Err(SymbolicNewtonError::SingularHessian)
));
}
}