Skip to content

Commit 68e4633

Browse files
authored
[Feat] Number of fit parameters can be explicitly passed to the fit functions (#269)
1 parent d6e6a43 commit 68e4633

File tree

2 files changed

+116
-36
lines changed

2 files changed

+116
-36
lines changed

pyerrors/fits.py

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -131,18 +131,18 @@ def func_b(a, x):
131131
Obs (e.g. results from a previous fit) or strings containing a value and an error formatted like
132132
0.548(23), 500(40) or 0.5(0.4)
133133
silent : bool, optional
134-
If true all output to the console is omitted (default False).
134+
If True all output to the console is omitted (default False).
135135
initial_guess : list
136136
can provide an initial guess for the input parameters. Relevant for
137137
non-linear fits with many parameters. In case of correlated fits the guess is used to perform
138138
an uncorrelated fit which then serves as guess for the correlated fit.
139139
method : str, optional
140140
can be used to choose an alternative method for the minimization of chisquare.
141141
The possible methods are the ones which can be used for scipy.optimize.minimize and
142-
migrad of iminuit. If no method is specified, Levenberg-Marquard is used.
142+
migrad of iminuit. If no method is specified, Levenberg–Marquardt is used.
143143
Reliable alternatives are migrad, Powell and Nelder-Mead.
144144
tol: float, optional
145-
can be used (only for combined fits and methods other than Levenberg-Marquard) to set the tolerance for convergence
145+
can be used (only for combined fits and methods other than Levenberg–Marquardt) to set the tolerance for convergence
146146
to a different value to either speed up convergence at the cost of a larger error on the fitted parameters (and possibly
147147
invalid estimates for parameter uncertainties) or smaller values to get more accurate parameter values
148148
The stopping criterion depends on the method, e.g. migrad: edm_max = 0.002 * tol * errordef (EDM criterion: edm < edm_max)
@@ -152,7 +152,7 @@ def func_b(a, x):
152152
In practice the correlation matrix is Cholesky decomposed and inverted (instead of the covariance matrix).
153153
This procedure should be numerically more stable as the correlation matrix is typically better conditioned (Jacobi preconditioning).
154154
inv_chol_cov_matrix [array,list], optional
155-
array: shape = (no of y values) X (no of y values)
155+
array: shape = (number of y values) X (number of y values)
156156
list: for an uncombined fit: [""]
157157
for a combined fit: list of keys belonging to the corr_matrix saved in the array, must be the same as the keys of the y dict in alphabetical order
158158
If correlated_fit=True is set as well, can provide an inverse covariance matrix (y errors, dy_f included!) of your own choosing for a correlated fit.
@@ -168,6 +168,9 @@ def func_b(a, x):
168168
If True, a quantile-quantile plot of the fit result is generated (default False).
169169
num_grad : bool
170170
Use numerical differentation instead of automatic differentiation to perform the error propagation (default False).
171+
n_parms : int, optional
172+
Number of fit parameters. Overrides automatic detection of parameter count.
173+
Useful when autodetection fails. Must match the length of initial_guess or priors (if provided).
171174
172175
Returns
173176
-------
@@ -269,26 +272,38 @@ def func_b(a, x):
269272
raise Exception("No y errors available, run the gamma method first.")
270273

271274
# number of fit parameters
272-
n_parms_ls = []
273-
for key in key_ls:
274-
if not callable(funcd[key]):
275-
raise TypeError('func (key=' + key + ') is not a function.')
276-
if np.asarray(xd[key]).shape[-1] != len(yd[key]):
277-
raise ValueError('x and y input (key=' + key + ') do not have the same length')
278-
for n_loc in range(100):
279-
try:
280-
funcd[key](np.arange(n_loc), x_all.T[0])
281-
except TypeError:
282-
continue
283-
except IndexError:
284-
continue
275+
if 'n_parms' in kwargs:
276+
n_parms = kwargs.get('n_parms')
277+
if not isinstance(n_parms, int):
278+
raise TypeError(
279+
f"'n_parms' must be an integer, got {n_parms!r} "
280+
f"of type {type(n_parms).__name__}."
281+
)
282+
if n_parms <= 0:
283+
raise ValueError(
284+
f"'n_parms' must be a positive integer, got {n_parms}."
285+
)
286+
else:
287+
n_parms_ls = []
288+
for key in key_ls:
289+
if not callable(funcd[key]):
290+
raise TypeError('func (key=' + key + ') is not a function.')
291+
if np.asarray(xd[key]).shape[-1] != len(yd[key]):
292+
raise ValueError('x and y input (key=' + key + ') do not have the same length')
293+
for n_loc in range(100):
294+
try:
295+
funcd[key](np.arange(n_loc), x_all.T[0])
296+
except TypeError:
297+
continue
298+
except IndexError:
299+
continue
300+
else:
301+
break
285302
else:
286-
break
287-
else:
288-
raise RuntimeError("Fit function (key=" + key + ") is not valid.")
289-
n_parms_ls.append(n_loc)
303+
raise RuntimeError("Fit function (key=" + key + ") is not valid.")
304+
n_parms_ls.append(n_loc)
290305

291-
n_parms = max(n_parms_ls)
306+
n_parms = max(n_parms_ls)
292307

293308
if len(key_ls) > 1:
294309
for key in key_ls:
@@ -535,17 +550,20 @@ def func(a, x):
535550
It is important that all numpy functions refer to autograd.numpy, otherwise the differentiation
536551
will not work.
537552
silent : bool, optional
538-
If true all output to the console is omitted (default False).
553+
If True all output to the console is omitted (default False).
539554
initial_guess : list
540555
can provide an initial guess for the input parameters. Relevant for non-linear
541556
fits with many parameters.
542557
expected_chisquare : bool
543-
If true prints the expected chisquare which is
558+
If True prints the expected chisquare which is
544559
corrected by effects caused by correlated input data.
545560
This can take a while as the full correlation matrix
546561
has to be calculated (default False).
547562
num_grad : bool
548-
Use numerical differentation instead of automatic differentiation to perform the error propagation (default False).
563+
Use numerical differentiation instead of automatic differentiation to perform the error propagation (default False).
564+
n_parms : int, optional
565+
Number of fit parameters. Overrides automatic detection of parameter count.
566+
Useful when autodetection fails. Must match the length of initial_guess (if provided).
549567
550568
Notes
551569
-----
@@ -575,19 +593,32 @@ def func(a, x):
575593
if not callable(func):
576594
raise TypeError('func has to be a function.')
577595

578-
for i in range(42):
579-
try:
580-
func(np.arange(i), x.T[0])
581-
except TypeError:
582-
continue
583-
except IndexError:
584-
continue
585-
else:
586-
break
596+
if 'n_parms' in kwargs:
597+
n_parms = kwargs.get('n_parms')
598+
if not isinstance(n_parms, int):
599+
raise TypeError(
600+
f"'n_parms' must be an integer, got {n_parms!r} "
601+
f"of type {type(n_parms).__name__}."
602+
)
603+
if n_parms <= 0:
604+
raise ValueError(
605+
f"'n_parms' must be a positive integer, got {n_parms}."
606+
)
587607
else:
588-
raise RuntimeError("Fit function is not valid.")
608+
for i in range(100):
609+
try:
610+
func(np.arange(i), x.T[0])
611+
except TypeError:
612+
continue
613+
except IndexError:
614+
continue
615+
else:
616+
break
617+
else:
618+
raise RuntimeError("Fit function is not valid.")
619+
620+
n_parms = i
589621

590-
n_parms = i
591622
if not silent:
592623
print('Fit with', n_parms, 'parameter' + 's' * (n_parms > 1))
593624

tests/fits_test.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,6 +1098,7 @@ def test_combined_fit_xerr():
10981098
}
10991099
xd = {k: np.transpose([[1 + .01 * np.random.uniform(), 2] for i in range(len(yd[k]))]) for k in fitd}
11001100
pe.fits.least_squares(xd, yd, fitd)
1101+
pe.fits.least_squares(xd, yd, fitd, n_parms=4)
11011102

