Repository: scikit-hep/pyhf

Issue:
 Participate in iminuit v2.0 beta?
Dear pyhf team,

I am about to finish a major rewrite of iminuit, version 2.0, that replaces Cython as the tool to wrap C++ Minuit2 with pybind11, which is going to solve several issues that the legacy code had. All the good things that this will bring are listed on top of this PR:
scikit-hep/iminuit#502

Switching to the new version of iminuit should be completely transparent to you, since the new version passes the comprehensive suite of unit tests of iminuit-v1.x. However, I would like to use this opportunity to finally remove interface that has been successively marked as deprecated in versions 1.3 to 1.5.

Therefore my two question to you:

 * Did you take note of the deprecation warnings in iminuit and did you keep up with the interface changes so far?
 * Are you interested in trying out a Beta release of v2.0 to work out any possible bugs in the new version before the release?

Best regards,
Hans, iminuit maintainer


Discussion:
Hi @HDembinski. Thanks for thinking of us and giving us this heads up.

> I am about to finish a major rewrite of iminuit, version 2.0, that replaces Cython as the tool to wrap C++ Minuit2 with pybind11, which is going to solve several issues that the legacy code had.

This is very cool to hear. Congratulations!

> Did you take note of the deprecation warnings in iminuit and did you keep up with the interface changes so far?

I know that I don't do a very good job of checking the warnings, but we can do some checks to make sure we're not hitting any.

> Are you interested in trying out a Beta release of v2.0 to work out any possible bugs in the new version before the release?

Very much so. We have a nightly workflow that runs the test suite using an install from the HEAD of the main branch of `iminuit` on GitHub (`develop` I think for you), so we should hit anything before release and might already be testing your `v2.0` beta.

https://github.com/scikit-hep/pyhf/blob/a3b34a5eb7ffa83832242533a883c2ae98102369/.github/workflows/dependencies-head.yml#L35-L55

If you already have a branch beyond the main `develop` that this beta version is on, then we can just add another workflow that catches that.
> Did you take note of the deprecation warnings in iminuit and did you keep up with the interface changes so far?

we took care of them all generally with the major rewrite of our underlying optimizer api.
Thank you! Since you are testing develop, you should already get the latest, but you can also install the beta from test.pypi:
`pip install -i https://test.pypi.org/simple/ iminuit`
> Thank you! Since you are testing develop, you should already get the latest

Yup! We're already starting to [see tests fail there](https://github.com/scikit-hep/pyhf/runs/1371708342?check_suite_focus=true#step:5:192) (`AttributeError: 'Minuit' object has no attribute 'ncalls'`), so it seems things are working nicely in terms of giving us early warning. We'll go ahead and setup a branch that we can start to do tests on that moves to using the new API.


@matthewfeickert Thank you for the feedback, but I am thoroughly confused. This looks like you are calling `minimize`, which should of course work (I am testing it) and it is adapted to the new interface.
Ah, it looks like you either reimplemented `minimize` from `iminuit` or you simply have a function with the same name that also returns a scipy OptimizeResult
we conostruct `Minuit` objects here: 

https://github.com/scikit-hep/pyhf/blob/master/src/pyhf/optimize/opt_minuit.py#L67
`ncalls` has been renamed to `nfcn` and `ngrads` to `ngrad` for consistency.

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
     'torch': ['torch~=1.2'],
     'jax': ['jax~=0.2.4', 'jaxlib~=0.1.56'],
     'xmlio': ['uproot3~=3.14'],  # Future proof against uproot4 API changes
