Skip to content

Commit 07fc449

Browse files
authored
feat: Configurable optimizer tolerance for termination (#1184)
* Add configurable optimizer tolerance for minimization termination - NB: SciPy defaults to None, iminuit defaults to 0.1 * Add tests to ensure the API for SciPy/iminuit
1 parent 2010b6d commit 07fc449

File tree

3 files changed

+30
-6
lines changed

3 files changed

+30
-6
lines changed

src/pyhf/optimize/opt_minuit.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class minuit_optimizer(OptimizerMixin):
1010
Optimizer that uses iminuit.Minuit.migrad.
1111
"""
1212

13-
__slots__ = ['name', 'errordef', 'steps', 'strategy']
13+
__slots__ = ['name', 'errordef', 'steps', 'strategy', 'tolerance']
1414

1515
def __init__(self, *args, **kwargs):
1616
"""
@@ -28,11 +28,13 @@ def __init__(self, *args, **kwargs):
2828
errordef (:obj:`float`): See minuit docs. Default is 1.0.
2929
steps (:obj:`int`): Number of steps for the bounds. Default is 1000.
3030
strategy (:obj:`int`): See :attr:`iminuit.Minuit.strategy`. Default is None.
31+
tolerance (:obj:`float`): tolerance for termination. See specific optimizer for detailed meaning. Default is 0.1.
3132
"""
3233
self.name = 'minuit'
3334
self.errordef = kwargs.pop('errordef', 1)
3435
self.steps = kwargs.pop('steps', 1000)
3536
self.strategy = kwargs.pop('strategy', None)
37+
self.tolerance = kwargs.pop('tolerance', 0.1)
3638
super().__init__(*args, **kwargs)
3739

3840
def _get_minimizer(
@@ -101,12 +103,14 @@ def _minimize(
101103
strategy = options.pop(
102104
'strategy', self.strategy if self.strategy else not do_grad
103105
)
106+
tolerance = options.pop('tolerance', self.tolerance)
104107
if options:
105108
raise exceptions.Unsupported(
106109
f"Unsupported options were passed in: {list(options.keys())}."
107110
)
108111

109112
minimizer.strategy = strategy
113+
minimizer.tol = tolerance
110114
minimizer.migrad(ncall=maxiter)
111115
# Following lines below come from:
112116
# https://github.com/scikit-hep/iminuit/blob/22f6ed7146c1d1f3274309656d8c04461dde5ba3/src/iminuit/_minimize.py#L106-L125

src/pyhf/optimize/opt_scipy.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@ class scipy_optimizer(OptimizerMixin):
99
Optimizer that uses :func:`scipy.optimize.minimize`.
1010
"""
1111

12-
__slots__ = ['name']
12+
__slots__ = ['name', 'tolerance']
1313

1414
def __init__(self, *args, **kwargs):
1515
"""
1616
Initialize the scipy_optimizer.
1717
18-
See :class:`pyhf.optimize.mixins.OptimizerMixin` for configuration options.
18+
See :class:`pyhf.optimize.mixins.OptimizerMixin` for other configuration options.
19+
20+
Args:
21+
tolerance (:obj:`float`): tolerance for termination. See specific optimizer for detailed meaning. Default is None.
1922
"""
2023
self.name = 'scipy'
24+
self.tolerance = kwargs.pop('tolerance', None)
2125
super().__init__(*args, **kwargs)
2226

2327
def _get_minimizer(
@@ -40,16 +44,18 @@ def _minimize(
4044
Same signature as :func:`scipy.optimize.minimize`.
4145
4246
Minimizer Options:
43-
maxiter (`int`): maximum number of iterations. Default is 100000.
44-
verbose (`bool`): print verbose output during minimization. Default is off.
45-
method (`str`): minimization routine. Default is 'SLSQP'.
47+
maxiter (:obj:`int`): maximum number of iterations. Default is 100000.
48+
verbose (:obj:`bool`): print verbose output during minimization. Default is off.
49+
method (:obj:`str`): minimization routine. Default is 'SLSQP'.
50+
tolerance (:obj:`float`): tolerance for termination. See specific optimizer for detailed meaning. Default is None.
4651
4752
Returns:
4853
fitresult (scipy.optimize.OptimizeResult): the fit result
4954
"""
5055
maxiter = options.pop('maxiter', self.maxiter)
5156
verbose = options.pop('verbose', self.verbose)
5257
method = options.pop('method', 'SLSQP')
58+
tolerance = options.pop('tolerance', self.tolerance)
5359
if options:
5460
raise exceptions.Unsupported(
5561
f"Unsupported options were passed in: {list(options.keys())}."
@@ -73,5 +79,6 @@ def _minimize(
7379
jac=do_grad,
7480
bounds=bounds,
7581
constraints=constraints,
82+
tol=tolerance,
7683
options=dict(maxiter=maxiter, disp=bool(verbose)),
7784
)

tests/test_optim.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,19 @@ def test_minuit_strategy_global(mocker, backend, strategy):
225225
assert spy.spy_return.minuit.strategy == 1
226226

227227

228+
def test_set_tolerance(backend):
229+
m = pyhf.simplemodels.hepdata_like([50.0], [100.0], [10.0])
230+
data = pyhf.tensorlib.astensor([125.0] + m.config.auxdata)
231+
232+
assert pyhf.infer.mle.fit(data, m, tolerance=0.01) is not None
233+
234+
pyhf.set_backend(pyhf.tensorlib, pyhf.optimize.scipy_optimizer(tolerance=0.01))
235+
assert pyhf.infer.mle.fit(data, m) is not None
236+
237+
pyhf.set_backend(pyhf.tensorlib, pyhf.optimize.minuit_optimizer(tolerance=0.01))
238+
assert pyhf.infer.mle.fit(data, m) is not None
239+
240+
228241
@pytest.mark.parametrize(
229242
'optimizer',
230243
[pyhf.optimize.scipy_optimizer, pyhf.optimize.minuit_optimizer],

0 commit comments

Comments
 (0)