11021103

11031104
def test_x_multidim_fit():
@@ -1340,6 +1341,54 @@ def test_combined_fit_constant_shape():
13401341
funcs = {"a": lambda a, x: a[0] + a[1] * x,
13411342
"": lambda a, x: a[1] + x * 0}
13421343
pe.fits.least_squares(x, y, funcs, method='migrad')
1344+
pe.fits.least_squares(x, y, funcs, method='migrad', n_parms=2)
1345+
1346+
def test_fit_n_parms():
1347+
# Function that fails if the number of parameters is not specified:
1348+
def fcn(p, x):
1349+
# Assumes first half of terms are A second half are E
1350+
NTerms = int(len(p)/2)
1351+
A = anp.array(p[0:NTerms])[:, np.newaxis] # shape (n, 1)
1352+
E_P = anp.array(p[NTerms:])[:, np.newaxis] # shape (n, 1)
1353+
# This if statement handles the case where x is a single value rather than an array
1354+
if isinstance(x, anp.float64) or isinstance(x, anp.int64) or isinstance(x, float) or isinstance(x, int):
1355+
x = anp.array([x])[np.newaxis, :] # shape (1, m)
1356+
else:
1357+
x = anp.array(x)[np.newaxis, :] # shape (1, m)
1358+
exp_term = anp.exp(-E_P * x)
1359+
weighted_sum = A * exp_term # shape (n, m)
1360+
return anp.mean(weighted_sum, axis=0) # shape(m)
1361+
1362+
c = pe.Corr([pe.pseudo_Obs(2. * np.exp(-.2 * t) + .4 * np.exp(+.4 * t) + .4 * np.exp(-.6 * t), .1, 'corr') for t in range(12)])
1363+
1364+
c.fit(fcn, n_parms=2)
1365+
c.fit(fcn, n_parms=4)
1366+
1367+
xf = [pe.pseudo_Obs(t, .05, 'corr') for t in range(c.T)]
1368+
yf = [c[t] for t in range(c.T)]
1369+
pe.fits.total_least_squares(xf, yf, fcn, n_parms=2)
1370+
pe.fits.total_least_squares(xf, yf, fcn, n_parms=4)
1371+
1372+
# Is expected to fail, this is what is fixed with n_parms
1373+
with pytest.raises(RuntimeError):
1374+
c.fit(fcn, )
1375+
with pytest.raises(RuntimeError):
1376+
pe.fits.total_least_squares(xf, yf, fcn, )
1377+
# Test for positivity
1378+
with pytest.raises(ValueError):
1379+
c.fit(fcn, n_parms=-2)
1380+
with pytest.raises(ValueError):
1381+
pe.fits.total_least_squares(xf, yf, fcn, n_parms=-4)
1382+
# Have to pass an interger
1383+
with pytest.raises(TypeError):
1384+
c.fit(fcn, n_parms=2.)
1385+
with pytest.raises(TypeError):
1386+
pe.fits.total_least_squares(xf, yf, fcn, n_parms=1.2343)
1387+
# Improper number of parameters (function should fail)
1388+
with pytest.raises(ValueError):
1389+
c.fit(fcn, n_parms=7)
1390+
with pytest.raises(ValueError):
1391+
pe.fits.total_least_squares(xf, yf, fcn, n_parms=5)
13431392

13441393

13451394
def fit_general(x, y, func, silent=False, **kwargs):

0 commit comments

Comments
 (0)