-    'minuit': ['iminuit~=1.5.3'],
+    'minuit': ['iminuit~=2.1'],
 }
 extras_require['backends'] = sorted(
     set(
diff --git a/src/pyhf/optimize/opt_minuit.py b/src/pyhf/optimize/opt_minuit.py
--- a/src/pyhf/optimize/opt_minuit.py
+++ b/src/pyhf/optimize/opt_minuit.py
@@ -58,17 +58,13 @@ def _get_minimizer(
             wrapped_objective = objective_and_grad
             jac = None
 
-        kwargs = dict(
-            fcn=wrapped_objective,
-            grad=jac,
-            start=init_pars,
-            error=step_sizes,
-            limit=init_bounds,
-            fix=fixed_bools,
-            print_level=self.verbose,
-            errordef=self.errordef,
-        )
-        return iminuit.Minuit.from_array_func(**kwargs)
+        minuit = iminuit.Minuit(wrapped_objective, init_pars, grad=jac)
+        minuit.errors = step_sizes
+        minuit.limits = init_bounds
+        minuit.fixed = fixed_bools
+        minuit.print_level = self.verbose
+        minuit.errordef = self.errordef
+        return minuit
 
     def _minimize(
         self,
@@ -113,7 +109,7 @@ def _minimize(
         minimizer.tol = tolerance
         minimizer.migrad(ncall=maxiter)
         # Following lines below come from:
-        # https://github.com/scikit-hep/iminuit/blob/64acac11cfa2fb91ccbd02d1b3c51f8a9e2cc484/src/iminuit/_minimize.py#L102-L121
+        # https://github.com/scikit-hep/iminuit/blob/23bad7697e39d363f259ca8349684df939b1b2e6/src/iminuit/_minimize.py#L111-L130
         message = "Optimization terminated successfully."
         if not minimizer.valid:
             message = "Optimization failed."
@@ -128,20 +124,20 @@ def _minimize(
         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.np_covariance()
+            hess_inv = minimizer.covariance
 
         unc = None
         if return_uncertainties:
-            unc = minimizer.np_errors()
+            unc = minimizer.errors
 
         return scipy.optimize.OptimizeResult(
-            x=minimizer.np_values(),
+            x=minimizer.values,
             unc=unc,
             success=minimizer.valid,
             fun=minimizer.fval,
             hess_inv=hess_inv,
             message=message,
-            nfev=minimizer.ncalls_total,
-            njev=minimizer.ngrads_total,
+            nfev=minimizer.nfcn,
+            njev=minimizer.ngrad,
             minuit=minimizer,
         )


Test patch:
diff --git a/tests/test_optim.py b/tests/test_optim.py
--- a/tests/test_optim.py
+++ b/tests/test_optim.py
@@ -499,10 +499,13 @@ def test_init_pars_sync_fixed_values_minuit(mocker):
     # patch all we need
     from pyhf.optimize import opt_minuit
 
-    minimizer = mocker.patch.object(opt_minuit, 'iminuit')
-    opt._get_minimizer(None, [9, 9, 9], [(0, 10)] * 3, fixed_vals=[(0, 1)])
-    assert minimizer.Minuit.from_array_func.call_args[1]['start'] == [1, 9, 9]
-    assert minimizer.Minuit.from_array_func.call_args[1]['fix'] == [True, False, False]
+    minuit = mocker.patch.object(getattr(opt_minuit, 'iminuit'), 'Minuit')
+    minimizer = opt._get_minimizer(None, [9, 9, 9], [(0, 10)] * 3, fixed_vals=[(0, 1)])
+    assert minuit.called
+    # python 3.6 does not have ::args attribute on ::call_args
+    # assert minuit.call_args.args[1] == [1, 9, 9]
+    assert minuit.call_args[0][1] == [1, 9, 9]
+    assert minimizer.fixed == [True, False, False]
 
 
 def test_step_sizes_fixed_parameters_minuit(mocker):
@@ -511,7 +514,9 @@ def test_step_sizes_fixed_parameters_minuit(mocker):
     # patch all we need
     from pyhf.optimize import opt_minuit
 
-    minimizer = mocker.patch.object(opt_minuit, 'iminuit')
-    opt._get_minimizer(None, [9, 9, 9], [(0, 10)] * 3, fixed_vals=[(0, 1)])
-    assert minimizer.Minuit.from_array_func.call_args[1]['fix'] == [True, False, False]
-    assert minimizer.Minuit.from_array_func.call_args[1]['error'] == [0.0, 0.01, 0.01]
+    minuit = mocker.patch.object(getattr(opt_minuit, 'iminuit'), 'Minuit')
+    minimizer = opt._get_minimizer(None, [9, 9, 9], [(0, 10)] * 3, fixed_vals=[(0, 1)])
+
+    assert minuit.called
+    assert minimizer.fixed == [True, False, False]
+    assert minimizer.errors == [0.0, 0.01, 0.01]