prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  use strategy 0 for user-provided gradients in minuit
  # Description
  
  since we have exact gradient we can disable the checks minuit does
  
  cc @alexander-held 
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `src/pyhf/optimize/opt_minuit.py`
  Content:
  ```
  1 """Minuit Optimizer Class."""
  2 from .. import default_backend, exceptions
  3 from .mixins import OptimizerMixin
  4 import scipy
  5 import iminuit
  6 
  7 
  8 class minuit_optimizer(OptimizerMixin):
  9     """
  10     Optimizer that uses iminuit.Minuit.migrad.
  11     """
  12 
  13     __slots__ = ['name', 'errordef', 'steps']
  14 
  15     def __init__(self, *args, **kwargs):
  16         """
  17         Create MINUIT Optimizer.
  18 
  19         .. note::
  20 
  21             ``errordef`` should be 1.0 for a least-squares cost function and 0.5
  22             for negative log-likelihood function. See page 37 of
  23             http://hep.fi.infn.it/minuit.pdf. This parameter is sometimes
  24             called ``UP`` in the ``MINUIT`` docs.
  25 
  26 
  27         Args:
  28             errordef (:obj:`float`): See minuit docs. Default is 1.0.
  29             steps (:obj:`int`): Number of steps for the bounds. Default is 1000.
  30         """
  31         self.name = 'minuit'
  32         self.errordef = kwargs.pop('errordef', 1)
  33         self.steps = kwargs.pop('steps', 1000)
  34         super().__init__(*args, **kwargs)
  35 
  36     def _get_minimizer(
  37         self, objective_and_grad, init_pars, init_bounds, fixed_vals=None, do_grad=False
  38     ):
  39 
  40         step_sizes = [(b[1] - b[0]) / float(self.steps) for b in init_bounds]
  41         fixed_vals = fixed_vals or []
  42         # Minuit wants True/False for each parameter
  43         fixed_bools = [False] * len(init_pars)
  44         for index, val in fixed_vals:
  45             fixed_bools[index] = True
  46             init_pars[index] = val
  47             step_sizes[index] = 0.0
  48 
  49         # Minuit requires jac=callable
  50         if do_grad:
  51             wrapped_objective = lambda pars: objective_and_grad(pars)[0]
  52             jac = lambda pars: objective_and_grad(pars)[1]
  53         else:
  54             wrapped_objective = objective_and_grad
  55             jac = None
  56 
  57         kwargs = dict(
  58             fcn=wrapped_objective,
  59             grad=jac,
  60             start=init_pars,
  61             error=step_sizes,
  62             limit=init_bounds,
  63             fix=fixed_bools,
  64             print_level=self.verbose,
  65             errordef=self.errordef,
  66         )
  67         return iminuit.Minuit.from_array_func(**kwargs)
  68 
  69     def _minimize(
  70         self,
  71         minimizer,
  72         func,
  73         x0,
  74         do_grad=False,
  75         bounds=None,
  76         fixed_vals=None,
  77         return_uncertainties=False,
  78         options={},
  79     ):
  80 
  81         """
  82         Same signature as :func:`scipy.optimize.minimize`.
  83 
  84         Note: an additional `minuit` is injected into the fitresult to get the
  85         underlying minimizer.
  86 
  87         Minimizer Options:
  88             maxiter (:obj:`int`): maximum number of iterations. Default is 100000.
  89             return_uncertainties (:obj:`bool`): Return uncertainties on the fitted parameters. Default is off.
  90 
  91         Returns:
  92             fitresult (scipy.optimize.OptimizeResult): the fit result
  93         """
  94         maxiter = options.pop('maxiter', self.maxiter)
  95         return_uncertainties = options.pop('return_uncertainties', False)
  96         if options:
  97             raise exceptions.Unsupported(
  98                 f"Unsupported options were passed in: {list(options.keys())}."
  99             )
  100 
  101         minimizer.migrad(ncall=maxiter)
  102         # Following lines below come from:
  103         # https://github.com/scikit-hep/iminuit/blob/22f6ed7146c1d1f3274309656d8c04461dde5ba3/src/iminuit/_minimize.py#L106-L125
  104         message = "Optimization terminated successfully."
  105         if not minimizer.valid:
  106             message = "Optimization failed."
  107             fmin = minimizer.fmin
  108             if fmin.has_reached_call_limit:
  109                 message += " Call limit was reached."
  110             if fmin.is_above_max_edm:
  111                 message += " Estimated distance to minimum too large."
  112 
  113         n = len(x0)
  114         hess_inv = default_backend.ones((n, n))
  115         if minimizer.valid:
  116             hess_inv = minimizer.np_covariance()
  117 
  118         unc = None
  119         if return_uncertainties:
  120             unc = minimizer.np_errors()
  121 
  122         return scipy.optimize.OptimizeResult(
  123             x=minimizer.np_values(),
  124             unc=unc,
  125             success=minimizer.valid,
  126             fun=minimizer.fval,
  127             hess_inv=hess_inv,
  128             message=message,
  129             nfev=minimizer.ncalls,
  130             njev=minimizer.ngrads,
  131             minuit=minimizer,
  132         )
  133
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: 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
  @@ -10,7 +10,7 @@
       Optimizer that uses iminuit.Minuit.migrad.
       """
   
  -    __slots__ = ['name', 'errordef', 'steps']
  +    __slots__ = ['name', 'errordef', 'steps', 'strategy']
   
       def __init__(self, *args, **kwargs):
           """
  @@ -27,10 +27,12 @@
           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.
           """
           self.name = 'minuit'
           self.errordef = kwargs.pop('errordef', 1)
           self.steps = kwargs.pop('steps', 1000)
  +        self.strategy = kwargs.pop('strategy', None)
           super().__init__(*args, **kwargs)
   
       def _get_minimizer(
  @@ -87,17 +89,24 @@
           Minimizer Options:
               maxiter (:obj:`int`): maximum number of iterations. Default is 100000.
               return_uncertainties (:obj:`bool`): Return uncertainties on the fitted parameters. Default is off.
  +            strategy (:obj:`int`): See :attr:`iminuit.Minuit.strategy`. Default is to configure in response to `do_grad`.
   
           Returns:
               fitresult (scipy.optimize.OptimizeResult): the fit result
           """
           maxiter = options.pop('maxiter', self.maxiter)
           return_uncertainties = options.pop('return_uncertainties', False)
  +        # 0: Fast, user-provided gradient
  +        # 1: Default, no user-provided gradient
  +        strategy = options.pop(
  +            'strategy', self.strategy if self.strategy else not do_grad
  +        )
           if options:
               raise exceptions.Unsupported(
                   f"Unsupported options were passed in: {list(options.keys())}."
               )
   
  +        minimizer.strategy = strategy
           minimizer.migrad(ncall=maxiter)
           # Following lines below come from:
           # https://github.com/scikit-hep/iminuit/blob/22f6ed7146c1d1f3274309656d8c04461dde5ba3/src/iminuit/_minimize.py#L106-L125
  @@ -113,6 +122,8 @@
           n = len(x0)
           hess_inv = default_backend.ones((n, n))
           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()
   
           unc = None
  
verification_info: {"golden_diff": "diff --git a/src/pyhf/optimize/opt_minuit.py b/src/pyhf/optimize/opt_minuit.py\n--- a/src/pyhf/optimize/opt_minuit.py\n+++ b/src/pyhf/optimize/opt_minuit.py\n@@ -10,7 +10,7 @@\n     Optimizer that uses iminuit.Minuit.migrad.\n     \"\"\"\n \n-    __slots__ = ['name', 'errordef', 'steps']\n+    __slots__ = ['name', 'errordef', 'steps', 'strategy']\n \n     def __init__(self, *args, **kwargs):\n         \"\"\"\n@@ -27,10 +27,12 @@\n         Args:\n             errordef (:obj:`float`): See minuit docs. Default is 1.0.\n             steps (:obj:`int`): Number of steps for the bounds. Default is 1000.\n+            strategy (:obj:`int`): See :attr:`iminuit.Minuit.strategy`. Default is None.\n         \"\"\"\n         self.name = 'minuit'\n         self.errordef = kwargs.pop('errordef', 1)\n         self.steps = kwargs.pop('steps', 1000)\n+        self.strategy = kwargs.pop('strategy', None)\n         super().__init__(*args, **kwargs)\n \n     def _get_minimizer(\n@@ -87,17 +89,24 @@\n         Minimizer Options:\n             maxiter (:obj:`int`): maximum number of iterations. Default is 100000.\n             return_uncertainties (:obj:`bool`): Return uncertainties on the fitted parameters. Default is off.\n+            strategy (:obj:`int`): See :attr:`iminuit.Minuit.strategy`. Default is to configure in response to `do_grad`.\n \n         Returns:\n             fitresult (scipy.optimize.OptimizeResult): the fit result\n         \"\"\"\n         maxiter = options.pop('maxiter', self.maxiter)\n         return_uncertainties = options.pop('return_uncertainties', False)\n+        # 0: Fast, user-provided gradient\n+        # 1: Default, no user-provided gradient\n+        strategy = options.pop(\n+            'strategy', self.strategy if self.strategy else not do_grad\n+        )\n         if options:\n             raise exceptions.Unsupported(\n                 f\"Unsupported options were passed in: {list(options.keys())}.\"\n             )\n \n+        minimizer.strategy = strategy\n         minimizer.migrad(ncall=maxiter)\n         # Following lines below come from:\n         # https://github.com/scikit-hep/iminuit/blob/22f6ed7146c1d1f3274309656d8c04461dde5ba3/src/iminuit/_minimize.py#L106-L125\n@@ -113,6 +122,8 @@\n         n = len(x0)\n         hess_inv = default_backend.ones((n, n))\n         if minimizer.valid:\n+            # 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.\n+            minimizer.hesse()\n             hess_inv = minimizer.np_covariance()\n \n         unc = None\n", "issue": "use strategy 0 for user-provided gradients in minuit\n# Description\r\n\r\nsince we have exact gradient we can disable the checks minuit does\r\n\r\ncc @alexander-held \n", "before_files": [{"content": "\"\"\"Minuit Optimizer Class.\"\"\"\nfrom .. import default_backend, exceptions\nfrom .mixins import OptimizerMixin\nimport scipy\nimport iminuit\n\n\nclass minuit_optimizer(OptimizerMixin):\n    \"\"\"\n    Optimizer that uses iminuit.Minuit.migrad.\n    \"\"\"\n\n    __slots__ = ['name', 'errordef', 'steps']\n\n    def __init__(self, *args, **kwargs):\n        \"\"\"\n        Create MINUIT Optimizer.\n\n        .. note::\n\n            ``errordef`` should be 1.0 for a least-squares cost function and 0.5\n            for negative log-likelihood function. See page 37 of\n            http://hep.fi.infn.it/minuit.pdf. This parameter is sometimes\n            called ``UP`` in the ``MINUIT`` docs.\n\n\n        Args:\n            errordef (:obj:`float`): See minuit docs. Default is 1.0.\n            steps (:obj:`int`): Number of steps for the bounds. Default is 1000.\n        \"\"\"\n        self.name = 'minuit'\n        self.errordef = kwargs.pop('errordef', 1)\n        self.steps = kwargs.pop('steps', 1000)\n        super().__init__(*args, **kwargs)\n\n    def _get_minimizer(\n        self, objective_and_grad, init_pars, init_bounds, fixed_vals=None, do_grad=False\n    ):\n\n        step_sizes = [(b[1] - b[0]) / float(self.steps) for b in init_bounds]\n        fixed_vals = fixed_vals or []\n        # Minuit wants True/False for each parameter\n        fixed_bools = [False] * len(init_pars)\n        for index, val in fixed_vals:\n            fixed_bools[index] = True\n            init_pars[index] = val\n            step_sizes[index] = 0.0\n\n        # Minuit requires jac=callable\n        if do_grad:\n            wrapped_objective = lambda pars: objective_and_grad(pars)[0]\n            jac = lambda pars: objective_and_grad(pars)[1]\n        else:\n            wrapped_objective = objective_and_grad\n            jac = None\n\n        kwargs = dict(\n            fcn=wrapped_objective,\n            grad=jac,\n            start=init_pars,\n            error=step_sizes,\n            limit=init_bounds,\n            fix=fixed_bools,\n            print_level=self.verbose,\n            errordef=self.errordef,\n        )\n        return iminuit.Minuit.from_array_func(**kwargs)\n\n    def _minimize(\n        self,\n        minimizer,\n        func,\n        x0,\n        do_grad=False,\n        bounds=None,\n        fixed_vals=None,\n        return_uncertainties=False,\n        options={},\n    ):\n\n        \"\"\"\n        Same signature as :func:`scipy.optimize.minimize`.\n\n        Note: an additional `minuit` is injected into the fitresult to get the\n        underlying minimizer.\n\n        Minimizer Options:\n            maxiter (:obj:`int`): maximum number of iterations. Default is 100000.\n            return_uncertainties (:obj:`bool`): Return uncertainties on the fitted parameters. Default is off.\n\n        Returns:\n            fitresult (scipy.optimize.OptimizeResult): the fit result\n        \"\"\"\n        maxiter = options.pop('maxiter', self.maxiter)\n        return_uncertainties = options.pop('return_uncertainties', False)\n        if options:\n            raise exceptions.Unsupported(\n                f\"Unsupported options were passed in: {list(options.keys())}.\"\n            )\n\n        minimizer.migrad(ncall=maxiter)\n        # Following lines below come from:\n        # https://github.com/scikit-hep/iminuit/blob/22f6ed7146c1d1f3274309656d8c04461dde5ba3/src/iminuit/_minimize.py#L106-L125\n        message = \"Optimization terminated successfully.\"\n        if not minimizer.valid:\n            message = \"Optimization failed.\"\n            fmin = minimizer.fmin\n            if fmin.has_reached_call_limit:\n                message += \" Call limit was reached.\"\n            if fmin.is_above_max_edm:\n                message += \" Estimated distance to minimum too large.\"\n\n        n = len(x0)\n        hess_inv = default_backend.ones((n, n))\n        if minimizer.valid:\n            hess_inv = minimizer.np_covariance()\n\n        unc = None\n        if return_uncertainties:\n            unc = minimizer.np_errors()\n\n        return scipy.optimize.OptimizeResult(\n            x=minimizer.np_values(),\n            unc=unc,\n            success=minimizer.valid,\n            fun=minimizer.fval,\n            hess_inv=hess_inv,\n            message=message,\n            nfev=minimizer.ncalls,\n            njev=minimizer.ngrads,\n            minuit=minimizer,\n        )\n", "path": "src/pyhf/optimize/opt_minuit.py"}], "after_files": [{"content": "\"\"\"Minuit Optimizer Class.\"\"\"\nfrom .. import default_backend, exceptions\nfrom .mixins import OptimizerMixin\nimport scipy\nimport iminuit\n\n\nclass minuit_optimizer(OptimizerMixin):\n    \"\"\"\n    Optimizer that uses iminuit.Minuit.migrad.\n    \"\"\"\n\n    __slots__ = ['name', 'errordef', 'steps', 'strategy']\n\n    def __init__(self, *args, **kwargs):\n        \"\"\"\n        Create MINUIT Optimizer.\n\n        .. note::\n\n            ``errordef`` should be 1.0 for a least-squares cost function and 0.5\n            for negative log-likelihood function. See page 37 of\n            http://hep.fi.infn.it/minuit.pdf. This parameter is sometimes\n            called ``UP`` in the ``MINUIT`` docs.\n\n\n        Args:\n            errordef (:obj:`float`): See minuit docs. Default is 1.0.\n            steps (:obj:`int`): Number of steps for the bounds. Default is 1000.\n            strategy (:obj:`int`): See :attr:`iminuit.Minuit.strategy`. Default is None.\n        \"\"\"\n        self.name = 'minuit'\n        self.errordef = kwargs.pop('errordef', 1)\n        self.steps = kwargs.pop('steps', 1000)\n        self.strategy = kwargs.pop('strategy', None)\n        super().__init__(*args, **kwargs)\n\n    def _get_minimizer(\n        self, objective_and_grad, init_pars, init_bounds, fixed_vals=None, do_grad=False\n    ):\n\n        step_sizes = [(b[1] - b[0]) / float(self.steps) for b in init_bounds]\n        fixed_vals = fixed_vals or []\n        # Minuit wants True/False for each parameter\n        fixed_bools = [False] * len(init_pars)\n        for index, val in fixed_vals:\n            fixed_bools[index] = True\n            init_pars[index] = val\n            step_sizes[index] = 0.0\n\n        # Minuit requires jac=callable\n        if do_grad:\n            wrapped_objective = lambda pars: objective_and_grad(pars)[0]\n            jac = lambda pars: objective_and_grad(pars)[1]\n        else:\n            wrapped_objective = objective_and_grad\n            jac = None\n\n        kwargs = dict(\n            fcn=wrapped_objective,\n            grad=jac,\n            start=init_pars,\n            error=step_sizes,\n            limit=init_bounds,\n            fix=fixed_bools,\n            print_level=self.verbose,\n            errordef=self.errordef,\n        )\n        return iminuit.Minuit.from_array_func(**kwargs)\n\n    def _minimize(\n        self,\n        minimizer,\n        func,\n        x0,\n        do_grad=False,\n        bounds=None,\n        fixed_vals=None,\n        return_uncertainties=False,\n        options={},\n    ):\n\n        \"\"\"\n        Same signature as :func:`scipy.optimize.minimize`.\n\n        Note: an additional `minuit` is injected into the fitresult to get the\n        underlying minimizer.\n\n        Minimizer Options:\n            maxiter (:obj:`int`): maximum number of iterations. Default is 100000.\n            return_uncertainties (:obj:`bool`): Return uncertainties on the fitted parameters. Default is off.\n            strategy (:obj:`int`): See :attr:`iminuit.Minuit.strategy`. Default is to configure in response to `do_grad`.\n\n        Returns:\n            fitresult (scipy.optimize.OptimizeResult): the fit result\n        \"\"\"\n        maxiter = options.pop('maxiter', self.maxiter)\n        return_uncertainties = options.pop('return_uncertainties', False)\n        # 0: Fast, user-provided gradient\n        # 1: Default, no user-provided gradient\n        strategy = options.pop(\n            'strategy', self.strategy if self.strategy else not do_grad\n        )\n        if options:\n            raise exceptions.Unsupported(\n                f\"Unsupported options were passed in: {list(options.keys())}.\"\n            )\n\n        minimizer.strategy = strategy\n        minimizer.migrad(ncall=maxiter)\n        # Following lines below come from:\n        # https://github.com/scikit-hep/iminuit/blob/22f6ed7146c1d1f3274309656d8c04461dde5ba3/src/iminuit/_minimize.py#L106-L125\n        message = \"Optimization terminated successfully.\"\n        if not minimizer.valid:\n            message = \"Optimization failed.\"\n            fmin = minimizer.fmin\n            if fmin.has_reached_call_limit:\n                message += \" Call limit was reached.\"\n            if fmin.is_above_max_edm:\n                message += \" Estimated distance to minimum too large.\"\n\n        n = len(x0)\n        hess_inv = default_backend.ones((n, n))\n        if minimizer.valid:\n            # 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.\n            minimizer.hesse()\n            hess_inv = minimizer.np_covariance()\n\n        unc = None\n        if return_uncertainties:\n            unc = minimizer.np_errors()\n\n        return scipy.optimize.OptimizeResult(\n            x=minimizer.np_values(),\n            unc=unc,\n            success=minimizer.valid,\n            fun=minimizer.fval,\n            hess_inv=hess_inv,\n            message=message,\n            nfev=minimizer.ncalls,\n            njev=minimizer.ngrads,\n            minuit=minimizer,\n        )\n", "path": "src/pyhf/optimize/opt_minuit.py"}]}