Skip to content

Commit 1a30804

Browse files
authored
Allow running Minos without Migrad first (#590)
1 parent ccedc44 commit 1a30804

7 files changed

+72
-47
lines changed

doc/changelog.rst

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ New features
1212
~~~~~~~~~~~~
1313
- Minuit object is now pickle-able and copy-able
1414
- More efficient internal conversion between Python objects and ``std::vector<double>``
15+
- ``Minuit.minos`` can now be called without calling ``Minuit.migrad`` first, which allows
16+
one to use an external minimiser to find a minimum and then compute Minos errors for it
1517

1618
Bug-fixes
1719
~~~~~~~~~

src/functionminimum.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ using namespace ROOT::Minuit2;
1818
MinimumSeed make_seed(const FCN& fcn, const MnUserFcn& mfcn,
1919
const MnUserParameterState& st, const MnStrategy& str);
2020

21-
FunctionMinimum init(const FCN& fcn, const MnUserParameterState& st, double fval,
21+
FunctionMinimum init(const FCN& fcn, const MnUserParameterState& st,
2222
const MnStrategy& str, double edm_goal);
2323

2424
py::tuple fmin_getstate(const FunctionMinimum&);

src/functionminimum_extra.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ MinimumSeed make_seed(const FCN& fcn, const MnUserFcn& mfcn,
2626
return gen(mfcn, gc, st, str);
2727
}
2828

29-
FunctionMinimum init(const FCN& fcn, const MnUserParameterState& st, double fval,
29+
FunctionMinimum init(const FCN& fcn, const MnUserParameterState& st,
3030
const MnStrategy& str, double edm_goal) {
3131
MnUserFcn mfcn(fcn, st.Trafo());
3232
MinimumSeed seed = make_seed(fcn, mfcn, st, str);
@@ -39,7 +39,7 @@ FunctionMinimum init(const FCN& fcn, const MnUserParameterState& st, double fval
3939
err(i) = std::sqrt(2. * mfcn.Up() * seed.Error().InvHessian()(i, i));
4040
}
4141

42-
MinimumParameters minp(val, err, fval);
42+
MinimumParameters minp(val, err, seed.Fval());
4343
std::vector<MinimumState> minstv(1, MinimumState(minp, seed.Edm(), fcn.nfcn_));
4444
if (minstv.back().Edm() < edm_goal) return FunctionMinimum(seed, minstv, fcn.Up());
4545
return FunctionMinimum(seed, minstv, fcn.Up(), FunctionMinimum::MnAboveMaxEdm());

src/iminuit/_minuit.py

+28-16
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
FCN,
55
MnContours,
66
MnHesse,
7+
MnMachinePrecision,
78
MnMigrad,
89
MnMinos,
910
MnPrint,
@@ -550,13 +551,7 @@ def migrad(self, ncall=None, iterate=5):
550551

551552
self._last_state = fm.state
552553

553-
# EDM goal
554-
# - taken from the source code, see VariableMeticBuilder::Minimum and
555-
# ModularFunctionMinimizer::Minimize
556-
# - goal is used to detect convergence but violations by 10x are also accepted;
557-
# see VariableMetricBuilder.cxx:425
558-
edm_goal = 2e-3 * max(self._tolerance * fm.errordef, migrad.precision.eps2)
559-
554+
edm_goal = self._migrad_edm_goal()
560555
self._fmin = mutil.FMin(fm, self.nfcn, self.ngrad, edm_goal)
561556
self._make_covariance()
562557

@@ -605,6 +600,8 @@ def simplex(self, ncall=None):
605600

606601
edm_goal = max(self._tolerance * fm.errordef, simplex.precision.eps2)
607602
self._fmin = mutil.FMin(fm, self.nfcn, self.ngrad, edm_goal)
603+
self._covariance = None
604+
self._merrors = mutil.MErrors()
608605

609606
return self # return self for method chaining and to autodisplay current state
610607

@@ -694,11 +691,11 @@ def run(ipar):
694691
run(0)
695692

696693
edm_goal = self._tolerance * self._fcn._errordef
697-
fm = FunctionMinimum(
698-
self._fcn, self._last_state, x[self.npar], self.strategy, edm_goal
699-
)
694+
fm = FunctionMinimum(self._fcn, self._last_state, self.strategy, edm_goal)
700695
self._last_state = fm.state
701696
self._fmin = mutil.FMin(fm, self.nfcn, self.ngrad, edm_goal)
697+
self._covariance = None
698+
self._merrors = mutil.MErrors()
702699

703700
return self # return self for method chaining and to autodisplay current state
704701

@@ -806,13 +803,16 @@ def minos(self, *parameters, cl=None, ncall=None):
806803
factor = chi2(1).ppf(cl)
807804

808805
if not self._fmin:
809-
raise RuntimeError(
810-
"MINOS require function to be at the minimum. Run MIGRAD first."
806+
# create a FunctionMinimum for MnMinos
807+
fm = FunctionMinimum(
808+
self._fcn, self._last_state, self._strategy, self._tolerance
811809
)
810+
else:
811+
fm = self._fmin._src
812812

813813
ncall = 0 if ncall is None else int(ncall)
814814

815-
if not self._fmin.is_valid:
815+
if not fm.is_valid:
816816
raise RuntimeError(
817817
"Function minimum is not valid. Make sure MIGRAD converged."
818818
)
@@ -833,7 +833,7 @@ def minos(self, *parameters, cl=None, ncall=None):
833833
pars.append(par)
834834

835835
with TemporaryErrordef(self._fcn, factor):
836-
minos = MnMinos(self._fcn, self._fmin._src, self.strategy)
836+
minos = MnMinos(self._fcn, fm, self.strategy)
837837
for par in pars:
838838
me = minos(self._var2pos[par], ncall, self._tolerance)
839839
self._merrors[par] = mutil.MError(
@@ -854,8 +854,9 @@ def minos(self, *parameters, cl=None, ncall=None):
854854
me.min,
855855
)
856856

857-
self._fmin._nfcn = self.nfcn
858-
self._fmin._ngrad = self.ngrad
857+
if self._fmin:
858+
self._fmin._nfcn = self.nfcn
859+
self._fmin._ngrad = self.ngrad
859860

860861
return self # return self for method chaining and to autodisplay current state
861862

@@ -1353,6 +1354,17 @@ def _make_covariance(self):
13531354
else:
13541355
self._covariance = None
13551356

1357+
def _migrad_edm_goal(self):
1358+
pr = MnMachinePrecision()
1359+
if self.precision is not None:
1360+
pr.eps = self.precision
1361+
# EDM goal
1362+
# - taken from the source code, see VariableMeticBuilder::Minimum and
1363+
# ModularFunctionMinimizer::Minimize
1364+
# - goal is used to detect convergence but violations by 10x are also accepted;
1365+
# see VariableMetricBuilder.cxx:425
1366+
return 2e-3 * max(self.tol * self.errordef, pr.eps2)
1367+
13561368
def __repr__(self):
13571369
s = []
13581370
if self.fmin is not None:

src/machineprecision.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ using namespace ROOT::Minuit2;
1818
void bind_machineprecision(py::module m) {
1919
py::class_<MnMachinePrecision>(m, "MnMachinePrecision")
2020

21+
.def(py::init<>())
22+
2123
.def_property("eps", &MnMachinePrecision::Eps, &MnMachinePrecision::SetPrecision)
2224
.def_property_readonly("eps2", &MnMachinePrecision::Eps2)
2325

tests/test_core.py

+28-28
Original file line numberDiff line numberDiff line change
@@ -197,43 +197,19 @@ def test_FunctionMinimum():
197197
st = MnUserParameterState()
198198
st.add("x", 0.01, 5)
199199
str = MnStrategy(1)
200-
fm1 = FunctionMinimum(fcn, st, 0.1, str, 0.2)
200+
fm1 = FunctionMinimum(fcn, st, str, 0.2)
201201
assert fm1.is_valid
202202
assert len(fm1.state) == 1
203-
assert fm1.fval == 0.1
204-
fm2 = FunctionMinimum(fcn, st, 0.1, str, 0)
203+
assert fm1.fval == 10.0001
204+
fm2 = FunctionMinimum(fcn, st, str, 0)
205205
assert not fm2.is_valid
206206

207207

208-
def test_MnUserTransformation_pickle():
209-
tr = MnUserTransformation()
210-
211-
pkl = pickle.dumps(tr)
212-
tr2 = pickle.loads(pkl)
213-
214-
assert len(tr2) == len(tr)
215-
216-
217-
def test_MinimumState_pickle():
218-
st = MinimumState(3)
219-
220-
pkl = pickle.dumps(st)
221-
st2 = pickle.loads(pkl)
222-
223-
assert st.vec == st2.vec
224-
assert st.fval == st2.fval
225-
assert st.edm == st2.edm
226-
assert st.nfcn == st2.nfcn
227-
assert st.is_valid == st2.is_valid
228-
assert st.has_parameters == st2.has_parameters
229-
assert st.has_covariance == st2.has_covariance
230-
231-
232208
def test_FunctionMinimum_pickle():
233209
st = MnUserParameterState()
234210
st.add("x", 1, 0.1)
235211
st.add("y", 2, 0.1, 1, 3)
236-
fm = FunctionMinimum(FCN(fn, None, False, 1), st, 123, 1, 0.1)
212+
fm = FunctionMinimum(FCN(fn, None, False, 1), st, 1, 0.1)
237213

238214
pkl = pickle.dumps(fm)
239215
fm2 = pickle.loads(pkl)
@@ -252,3 +228,27 @@ def test_FunctionMinimum_pickle():
252228
assert fm.is_above_max_edm == fm2.is_above_max_edm
253229
assert fm.has_reached_call_limit == fm2.has_reached_call_limit
254230
assert fm.errordef == fm2.errordef
231+
232+
233+
def test_MnUserTransformation_pickle():
234+
tr = MnUserTransformation()
235+
236+
pkl = pickle.dumps(tr)
237+
tr2 = pickle.loads(pkl)
238+
239+
assert len(tr2) == len(tr)
240+
241+
242+
def test_MinimumState_pickle():
243+
st = MinimumState(3)
244+
245+
pkl = pickle.dumps(st)
246+
st2 = pickle.loads(pkl)
247+
248+
assert st.vec == st2.vec
249+
assert st.fval == st2.fval
250+
assert st.edm == st2.edm
251+
assert st.nfcn == st2.nfcn
252+
assert st.is_valid == st2.is_valid
253+
assert st.has_parameters == st2.has_parameters
254+
assert st.has_covariance == st2.has_covariance

tests/test_minuit.py

+9
Original file line numberDiff line numberDiff line change
@@ -1459,3 +1459,12 @@ def test_minos_new_min():
14591459
# ...but interval is correct
14601460
assert m.merrors["x"].lower == approx(-0.9, abs=1e-2)
14611461
assert m.merrors["x"].upper == approx(1.1, abs=1e-2)
1462+
1463+
1464+
@pytest.mark.parametrize("x", (0.9, 1.0, 1.1))
1465+
def test_minos_without_migrad(x):
1466+
m = Minuit(lsq(lambda x: (x - 1) ** 2), x)
1467+
m.minos()
1468+
me = m.merrors["x"]
1469+
assert x + me.lower == approx(0, abs=5e-3)
1470+
assert x + me.upper == approx(2, abs=5e-3)

0 commit comments

Comments
 (0)