Alternative minimizers (Optim.jl)

MIGRAD is NativeMinuit's workhorse, but it is not the only minimizer you can point at a Minuit. optim(m) is the Julia-native analogue of iminuit's Minuit.scipy() escape hatch: it minimises the FCN with any optimizer from Optim.jl — LBFGS, BFGS, Nelder-Mead, Newton, … — starting from m's current values, then writes the optimum back into m so you can carry on with NativeMinuit's hesse! / minos! exactly as after a migrad!.

It is a package extension (like the Plots / DataFrames / ForwardDiff ones): Optim pulls in a sizeable transitive stack, so it is not a hard dependency. Activate the bridge with using Optim; without it, optim(m) raises a helpful "load Optim" message rather than a bare MethodError.

using NativeMinuit, Optim

m = Minuit(fcn, x0)
optim(m; method = :lbfgs)   # minimise with Optim's LBFGS (or:  m |> optim)
hesse!(m)                   # covariance / symmetric errors, à la iminuit

When to reach for it

MIGRAD converges quickly and gives you a covariance for free, so it stays the default. Use optim when:

  • You want to cross-check a MIGRAD minimum. Re-minimising with a structurally different algorithm (e.g. derivative-free Nelder-Mead, or a trust-region-flavoured method) and landing on the same point is a cheap, reassuring sanity check that MIGRAD did not stop short.
  • MIGRAD struggles on a hard landscape. On stiff / ill-conditioned problems a different optimizer sometimes makes progress where the DFP update stalls. This is exactly the role iminuit's m.scipy() plays.
  • You specifically want a particular optimizer — say LBFGS with your own analytical gradient, or Newton's method on a smooth problem.

For a robust, gradient-free fallback that stays inside pure NativeMinuit (no Optim dependency), simplex(m) runs NativeMinuit's own Nelder-Mead.

Choosing the method

Pass method = … a name (case / dash / underscore insensitive) from this table:

methodOptim optimizerOrder
:lbfgs, "L-BFGS-B"LBFGS()first
:bfgsBFGS()first
:conjugategradient, :cgConjugateGradient()first
:gradientdescentGradientDescent()first
:neldermead, :simplexNelderMead()derivative-free
:newtonNewton()second

The default is :lbfgs. For full control over the optimizer object itself — its line search, history length, and so on — use the minimize_with alias and hand it a constructed Optim optimizer, bypassing the name table entirely:

using NativeMinuit, Optim

minimize_with(m, LBFGS())                  # an Optim optimizer object
minimize_with(m, NelderMead())
minimize_with(m; method = :bfgs, tol = 1e-10)   # by name, identical to optim(m; …)

minimize_with and optim are the same bridge under two names; optim mirrors iminuit's m.scipy, minimize_with reads more clearly when you pass an optimizer object.

Gradients, bounds, and fixed parameters

  • Fixed parameters are held out of the optimisation and restored afterwards; their values are untouched. If every parameter is fixed, optim throws (nothing to minimise).
  • Box limits are honoured through Optim's Fminbox. Fminbox requires a first-order inner optimizer, so derivative-free (:neldermead) and second-order (:newton) methods cannot be combined with limits — use a first-order method (:lbfgs / :bfgs / :conjugategradient / :gradientdescent) for bounded fits, or remove the limits. A clear error tells you which case you hit.
  • Analytical gradients. When the Minuit was built with grad = …, that gradient is passed through to first-order optimizers automatically. For derivative-free and Newton methods Optim builds the derivatives it needs from the objective itself.

Tuning the optimizer

KeywordMaps to (Optim)Meaning
methodoptimizer selector (table above); default :lbfgs
ncall / maxcallf_calls_limitfunction-evaluation budget
tolg_tolgradient-norm convergence tolerance
optionsOptim.Optionsfull options object; overrides the three above
optim(m; method = :bfgs, maxcall = 10_000, tol = 1e-9)

# Full control — pass an Optim.Options directly:
using Optim
optim(m; method = :lbfgs, options = Optim.Options(g_tol = 1e-12, iterations = 5_000))
Bounded (Fminbox) fits

For bounded fits ncall / maxcall / tol configure Fminbox's inner optimizer (per outer iteration), not the global call budget or the outer stop criterion. For hard control of the outer Fminbox loop pass a full options = Optim.Options(outer_iterations = …, outer_g_abstol = …).

How the result maps back

optim / minimize_with return m and update it in place, just like migrad!:

  • m.values / m.fval hold the converged point and FCN value.
  • m.valid reflects whether Optim reported convergence.
  • the FCN-evaluation count Optim spent is surfaced as the minimum's nfcn.
  • any previously cached MINOS errors are cleared (they are stale at the new point).

What it does not do by itself is produce a covariance — neither does iminuit's m.scipy. The optimum is seeded back the same way migrad constructs its minimum (a diagonal seed at the converged point), so the natural next step is to refine the errors with NativeMinuit's own machinery:

using NativeMinuit, Optim

m = Minuit(cost, x0)
optim(m; method = :lbfgs)   # alternative minimizer finds the minimum
hesse!(m)                   # full covariance + symmetric errors
minos!(m)                   # asymmetric errors, if you want them

This is the Julia-native counterpart of iminuit's scipy-then-hesse() flow: a different optimizer locates the minimum, and you still get NativeMinuit's full HESSE / MINOS error analysis afterwards (see Error analysis).

See also