Skip to content

Commit dbe5912

Browse files
committed
Merge branch 'develop'
2 parents 6521e16 + 5bcbe5c commit dbe5912

File tree

9 files changed

+129
-51
lines changed

9 files changed

+129
-51
lines changed

.github/workflows/pytest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,4 @@ jobs:
4343
uv pip freeze --system
4444
4545
- name: Run tests
46-
run: pytest --cov=pyerrors -vv
46+
run: pytest --cov=pyerrors -vv -Werror

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [2.15.0] - 2025-10-10
6+
7+
### Added
8+
- Option to explicitly specify the number of fit parameters added.
9+
510
## [2.14.0] - 2025-03-09
611

712
### Added

examples/06_gevp.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@
151151
"\n",
152152
"$$C_{\\textrm{projected}}(t)=v_1^T \\underline{C}(t) v_2$$\n",
153153
"\n",
154-
"If we choose the vectors to be $v_1=v_2=(0,1,0,0)$, we should get the same correlator as in the cell above. \n",
154+
"If we choose the vectors to be $v_1=v_2=(1,0,0,0)$, we should get the same correlator as in the cell above. \n",
155155
"\n",
156156
"Thinking about it this way is usefull in the Context of the generalized eigenvalue problem (GEVP), used to find the source-sink combination, which best describes a certain energy eigenstate.\n",
157157
"A good introduction is found in https://arxiv.org/abs/0902.1265."

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

pyerrors/input/json.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,6 @@ def _ol_from_dict(ind, reps='DICTOBS'):
571571
counter = 0
572572

573573
def dict_replace_obs(d):
574-
nonlocal ol
575574
nonlocal counter
576575
x = {}
577576
for k, v in d.items():
@@ -592,7 +591,6 @@ def dict_replace_obs(d):
592591
return x
593592

594593
def list_replace_obs(li):
595-
nonlocal ol
596594
nonlocal counter
597595
x = []
598596
for e in li:
@@ -613,7 +611,6 @@ def list_replace_obs(li):
613611
return x
614612

615613
def obslist_replace_obs(li):
616-
nonlocal ol
617614
nonlocal counter
618615
il = []
619616
for e in li:
@@ -694,7 +691,6 @@ def _od_from_list_and_dict(ol, ind, reps='DICTOBS'):
694691

695692
def dict_replace_string(d):
696693
nonlocal counter
697-
nonlocal ol
698694
x = {}
699695
for k, v in d.items():
700696
if isinstance(v, dict):
@@ -710,7 +706,6 @@ def dict_replace_string(d):
710706

711707
def list_replace_string(li):
712708
nonlocal counter
713-
nonlocal ol
714709
x = []
715710
for e in li:
716711
if isinstance(e, list):

pyerrors/input/pandas.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import warnings
22
import gzip
33
import sqlite3
4+
from contextlib import closing
45
import pandas as pd
56
from ..obs import Obs
67
from ..correlators import Corr
@@ -29,9 +30,8 @@ def to_sql(df, table_name, db, if_exists='fail', gz=True, **kwargs):
2930
None
3031
"""
3132
se_df = _serialize_df(df, gz=gz)
32-
con = sqlite3.connect(db)
33-
se_df.to_sql(table_name, con, if_exists=if_exists, index=False, **kwargs)
34-
con.close()
33+
with closing(sqlite3.connect(db)) as con:
34+
se_df.to_sql(table_name, con=con, if_exists=if_exists, index=False, **kwargs)
3535

3636

3737
def read_sql(sql, db, auto_gamma=False, **kwargs):
@@ -52,9 +52,8 @@ def read_sql(sql, db, auto_gamma=False, **kwargs):
5252
data : pandas.DataFrame
5353
Dataframe with the content of the sqlite database.
5454
"""
55-
con = sqlite3.connect(db)
56-
extract_df = pd.read_sql(sql, con, **kwargs)
57-
con.close()
55+
with closing(sqlite3.connect(db)) as con:
56+
extract_df = pd.read_sql(sql, con=con, **kwargs)
5857
return _deserialize_df(extract_df, auto_gamma=auto_gamma)
5958

6059

pyerrors/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2.14.0"
1+
__version__ = "2.15.0"

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
classifiers=[
3131
'Development Status :: 5 - Production/Stable',
3232
'Intended Audience :: Science/Research',
33-
'License :: OSI Approved :: MIT License',
3433
'Programming Language :: Python :: 3',
3534
'Programming Language :: Python :: 3.9',
3635
'Programming Language :: Python :: 3.10',

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)