# `Sidereon.LeastSquares`
[🔗](https://github.com/neilberkman/sidereon-ex/blob/main/lib/sidereon/least_squares.ex#L1)

Generic data-driven trust-region least squares.

Pick a built-in residual kind (`:linear`, `:polynomial`, or `:exponential`),
hand over the data arrays, and the whole trust-region iteration runs in Rust:
the residual and Jacobian for every step are evaluated inside the
`trust-region-least-squares` engine, so a fit pays one boundary crossing in and
one out, never one per function evaluation. This mirrors SciPy's
`least_squares(method="trf")` on its unbounded path.

## Residual kinds

  * `%{kind: :linear, a: rows, b: rhs}` - dense linear least squares, with `a`
    the `m`-by-`n` design matrix (a list of `m` rows of `n` numbers) and `b`
    the length-`m` right-hand side. Solves `min ||a x - b||`.
  * `%{kind: :polynomial, degree: d, t: ts, y: ys}` - polynomial fit of degree
    `d` (so `n = d + 1` coefficients, lowest-order first) over the `t`/`y`
    sample pairs.
  * `%{kind: :exponential, t: ts, y: ys}` - the three-parameter model
    `y = amp * exp(rate * t) + offset`, i.e. `x = [amp, rate, offset]`.

## Options

  * `:x0` - starting parameter vector. Defaults to zeros for `:linear` and
    `:polynomial`, and `[1.0, 0.0, 0.0]` for `:exponential`.
  * `:loss` - `:linear` (default), `:soft_l1`, `:huber`, `:cauchy`, `:arctan`.
  * `:f_scale` - robust-loss soft-margin scale (default `1.0`; only consulted
    for a robust loss).
  * `:x_scale` - `:unit` (default), `:jac`, or a list of positive per-parameter
    scales.
  * `:max_nfev` - residual-evaluation budget (default SciPy's `100 * n`).
  * `:ftol`, `:xtol`, `:gtol` - convergence tolerances (SciPy defaults
    `1.0e-8`, `1.0e-8`, `1.0e-10`).
  * `:backend` - `:native` (default, in-crate nalgebra SVD; works everywhere)
    or `:lapack` (host LAPACK/numpy BLAS for bit-for-bit SciPy parity, requires
    the `TRUST_REGION_LEAST_SQUARES_LAPACK_PATH` environment variable).

## Result

`least_squares/2` returns `{:ok, %Sidereon.LeastSquares.Result{}}` or
`{:error, reason}` where `reason` is a typed atom from the solver
(`:insufficient_rows`, `:non_finite_parameters`, ...).
`least_squares_drop_one/2` returns `{:ok,
%Sidereon.LeastSquares.DropOneReport{}}`: the base solve over all rows plus one
re-solve per masked residual row, with the per-row cost deltas (leave-one-out
RAIM/FDE).

# `spec`

```elixir
@type spec() ::
  %{kind: :linear, a: [[number()]], b: [number()]}
  | %{
      kind: :polynomial,
      degree: non_neg_integer(),
      t: [number()],
      y: [number()]
    }
  | %{kind: :exponential, t: [number()], y: [number()]}
```

# `least_squares`

```elixir
@spec least_squares(
  spec(),
  keyword()
) :: {:ok, Sidereon.LeastSquares.Result.t()} | {:error, atom()}
```

Solve a data-driven least-squares problem. See the module doc for the `spec`
shapes and options.

# `least_squares_drop_one`

```elixir
@spec least_squares_drop_one(
  spec(),
  keyword()
) :: {:ok, Sidereon.LeastSquares.DropOneReport.t()} | {:error, atom()}
```

Leave-one-out (drop-one) sweep over the residual rows for RAIM/FDE. Same
`spec`/options as `least_squares/2`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
