Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Add type hints #251

Open
wants to merge 24 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3db8eb2
[Feat] Add type hints to pyerrors modules
fjosw Dec 25, 2024
9fe375a
[Feat] Added type hints to input modules
fjosw Dec 25, 2024
8d86295
[Feat] Fixed a few type hints manually
fjosw Dec 25, 2024
2d34b35
[Fix] Fix ruff errors and a few type annotations
fjosw Dec 25, 2024
a9e082c
[Fix] Fix type annotations for first part of obs.py
fjosw Jan 3, 2025
0198256
[Fix] Fixed most type annotations in obs.py
fjosw Jan 3, 2025
9389ad6
[CI] Add typing extension package to pytest run for pytthon 3.9
fjosw Jan 3, 2025
23d4f4c
[Fix] Fix type hints in misc.py and remove strict zips for python 3.9
fjosw Jan 3, 2025
1916de1
[Fix] Start fixing remaining type hints
fjosw Jan 3, 2025
3654635
[Fix] Removed unused imports
fjosw Jan 3, 2025
4f1606d
[CI] Add E252 to flake8 exceptions
fjosw Jan 3, 2025
d45b43e
[Fix] Fixed remaining flake8 errors
fjosw Jan 3, 2025
1c6053e
[Fix] Simplify type hints
fjosw Jan 3, 2025
b8700ef
[Fix] Fix type annotations in json.py
fjosw Jan 3, 2025
6d5a9b9
[Fix] Simplify type annotations in input modules
fjosw Jan 3, 2025
6a990c1
[Fix] Fix ruff
fjosw Jan 3, 2025
9c960ae
[Fix] Correct type hints in fits.py
fjosw Jan 5, 2025
5376a8a
[Fix] Further type fixes in fits and sfcf
fjosw Jan 5, 2025
336117c
[Fix] More type hint fixing
fjosw Jan 5, 2025
dba277f
Merge branch 'develop' into feat/typehints
jkuhl-uni Jan 6, 2025
52b91d8
add typehints for other util functions
jkuhl-uni Jan 9, 2025
cb9d942
make deriv structure like second_deriv
jkuhl-uni Jan 9, 2025
2f40ff8
add typing for read_pbp
jkuhl-uni Jan 9, 2025
bbf0b68
[Fix] Additional typehints
fjosw Jan 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/flake8.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ jobs:
- name: flake8 Lint
uses: py-actions/flake8@v2
with:
ignore: "E501,W503"
ignore: "E501,W503,E252"
exclude: "__init__.py, input/__init__.py"
path: "pyerrors"
1 change: 1 addition & 0 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
pip install pytest-cov
pip install pytest-benchmark
pip install hypothesis
pip install typing_extensions
pip freeze

- name: Run tests
Expand Down
181 changes: 91 additions & 90 deletions pyerrors/correlators.py

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions pyerrors/covobs.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from __future__ import annotations
import numpy as np
from numpy import ndarray
from typing import Any, Optional, Union


class Covobs:

def __init__(self, mean, cov, name, pos=None, grad=None):
def __init__(self, mean: Optional[Union[float, int]], cov: Any, name: str, pos: Optional[int]=None, grad: Optional[Union[ndarray, list[float]]]=None):
""" Initialize Covobs object.

Parameters
Expand Down Expand Up @@ -39,12 +42,12 @@ def __init__(self, mean, cov, name, pos=None, grad=None):
self._set_grad(grad)
self.value = mean

def errsq(self):
def errsq(self) -> float:
""" Return the variance (= square of the error) of the Covobs
"""
return np.dot(np.transpose(self.grad), np.dot(self.cov, self.grad)).item()

def _set_cov(self, cov):
def _set_cov(self, cov: Any):
""" Set the covariance matrix of the covobs

Parameters
Expand Down Expand Up @@ -79,7 +82,7 @@ def _set_cov(self, cov):
if ev < 0:
raise Exception('Covariance matrix is not positive-semidefinite!')

