diff --git a/src/pyhf/optimize/mixins.py b/src/pyhf/optimize/mixins.py index 11434f916b..a1aaf4f02d 100644 --- a/src/pyhf/optimize/mixins.py +++ b/src/pyhf/optimize/mixins.py @@ -29,11 +29,23 @@ def __init__(self, **kwargs): ) def _internal_minimize( - self, func, x0, do_grad=False, bounds=None, fixed_vals=None, options={} + self, + func, + x0, + do_grad=False, + bounds=None, + fixed_vals=None, + options={}, + par_names=None, ): minimizer = self._get_minimizer( - func, x0, bounds, fixed_vals=fixed_vals, do_grad=do_grad + func, + x0, + bounds, + fixed_vals=fixed_vals, + do_grad=do_grad, + par_names=par_names, ) result = self._minimize( minimizer, @@ -157,7 +169,21 @@ def minimize( do_stitch=do_stitch, ) - result = self._internal_minimize(**minimizer_kwargs, options=kwargs) + # handle non-pyhf ModelConfigs + try: + par_names = pdf.config.par_names() + except AttributeError: + par_names = None + + # need to remove parameters that are fixed in the fit + if par_names and do_stitch and fixed_vals: + for index, _ in fixed_vals: + par_names[index] = None + par_names = [name for name in par_names if name] + + result = self._internal_minimize( + **minimizer_kwargs, options=kwargs, par_names=par_names + ) result = self._internal_postprocess( result, stitch_pars, return_uncertainties=return_uncertainties ) diff --git a/src/pyhf/optimize/opt_minuit.py b/src/pyhf/optimize/opt_minuit.py index bcfc3d2561..99b605255b 100644 --- a/src/pyhf/optimize/opt_minuit.py +++ b/src/pyhf/optimize/opt_minuit.py @@ -40,7 +40,13 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def _get_minimizer( - self, objective_and_grad, init_pars, init_bounds, fixed_vals=None, do_grad=False + self, + objective_and_grad, + init_pars, + init_bounds, + fixed_vals=None, + do_grad=False, + par_names=None, ): step_sizes = [(b[1] - b[0]) / float(self.steps) for b in init_bounds] @@ -60,7 +66,7 @@ def _get_minimizer( wrapped_objective = objective_and_grad jac = None - minuit = iminuit.Minuit(wrapped_objective, init_pars, grad=jac) + minuit = iminuit.Minuit(wrapped_objective, init_pars, grad=jac, name=par_names) minuit.errors = step_sizes minuit.limits = init_bounds minuit.fixed = fixed_bools diff --git a/src/pyhf/optimize/opt_scipy.py b/src/pyhf/optimize/opt_scipy.py index 2be78f0bda..5ab034ff59 100644 --- a/src/pyhf/optimize/opt_scipy.py +++ b/src/pyhf/optimize/opt_scipy.py @@ -31,7 +31,13 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def _get_minimizer( - self, objective_and_grad, init_pars, init_bounds, fixed_vals=None, do_grad=False + self, + objective_and_grad, + init_pars, + init_bounds, + fixed_vals=None, + do_grad=False, + par_names=None, ): return scipy.optimize.minimize diff --git a/src/pyhf/pdf.py b/src/pyhf/pdf.py index 88fa48d33b..62eda1eb33 100644 --- a/src/pyhf/pdf.py +++ b/src/pyhf/pdf.py @@ -319,6 +319,38 @@ def par_slice(self, name): """ return self.par_map[name]['slice'] + def par_names(self, fstring='{name}[{index}]'): + """ + The names of the parameters in the model including binned-parameter indexing. + + Args: + fstring (:obj:`str`): Format string for the parameter names using ``name`` and ``index`` variables. Default: ``'{name}[{index}]'``. + + Returns: + :obj:`list`: Names of the model parameters. + + Example: + >>> import pyhf + >>> model = pyhf.simplemodels.uncorrelated_background( + ... signal=[12.0, 11.0], bkg=[50.0, 52.0], bkg_uncertainty=[3.0, 7.0] + ... ) + >>> model.config.par_names() + ['mu', 'uncorr_bkguncrt[0]', 'uncorr_bkguncrt[1]'] + >>> model.config.par_names(fstring='{name}_{index}') + ['mu', 'uncorr_bkguncrt_0', 'uncorr_bkguncrt_1'] + """ + _names = [] + for name in self.par_order: + _npars = self.param_set(name).n_parameters + if _npars == 1: + _names.append(name) + continue + + _names.extend( + [fstring.format(name=name, index=idx) for idx in range(_npars)] + ) + return _names + def param_set(self, name): """ The :class:`~pyhf.parameters.paramset` for the model parameter ``name``. diff --git a/tests/test_optim.py b/tests/test_optim.py index 1c8ccbb7d0..dfcd64c1bb 100644 --- a/tests/test_optim.py +++ b/tests/test_optim.py @@ -573,3 +573,17 @@ def test_bad_solver_options_scipy(mocker): model = pyhf.simplemodels.uncorrelated_background([50.0], [100.0], [10.0]) data = pyhf.tensorlib.astensor([125.0] + model.config.auxdata) assert pyhf.infer.mle.fit(data, model).tolist() + + +def test_minuit_param_names(mocker): + pyhf.set_backend('numpy', 'minuit') + pdf = pyhf.simplemodels.uncorrelated_background([5], [10], [3.5]) + data = [10] + pdf.config.auxdata + _, result = pyhf.infer.mle.fit(data, pdf, return_result_obj=True) + assert 'minuit' in result + assert result.minuit.parameters == ('mu', 'uncorr_bkguncrt') + + pdf.config.par_names = mocker.Mock(return_value=None) + _, result = pyhf.infer.mle.fit(data, pdf, return_result_obj=True) + assert 'minuit' in result + assert result.minuit.parameters == ('x0', 'x1') diff --git a/tests/test_simplemodels.py b/tests/test_simplemodels.py index 7acb3bf9a3..b4bf6db813 100644 --- a/tests/test_simplemodels.py +++ b/tests/test_simplemodels.py @@ -13,6 +13,7 @@ def test_correlated_background(): assert model.config.channels == ["single_channel"] assert model.config.samples == ["background", "signal"] assert model.config.par_order == ["mu", "correlated_bkg_uncertainty"] + assert model.config.par_names() == ['mu', 'correlated_bkg_uncertainty'] assert model.config.suggested_init() == [1.0, 0.0] @@ -23,6 +24,11 @@ def test_uncorrelated_background(): assert model.config.channels == ["singlechannel"] assert model.config.samples == ["background", "signal"] assert model.config.par_order == ["mu", "uncorr_bkguncrt"] + assert model.config.par_names() == [ + 'mu', + 'uncorr_bkguncrt[0]', + 'uncorr_bkguncrt[1]', + ] assert model.config.suggested_init() == [1.0, 1.0, 1.0]