Skip to content

Curve fitting

The Nautilus.CurveFit module provides a single export: lm_scalar_1param, a Levenberg-Marquardt optimizer for single-parameter curve fitting.

Signature:

lm_scalar_1param(
model: f32 -> f32 -> f32, // model(x, theta) -> predicted y
dmodel: f32 -> f32 -> f32, // dmodel(x, theta) -> d(predicted_y)/d(theta)
xs: tensor[n, f32], // input data points
ys: tensor[n, f32], // observed y-values
theta0: f32, // initial parameter guess
lambda0: f32, // initial damping factor
tol: f32, // convergence tolerance on |delta_theta|
max_iters: int64 // iteration cap
) -> f32 // fitted theta

The optimizer iterates the damped Gauss-Newton update:

theta_next = theta + (J^T J * (1 + lambda))^{-1} * J^T r

where J is the Jacobian (here a scalar dmodel(x, theta) per data point) and r is the residual vector y - model(x, theta). Convergence is declared when |theta_next - theta| < tol. Returns NaN for empty data.

import Nautilus.CurveFit (lm_scalar_1param)
def linear_model(x: f32, theta: f32) -> f32 = mul(theta, x)
def linear_dmodel(x: f32, theta: f32) -> f32 = x
def fit_slope[n](xs: tensor[n, f32], ys: tensor[n, f32]) -> f32 =
lm_scalar_1param(
linear_model, linear_dmodel, xs, ys,
cast(0.0, f32), // initial guess
cast(0.01, f32), // damping
cast(1.0e-8, f32), // tolerance
cast(100, int64) // max iterations
)

For a dataset generated by y = 2*x + noise, this converges to approximately 2.0.

  • Single parameter only. Multi-parameter Levenberg-Marquardt (using inv_2x2/inv_3x3 for the normal equations) is structurally possible but not yet exported.
  • The caller must supply the analytical derivative dmodel. There is no automatic differentiation fallback in this function.
  • The damping factor lambda0 is held constant (no adaptive lambda scheduling as in full LM implementations).