def _set_grad(self, grad):
def _set_grad(self, grad: Union[list[float], ndarray]):
""" Set the gradient of the covobs

Parameters
Expand All @@ -96,9 +99,9 @@ def _set_grad(self, grad):
raise Exception('Invalid dimension of grad!')

@property
def cov(self):
def cov(self) -> ndarray:
return self._cov

@property
def grad(self):
def grad(self) -> ndarray:
return self._grad
8 changes: 5 additions & 3 deletions pyerrors/dirac.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from __future__ import annotations
import numpy as np
from numpy import ndarray


gammaX = np.array(
Expand All @@ -22,7 +24,7 @@
dtype=complex)


def epsilon_tensor(i, j, k):
def epsilon_tensor(i: int, j: int, k: int) -> float:
"""Rank-3 epsilon tensor

Based on https://codegolf.stackexchange.com/a/160375
Expand All @@ -39,7 +41,7 @@ def epsilon_tensor(i, j, k):
return (i - j) * (j - k) * (k - i) / 2


def epsilon_tensor_rank4(i, j, k, o):
def epsilon_tensor_rank4(i: int, j: int, k: int, o: int) -> float:
"""Rank-4 epsilon tensor

Extension of https://codegolf.stackexchange.com/a/160375
Expand All @@ -57,7 +59,7 @@ def epsilon_tensor_rank4(i, j, k, o):
return (i - j) * (j - k) * (k - i) * (i - o) * (j - o) * (o - k) / 12


def Grid_gamma(gamma_tag):
def Grid_gamma(gamma_tag: str) -> ndarray:
"""Returns gamma matrix in Grid labeling."""
if gamma_tag == 'Identity':
g = identity
Expand Down
98 changes: 61 additions & 37 deletions pyerrors/fits.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import annotations
import gc
from collections.abc import Sequence
import warnings
Expand All @@ -15,6 +16,8 @@
from numdifftools import Jacobian as num_jacobian
from numdifftools import Hessian as num_hessian
from .obs import Obs, derived_observable, covariance, cov_Obs, invert_corr_cov_cholesky
from numpy import ndarray
from typing import Any, Callable, Optional, Union


class Fit_result(Sequence):
Expand All @@ -33,13 +36,31 @@ class Fit_result(Sequence):
Hotelling t-squared p-value for correlated fits.
"""

def __init__(self):
self.fit_parameters = None

def __getitem__(self, idx):
def __init__(self) -> None:
self.fit_parameters: Optional[list] = None
self.fit_function: Optional[Union[Callable, dict[str, Callable]]] = None
self.priors: Optional[Union[list[Obs], dict[int, Obs]]] = None
self.method: Optional[str] = None
self.iterations: Optional[int] = None
self.chisquare: Optional[float] = None
self.odr_chisquare: Optional[float] = None
self.dof: Optional[int] = None
self.p_value: Optional[float] = None
self.message: Optional[str] = None
self.t2_p_value: Optional[float] = None
self.chisquare_by_dof: Optional[float] = None
self.chisquare_by_expected_chisquare: Optional[float] = None
self.residual_variance: Optional[float] = None
self.xplus: Optional[float] = None

def __getitem__(self, idx: int) -> Obs:
if self.fit_parameters is None:
raise TypeError('No fit parameters available.')
return self.fit_parameters[idx]

def __len__(self):
def __len__(self) -> int:
if self.fit_parameters is None:
raise TypeError('No fit parameters available.')
return len(self.fit_parameters)

def gamma_method(self, **kwargs):
Expand All @@ -48,29 +69,31 @@ def gamma_method(self, **kwargs):

gm = gamma_method

def __str__(self):
def __str__(self) -> str:
my_str = 'Goodness of fit:\n'
if hasattr(self, 'chisquare_by_dof'):
if self.chisquare_by_dof is not None:
my_str += '\u03C7\u00b2/d.o.f. = ' + f'{self.chisquare_by_dof:2.6f}' + '\n'
elif hasattr(self, 'residual_variance'):
elif self.residual_variance is not None:
my_str += 'residual variance = ' + f'{self.residual_variance:2.6f}' + '\n'
if hasattr(self, 'chisquare_by_expected_chisquare'):
if self.chisquare_by_expected_chisquare is not None:
my_str += '\u03C7\u00b2/\u03C7\u00b2exp = ' + f'{self.chisquare_by_expected_chisquare:2.6f}' + '\n'
if hasattr(self, 'p_value'):
if self.p_value is not None:
my_str += 'p-value = ' + f'{self.p_value:2.4f}' + '\n'
if hasattr(self, 't2_p_value'):
if self.t2_p_value is not None:
my_str += 't\u00B2p-value = ' + f'{self.t2_p_value:2.4f}' + '\n'
my_str += 'Fit parameters:\n'
if self.fit_parameters is None:
raise TypeError('No fit parameters available.')
for i_par, par in enumerate(self.fit_parameters):
my_str += str(i_par) + '\t' + ' ' * int(par >= 0) + str(par).rjust(int(par < 0.0)) + '\n'
return my_str

