Skip to content

Fit ​

optimization.fit.fit differentiates a loss on env.run output all the way back to the cell parameters and steps them. It works for any differentiable livn model.

python
from optimization.fit import fit
from optimization.losses import voltage_mse

theta, history = fit(
    env,
    target_voltage,
    lambda run, target: voltage_mse(run.voltage, target),
    {"E_L": -70.0, "tau_m": 8.0},
    duration=200.0,
    stimulus=stimulus,
    dt=0.1,
    steps=200,
    learning_rate=0.5,
)

See the worked example for a runnable version.

Batching ​

Batching is per-cell parameter arrays inside one Env(N), not a vmap over N separate environments. Every cell parameter is already an (n_cells,) array:

python
fit(env, target, loss, {"E_L": np.full(env.num_cells, -70.0)}, ...)

fits N distinct parameter sets in a single simulation per step. A scalar init value applies to every cell and fits one shared parameter instead.

For the fit to recover N different answers, the loss must sum over cells rather than average into one scalar target — a sum keeps each cell's gradient independent of the others. (In an unconnected env, that is; with recurrent weights the cells are genuinely coupled and the fit is joint.)

Arguments ​

argumentmeaning
enva differentiable env, already init()-ed, with the channels the loss needs enabled
targetwhatever loss compares against — traces, spike times, a dict of both
lossloss(run, target) -> scalar, called with each iteration's Run
init{name: value} starting parameters; scalar or (n_cells,)
duration, stimulus, dtthe run to repeat each step; dt is the recording grid the target and loss must agree on
optimizer, learning_rate, stepsany optax optimizer; defaults to adam(learning_rate). Line-search optimizers (optax.lbfgs, scale_by_backtracking_linesearch) work unchanged
transformoptimize in an unconstrained space — see Transforms. True uses the defaults, a {name: bijector} dict overrides
prior, prior_weight, prior_weightsthe param_prior term
run_kwargsextra env.run arguments
callbackcallback(step, theta, value) after each step
jitcompile the value-and-gradient step; set False to debug the inner objective

Return value ​

(theta, history). history["loss"] has steps + 1 entries — one per step plus a final evaluation at the returned theta, so the last entry is the loss of what you get back. history["params"][name] tracks each parameter over the same points, which is what you plot to see whether a fit converged or is still moving.

Transforms ​

fit optimizes raw parameter values by default, and that is often wrong, say, if the value can only be positive.

Pass transform=True to fit in a space where the constraints cannot be violated:

python
theta, history = fit(env, target, loss, {"tau_m": 8.0, "V_threshold_base": 25.0},
                     duration=300.0, stimulus=stimulus, dt=0.05, transform=True)

By default, this is using:

bijectorforexample
logstrictly positive, no natural ceilingsigma, alpha, b_v
logitfitted inside [0, 1]f_v, the voltage-reset multiplier
boundedpositive and capped, or signed and cappedtau_m, g_L, t_ref, E_L, V_threshold_base
identitygenuinely unconstrainedanything unclassified

bounded is a logit over a box from optimization.transforms.BOUNDS, e.g. V_threshold_base: (1.0, 150.0) mV. Override either per call:

python
fit(..., transform={"tau_m": "log", "V_threshold_base": "identity"})

Released under the MIT License.