symplex 0.22.2

Exact symbolic mathematics for Rust: calculus, summation, solving, linear algebra, transforms, compile-time dimensional analysis, and Rust/C code generation
Documentation
# API Patterns

Every public operation in symplex follows one of a small number of patterns. Once you know which pattern a method uses, you know its return type and what a "failure" looks like.

## Pattern 1 — Always returns `Ex`

Operations for which "unchanged" or "unevaluated" is a legitimate answer never fail:

```rust
# use symplex::prelude::*;
# let ctx = Context::new();
# let (x, k, t, s, y) = (ctx.symbol("x"), ctx.symbol("k"), ctx.symbol("t"), ctx.symbol("s"), ctx.symbol("y"));
# let (a, b, v) = (ctx.int(0), ctx.int(1), ctx.int(2));
# let expr = x.sin();
# let rules = RuleSet::from_rules(vec![]);
expr.simplify();         expr.expand();          expr.eval();
expr.factor(&x);         expr.subs(&x, &v);      expr.rewrite(&rules);
expr.diff(&x);           // Derivative(f, x) if it cannot differentiate
expr.integrate(&x);      // Integral(f, x) if no closed form
expr.integrate_definite(&x, &a, &b);  // Integral node if undecided
expr.limit(&x, &a);      // Limit(f, x, a)
expr.summation(&k, &a, &b);           // Sum node
expr.laplace(&t, &s);    // LaplaceTransform node
expr.solve_ode(&y, &x);  // DSolve node
```

Check with `has_unevaluated()`. Note that `RootOf` and `RootSum` are exact algebraic answers and are **not** counted.

## Pattern 2 — `try_` twin returns `Result<Ex>`

Every Pattern-1 method that can produce an unevaluated form has a `try_` twin that returns `Err` instead. The twin calls the base method and checks `has_unevaluated()`, so there is no behavioural drift between the two.

```rust
# use symplex::prelude::*;
# let ctx = Context::new();
# let (x, k) = (ctx.symbol("x"), ctx.symbol("k"));
# let (a, b, lo, hi) = (ctx.int(0), ctx.int(1), ctx.int(1), ctx.int(10));
# let expr = x.powi(2);
let anti = expr.try_integrate(&x)?;                       // Err(ComputationFailed) if unevaluated
let val  = expr.try_integrate_definite(&x, &a, &b)?;      // Err(Divergent) if proven divergent
let lim  = expr.try_limit_right(&x, &a)?;
let sum  = expr.try_summation(&k, &lo, &hi)?;
# Ok::<(), SymplexError>(())
```

Available twins: `try_diff`, `try_integrate`, `try_integrate_definite`, `try_limit`, `try_limit_left`, `try_limit_right`, `try_limit_dir`, `try_series`, `try_maclaurin`, `try_series_at_infinity`, `try_summation`, `try_product_over`, `try_laplace`, `try_inverse_laplace`, `try_residue`, `try_gosper_sum`, `try_solve_ode`, `try_solve_gt`/`ge`/`lt`/`le`.

## Pattern 3 — Numeric boundary → `Result`

Crossing from symbols to numbers can fail (free symbols, unsupported node, precision exhausted, non-convergence):

```rust
# use symplex::prelude::*;
# let ctx = Context::new();
# let x = ctx.symbol("x");
# let expr = x.powi(2) - 2;
# let (a, b) = (ctx.int(0), ctx.int(1));
expr.eval_f64();                       // Result<f64>
expr.eval_complex64();                 // Result<Complex64>  (num_complex; in the prelude)
expr.eval_decimal(50);                 // Result<String>
expr.compile(&["x"]);                  // Result<CompiledFn>
Ex::compile_many(&[&a, &b], &["x"]);   // Result<CompiledFnVec>
expr.to_rust_fn("f", &["x"]);          // Result<String>
expr.to_c_fn("f", &["x"]);             // Result<String>
expr.integrate_numeric(&x, &a, &b);    // Result<f64>
expr.nroots(&x, 12);                   // Result<Vec<Complex64>>
expr.textplot(&x, -2.0, 2.0);          // Result<String>  (all plotting methods)
```

## Pattern 4 — Queries → `Option`

Three-valued questions return `Option<bool>` (yes / no / cannot decide) and structural queries return `Option<T>`:

```rust
# use symplex::prelude::*;
# use symplex::vector;
# let ctx = Context::new();
# let (x, y, k) = (ctx.symbol("x"), ctx.symbol("y"), ctx.symbol("k"));
# let expr = x.powi(2) + 1;
# let (other, g, e) = (x.powi(2) + 1, &x - 1, ctx.int(0));
# let set = ctx.interval(&ctx.int(0), &ctx.int(1), IntervalKind::Closed);
# let t = ctx.reals();
# let matrix = Matrix::identity(&ctx, 2);
# let bool_ex = x.gt(&ctx.int(0));
# let f = Matrix::col_vector(vec![y.clone(), x.clone()]);
# let vars = [&x, &y];
expr.is_positive();  expr.is_real();  expr.is_integer();  expr.equals(&other);
expr.is_convergent(&k);  expr.is_absolutely_convergent(&k);  expr.is_real_valued();
set.contains(&e);  set.is_subset(&t);  set.is_disjoint(&t);  set.is_empty();  set.is_open();
matrix.is_symmetric();  matrix.is_orthogonal();  matrix.is_positive_definite();  matrix.is_diagonalizable();
bool_ex.is_tautology();  bool_ex.satisfiable();
vector::is_conservative(&f, &vars);

expr.degree(&x);  expr.coeff(&x, 2);  expr.resultant(&g, &x);  expr.discriminant(&x);
expr.hypergeometric_ratio(&k);  expr.as_i64();  expr.as_rational();  set.inf();  set.measure();
```