def __repr__(self):
def __repr__(self) -> str:
m = max(map(len, list(self.__dict__.keys()))) + 1
return '\n'.join([key.rjust(m) + ': ' + repr(value) for key, value in sorted(self.__dict__.items())])


def least_squares(x, y, func, priors=None, silent=False, **kwargs):
def least_squares(x: Any, y: Union[dict[str, ndarray], list[Obs], ndarray, dict[str, list[Obs]]], func: Union[Callable, dict[str, Callable]], priors: Optional[Union[dict[int, str], list[str], list[Obs], dict[int, Obs]]]=None, silent: bool=False, **kwargs) -> Fit_result:
r'''Performs a non-linear fit to y = func(x).
```

Expand Down Expand Up @@ -335,9 +358,8 @@ def func_b(a, x):
p_f = dp_f = np.array([])
prior_mask = []
loc_priors = []

if 'initial_guess' in kwargs:
x0 = kwargs.get('initial_guess')
x0 = kwargs.get('initial_guess')
if x0 is not None:
if len(x0) != n_parms:
raise ValueError('Initial guess does not have the correct length: %d vs. %d' % (len(x0), n_parms))
else:
Expand All @@ -356,8 +378,8 @@ def chisqfunc_uncorr(p):
return anp.sum(general_chisqfunc_uncorr(p, y_f, p_f) ** 2)

if kwargs.get('correlated_fit') is True:
if 'inv_chol_cov_matrix' in kwargs:
chol_inv = kwargs.get('inv_chol_cov_matrix')
chol_inv = kwargs.get('inv_chol_cov_matrix')
if chol_inv is not None:
if (chol_inv[0].shape[0] != len(dy_f)):
raise TypeError('The number of columns of the inverse covariance matrix handed over needs to be equal to the number of y errors.')
if (chol_inv[0].shape[0] != chol_inv[0].shape[1]):
Expand Down Expand Up @@ -386,17 +408,17 @@ def chisqfunc(p):

if output.method != 'Levenberg-Marquardt':
if output.method == 'migrad':
tolerance = 1e-4 # default value of 1e-1 set by iminuit can be problematic
if 'tol' in kwargs:
tolerance = kwargs.get('tol')
tolerance = kwargs.get('tol')
if tolerance is None:
tolerance = 1e-4 # default value of 1e-1 set by iminuit can be problematic
fit_result = iminuit.minimize(chisqfunc_uncorr, x0, tol=tolerance) # Stopping criterion 0.002 * tol * errordef
if kwargs.get('correlated_fit') is True:
fit_result = iminuit.minimize(chisqfunc, fit_result.x, tol=tolerance)
output.iterations = fit_result.nfev
else:
tolerance = 1e-12
if 'tol' in kwargs:
tolerance = kwargs.get('tol')
tolerance = kwargs.get('tol')
if tolerance is None:
tolerance = 1e-12
fit_result = scipy.optimize.minimize(chisqfunc_uncorr, x0, method=kwargs.get('method'), tol=tolerance)
if kwargs.get('correlated_fit') is True:
fit_result = scipy.optimize.minimize(chisqfunc, fit_result.x, method=kwargs.get('method'), tol=tolerance)
Expand Down Expand Up @@ -426,8 +448,8 @@ def chisqfunc_residuals(p):
if not fit_result.success:
raise Exception('The minimization procedure did not converge.')

output.chisquare = chisquare
output.dof = y_all.shape[-1] - n_parms + len(loc_priors)
output.chisquare = float(chisquare)
output.dof = int(y_all.shape[-1] - n_parms + len(loc_priors))
output.p_value = 1 - scipy.stats.chi2.cdf(output.chisquare, output.dof)
if output.dof > 0:
output.chisquare_by_dof = output.chisquare / output.dof
Expand Down Expand Up @@ -503,7 +525,7 @@ def chisqfunc_compact(d):
return output


def total_least_squares(x, y, func, silent=False, **kwargs):
def total_least_squares(x: list[Obs], y: list[Obs], func: Callable, silent: bool=False, **kwargs) -> Fit_result:
r'''Performs a non-linear fit to y = func(x) and returns a list of Obs corresponding to the fit parameters.

