Skip to content

Curve fitting

Nautilus.CurveFit exports two Levenberg-Marquardt fitters:

  • lm_scalar_1param, which takes an analytical scalar derivative;
  • lm_scalar_nparam, which fits a tensor parameter vector using a finite-difference Jacobian.
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],
ys: &tensor[n, f32],
theta0: f32,
lambda0: f32,
tol: f32,
max_iters: i64
) -> f32

The optimizer applies a damped Gauss-Newton update. Here each Jacobian entry is the caller-supplied dmodel(x, theta). Convergence is declared when |theta_next - theta| < tol; empty data returns NaN.

module Nautilus.BookFitSlope
import Nautilus.CurveFit (lm_scalar_1param)
export (fit_slope)
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), cast(0.01, f32), cast(1e-8, f32), cast(100, i64))

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

lm_scalar_nparam(
model: &tensor[n, f32] -> &tensor[m, f32] -> tensor[m, f32],
x: &tensor[m, f32],
y: &tensor[m, f32],
theta0: tensor[n, f32],
tol: f32,
max_iters: i64
) -> tensor[n, f32]

model(theta, x) predicts the m observations. The fitter forms J^T J + 0.01 I, solves the damped normal equations with conjugate gradient, and updates the n parameters. It runs exactly max_iters iterations; tol is currently unused and does not stop iterations early.

The Jacobian uses forward differences with eps=1e-5. The fitter does not differentiate through the full optimization loop.

Scale parameters and predictions to roughly O(1). At larger magnitudes an eps=1e-5 perturbation can fall below an f32 ULP, produce a zero Jacobian column, and permanently stall the fit.

  • Both APIs are f32-only.
  • lm_scalar_1param requires an analytical derivative and keeps its supplied damping factor fixed.
  • lm_scalar_nparam keeps lambda fixed at 0.01, does not use tol for early exit, and uses finite differences for the Jacobian.