`None` is a real answer — do not `unwrap()` it. A symbolic entry usually means the question cannot be decided without assumptions.

## Pattern 5 — Structural preconditions → `Result`

Operations whose input must have a particular shape:

```rust
# use symplex::prelude::*;
# let ctx = Context::new();
# let x = ctx.symbol("x");
# let matrix = Matrix::identity(&ctx, 2);
# let other = Matrix::identity(&ctx, 2);
# let (i, j) = (0, 0);
# let rows = vec![vec![ctx.int(1), ctx.int(2)], vec![ctx.int(3), ctx.int(4)]];
# let (lhs, rhs) = (x.sin().powi(2), 1 - x.cos().powi(2));
# let bool_ex = x.gt(&ctx.int(0));
# let atoms = [bool_ex.clone()];
matrix.det();            matrix.inv();          matrix.matmul(&other);
matrix.cholesky();       matrix.lu();           matrix.minor(i, j);
matrix.eigenvals();      matrix.jordan_form();  matrix.qr();
Matrix::new(rows);       Matrix::from_i64(&ctx, &[&[1, 2], &[3, 4]]);
Rule::try_new("sin2", &lhs, &rhs);              bool_ex.truth_table(&atoms);
```

## Pattern 6 — Mathematical outcomes as `Err` or enum variants

Solvers distinguish "no method" from "the answer is: none" or "the answer is: all":

| Call | Outcome | Representation |
|------|---------|----------------|
| `solve` | identity | `Err(SymplexError::InfiniteSolutions { .. })` |
| `solve` | contradiction / range violation | `Err(SymplexError::NoSolution { .. })` |
| `solve_system_ex` | positive-dimensional | `Err(InfiniteSolutions)` |
| `linsolve` | contradictory system | `Ok(LinearSolution::Inconsistent)` |
| `linsolve` | under-determined | `Ok(LinearSolution::Parametric { .. })` |
| `try_integrate_definite` | divergent | `Err(SymplexError::Divergent { .. })` |
| `laplace_final_value` | unstable pole | `Err(Divergent)` |
| `fourier_transform`, `mellin_transform`, `z_transform` | not in table / missing sign assumption | `Err(ComputationFailed)` (no unevaluated node exists for these) |

## Ownership and references

`Ex` is `Clone` (cheap: an `Arc` bump and a `u32`) but not `Copy`. Operators are implemented on references and values (`&x + &y`, `&x * 2`, `x.clone() / 3`, `2 * &x`, `x += 1`), and scalars of type `i32`, `i64`, `u32`, `u64`, `i128`, `f64`, `BigInt`, `Ratio<BigInt>` are accepted through the `Scalar`/`ToEx` traits. Methods take `&Ex` arguments. Collections use `Context::sum(iter)` / `Context::product(iter)` or `Option<Ex>` — `iter.sum::<Ex>()` panics on an empty iterator because there is no context to build `0` in.

## Contexts

Everything belongs to a `Context`. Mixing expressions from different contexts panics with a clear message (treated as a logic error like indexing out of bounds). The library never calls `unwrap`/`expect`/`panic!`/`unreachable!` on user data (ratchet `tests/unit/test_no_panics.rs`); the remaining `assert!`s on caller-supplied *shapes* (e.g. `Matrix::zeros(0, n)`, `Context::symbol("")`) are documented under `# Panics` on each item and counted by the same ratchet. `Context` is `Clone`; clones share the arena. `Context::compact(&roots)` garbage-collects into a fresh context.

## Naming conventions

| Suffix / prefix | Meaning | Example |
|-----------------|---------|---------|
| `try_` | `Result` twin of a Pattern-1 method | `try_integrate` |
| `_with` | same operation with an options struct | `simplify_with(&SimplifyOpts)`, `rewrite_with(&rules, &RewriteOpts)`, `integrate_numeric_with(…, &QuadOpts)` |
| `_traced` | also returns `Vec<Step>` | `simplify_traced`, `rewrite_traced` |
| `_or_empty` | swallow the error into an empty `Vec` | `solve_or_empty` |
| `_general` | complete solution family | `solve_general` |
| `_ivp` | with initial conditions | `solve_ode_ivp` |
| `_all` | all variables (multivariate) | `factor_all`, `sqrt_mod_all` |
| `is_*` | three-valued query | `is_positive`, `is_symmetric` |
| `as_*` | cheap structural view | `as_rational`, `as_numer_denom`, `as_intervals` |
| `from_*` | constructor on `Context`/types | `from_f64`, `from_ratio`, `from_coefficients` |