Parameters
Expand Down Expand Up @@ -600,8 +622,8 @@ def func(a, x):
if np.any(np.asarray(dy_f) <= 0.0):
raise Exception('No y errors available, run the gamma method first.')

if 'initial_guess' in kwargs:
x0 = kwargs.get('initial_guess')
x0 = kwargs.get('initial_guess')
if x0 is not None:
if len(x0) != n_parms:
raise Exception('Initial guess does not have the correct length: %d vs. %d' % (len(x0), n_parms))
else:
Expand Down Expand Up @@ -707,7 +729,7 @@ def odr_chisquare_compact_y(d):
return output


def fit_lin(x, y, **kwargs):
def fit_lin(x: Sequence[Union[Obs, int, float]], y: Sequence[Obs], **kwargs) -> list[Obs]:
"""Performs a linear fit to y = n + m * x and returns two Obs n, m.

Parameters
Expand Down Expand Up @@ -738,7 +760,7 @@ def f(a, x):
raise TypeError('Unsupported types for x')


def qqplot(x, o_y, func, p, title=""):
def qqplot(x: ndarray, o_y: list[Obs], func: Callable, p: list[Obs], title: str=""):
"""Generates a quantile-quantile plot of the fit result which can be used to
check if the residuals of the fit are gaussian distributed.

Expand Down Expand Up @@ -768,7 +790,7 @@ def qqplot(x, o_y, func, p, title=""):
plt.draw()


def residual_plot(x, y, func, fit_res, title=""):
def residual_plot(x: ndarray, y: list[Obs], func: Callable, fit_res: list[Obs], title: str=""):
"""Generates a plot which compares the fit to the data and displays the corresponding residuals

For uncorrelated data the residuals are expected to be distributed ~N(0,1).
Expand Down Expand Up @@ -805,7 +827,7 @@ def residual_plot(x, y, func, fit_res, title=""):
plt.draw()


def error_band(x, func, beta):
def error_band(x: list[int], func: Callable, beta: list[Obs]) -> ndarray:
"""Calculate the error band for an array of sample values x, for given fit function func with optimized parameters beta.

Returns
Expand All @@ -829,7 +851,7 @@ def error_band(x, func, beta):
return err


def ks_test(objects=None):
def ks_test(objects: Optional[list[Fit_result]]=None):
"""Performs a Kolmogorov–Smirnov test for the p-values of all fit object.

Parameters
Expand Down Expand Up @@ -873,7 +895,7 @@ def ks_test(objects=None):
print(scipy.stats.kstest(p_values, 'uniform'))


def _extract_val_and_dval(string):
def _extract_val_and_dval(string: str) -> tuple[float, float]:
split_string = string.split('(')
if '.' in split_string[0] and '.' not in split_string[1][:-1]:
factor = 10 ** -len(split_string[0].partition('.')[2])
Expand All @@ -882,11 +904,13 @@ def _extract_val_and_dval(string):
return float(split_string[0]), float(split_string[1][:-1]) * factor


def _construct_prior_obs(i_prior, i_n):
def _construct_prior_obs(i_prior: Union[Obs, str], i_n: int) -> Obs:
if isinstance(i_prior, Obs):
return i_prior
elif isinstance(i_prior, str):
loc_val, loc_dval = _extract_val_and_dval(i_prior)
return cov_Obs(loc_val, loc_dval ** 2, '#prior' + str(i_n) + f"_{np.random.randint(2147483647):010d}")
prior_obs = cov_Obs(loc_val, loc_dval ** 2, '#prior' + str(i_n) + f"_{np.random.randint(2147483647):010d}")
assert isinstance(prior_obs, Obs)
return prior_obs
else:
raise TypeError("Prior entries need to be 'Obs' or 'str'.")
Loading
Loading