Source code for pyhf.optimize.opt_minuit

"""Minuit Optimizer Class."""
from pyhf import exceptions
from pyhf.optimize.mixins import OptimizerMixin
import scipy
import iminuit

[docs]
class minuit_optimizer(OptimizerMixin):
"""
Optimizer that minimizes via :meth:iminuit.Minuit.migrad.
"""

__slots__ = ['name', 'errordef', 'steps', 'strategy', 'tolerance']

[docs]
def __init__(self, *args, **kwargs):
"""
Create :class:iminuit.Minuit optimizer.

.. note::

errordef should be 1.0 for a least-squares cost function and 0.50
for negative log-likelihood function --- see MINUIT: Function Minimization
and Error Analysis Reference Manual <https://cdsweb.cern.ch/record/2296388/>_
Section 7.1: Function normalization and ERROR DEF.
This parameter is sometimes called UP in the MINUIT docs.

Args:
errordef (:obj:float): See minuit docs. Default is 1.0.
steps (:obj:int): Number of steps for the bounds. Default is 1000.
strategy (:obj:int): See :attr:iminuit.Minuit.strategy.
Default is None, which results in either
:attr:iminuit.Minuit.strategy 0 or 1 from the evaluation of
int(not pyhf.tensorlib.default_do_grad).
tolerance (:obj:float): Tolerance for termination.
See specific optimizer for detailed meaning.
Default is 0.1.
"""
self.name = 'minuit'
self.errordef = kwargs.pop('errordef', 1)
self.steps = kwargs.pop('steps', 1000)
self.strategy = kwargs.pop('strategy', None)
self.tolerance = kwargs.pop('tolerance', 0.1)
super().__init__(*args, **kwargs)

[docs]
def _get_minimizer(
self,
init_pars,
init_bounds,
fixed_vals=None,
par_names=None,
):
fixed_vals = fixed_vals or []
# Minuit wants True/False for each parameter
fixed_bools = [False] * len(init_pars)
for index, val in fixed_vals:
fixed_bools[index] = True
init_pars[index] = val

# Minuit requires jac=callable
wrapped_objective = lambda pars: objective_and_grad(pars)[0]  # noqa: E731
jac = lambda pars: objective_and_grad(pars)[1]  # noqa: E731
else:
jac = None

minuit = iminuit.Minuit(wrapped_objective, init_pars, grad=jac, name=par_names)
minuit.limits = init_bounds
minuit.fixed = fixed_bools
minuit.print_level = self.verbose
minuit.errordef = self.errordef
return minuit

[docs]
def _minimize(
self,
minimizer,
func,
x0,
bounds=None,
fixed_vals=None,
options={},
):
"""
Same signature as :func:scipy.optimize.minimize.

Note: an additional minuit is injected into the fitresult to get the
underlying minimizer.

Minimizer Options:
* maxiter (:obj:int): Maximum number of iterations. Default is 100000.
* strategy (:obj:int): See :attr:iminuit.Minuit.strategy.
Default is to configure in response to do_grad.
* tolerance (:obj:float): Tolerance for termination.
See specific optimizer for detailed meaning.
Default is 0.1.

Returns:
fitresult (scipy.optimize.OptimizeResult): the fit result
"""
maxiter = options.pop('maxiter', self.maxiter)
# do_grad value results in iminuit.Minuit.strategy of either:
#   0: Fast. Does not check a user-provided gradient.
strategy = options.pop("strategy", self.strategy)
# Guard against None from either self.strategy defaulting to None or
# passing strategy=None as options kwarg
if strategy is None:
strategy = 0 if do_grad else 1
tolerance = options.pop('tolerance', self.tolerance)
if options:
raise exceptions.Unsupported(
f"Unsupported options were passed in: {list(options.keys())}."
)

minimizer.strategy = strategy
minimizer.tol = tolerance
# Following lines below come from:
message = "Optimization terminated successfully."
if not minimizer.valid:
message = "Optimization failed."
fmin = minimizer.fmin
if fmin.has_reached_call_limit:
message += " Call limit was reached."
if fmin.is_above_max_edm:
message += " Estimated distance to minimum too large."

hess_inv = None
corr = None
unc = None
if minimizer.valid:
# Extra call to hesse() after migrad() is always needed for good error estimates. If you pass a user-provided gradient to MINUIT, convergence is faster.
minimizer.hesse()
hess_inv = minimizer.covariance
corr = hess_inv.correlation()
unc = minimizer.errors

return scipy.optimize.OptimizeResult(
x=minimizer.values,
unc=unc,
corr=corr,
success=minimizer.valid,
fun=minimizer.fval,
hess_inv=hess_inv,
message=message,
nfev=minimizer.nfcn,