use crate::api::expr::Ex;
use crate::base::errors::SymplexError;
use crate::domains::matrix::{Matrix, all3, ex_is_zero};
pub use crate::domains::matrix_decomp::hessian;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum CoordinateSystem {
Cartesian,
Cylindrical,
Spherical,
}
impl CoordinateSystem {
pub fn scale_factors(self, vars: &[&Ex]) -> Vec<Ex> {
match self {
CoordinateSystem::Cartesian => {
let one = vars[0].context().one();
vec![one; vars.len()]
}
CoordinateSystem::Cylindrical => {
assert_eq!(
vars.len(),
3,
"cylindrical coordinates need exactly 3 variables (r, phi, z)"
);
let one = vars[0].context().one();
vec![one.clone(), vars[0].clone(), one]
}
CoordinateSystem::Spherical => {
assert_eq!(
vars.len(),
3,
"spherical coordinates need exactly 3 variables (r, theta, phi)"
);
let one = vars[0].context().one();
vec![one, vars[0].clone(), vars[0] * &vars[1].sin()]
}
}
}
}
fn check_field(field: &Matrix, vars: &[&Ex], op: &str) {
assert_eq!(
field.nrows(),
vars.len(),
"{op}: field dimension ({}) must match variable count ({})",
field.nrows(),
vars.len()
);
assert_eq!(field.ncols(), 1, "{op}: field must be a column vector");
}
pub fn gradient(f: &Ex, vars: &[&Ex]) -> Matrix {
gradient_in(f, vars, CoordinateSystem::Cartesian)
}
pub fn gradient_in(f: &Ex, vars: &[&Ex], cs: CoordinateSystem) -> Matrix {
assert!(!vars.is_empty(), "gradient: vars must be non-empty");
let h = cs.scale_factors(vars);
let partials: Vec<Ex> = vars
.iter()
.zip(h.iter())
.map(|(v, hi)| {
let d = f.diff(v);
if hi.is_one_structural() { d } else { &d / hi }
})
.collect();
Matrix::col_vector(partials)
}
pub fn divergence(field: &Matrix, vars: &[&Ex]) -> Ex {
divergence_in(field, vars, CoordinateSystem::Cartesian)
}
pub fn divergence_in(field: &Matrix, vars: &[&Ex], cs: CoordinateSystem) -> Ex {
check_field(field, vars, "divergence");
let ctx = vars[0].context();
if cs == CoordinateSystem::Cartesian {
let mut sum = ctx.zero();
for (i, var) in vars.iter().enumerate() {
sum = &sum + &field.get(i, 0).diff(var);
}
return sum;
}
let h = cs.scale_factors(vars);
let jac = &(&h[0] * &h[1]) * &h[2];
let mut sum = ctx.zero();
for (i, var) in vars.iter().enumerate() {
let weight = &jac / &h[i];
let term = (&weight * field.get(i, 0)).diff(var);
sum = &sum + &term;
}
&sum / &jac
}
pub fn curl(field: &Matrix, vars: &[&Ex]) -> Matrix {
curl_in(field, vars, CoordinateSystem::Cartesian)
}
pub fn curl_in(field: &Matrix, vars: &[&Ex], cs: CoordinateSystem) -> Matrix {
assert_eq!(field.nrows(), 3, "curl requires 3D vector field");
assert_eq!(vars.len(), 3, "curl requires 3 variables");
assert_eq!(field.ncols(), 1, "field must be a column vector");
let h = cs.scale_factors(vars);
let hf: Vec<Ex> = (0..3).map(|i| &h[i] * field.get(i, 0)).collect();
let comp = |j: usize, k: usize| {
let num = &hf[k].diff(vars[j]) - &hf[j].diff(vars[k]);
let denom = &h[j] * &h[k];
if denom.is_one_structural() {
num
} else {
&num / &denom
}
};
Matrix::col_vector(vec![comp(1, 2), comp(2, 0), comp(0, 1)])
}
pub fn laplacian(f: &Ex, vars: &[&Ex]) -> Ex {
laplacian_in(f, vars, CoordinateSystem::Cartesian)
}
pub fn laplacian_in(f: &Ex, vars: &[&Ex], cs: CoordinateSystem) -> Ex {
let grad = gradient_in(f, vars, cs);
divergence_in(&grad, vars, cs)
}
pub fn directional_derivative(f: &Ex, vars: &[&Ex], direction: &Matrix) -> Ex {
check_field(direction, vars, "directional_derivative");
let grad = gradient(f, vars);
crate::domains::matrix::dot(&grad, direction)
}
pub fn is_conservative(field: &Matrix, vars: &[&Ex]) -> Option<bool> {
if field.nrows() != 3 || vars.len() != 3 || field.ncols() != 1 {
return Some(false);
}
let c = curl(field, vars);
all3(c.iter().map(ex_is_zero))
}
pub fn is_irrotational(field: &Matrix, vars: &[&Ex]) -> Option<bool> {
is_conservative(field, vars)
}
pub fn is_solenoidal(field: &Matrix, vars: &[&Ex]) -> Option<bool> {
ex_is_zero(&divergence(field, vars))
}
pub fn scalar_potential(field: &Matrix, vars: &[&Ex]) -> Result<Ex, SymplexError> {
if field.ncols() != 1 || field.nrows() != vars.len() || vars.is_empty() {
return Err(SymplexError::InvalidArgument {
operation: "scalar_potential",
reason: format!(
"field must be an n×1 column vector with n == vars.len() (got {}×{} and {} variables)",
field.nrows(),
field.ncols(),
vars.len()
),
});
}
let ctx = vars[0].context();
let mut phi = ctx.zero();
for (i, var) in vars.iter().enumerate() {
let residual = (field.get(i, 0) - &phi.diff(var)).expand();
if residual.is_zero_structural() {
continue;
}
let anti = residual
.try_integrate(var)
.map_err(|e| SymplexError::ComputationFailed {
operation: "scalar_potential",
reason: format!("could not integrate component {i} ({residual}): {e}"),
})?;
phi = (&phi + &anti).expand();
}
for (i, var) in vars.iter().enumerate() {
let diff = (&phi.diff(var) - field.get(i, 0)).expand().simplify();
if !diff.is_zero_structural() {
return Err(SymplexError::ComputationFailed {
operation: "scalar_potential",
reason: format!(
"field is not conservative: ∂φ/∂{} − F_{} = {} ≠ 0",
var,
i + 1,
diff
),
});
}
}
Ok(phi)
}
fn definite(g: &Ex, t: &Ex, a: &Ex, b: &Ex) -> Ex {
let anti = g.integrate(t);
(&anti.subs(t, b) - &anti.subs(t, a)).eval()
}
pub fn line_integral_scalar(f: &Ex, vars: &[&Ex], curve: &[Ex], t: &Ex, a: &Ex, b: &Ex) -> Ex {
assert!(
!vars.is_empty(),
"line_integral_scalar: vars must be non-empty"
);
assert_eq!(
curve.len(),
vars.len(),
"line_integral_scalar: curve has {} components but {} variables",
curve.len(),
vars.len()
);
let ctx = t.context();
let mut f_on_curve = f.clone();
let mut speed_sq = ctx.zero();
for (v, c) in vars.iter().zip(curve.iter()) {
f_on_curve = f_on_curve.subs(v, c);
speed_sq += c.diff(t).powi(2);
}
let integrand = (&f_on_curve * &speed_sq.simplify().sqrt()).simplify();
definite(&integrand, t, a, b)
}
pub fn line_integral_vector(
field: &Matrix,
vars: &[&Ex],
curve: &[Ex],
t: &Ex,
a: &Ex,
b: &Ex,
) -> Ex {
check_field(field, vars, "line_integral_vector");
assert_eq!(
curve.len(),
vars.len(),
"line_integral_vector: curve has {} components but {} variables",
curve.len(),
vars.len()
);
let ctx = t.context();
let mut integrand = ctx.zero();
for (i, c) in curve.iter().enumerate() {
let mut fi = field.get(i, 0).clone();
for (v, cc) in vars.iter().zip(curve.iter()) {
fi = fi.subs(v, cc);
}
integrand += &fi * &c.diff(t);
}
definite(&integrand.simplify(), t, a, b)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::context::Context;
#[test]
fn cartesian_operators_unchanged() {
let ctx = Context::new();
let (x, y, z) = (ctx.symbol("x"), ctx.symbol("y"), ctx.symbol("z"));
let f = &(&x.powi(2) * &y) + &z.powi(3);
let g = gradient(&f, &[&x, &y, &z]);
assert_eq!(g.get(0, 0), &(&(&x * 2) * &y));
assert_eq!(g.get(2, 0), &(&z.powi(2) * 3));
assert_eq!(laplacian(&f, &[&x, &y, &z]), &y * 2 + &z * 6);
let field = Matrix::col_vector(vec![x.clone(), y.clone(), z.clone()]);
assert_eq!(divergence(&field, &[&x, &y, &z]), ctx.int(3));
assert_eq!(is_conservative(&field, &[&x, &y, &z]), Some(true));
assert_eq!(is_irrotational(&field, &[&x, &y, &z]), Some(true));
assert_eq!(is_solenoidal(&field, &[&x, &y, &z]), Some(false));
let a = ctx.symbol("a");
let unknown = Matrix::col_vector(vec![ctx.int(0), ctx.int(0), &a * &x]);
assert_eq!(is_conservative(&unknown, &[&x, &y, &z]), None);
}
#[test]
fn cylindrical_identities() {
let ctx = Context::new();
let (r, ph, z) = (ctx.symbol("r"), ctx.symbol("phi"), ctx.symbol("z"));
let vars = [&r, &ph, &z];
let l = laplacian_in(&r.powi(2), &vars, CoordinateSystem::Cylindrical).simplify();
assert_eq!(l, ctx.int(4));
let l2 = laplacian_in(&r.ln(), &vars, CoordinateSystem::Cylindrical).simplify();
assert!(l2.is_zero_structural());
let f = Matrix::col_vector(vec![r.clone(), ctx.int(0), ctx.int(0)]);
assert_eq!(
divergence_in(&f, &vars, CoordinateSystem::Cylindrical).simplify(),
ctx.int(2)
);
let g = gradient_in(
&(&r.powi(2) * &ph.cos()),
&vars,
CoordinateSystem::Cylindrical,
);
let c = curl_in(&g, &vars, CoordinateSystem::Cylindrical).simplify();
for i in 0..3 {
assert!(
c.get(i, 0).is_zero_structural(),
"curl grad component {i} = {}",
c.get(i, 0)
);
}
let vortex = Matrix::col_vector(vec![ctx.int(0), ctx.int(1) / &r, ctx.int(0)]);
let cv = curl_in(&vortex, &vars, CoordinateSystem::Cylindrical).simplify();
assert!(cv.get(2, 0).is_zero_structural());
}
#[test]
fn spherical_identities() {
let ctx = Context::new();
let (r, th, ph) = (ctx.symbol("r"), ctx.symbol("theta"), ctx.symbol("phi"));
let vars = [&r, &th, &ph];
assert_eq!(
CoordinateSystem::Spherical.scale_factors(&vars),
vec![ctx.one(), r.clone(), &r * &th.sin()]
);
let l = laplacian_in(&(ctx.int(1) / &r), &vars, CoordinateSystem::Spherical).simplify();
assert!(l.is_zero_structural(), "{l}");
let l2 = laplacian_in(&r.powi(2), &vars, CoordinateSystem::Spherical).simplify();
assert_eq!(l2, ctx.int(6));
let l3 = laplacian_in(&(&r * &th.cos()), &vars, CoordinateSystem::Spherical).simplify();
assert!(l3.is_zero_structural(), "{l3}");
let coulomb = Matrix::col_vector(vec![ctx.int(1) / r.powi(2), ctx.int(0), ctx.int(0)]);
let d = divergence_in(&coulomb, &vars, CoordinateSystem::Spherical).simplify();
assert!(d.is_zero_structural(), "{d}");
let g = gradient_in(
&(&r.powi(3) * &(&th.sin() * &ph.cos())),
&vars,
CoordinateSystem::Spherical,
);
let c = curl_in(&g, &vars, CoordinateSystem::Spherical).simplify();
for i in 0..3 {
assert!(
c.get(i, 0).is_zero_structural(),
"component {i} = {}",
c.get(i, 0)
);
}
}
#[test]
fn directional_and_potential() {
let ctx = Context::new();
let (x, y, z) = (ctx.symbol("x"), ctx.symbol("y"), ctx.symbol("z"));
let f = &x * &y;
let d = Matrix::col_vector(vec![ctx.int(2), ctx.int(-1)]);
assert_eq!(
directional_derivative(&f, &[&x, &y], &d).expand(),
&y * 2 - &x
);
let phi = &(&x.powi(2) * &y) + &(&y * &z.sin()) + &z.powi(3);
let field = gradient(&phi, &[&x, &y, &z]);
let back = scalar_potential(&field, &[&x, &y, &z]).unwrap();
assert!(
(&back - &phi).expand().simplify().is_zero_structural(),
"{back}"
);
let rot = Matrix::col_vector(vec![-&y, x.clone(), ctx.int(0)]);
assert!(scalar_potential(&rot, &[&x, &y, &z]).is_err());
assert!(scalar_potential(&rot, &[&x, &y]).is_err());
let p2 =
scalar_potential(&Matrix::col_vector(vec![y.clone(), x.clone()]), &[&x, &y]).unwrap();
assert!((&p2 - &(&x * &y)).expand().is_zero_structural());
}
#[test]
fn line_integrals() {
let ctx = Context::new();
let (x, y, t) = (ctx.symbol("x"), ctx.symbol("y"), ctx.symbol("t"));
let seg = [&t * 3, &t * 4];
let len = line_integral_scalar(&ctx.int(1), &[&x, &y], &seg, &t, &ctx.int(0), &ctx.int(1));
assert_eq!(len.simplify(), ctx.int(5));
let s = line_integral_scalar(&(&x + &y), &[&x, &y], &seg, &t, &ctx.int(0), &ctx.int(1));
assert_eq!(s.simplify(), ctx.rational(35, 2));
let f = Matrix::col_vector(vec![y.clone(), x.clone()]);
let w = line_integral_vector(&f, &[&x, &y], &seg, &t, &ctx.int(0), &ctx.int(1));
assert_eq!(w.simplify(), ctx.int(12));
let rot = Matrix::col_vector(vec![-&y, x.clone()]);
let circle = [t.cos(), t.sin()];
let circ = line_integral_vector(&rot, &[&x, &y], &circle, &t, &ctx.int(0), &(ctx.pi() * 2));
assert_eq!(circ.simplify(), ctx.pi() * 2);
}
#[test]
#[should_panic(expected = "exactly 3 variables")]
fn cylindrical_needs_three_vars() {
let ctx = Context::new();
let (r, ph) = (ctx.symbol("r"), ctx.symbol("phi"));
let _ = gradient_in(&r, &[&r, &ph], CoordinateSystem::Cylindrical);
}
}