We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Documentation of default Minuit strategy choice
### Summary

The documentation of `minuit_optimizer` lists `strategy` as a possible kwarg with default value of `None`, but it does not explain what that choice leads to. `pyhf` uses `0`/`1` depending on whether user-provided gradients are used (#1172 / #1183), and I believe it would be good to have that documented on the optimizer page. I'd be happy to submit a PR.

### Documentation Page Link

https://pyhf.readthedocs.io/en/v0.6.3/_generated/pyhf.optimize.opt_minuit.minuit_optimizer.html#pyhf.optimize.opt_minuit.minuit_optimizer

### Code of Conduct

- [X] I agree to follow the Code of Conduct

--- 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 pyhf import exceptions
3 from pyhf.optimize.mixins import OptimizerMixin
4 import scipy
5 import iminuit
6 
7 
8 class minuit_optimizer(OptimizerMixin):
9     """
10     Optimizer that minimizes via :meth:`iminuit.Minuit.migrad`.
11     """
12 
13     __slots__ = ['name', 'errordef', 'steps', 'strategy', 'tolerance']
14 
15     def __init__(self, *args, **kwargs):
16         """
17         Create :class:`iminuit.Minuit` optimizer.
18 
19         .. note::
20 
21             ``errordef`` should be 1.0 for a least-squares cost function and 0.50
22             for negative log-likelihood function --- see `MINUIT: Function Minimization
23             and Error Analysis Reference Manual <https://cdsweb.cern.ch/record/2296388/>`_
24             Section 7.1: Function normalization and ERROR DEF.
25             This parameter is sometimes called ``UP`` in the ``MINUIT`` docs.
26 
27 
28         Args:
29             errordef (:obj:`float`): See minuit docs. Default is ``1.0``.
30             steps (:obj:`int`): Number of steps for the bounds. Default is ``1000``.
31             strategy (:obj:`int`): See :attr:`iminuit.Minuit.strategy`. Default is ``None``.
32             tolerance (:obj:`float`): Tolerance for termination.
33               See specific optimizer for detailed meaning.
34               Default is ``0.1``.
35         """
36         self.name = 'minuit'
37         self.errordef = kwargs.pop('errordef', 1)
38         self.steps = kwargs.pop('steps', 1000)
39         self.strategy = kwargs.pop('strategy', None)
40         self.tolerance = kwargs.pop('tolerance', 0.1)
41         super().__init__(*args, **kwargs)
42 
43     def _get_minimizer(
44         self,
45         objective_and_grad,
46         init_pars,
47         init_bounds,
48         fixed_vals=None,
49         do_grad=False,
50         par_names=None,
51     ):
52         fixed_vals = fixed_vals or []
53         # Minuit wants True/False for each parameter
54         fixed_bools = [False] * len(init_pars)
55         for index, val in fixed_vals:
56             fixed_bools[index] = True
57             init_pars[index] = val
58 
59         # Minuit requires jac=callable
60         if do_grad:
61             wrapped_objective = lambda pars: objective_and_grad(pars)[0]  # noqa: E731
62             jac = lambda pars: objective_and_grad(pars)[1]  # noqa: E731
63         else:
64             wrapped_objective = objective_and_grad
65             jac = None
66 
67         minuit = iminuit.Minuit(wrapped_objective, init_pars, grad=jac, name=par_names)
68         minuit.limits = init_bounds
69         minuit.fixed = fixed_bools
70         minuit.print_level = self.verbose
71         minuit.errordef = self.errordef
72         return minuit
73 
74     def _minimize(
75         self,
76         minimizer,
77         func,
78         x0,
79         do_grad=False,
80         bounds=None,
81         fixed_vals=None,
82         options={},
83     ):
84         """
85         Same signature as :func:`scipy.optimize.minimize`.
86 
87         Note: an additional `minuit` is injected into the fitresult to get the
88         underlying minimizer.
89 
90         Minimizer Options:
91           * maxiter (:obj:`int`): Maximum number of iterations. Default is ``100000``.
92           * strategy (:obj:`int`): See :attr:`iminuit.Minuit.strategy`.
93             Default is to configure in response to ``do_grad``.
94           * tolerance (:obj:`float`): Tolerance for termination.
95             See specific optimizer for detailed meaning.
96             Default is ``0.1``.
97 
98         Returns:
99             fitresult (scipy.optimize.OptimizeResult): the fit result
100         """
101         maxiter = options.pop('maxiter', self.maxiter)
102         # 0: Fast, user-provided gradient
103         # 1: Default, no user-provided gradient
104         strategy = options.pop(
105             'strategy', self.strategy if self.strategy is not None else not do_grad
106         )
107         tolerance = options.pop('tolerance', self.tolerance)
108         if options:
109             raise exceptions.Unsupported(
110                 f"Unsupported options were passed in: {list(options)}."
111             )
112 
113         minimizer.strategy = strategy
114         minimizer.tol = tolerance
115         minimizer.migrad(ncall=maxiter)
116         # Following lines below come from:
117         # https://github.com/scikit-hep/iminuit/blob/23bad7697e39d363f259ca8349684df939b1b2e6/src/iminuit/_minimize.py#L111-L130
118         message = "Optimization terminated successfully."
119         if not minimizer.valid:
120             message = "Optimization failed."
121             fmin = minimizer.fmin
122             if fmin.has_reached_call_limit:
123                 message += " Call limit was reached."
124             if fmin.is_above_max_edm:
125                 message += " Estimated distance to minimum too large."
126 
127         hess_inv = None
128         corr = None
129         unc = None
130         if minimizer.valid:
131             # 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.
132             minimizer.hesse()
133             hess_inv = minimizer.covariance
134             corr = hess_inv.correlation()
135             unc = minimizer.errors
136 
137         return scipy.optimize.OptimizeResult(
138             x=minimizer.values,
139             unc=unc,
140             corr=corr,
141             success=minimizer.valid,
142             fun=minimizer.fval,
143             hess_inv=hess_inv,
144             message=message,
145             nfev=minimizer.nfcn,
146             njev=minimizer.ngrad,
147             minuit=minimizer,
148         )
149
```
--- 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()

```

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
@@ -28,7 +28,10 @@
         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``.
+            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``.
@@ -99,11 +102,14 @@
             fitresult (scipy.optimize.OptimizeResult): the fit result
         """
         maxiter = options.pop('maxiter', self.maxiter)
-        # 0: Fast, user-provided gradient
-        # 1: Default, no user-provided gradient
-        strategy = options.pop(
-            'strategy', self.strategy if self.strategy is not None else not do_grad
-        )
+        # do_grad value results in iminuit.Minuit.strategy of either:
+        #   0: Fast. Does not check a user-provided gradient.
+        #   1: Default. Checks user-provided gradient against numerical 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(

{"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@@ -28,7 +28,10 @@\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+            strategy (:obj:`int`): See :attr:`iminuit.Minuit.strategy`.\n+              Default is ``None``, which results in either\n+              :attr:`iminuit.Minuit.strategy` ``0`` or ``1`` from the evaluation of\n+              ``int(not pyhf.tensorlib.default_do_grad)``.\n             tolerance (:obj:`float`): Tolerance for termination.\n               See specific optimizer for detailed meaning.\n               Default is ``0.1``.\n@@ -99,11 +102,14 @@\n             fitresult (scipy.optimize.OptimizeResult): the fit result\n         \"\"\"\n         maxiter = options.pop('maxiter', self.maxiter)\n-        # 0: Fast, user-provided gradient\n-        # 1: Default, no user-provided gradient\n-        strategy = options.pop(\n-            'strategy', self.strategy if self.strategy is not None else not do_grad\n-        )\n+        # do_grad value results in iminuit.Minuit.strategy of either:\n+        #   0: Fast. Does not check a user-provided gradient.\n+        #   1: Default. Checks user-provided gradient against numerical gradient.\n+        strategy = options.pop(\"strategy\", self.strategy)\n+        # Guard against None from either self.strategy defaulting to None or\n+        # passing strategy=None as options kwarg\n+        if strategy is None:\n+            strategy = 0 if do_grad else 1\n         tolerance = options.pop('tolerance', self.tolerance)\n         if options:\n             raise exceptions.Unsupported(\n", "issue": "Documentation of default Minuit strategy choice\n### Summary\n\nThe documentation of `minuit_optimizer` lists `strategy` as a possible kwarg with default value of `None`, but it does not explain what that choice leads to. `pyhf` uses `0`/`1` depending on whether user-provided gradients are used (#1172 / #1183), and I believe it would be good to have that documented on the optimizer page. I'd be happy to submit a PR.\n\n### Documentation Page Link\n\nhttps://pyhf.readthedocs.io/en/v0.6.3/_generated/pyhf.optimize.opt_minuit.minuit_optimizer.html#pyhf.optimize.opt_minuit.minuit_optimizer\n\n### Code of Conduct\n\n- [X] I agree to follow the Code of Conduct\n", "before_files": [{"content": "\"\"\"Minuit Optimizer Class.\"\"\"\nfrom pyhf import exceptions\nfrom pyhf.optimize.mixins import OptimizerMixin\nimport scipy\nimport iminuit\n\n\nclass minuit_optimizer(OptimizerMixin):\n    \"\"\"\n    Optimizer that minimizes via :meth:`iminuit.Minuit.migrad`.\n    \"\"\"\n\n    __slots__ = ['name', 'errordef', 'steps', 'strategy', 'tolerance']\n\n    def __init__(self, *args, **kwargs):\n        \"\"\"\n        Create :class:`iminuit.Minuit` optimizer.\n\n        .. note::\n\n            ``errordef`` should be 1.0 for a least-squares cost function and 0.50\n            for negative log-likelihood function --- see `MINUIT: Function Minimization\n            and Error Analysis Reference Manual <https://cdsweb.cern.ch/record/2296388/>`_\n            Section 7.1: Function normalization and ERROR DEF.\n            This parameter is sometimes 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            tolerance (:obj:`float`): Tolerance for termination.\n              See specific optimizer for detailed meaning.\n              Default is ``0.1``.\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        self.tolerance = kwargs.pop('tolerance', 0.1)\n        super().__init__(*args, **kwargs)\n\n    def _get_minimizer(\n        self,\n        objective_and_grad,\n        init_pars,\n        init_bounds,\n        fixed_vals=None,\n        do_grad=False,\n        par_names=None,\n    ):\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\n        # Minuit requires jac=callable\n        if do_grad:\n            wrapped_objective = lambda pars: objective_and_grad(pars)[0]  # noqa: E731\n            jac = lambda pars: objective_and_grad(pars)[1]  # noqa: E731\n        else:\n            wrapped_objective = objective_and_grad\n            jac = None\n\n        minuit = iminuit.Minuit(wrapped_objective, init_pars, grad=jac, name=par_names)\n        minuit.limits = init_bounds\n        minuit.fixed = fixed_bools\n        minuit.print_level = self.verbose\n        minuit.errordef = self.errordef\n        return minuit\n\n    def _minimize(\n        self,\n        minimizer,\n        func,\n        x0,\n        do_grad=False,\n        bounds=None,\n        fixed_vals=None,\n        options={},\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          * strategy (:obj:`int`): See :attr:`iminuit.Minuit.strategy`.\n            Default is to configure in response to ``do_grad``.\n          * tolerance (:obj:`float`): Tolerance for termination.\n            See specific optimizer for detailed meaning.\n            Default is ``0.1``.\n\n        Returns:\n            fitresult (scipy.optimize.OptimizeResult): the fit result\n        \"\"\"\n        maxiter = options.pop('maxiter', self.maxiter)\n        # 0: Fast, user-provided gradient\n        # 1: Default, no user-provided gradient\n        strategy = options.pop(\n            'strategy', self.strategy if self.strategy is not None else not do_grad\n        )\n        tolerance = options.pop('tolerance', self.tolerance)\n        if options:\n            raise exceptions.Unsupported(\n                f\"Unsupported options were passed in: {list(options)}.\"\n            )\n\n        minimizer.strategy = strategy\n        minimizer.tol = tolerance\n        minimizer.migrad(ncall=maxiter)\n        # Following lines below come from:\n        # https://github.com/scikit-hep/iminuit/blob/23bad7697e39d363f259ca8349684df939b1b2e6/src/iminuit/_minimize.py#L111-L130\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        hess_inv = None\n        corr = None\n        unc = None\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.covariance\n            corr = hess_inv.correlation()\n            unc = minimizer.errors\n\n        return scipy.optimize.OptimizeResult(\n            x=minimizer.values,\n            unc=unc,\n            corr=corr,\n            success=minimizer.valid,\n            fun=minimizer.fval,\n            hess_inv=hess_inv,\n            message=message,\n            nfev=minimizer.nfcn,\n            njev=minimizer.ngrad,\n            minuit=minimizer,\n        )\n", "path": "src/pyhf/optimize/opt_minuit.py"}], "after_files": [{"content": "\"\"\"Minuit Optimizer Class.\"\"\"\nfrom pyhf import exceptions\nfrom pyhf.optimize.mixins import OptimizerMixin\nimport scipy\nimport iminuit\n\n\nclass minuit_optimizer(OptimizerMixin):\n    \"\"\"\n    Optimizer that minimizes via :meth:`iminuit.Minuit.migrad`.\n    \"\"\"\n\n    __slots__ = ['name', 'errordef', 'steps', 'strategy', 'tolerance']\n\n    def __init__(self, *args, **kwargs):\n        \"\"\"\n        Create :class:`iminuit.Minuit` optimizer.\n\n        .. note::\n\n            ``errordef`` should be 1.0 for a least-squares cost function and 0.50\n            for negative log-likelihood function --- see `MINUIT: Function Minimization\n            and Error Analysis Reference Manual <https://cdsweb.cern.ch/record/2296388/>`_\n            Section 7.1: Function normalization and ERROR DEF.\n            This parameter is sometimes 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`.\n              Default is ``None``, which results in either\n              :attr:`iminuit.Minuit.strategy` ``0`` or ``1`` from the evaluation of\n              ``int(not pyhf.tensorlib.default_do_grad)``.\n            tolerance (:obj:`float`): Tolerance for termination.\n              See specific optimizer for detailed meaning.\n              Default is ``0.1``.\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        self.tolerance = kwargs.pop('tolerance', 0.1)\n        super().__init__(*args, **kwargs)\n\n    def _get_minimizer(\n        self,\n        objective_and_grad,\n        init_pars,\n        init_bounds,\n        fixed_vals=None,\n        do_grad=False,\n        par_names=None,\n    ):\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\n        # Minuit requires jac=callable\n        if do_grad:\n            wrapped_objective = lambda pars: objective_and_grad(pars)[0]  # noqa: E731\n            jac = lambda pars: objective_and_grad(pars)[1]  # noqa: E731\n        else:\n            wrapped_objective = objective_and_grad\n            jac = None\n\n        minuit = iminuit.Minuit(wrapped_objective, init_pars, grad=jac, name=par_names)\n        minuit.limits = init_bounds\n        minuit.fixed = fixed_bools\n        minuit.print_level = self.verbose\n        minuit.errordef = self.errordef\n        return minuit\n\n    def _minimize(\n        self,\n        minimizer,\n        func,\n        x0,\n        do_grad=False,\n        bounds=None,\n        fixed_vals=None,\n        options={},\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          * strategy (:obj:`int`): See :attr:`iminuit.Minuit.strategy`.\n            Default is to configure in response to ``do_grad``.\n          * tolerance (:obj:`float`): Tolerance for termination.\n            See specific optimizer for detailed meaning.\n            Default is ``0.1``.\n\n        Returns:\n            fitresult (scipy.optimize.OptimizeResult): the fit result\n        \"\"\"\n        maxiter = options.pop('maxiter', self.maxiter)\n        # do_grad value results in iminuit.Minuit.strategy of either:\n        #   0: Fast. Does not check a user-provided gradient.\n        #   1: Default. Checks user-provided gradient against numerical gradient.\n        strategy = options.pop(\"strategy\", self.strategy)\n        # Guard against None from either self.strategy defaulting to None or\n        # passing strategy=None as options kwarg\n        if strategy is None:\n            strategy = 0 if do_grad else 1\n        tolerance = options.pop('tolerance', self.tolerance)\n        if options:\n            raise exceptions.Unsupported(\n                f\"Unsupported options were passed in: {list(options)}.\"\n            )\n\n        minimizer.strategy = strategy\n        minimizer.tol = tolerance\n        minimizer.migrad(ncall=maxiter)\n        # Following lines below come from:\n        # https://github.com/scikit-hep/iminuit/blob/23bad7697e39d363f259ca8349684df939b1b2e6/src/iminuit/_minimize.py#L111-L130\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        hess_inv = None\n        corr = None\n        unc = None\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.covariance\n            corr = hess_inv.correlation()\n            unc = minimizer.errors\n\n        return scipy.optimize.OptimizeResult(\n            x=minimizer.values,\n            unc=unc,\n            corr=corr,\n            success=minimizer.valid,\n            fun=minimizer.fval,\n            hess_inv=hess_inv,\n            message=message,\n            nfev=minimizer.nfcn,\n            njev=minimizer.ngrad,\n            minuit=minimizer,\n        )\n", "path": "src/pyhf/optimize/opt_minuit.py"}]}