From 64b784a834b8a31ca80a6a33a4e0c1059ffa2c22 Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Thu, 11 Aug 2022 23:53:41 -0700 Subject: [PATCH 01/21] drop tensor, fix up missing imports --- .pre-commit-config.yaml | 2 ++ pyproject.toml | 12 +++--------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 872031c3af..5cd71c50de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -65,6 +65,8 @@ repos: - id: mypy files: src args: [] + additional_dependencies: + ['numpy', 'torch', 'jax', 'types-tqdm', 'click', 'types-jsonpatch', 'types-pyyaml', 'types-jsonschema', 'importlib_metadata', 'packaging'] - repo: https://github.com/nbQA-dev/nbQA rev: 1.4.0 diff --git a/pyproject.toml b/pyproject.toml index 263802fb50..53187fdd8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,23 +109,17 @@ warn_unreachable = true [[tool.mypy.overrides]] module = [ 'matplotlib.*', - 'jax.*', - 'click.*', - 'click_completion.*', - 'numpy.*', 'scipy.*', - 'jsonpatch.*', + 'tensorflow.*', + 'tensorflow_probability.*', 'uproot.*', - 'yaml.*', - 'jsonschema.*', - 'tqdm.*', ] ignore_missing_imports = true + [[tool.mypy.overrides]] module = [ 'pyhf', - 'pyhf.tensor.*', 'pyhf.optimize.*', 'pyhf.contrib.*', 'pyhf.infer.*', From 6079e6f920f28865e61fb5f621ba3f8c6089a54f Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Fri, 12 Aug 2022 11:06:22 -0700 Subject: [PATCH 02/21] get precommit working --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5cd71c50de..7a216a36ed 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: files: src args: [] additional_dependencies: - ['numpy', 'torch', 'jax', 'types-tqdm', 'click', 'types-jsonpatch', 'types-pyyaml', 'types-jsonschema', 'importlib_metadata', 'packaging'] + ['numpy', 'types-tqdm', 'click', 'types-jsonpatch', 'types-pyyaml', 'types-jsonschema', 'importlib_metadata', 'packaging'] - repo: https://github.com/nbQA-dev/nbQA rev: 1.4.0 diff --git a/pyproject.toml b/pyproject.toml index 53187fdd8b..b108e29be8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,10 +108,12 @@ warn_unreachable = true [[tool.mypy.overrides]] module = [ + 'jax.*', 'matplotlib.*', 'scipy.*', 'tensorflow.*', 'tensorflow_probability.*', + 'torch.*', 'uproot.*', ] ignore_missing_imports = true From 15c17ca4ccbc3726b4f087ce08308d94f5046867 Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Sat, 13 Aug 2022 09:39:16 -0700 Subject: [PATCH 03/21] start typing numpy backend --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index b108e29be8..af3d331ad0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -142,5 +142,11 @@ module = [ 'pyhf.pdf', 'pyhf.simplemodels', 'pyhf.probability', + 'pyhf.tensor.common.*', + 'pyhf.tensor.manager.*', + 'pyhf.tensor', + 'pyhf.tensor.jax_backend.*', + 'pyhf.tensor.tensorflow_backend.*', + 'pyhf.tensor.pytorch_backend.*', ] ignore_errors = true From 2e05145971e64fab025c076a0aafee94c4a84d7f Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Sat, 13 Aug 2022 09:39:46 -0700 Subject: [PATCH 04/21] first typed up version --- src/pyhf/tensor/numpy_backend.py | 150 ++++++++++++++++++++----------- 1 file changed, 96 insertions(+), 54 deletions(-) diff --git a/src/pyhf/tensor/numpy_backend.py b/src/pyhf/tensor/numpy_backend.py index f97e05d198..4b7cf3b379 100644 --- a/src/pyhf/tensor/numpy_backend.py +++ b/src/pyhf/tensor/numpy_backend.py @@ -5,56 +5,74 @@ from scipy import special from scipy.stats import norm, poisson +from typing import TypeVar, Callable, Literal, Sequence, Generic, Tuple, Mapping +from numpy.typing import ( + NDArray, + NBitBase, + ArrayLike, +) + +Shape = Tuple[int, ...] log = logging.getLogger(__name__) +T = TypeVar("T", bound=NBitBase) +Tensor = NDArray[np.floating[T]] + -class _BasicPoisson: - def __init__(self, rate): +class _BasicPoisson(Generic[T]): + def __init__(self, rate: Tensor[T]): self.rate = rate - def sample(self, sample_shape): + def sample(self, sample_shape: NDArray[np.integer[T]]) -> Tensor[T]: return poisson(self.rate).rvs(size=sample_shape + self.rate.shape) - def log_prob(self, value): + def log_prob(self, value: NDArray[np.number[T]]) -> Tensor[T]: tensorlib = numpy_backend() return tensorlib.poisson_logpdf(value, self.rate) -class _BasicNormal: - def __init__(self, loc, scale): +class _BasicNormal(Generic[T]): + def __init__(self, loc: NDArray[np.number[T]], scale: NDArray[np.number[T]]): self.loc = loc self.scale = scale - def sample(self, sample_shape): + def sample(self, sample_shape: NDArray[np.integer[T]]) -> Tensor[T]: return norm(self.loc, self.scale).rvs(size=sample_shape + self.loc.shape) - def log_prob(self, value): + def log_prob(self, value: NDArray[np.number[T]]) -> Tensor[T]: tensorlib = numpy_backend() return tensorlib.normal_logpdf(value, self.loc, self.scale) -class numpy_backend: +class numpy_backend(Generic[T]): """NumPy backend for pyhf""" __slots__ = ['name', 'precision', 'dtypemap', 'default_do_grad'] - def __init__(self, **kwargs): + def __init__(self, **kwargs: dict[str, str]): self.name = 'numpy' self.precision = kwargs.get('precision', '64b') - self.dtypemap = { + self.dtypemap: Mapping[ + Literal['float', 'int', 'bool'], np.floating[T] | np.integer[T] | np.bool_ + ] = { 'float': np.float64 if self.precision == '64b' else np.float32, 'int': np.int64 if self.precision == '64b' else np.int32, 'bool': np.bool_, } self.default_do_grad = False - def _setup(self): + def _setup(self) -> None: """ Run any global setups for the numpy lib. """ - def clip(self, tensor_in, min_value, max_value): + def clip( + self, + tensor_in: Tensor[T], + min_value: np.integer[T] | np.floating[T], + max_value: np.integer[T] | np.floating[T], + ) -> Tensor[T]: """ Clips (limits) the tensor values to be within a specified min and max. @@ -76,7 +94,7 @@ def clip(self, tensor_in, min_value, max_value): """ return np.clip(tensor_in, min_value, max_value) - def erf(self, tensor_in): + def erf(self, tensor_in: Tensor[T]) -> Tensor[T]: """ The error function of complex argument. @@ -96,7 +114,7 @@ def erf(self, tensor_in): """ return special.erf(tensor_in) - def erfinv(self, tensor_in): + def erfinv(self, tensor_in: Tensor[T]) -> Tensor[T]: """ The inverse of the error function of complex argument. @@ -116,7 +134,7 @@ def erfinv(self, tensor_in): """ return special.erfinv(tensor_in) - def tile(self, tensor_in, repeats): + def tile(self, tensor_in: Tensor[T], repeats: int | Sequence[int]) -> Tensor[T]: """ Repeat tensor data along a specific dimension @@ -138,7 +156,12 @@ def tile(self, tensor_in, repeats): """ return np.tile(tensor_in, repeats) - def conditional(self, predicate, true_callable, false_callable): + def conditional( + self, + predicate: NDArray[np.bool_], + true_callable: Callable[[], Tensor[T]], + false_callable: Callable[[], Tensor[T]], + ) -> Tensor[T]: """ Runs a callable conditional on the boolean value of the evaluation of a predicate @@ -162,7 +185,7 @@ def conditional(self, predicate, true_callable, false_callable): """ return true_callable() if predicate else false_callable() - def tolist(self, tensor_in): + def tolist(self, tensor_in: ArrayLike) -> list[T]: try: return tensor_in.tolist() except AttributeError: @@ -170,19 +193,21 @@ def tolist(self, tensor_in): return tensor_in raise - def outer(self, tensor_in_1, tensor_in_2): + def outer(self, tensor_in_1: Tensor[T], tensor_in_2: Tensor[T]) -> Tensor[T]: return np.outer(tensor_in_1, tensor_in_2) - def gather(self, tensor, indices): + def gather(self, tensor: Tensor[T], indices: NDArray[np.integer[T]]) -> Tensor[T]: return tensor[indices] - def boolean_mask(self, tensor, mask): + def boolean_mask(self, tensor: Tensor[T], mask: NDArray[np.bool_]) -> Tensor[T]: return tensor[mask] - def isfinite(self, tensor): + def isfinite(self, tensor: Tensor[T]) -> NDArray[np.bool_]: return np.isfinite(tensor) - def astensor(self, tensor_in, dtype='float'): + def astensor( + self, tensor_in: ArrayLike, dtype: Literal['float'] = 'float' + ) -> Tensor[T]: """ Convert to a NumPy array. @@ -213,18 +238,20 @@ def astensor(self, tensor_in, dtype='float'): return np.asarray(tensor_in, dtype=dtype) - def sum(self, tensor_in, axis=None): + def sum(self, tensor_in: Tensor[T], axis: int | None = None) -> Tensor[T]: return np.sum(tensor_in, axis=axis) - def product(self, tensor_in, axis=None): + def product(self, tensor_in: Tensor[T], axis: int | None = None) -> Tensor[T]: return np.product(tensor_in, axis=axis) - def abs(self, tensor): + def abs(self, tensor: Tensor[T]) -> Tensor[T]: return np.abs(tensor) - def ones(self, shape, dtype="float"): + def ones( + self, shape: Shape, dtype_str: Literal["float", "int", "bool"] = "float" + ) -> Tensor[T]: try: - dtype = self.dtypemap[dtype] + dtype = self.dtypemap[dtype_str] except KeyError: log.error( f"Invalid dtype: dtype must be one of {list(self.dtypemap.keys())}.", @@ -234,9 +261,11 @@ def ones(self, shape, dtype="float"): return np.ones(shape, dtype=dtype) - def zeros(self, shape, dtype="float"): + def zeros( + self, shape: Shape, dtype_str: Literal["float", "int", "bool"] = "float" + ) -> Tensor[T]: try: - dtype = self.dtypemap[dtype] + dtype = self.dtypemap[dtype_str] except KeyError: log.error( f"Invalid dtype: dtype must be one of {list(self.dtypemap.keys())}.", @@ -246,22 +275,30 @@ def zeros(self, shape, dtype="float"): return np.zeros(shape, dtype=dtype) - def power(self, tensor_in_1, tensor_in_2): + def power(self, tensor_in_1: Tensor[T], tensor_in_2: Tensor[T]) -> Tensor[T]: return np.power(tensor_in_1, tensor_in_2) - def sqrt(self, tensor_in): + def sqrt(self, tensor_in: Tensor[T]) -> Tensor[T]: return np.sqrt(tensor_in) - def divide(self, tensor_in_1, tensor_in_2): + def divide(self, tensor_in_1: Tensor[T], tensor_in_2: Tensor[T]) -> Tensor[T]: return np.divide(tensor_in_1, tensor_in_2) - def log(self, tensor_in): + def log(self, tensor_in: Tensor[T]) -> Tensor[T]: return np.log(tensor_in) - def exp(self, tensor_in): + def exp(self, tensor_in: Tensor[T]) -> Tensor[T]: return np.exp(tensor_in) - def percentile(self, tensor_in, q, axis=None, interpolation="linear"): + def percentile( + self, + tensor_in: Tensor[T], + q: Tensor[T], + axis: None | Shape = None, + interpolation: Literal[ + "linear", "lower", "higher", "midpoint", "nearest" + ] = "linear", + ) -> Tensor[T]: r""" Compute the :math:`q`-th percentile of the tensor along the specified axis. @@ -297,15 +334,18 @@ def percentile(self, tensor_in, q, axis=None, interpolation="linear"): NumPy ndarray: The value of the :math:`q`-th percentile of the tensor along the specified axis. """ - return np.percentile(tensor_in, q, axis=axis, interpolation=interpolation) + # see numpy/numpy#22125 + return np.percentile(tensor_in, q, axis=axis, interpolation=interpolation) # type: ignore[call-overload] - def stack(self, sequence, axis=0): + def stack(self, sequence: Sequence[Tensor[T]], axis: int = 0) -> Tensor[T]: return np.stack(sequence, axis=axis) - def where(self, mask, tensor_in_1, tensor_in_2): + def where( + self, mask: NDArray[np.bool_], tensor_in_1: Tensor[T], tensor_in_2: Tensor[T] + ) -> Tensor[T]: return np.where(mask, tensor_in_1, tensor_in_2) - def concatenate(self, sequence, axis=0): + def concatenate(self, sequence: Tensor[T], axis: None | int = 0) -> Tensor[T]: """ Join a sequence of arrays along an existing axis. @@ -319,7 +359,7 @@ def concatenate(self, sequence, axis=0): """ return np.concatenate(sequence, axis=axis) - def simple_broadcast(self, *args): + def simple_broadcast(self, *args: Sequence[Tensor[T]]) -> Sequence[Tensor[T]]: """ Broadcast a sequence of 1 dimensional arrays. @@ -341,13 +381,13 @@ def simple_broadcast(self, *args): """ return np.broadcast_arrays(*args) - def shape(self, tensor): + def shape(self, tensor: Tensor[T]) -> Shape: return tensor.shape - def reshape(self, tensor, newshape): + def reshape(self, tensor: Tensor[T], newshape: Shape) -> Tensor[T]: return np.reshape(tensor, newshape) - def ravel(self, tensor): + def ravel(self, tensor: Tensor[T]) -> Tensor[T]: """ Return a flattened view of the tensor, not a copy. @@ -367,7 +407,7 @@ def ravel(self, tensor): """ return np.ravel(tensor) - def einsum(self, subscripts, *operands): + def einsum(self, subscripts: str, *operands: Sequence[Tensor[T]]) -> Tensor[T]: """ Evaluates the Einstein summation convention on the operands. @@ -386,10 +426,10 @@ def einsum(self, subscripts, *operands): """ return np.einsum(subscripts, *operands) - def poisson_logpdf(self, n, lam): + def poisson_logpdf(self, n: Tensor[T], lam: Tensor[T]) -> Tensor[T]: return xlogy(n, lam) - lam - gammaln(n + 1.0) - def poisson(self, n, lam): + def poisson(self, n: Tensor[T], lam: Tensor[T]) -> Tensor[T]: r""" The continuous approximation, using :math:`n! = \Gamma\left(n+1\right)`, to the probability mass function of the Poisson distribution evaluated @@ -433,7 +473,7 @@ def poisson(self, n, lam): lam = np.asarray(lam) return np.exp(xlogy(n, lam) - lam - gammaln(n + 1.0)) - def normal_logpdf(self, x, mu, sigma): + def normal_logpdf(self, x: Tensor[T], mu: Tensor[T], sigma: Tensor[T]) -> Tensor[T]: # this is much faster than # norm.logpdf(x, loc=mu, scale=sigma) # https://codereview.stackexchange.com/questions/69718/fastest-computation-of-n-likelihoods-on-normal-distributions @@ -446,7 +486,7 @@ def normal_logpdf(self, x, mu, sigma): # def normal_logpdf(self, x, mu, sigma): # return norm.logpdf(x, loc=mu, scale=sigma) - def normal(self, x, mu, sigma): + def normal(self, x: Tensor[T], mu: Tensor[T], sigma: Tensor[T]) -> Tensor[T]: r""" The probability density function of the Normal distribution evaluated at :code:`x` given parameters of mean of :code:`mu` and standard deviation @@ -474,7 +514,9 @@ def normal(self, x, mu, sigma): """ return norm.pdf(x, loc=mu, scale=sigma) - def normal_cdf(self, x, mu=0, sigma=1): + def normal_cdf( + self, x: Tensor[T], mu: float | Tensor[T] = 0, sigma: float | Tensor[T] = 1 + ) -> Tensor[T]: """ The cumulative distribution function for the Normal distribution @@ -498,7 +540,7 @@ def normal_cdf(self, x, mu=0, sigma=1): """ return norm.cdf(x, loc=mu, scale=sigma) - def poisson_dist(self, rate): + def poisson_dist(self, rate: Tensor[T]) -> _BasicPoisson[T]: r""" The Poisson distribution with rate parameter :code:`rate`. @@ -519,7 +561,7 @@ def poisson_dist(self, rate): """ return _BasicPoisson(rate) - def normal_dist(self, mu, sigma): + def normal_dist(self, mu: Tensor[T], sigma: Tensor[T]) -> _BasicNormal[T]: r""" The Normal distribution with mean :code:`mu` and standard deviation :code:`sigma`. @@ -543,7 +585,7 @@ def normal_dist(self, mu, sigma): """ return _BasicNormal(mu, sigma) - def to_numpy(self, tensor_in): + def to_numpy(self, tensor_in: Tensor[T]) -> Tensor[T]: """ Return the input tensor as it already is a :class:`numpy.ndarray`. This API exists only for ``pyhf.tensorlib`` compatibility. @@ -571,7 +613,7 @@ def to_numpy(self, tensor_in): """ return tensor_in - def transpose(self, tensor_in): + def transpose(self, tensor_in: Tensor[T]) -> Tensor[T]: """ Transpose the tensor. From a4fb57ae6ba3328f9f7b54e3a97621b4bc2d9bca Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Tue, 16 Aug 2022 15:37:38 -0700 Subject: [PATCH 05/21] get numpy_backend typed up? --- src/pyhf/tensor/numpy_backend.py | 150 ++++++++++++++++--------------- src/pyhf/typing.py | 23 ++++- 2 files changed, 96 insertions(+), 77 deletions(-) diff --git a/src/pyhf/tensor/numpy_backend.py b/src/pyhf/tensor/numpy_backend.py index 4b7cf3b379..6a6709f834 100644 --- a/src/pyhf/tensor/numpy_backend.py +++ b/src/pyhf/tensor/numpy_backend.py @@ -5,47 +5,48 @@ from scipy import special from scipy.stats import norm, poisson -from typing import TypeVar, Callable, Literal, Sequence, Generic, Tuple, Mapping +from pyhf.typing import TensorBackend, Shape, PDF + +from typing import TypeVar, Callable, Literal, Sequence, Generic, Mapping, cast from numpy.typing import ( NDArray, NBitBase, ArrayLike, + DTypeLike, ) -Shape = Tuple[int, ...] +T = TypeVar("T", bound=NBitBase) +Tensor = NDArray[np.number[T]] | NDArray[np.bool_] log = logging.getLogger(__name__) -T = TypeVar("T", bound=NBitBase) -Tensor = NDArray[np.floating[T]] - -class _BasicPoisson(Generic[T]): +class _BasicPoisson(PDF): def __init__(self, rate: Tensor[T]): self.rate = rate - def sample(self, sample_shape: NDArray[np.integer[T]]) -> Tensor[T]: - return poisson(self.rate).rvs(size=sample_shape + self.rate.shape) + def sample(self, sample_shape: Shape) -> ArrayLike: + return poisson(self.rate).rvs(size=sample_shape + self.rate.shape) # type: ignore[no-any-return] - def log_prob(self, value: NDArray[np.number[T]]) -> Tensor[T]: - tensorlib = numpy_backend() + def log_prob(self, value: NDArray[np.number[T]]) -> ArrayLike: + tensorlib: numpy_backend[T] = numpy_backend() return tensorlib.poisson_logpdf(value, self.rate) -class _BasicNormal(Generic[T]): - def __init__(self, loc: NDArray[np.number[T]], scale: NDArray[np.number[T]]): +class _BasicNormal(PDF): + def __init__(self, loc: Tensor[T], scale: Tensor[T]): self.loc = loc self.scale = scale - def sample(self, sample_shape: NDArray[np.integer[T]]) -> Tensor[T]: - return norm(self.loc, self.scale).rvs(size=sample_shape + self.loc.shape) + def sample(self, sample_shape: Shape) -> ArrayLike: + return norm(self.loc, self.scale).rvs(size=sample_shape + self.loc.shape) # type: ignore[no-any-return] - def log_prob(self, value: NDArray[np.number[T]]) -> Tensor[T]: - tensorlib = numpy_backend() + def log_prob(self, value: NDArray[np.number[T]]) -> ArrayLike: + tensorlib: numpy_backend[T] = numpy_backend() return tensorlib.normal_logpdf(value, self.loc, self.scale) -class numpy_backend(Generic[T]): +class numpy_backend(Generic[T], TensorBackend): """NumPy backend for pyhf""" __slots__ = ['name', 'precision', 'dtypemap', 'default_do_grad'] @@ -54,7 +55,8 @@ def __init__(self, **kwargs: dict[str, str]): self.name = 'numpy' self.precision = kwargs.get('precision', '64b') self.dtypemap: Mapping[ - Literal['float', 'int', 'bool'], np.floating[T] | np.integer[T] | np.bool_ + Literal['float', 'int', 'bool'], + DTypeLike, # Type[np.floating[T]] | Type[np.integer[T]] | Type[np.bool_], ] = { 'float': np.float64 if self.precision == '64b' else np.float32, 'int': np.int64 if self.precision == '64b' else np.int32, @@ -72,7 +74,7 @@ def clip( tensor_in: Tensor[T], min_value: np.integer[T] | np.floating[T], max_value: np.integer[T] | np.floating[T], - ) -> Tensor[T]: + ) -> ArrayLike: """ Clips (limits) the tensor values to be within a specified min and max. @@ -92,9 +94,9 @@ def clip( Returns: NumPy ndarray: A clipped `tensor` """ - return np.clip(tensor_in, min_value, max_value) + return cast(Tensor[T], np.clip(tensor_in, min_value, max_value)) - def erf(self, tensor_in: Tensor[T]) -> Tensor[T]: + def erf(self, tensor_in: Tensor[T]) -> ArrayLike: """ The error function of complex argument. @@ -112,9 +114,9 @@ def erf(self, tensor_in: Tensor[T]) -> Tensor[T]: Returns: NumPy ndarray: The values of the error function at the given points. """ - return special.erf(tensor_in) + return special.erf(tensor_in) # type: ignore[no-any-return] - def erfinv(self, tensor_in: Tensor[T]) -> Tensor[T]: + def erfinv(self, tensor_in: Tensor[T]) -> ArrayLike: """ The inverse of the error function of complex argument. @@ -132,9 +134,9 @@ def erfinv(self, tensor_in: Tensor[T]) -> Tensor[T]: Returns: NumPy ndarray: The values of the inverse of the error function at the given points. """ - return special.erfinv(tensor_in) + return special.erfinv(tensor_in) # type: ignore[no-any-return] - def tile(self, tensor_in: Tensor[T], repeats: int | Sequence[int]) -> Tensor[T]: + def tile(self, tensor_in: Tensor[T], repeats: int | Sequence[int]) -> ArrayLike: """ Repeat tensor data along a specific dimension @@ -154,14 +156,14 @@ def tile(self, tensor_in: Tensor[T], repeats: int | Sequence[int]) -> Tensor[T]: Returns: NumPy ndarray: The tensor with repeated axes """ - return np.tile(tensor_in, repeats) + return cast(Tensor[T], np.tile(tensor_in, repeats)) def conditional( self, predicate: NDArray[np.bool_], true_callable: Callable[[], Tensor[T]], false_callable: Callable[[], Tensor[T]], - ) -> Tensor[T]: + ) -> ArrayLike: """ Runs a callable conditional on the boolean value of the evaluation of a predicate @@ -185,29 +187,29 @@ def conditional( """ return true_callable() if predicate else false_callable() - def tolist(self, tensor_in: ArrayLike) -> list[T]: + def tolist(self, tensor_in: Tensor[T] | list[T]) -> list[T]: try: - return tensor_in.tolist() + return cast(list[T], tensor_in.tolist()) # type: ignore[union-attr] except AttributeError: if isinstance(tensor_in, list): return tensor_in raise - def outer(self, tensor_in_1: Tensor[T], tensor_in_2: Tensor[T]) -> Tensor[T]: - return np.outer(tensor_in_1, tensor_in_2) + def outer(self, tensor_in_1: Tensor[T], tensor_in_2: Tensor[T]) -> ArrayLike: + return np.outer(tensor_in_1, tensor_in_2) # type: ignore[arg-type] - def gather(self, tensor: Tensor[T], indices: NDArray[np.integer[T]]) -> Tensor[T]: - return tensor[indices] + def gather(self, tensor: Tensor[T], indices: NDArray[np.integer[T]]) -> ArrayLike: + return tensor[indices] # type: ignore[no-any-return] - def boolean_mask(self, tensor: Tensor[T], mask: NDArray[np.bool_]) -> Tensor[T]: - return tensor[mask] + def boolean_mask(self, tensor: Tensor[T], mask: NDArray[np.bool_]) -> ArrayLike: + return tensor[mask] # type: ignore[no-any-return] def isfinite(self, tensor: Tensor[T]) -> NDArray[np.bool_]: return np.isfinite(tensor) def astensor( - self, tensor_in: ArrayLike, dtype: Literal['float'] = 'float' - ) -> Tensor[T]: + self, tensor_in: ArrayLike, dtype_str: Literal['float'] = 'float' + ) -> ArrayLike: """ Convert to a NumPy array. @@ -229,7 +231,7 @@ def astensor( `numpy.ndarray`: A multi-dimensional, fixed-size homogeneous array. """ try: - dtype = self.dtypemap[dtype] + dtype = self.dtypemap[dtype_str] except KeyError: log.error( 'Invalid dtype: dtype must be float, int, or bool.', exc_info=True @@ -238,18 +240,18 @@ def astensor( return np.asarray(tensor_in, dtype=dtype) - def sum(self, tensor_in: Tensor[T], axis: int | None = None) -> Tensor[T]: + def sum(self, tensor_in: Tensor[T], axis: int | None = None) -> ArrayLike: return np.sum(tensor_in, axis=axis) - def product(self, tensor_in: Tensor[T], axis: int | None = None) -> Tensor[T]: - return np.product(tensor_in, axis=axis) + def product(self, tensor_in: Tensor[T], axis: Shape | None = None) -> ArrayLike: + return np.product(tensor_in, axis=axis) # type: ignore[arg-type] - def abs(self, tensor: Tensor[T]) -> Tensor[T]: + def abs(self, tensor: Tensor[T]) -> ArrayLike: return np.abs(tensor) def ones( self, shape: Shape, dtype_str: Literal["float", "int", "bool"] = "float" - ) -> Tensor[T]: + ) -> ArrayLike: try: dtype = self.dtypemap[dtype_str] except KeyError: @@ -263,7 +265,7 @@ def ones( def zeros( self, shape: Shape, dtype_str: Literal["float", "int", "bool"] = "float" - ) -> Tensor[T]: + ) -> ArrayLike: try: dtype = self.dtypemap[dtype_str] except KeyError: @@ -275,19 +277,19 @@ def zeros( return np.zeros(shape, dtype=dtype) - def power(self, tensor_in_1: Tensor[T], tensor_in_2: Tensor[T]) -> Tensor[T]: + def power(self, tensor_in_1: Tensor[T], tensor_in_2: Tensor[T]) -> ArrayLike: return np.power(tensor_in_1, tensor_in_2) - def sqrt(self, tensor_in: Tensor[T]) -> Tensor[T]: + def sqrt(self, tensor_in: Tensor[T]) -> ArrayLike: return np.sqrt(tensor_in) - def divide(self, tensor_in_1: Tensor[T], tensor_in_2: Tensor[T]) -> Tensor[T]: + def divide(self, tensor_in_1: Tensor[T], tensor_in_2: Tensor[T]) -> ArrayLike: return np.divide(tensor_in_1, tensor_in_2) - def log(self, tensor_in: Tensor[T]) -> Tensor[T]: + def log(self, tensor_in: Tensor[T]) -> ArrayLike: return np.log(tensor_in) - def exp(self, tensor_in: Tensor[T]) -> Tensor[T]: + def exp(self, tensor_in: Tensor[T]) -> ArrayLike: return np.exp(tensor_in) def percentile( @@ -298,7 +300,7 @@ def percentile( interpolation: Literal[ "linear", "lower", "higher", "midpoint", "nearest" ] = "linear", - ) -> Tensor[T]: + ) -> ArrayLike: r""" Compute the :math:`q`-th percentile of the tensor along the specified axis. @@ -335,17 +337,17 @@ def percentile( """ # see numpy/numpy#22125 - return np.percentile(tensor_in, q, axis=axis, interpolation=interpolation) # type: ignore[call-overload] + return np.percentile(tensor_in, q, axis=axis, interpolation=interpolation) # type: ignore[call-overload,no-any-return] - def stack(self, sequence: Sequence[Tensor[T]], axis: int = 0) -> Tensor[T]: + def stack(self, sequence: Sequence[Tensor[T]], axis: int = 0) -> ArrayLike: return np.stack(sequence, axis=axis) def where( self, mask: NDArray[np.bool_], tensor_in_1: Tensor[T], tensor_in_2: Tensor[T] - ) -> Tensor[T]: + ) -> ArrayLike: return np.where(mask, tensor_in_1, tensor_in_2) - def concatenate(self, sequence: Tensor[T], axis: None | int = 0) -> Tensor[T]: + def concatenate(self, sequence: Tensor[T], axis: None | int = 0) -> ArrayLike: """ Join a sequence of arrays along an existing axis. @@ -384,10 +386,10 @@ def simple_broadcast(self, *args: Sequence[Tensor[T]]) -> Sequence[Tensor[T]]: def shape(self, tensor: Tensor[T]) -> Shape: return tensor.shape - def reshape(self, tensor: Tensor[T], newshape: Shape) -> Tensor[T]: + def reshape(self, tensor: Tensor[T], newshape: Shape) -> ArrayLike: return np.reshape(tensor, newshape) - def ravel(self, tensor: Tensor[T]) -> Tensor[T]: + def ravel(self, tensor: Tensor[T]) -> ArrayLike: """ Return a flattened view of the tensor, not a copy. @@ -407,7 +409,7 @@ def ravel(self, tensor: Tensor[T]) -> Tensor[T]: """ return np.ravel(tensor) - def einsum(self, subscripts: str, *operands: Sequence[Tensor[T]]) -> Tensor[T]: + def einsum(self, subscripts: str, *operands: Sequence[Tensor[T]]) -> ArrayLike: """ Evaluates the Einstein summation convention on the operands. @@ -424,12 +426,12 @@ def einsum(self, subscripts: str, *operands: Sequence[Tensor[T]]) -> Tensor[T]: Returns: tensor: the calculation based on the Einstein summation convention """ - return np.einsum(subscripts, *operands) + return np.einsum(subscripts, *operands) # type: ignore[arg-type,no-any-return] - def poisson_logpdf(self, n: Tensor[T], lam: Tensor[T]) -> Tensor[T]: - return xlogy(n, lam) - lam - gammaln(n + 1.0) + def poisson_logpdf(self, n: Tensor[T], lam: Tensor[T]) -> ArrayLike: + return cast(ArrayLike, xlogy(n, lam) - lam - gammaln(n + 1.0)) - def poisson(self, n: Tensor[T], lam: Tensor[T]) -> Tensor[T]: + def poisson(self, n: Tensor[T], lam: Tensor[T]) -> ArrayLike: r""" The continuous approximation, using :math:`n! = \Gamma\left(n+1\right)`, to the probability mass function of the Poisson distribution evaluated @@ -469,11 +471,11 @@ def poisson(self, n: Tensor[T], lam: Tensor[T]) -> Tensor[T]: Returns: NumPy float: Value of the continuous approximation to Poisson(n|lam) """ - n = np.asarray(n) - lam = np.asarray(lam) - return np.exp(xlogy(n, lam) - lam - gammaln(n + 1.0)) + _n = np.asarray(n) + _lam = np.asarray(lam) + return np.exp(xlogy(_n, _lam) - _lam - gammaln(_n + 1.0)) # type: ignore[no-any-return,operator] - def normal_logpdf(self, x: Tensor[T], mu: Tensor[T], sigma: Tensor[T]) -> Tensor[T]: + def normal_logpdf(self, x: Tensor[T], mu: Tensor[T], sigma: Tensor[T]) -> ArrayLike: # this is much faster than # norm.logpdf(x, loc=mu, scale=sigma) # https://codereview.stackexchange.com/questions/69718/fastest-computation-of-n-likelihoods-on-normal-distributions @@ -481,12 +483,12 @@ def normal_logpdf(self, x: Tensor[T], mu: Tensor[T], sigma: Tensor[T]) -> Tensor root2pi = np.sqrt(2 * np.pi) prefactor = -np.log(sigma * root2pi) summand = -np.square(np.divide((x - mu), (root2 * sigma))) - return prefactor + summand + return prefactor + summand # type: ignore[no-any-return] # def normal_logpdf(self, x, mu, sigma): # return norm.logpdf(x, loc=mu, scale=sigma) - def normal(self, x: Tensor[T], mu: Tensor[T], sigma: Tensor[T]) -> Tensor[T]: + def normal(self, x: Tensor[T], mu: Tensor[T], sigma: Tensor[T]) -> ArrayLike: r""" The probability density function of the Normal distribution evaluated at :code:`x` given parameters of mean of :code:`mu` and standard deviation @@ -512,11 +514,11 @@ def normal(self, x: Tensor[T], mu: Tensor[T], sigma: Tensor[T]) -> Tensor[T]: Returns: NumPy float: Value of Normal(x|mu, sigma) """ - return norm.pdf(x, loc=mu, scale=sigma) + return norm.pdf(x, loc=mu, scale=sigma) # type: ignore[no-any-return] def normal_cdf( self, x: Tensor[T], mu: float | Tensor[T] = 0, sigma: float | Tensor[T] = 1 - ) -> Tensor[T]: + ) -> ArrayLike: """ The cumulative distribution function for the Normal distribution @@ -538,9 +540,9 @@ def normal_cdf( Returns: NumPy float: The CDF """ - return norm.cdf(x, loc=mu, scale=sigma) + return norm.cdf(x, loc=mu, scale=sigma) # type: ignore[no-any-return] - def poisson_dist(self, rate: Tensor[T]) -> _BasicPoisson[T]: + def poisson_dist(self, rate: Tensor[T]) -> PDF: r""" The Poisson distribution with rate parameter :code:`rate`. @@ -561,7 +563,7 @@ def poisson_dist(self, rate: Tensor[T]) -> _BasicPoisson[T]: """ return _BasicPoisson(rate) - def normal_dist(self, mu: Tensor[T], sigma: Tensor[T]) -> _BasicNormal[T]: + def normal_dist(self, mu: Tensor[T], sigma: Tensor[T]) -> PDF: r""" The Normal distribution with mean :code:`mu` and standard deviation :code:`sigma`. @@ -585,7 +587,7 @@ def normal_dist(self, mu: Tensor[T], sigma: Tensor[T]) -> _BasicNormal[T]: """ return _BasicNormal(mu, sigma) - def to_numpy(self, tensor_in: Tensor[T]) -> Tensor[T]: + def to_numpy(self, tensor_in: Tensor[T]) -> ArrayLike: """ Return the input tensor as it already is a :class:`numpy.ndarray`. This API exists only for ``pyhf.tensorlib`` compatibility. @@ -613,7 +615,7 @@ def to_numpy(self, tensor_in: Tensor[T]) -> Tensor[T]: """ return tensor_in - def transpose(self, tensor_in: Tensor[T]) -> Tensor[T]: + def transpose(self, tensor_in: Tensor[T]) -> ArrayLike: """ Transpose the tensor. diff --git a/src/pyhf/typing.py b/src/pyhf/typing.py index 50a6237199..9cd17f2265 100644 --- a/src/pyhf/typing.py +++ b/src/pyhf/typing.py @@ -1,11 +1,12 @@ import os import sys -from typing import MutableSequence, Sequence, Union +from typing import MutableSequence, Sequence, Union, Tuple, SupportsIndex, Any + if sys.version_info >= (3, 8): - from typing import Literal, TypedDict + from typing import Literal, TypedDict, Protocol else: - from typing_extensions import Literal, TypedDict + from typing_extensions import Literal, TypedDict, Protocol __all__ = ( "PathOrStr", @@ -27,9 +28,13 @@ "Workspace", ) + # TODO: Switch to os.PathLike[str] once Python 3.8 support dropped PathOrStr = Union[str, "os.PathLike[str]"] +Shape = Tuple[int, ...] +ShapeLike = Union[SupportsIndex, Sequence[SupportsIndex]] + class ParameterBase(TypedDict, total=False): auxdata: Sequence[float] @@ -132,3 +137,15 @@ class Workspace(TypedDict): measurements: Sequence[Measurement] channels: Sequence[Channel] observations: Sequence[Observation] + + +class TensorBackend(Protocol): + ... + + +class PDF(Protocol): + def sample(self, sample_shape: Shape) -> Any: + ... + + def log_prob(self, value: Any) -> Any: + ... From 8dea9fbf1d71cfd569fe0c29caab5289617149e1 Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Tue, 16 Aug 2022 15:43:03 -0700 Subject: [PATCH 06/21] pass --- src/pyhf/readxml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyhf/readxml.py b/src/pyhf/readxml.py index f46620cb90..212ff6a01e 100644 --- a/src/pyhf/readxml.py +++ b/src/pyhf/readxml.py @@ -438,7 +438,7 @@ def parse( mounts = mounts or [] toplvl = ET.parse(configfile) inputs = tqdm.tqdm( - [x.text for x in toplvl.findall('Input')], + [x.text for x in toplvl.findall('Input') if x.text], unit='channel', disable=not (track_progress), ) From 8ce8d5e95a143344fd98003b2ed6db2a3ac5d034 Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Tue, 16 Aug 2022 15:48:54 -0700 Subject: [PATCH 07/21] drop cast for now --- src/pyhf/tensor/numpy_backend.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pyhf/tensor/numpy_backend.py b/src/pyhf/tensor/numpy_backend.py index 6a6709f834..2473bdab18 100644 --- a/src/pyhf/tensor/numpy_backend.py +++ b/src/pyhf/tensor/numpy_backend.py @@ -7,7 +7,7 @@ from pyhf.typing import TensorBackend, Shape, PDF -from typing import TypeVar, Callable, Literal, Sequence, Generic, Mapping, cast +from typing import TypeVar, Callable, Literal, Sequence, Generic, Mapping from numpy.typing import ( NDArray, NBitBase, @@ -94,7 +94,7 @@ def clip( Returns: NumPy ndarray: A clipped `tensor` """ - return cast(Tensor[T], np.clip(tensor_in, min_value, max_value)) + return np.clip(tensor_in, min_value, max_value) def erf(self, tensor_in: Tensor[T]) -> ArrayLike: """ @@ -156,7 +156,7 @@ def tile(self, tensor_in: Tensor[T], repeats: int | Sequence[int]) -> ArrayLike: Returns: NumPy ndarray: The tensor with repeated axes """ - return cast(Tensor[T], np.tile(tensor_in, repeats)) + return np.tile(tensor_in, repeats) def conditional( self, @@ -189,7 +189,7 @@ def conditional( def tolist(self, tensor_in: Tensor[T] | list[T]) -> list[T]: try: - return cast(list[T], tensor_in.tolist()) # type: ignore[union-attr] + return tensor_in.tolist() # type: ignore[union-attr,no-any-return] except AttributeError: if isinstance(tensor_in, list): return tensor_in @@ -429,7 +429,7 @@ def einsum(self, subscripts: str, *operands: Sequence[Tensor[T]]) -> ArrayLike: return np.einsum(subscripts, *operands) # type: ignore[arg-type,no-any-return] def poisson_logpdf(self, n: Tensor[T], lam: Tensor[T]) -> ArrayLike: - return cast(ArrayLike, xlogy(n, lam) - lam - gammaln(n + 1.0)) + return xlogy(n, lam) - lam - gammaln(n + 1.0) # type: ignore[no-any-return] def poisson(self, n: Tensor[T], lam: Tensor[T]) -> ArrayLike: r""" From 314441cfcbd7f2fef774a9caecd8d790dabeff9f Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Tue, 16 Aug 2022 16:12:44 -0700 Subject: [PATCH 08/21] fix up --- src/pyhf/tensor/numpy_backend.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pyhf/tensor/numpy_backend.py b/src/pyhf/tensor/numpy_backend.py index 2473bdab18..4529c5a7e6 100644 --- a/src/pyhf/tensor/numpy_backend.py +++ b/src/pyhf/tensor/numpy_backend.py @@ -5,7 +5,7 @@ from scipy import special from scipy.stats import norm, poisson -from pyhf.typing import TensorBackend, Shape, PDF +from pyhf.typing import TensorBackend, Shape from typing import TypeVar, Callable, Literal, Sequence, Generic, Mapping from numpy.typing import ( @@ -21,7 +21,7 @@ log = logging.getLogger(__name__) -class _BasicPoisson(PDF): +class _BasicPoisson: def __init__(self, rate: Tensor[T]): self.rate = rate @@ -33,7 +33,7 @@ def log_prob(self, value: NDArray[np.number[T]]) -> ArrayLike: return tensorlib.poisson_logpdf(value, self.rate) -class _BasicNormal(PDF): +class _BasicNormal: def __init__(self, loc: Tensor[T], scale: Tensor[T]): self.loc = loc self.scale = scale @@ -542,7 +542,7 @@ def normal_cdf( """ return norm.cdf(x, loc=mu, scale=sigma) # type: ignore[no-any-return] - def poisson_dist(self, rate: Tensor[T]) -> PDF: + def poisson_dist(self, rate: Tensor[T]) -> _BasicPoisson: r""" The Poisson distribution with rate parameter :code:`rate`. @@ -563,7 +563,7 @@ def poisson_dist(self, rate: Tensor[T]) -> PDF: """ return _BasicPoisson(rate) - def normal_dist(self, mu: Tensor[T], sigma: Tensor[T]) -> PDF: + def normal_dist(self, mu: Tensor[T], sigma: Tensor[T]) -> _BasicNormal: r""" The Normal distribution with mean :code:`mu` and standard deviation :code:`sigma`. From d7934edc1acf12789152aba2b7d71dfa950f612f Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Tue, 16 Aug 2022 16:24:43 -0700 Subject: [PATCH 09/21] more --- src/pyhf/tensor/numpy_backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyhf/tensor/numpy_backend.py b/src/pyhf/tensor/numpy_backend.py index 4529c5a7e6..3b7dec18a9 100644 --- a/src/pyhf/tensor/numpy_backend.py +++ b/src/pyhf/tensor/numpy_backend.py @@ -7,7 +7,7 @@ from pyhf.typing import TensorBackend, Shape -from typing import TypeVar, Callable, Literal, Sequence, Generic, Mapping +from typing import TypeVar, Callable, Literal, Sequence, Generic, Mapping, Union from numpy.typing import ( NDArray, NBitBase, @@ -16,7 +16,7 @@ ) T = TypeVar("T", bound=NBitBase) -Tensor = NDArray[np.number[T]] | NDArray[np.bool_] +Tensor = Union[NDArray[np.number[T]], NDArray[np.bool_]] log = logging.getLogger(__name__) From 536d2cd35878f4aaf431ffbf3b9ea0efb620cc7c Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Tue, 23 Aug 2022 11:50:28 -0700 Subject: [PATCH 10/21] drop TensorBackend as its protocol --- src/pyhf/tensor/numpy_backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyhf/tensor/numpy_backend.py b/src/pyhf/tensor/numpy_backend.py index 3b7dec18a9..c0fa805392 100644 --- a/src/pyhf/tensor/numpy_backend.py +++ b/src/pyhf/tensor/numpy_backend.py @@ -5,7 +5,7 @@ from scipy import special from scipy.stats import norm, poisson -from pyhf.typing import TensorBackend, Shape +from pyhf.typing import Shape from typing import TypeVar, Callable, Literal, Sequence, Generic, Mapping, Union from numpy.typing import ( @@ -46,7 +46,7 @@ def log_prob(self, value: NDArray[np.number[T]]) -> ArrayLike: return tensorlib.normal_logpdf(value, self.loc, self.scale) -class numpy_backend(Generic[T], TensorBackend): +class numpy_backend(Generic[T]): """NumPy backend for pyhf""" __slots__ = ['name', 'precision', 'dtypemap', 'default_do_grad'] From 41dc1a8fdf3ba32d05534c6c25a913fa784b569f Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Tue, 23 Aug 2022 12:07:26 -0700 Subject: [PATCH 11/21] fix up --- src/pyhf/tensor/numpy_backend.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/pyhf/tensor/numpy_backend.py b/src/pyhf/tensor/numpy_backend.py index c0fa805392..7bf0bb2e0e 100644 --- a/src/pyhf/tensor/numpy_backend.py +++ b/src/pyhf/tensor/numpy_backend.py @@ -1,4 +1,5 @@ """NumPy Tensor Library Module.""" +from __future__ import annotations import numpy as np import logging from scipy.special import gammaln, xlogy @@ -7,6 +8,7 @@ from pyhf.typing import Shape +import sys from typing import TypeVar, Callable, Literal, Sequence, Generic, Mapping, Union from numpy.typing import ( NDArray, @@ -16,7 +18,11 @@ ) T = TypeVar("T", bound=NBitBase) -Tensor = Union[NDArray[np.number[T]], NDArray[np.bool_]] + +if sys.version_info >= (3, 9): + Tensor = Union[NDArray[np.number[T]], NDArray[np.bool_]] +else: + Tensor = NDArray log = logging.getLogger(__name__) @@ -208,7 +214,7 @@ def isfinite(self, tensor: Tensor[T]) -> NDArray[np.bool_]: return np.isfinite(tensor) def astensor( - self, tensor_in: ArrayLike, dtype_str: Literal['float'] = 'float' + self, tensor_in: ArrayLike, dtype: Literal['float'] = 'float' ) -> ArrayLike: """ Convert to a NumPy array. @@ -231,14 +237,14 @@ def astensor( `numpy.ndarray`: A multi-dimensional, fixed-size homogeneous array. """ try: - dtype = self.dtypemap[dtype_str] + dtype_obj = self.dtypemap[dtype] except KeyError: log.error( 'Invalid dtype: dtype must be float, int, or bool.', exc_info=True ) raise - return np.asarray(tensor_in, dtype=dtype) + return np.asarray(tensor_in, dtype=dtype_obj) def sum(self, tensor_in: Tensor[T], axis: int | None = None) -> ArrayLike: return np.sum(tensor_in, axis=axis) @@ -250,10 +256,10 @@ def abs(self, tensor: Tensor[T]) -> ArrayLike: return np.abs(tensor) def ones( - self, shape: Shape, dtype_str: Literal["float", "int", "bool"] = "float" + self, shape: Shape, dtype: Literal["float", "int", "bool"] = "float" ) -> ArrayLike: try: - dtype = self.dtypemap[dtype_str] + dtype_obj = self.dtypemap[dtype] except KeyError: log.error( f"Invalid dtype: dtype must be one of {list(self.dtypemap.keys())}.", @@ -261,13 +267,13 @@ def ones( ) raise - return np.ones(shape, dtype=dtype) + return np.ones(shape, dtype=dtype_obj) def zeros( - self, shape: Shape, dtype_str: Literal["float", "int", "bool"] = "float" + self, shape: Shape, dtype: Literal["float", "int", "bool"] = "float" ) -> ArrayLike: try: - dtype = self.dtypemap[dtype_str] + dtype_obj = self.dtypemap[dtype] except KeyError: log.error( f"Invalid dtype: dtype must be one of {list(self.dtypemap.keys())}.", @@ -275,7 +281,7 @@ def zeros( ) raise - return np.zeros(shape, dtype=dtype) + return np.zeros(shape, dtype=dtype_obj) def power(self, tensor_in_1: Tensor[T], tensor_in_2: Tensor[T]) -> ArrayLike: return np.power(tensor_in_1, tensor_in_2) From 50fcc4a58503e3fcf7109df18ed6489e91cc6c85 Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Tue, 23 Aug 2022 12:11:16 -0700 Subject: [PATCH 12/21] sigh --- src/pyhf/typing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyhf/typing.py b/src/pyhf/typing.py index 9cd17f2265..bf9c7f134c 100644 --- a/src/pyhf/typing.py +++ b/src/pyhf/typing.py @@ -1,12 +1,12 @@ import os import sys -from typing import MutableSequence, Sequence, Union, Tuple, SupportsIndex, Any +from typing import MutableSequence, Sequence, Union, Tuple, Any if sys.version_info >= (3, 8): - from typing import Literal, TypedDict, Protocol + from typing import Literal, TypedDict, Protocol, SupportsIndex else: - from typing_extensions import Literal, TypedDict, Protocol + from typing_extensions import Literal, TypedDict, Protocol, SupportsIndex __all__ = ( "PathOrStr", From d1a84d78a03d0b2b0bc52b7b01183ce399e31bab Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Tue, 23 Aug 2022 12:20:02 -0700 Subject: [PATCH 13/21] run pre-commit with 3.7 and 3.10 --- .pre-commit-config.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7a216a36ed..f995831229 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -62,11 +62,16 @@ repos: - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.971 hooks: - - id: mypy + - &mypy + id: mypy + name: mypy with Python 3.7 files: src - args: [] additional_dependencies: ['numpy', 'types-tqdm', 'click', 'types-jsonpatch', 'types-pyyaml', 'types-jsonschema', 'importlib_metadata', 'packaging'] + args: ["--python-version=3.7"] + - <<: *mypy + name: mypy with Python 3.10 + args: ["--python-version=3.10"] - repo: https://github.com/nbQA-dev/nbQA rev: 1.4.0 From 7d4691b5068ccf3274547ae8e4bc6e1205113162 Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Tue, 23 Aug 2022 12:24:49 -0700 Subject: [PATCH 14/21] get it working in 3.7 --- src/pyhf/tensor/numpy_backend.py | 9 ++++----- src/pyhf/typing.py | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pyhf/tensor/numpy_backend.py b/src/pyhf/tensor/numpy_backend.py index 7bf0bb2e0e..795e50b3f3 100644 --- a/src/pyhf/tensor/numpy_backend.py +++ b/src/pyhf/tensor/numpy_backend.py @@ -6,10 +6,9 @@ from scipy import special from scipy.stats import norm, poisson -from pyhf.typing import Shape +from pyhf.typing import Shape, Literal -import sys -from typing import TypeVar, Callable, Literal, Sequence, Generic, Mapping, Union +from typing import TypeVar, Callable, Sequence, Generic, Mapping, Union, TYPE_CHECKING from numpy.typing import ( NDArray, NBitBase, @@ -19,10 +18,10 @@ T = TypeVar("T", bound=NBitBase) -if sys.version_info >= (3, 9): +if TYPE_CHECKING: Tensor = Union[NDArray[np.number[T]], NDArray[np.bool_]] else: - Tensor = NDArray + Tensor = Generic log = logging.getLogger(__name__) diff --git a/src/pyhf/typing.py b/src/pyhf/typing.py index bf9c7f134c..fc1934bb40 100644 --- a/src/pyhf/typing.py +++ b/src/pyhf/typing.py @@ -26,6 +26,7 @@ "Channel", "Observation", "Workspace", + "Literal", ) From 38b7893c10d93a18804505b47dcda718ab2dde8f Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Fri, 12 Aug 2022 02:30:53 -0400 Subject: [PATCH 15/21] test: Ignore TYPE_CHECKING for coverage (#1937) * Add .coveragerc file with configuration to ignore TYPE_CHECKING. - ref: https://coverage.readthedocs.io/en/6.4.3/excluding.html#advanced-exclusion --- .coveragerc | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000..0856c037f2 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[report] +exclude_lines = + if TYPE_CHECKING: From c936365fe385ec6dc1ba49830e2445da7838262e Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Tue, 23 Aug 2022 12:41:05 -0700 Subject: [PATCH 16/21] skip the typing.py file --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.coveragerc b/.coveragerc index 0856c037f2..6662eda9e1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,5 @@ [report] exclude_lines = if TYPE_CHECKING: +omit = + src/pyhf/typing.py From 0916c4f65e4d1296fd3f0fcf4b018b1de60f5bae Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Tue, 23 Aug 2022 13:04:00 -0700 Subject: [PATCH 17/21] tell codecov to ignore typing.py --- codecov.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/codecov.yml b/codecov.yml index 914f860929..61da747a9d 100644 --- a/codecov.yml +++ b/codecov.yml @@ -3,3 +3,6 @@ coverage: project: default: threshold: 0.2% + +ignore: + - "**/typing.py" From b222f857941d045976b43a8eb34eff3b68ae46b1 Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Fri, 26 Aug 2022 02:10:03 -0500 Subject: [PATCH 18/21] Add comment that the Pythons type checked are extrema --- .pre-commit-config.yaml | 1 + pyproject.toml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f995831229..67e9854ef3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,6 +61,7 @@ repos: - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.971 + # check the oldest and newest supported Pythons hooks: - &mypy id: mypy diff --git a/pyproject.toml b/pyproject.toml index af3d331ad0..2708c44b7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -118,7 +118,6 @@ module = [ ] ignore_missing_imports = true - [[tool.mypy.overrides]] module = [ 'pyhf', From 41cd3afd06ede919165385a8687e9eefa513e262 Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Fri, 26 Aug 2022 07:12:20 -0400 Subject: [PATCH 19/21] Apply suggestions from code review Co-authored-by: Matthew Feickert --- src/pyhf/tensor/numpy_backend.py | 18 +++++++----------- src/pyhf/typing.py | 7 +++---- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/pyhf/tensor/numpy_backend.py b/src/pyhf/tensor/numpy_backend.py index 795e50b3f3..9ad28dc879 100644 --- a/src/pyhf/tensor/numpy_backend.py +++ b/src/pyhf/tensor/numpy_backend.py @@ -1,20 +1,16 @@ """NumPy Tensor Library Module.""" from __future__ import annotations -import numpy as np + import logging -from scipy.special import gammaln, xlogy +from typing import TYPE_CHECKING, Callable, Generic, Mapping, Sequence, TypeVar, Union + +import numpy as np +from numpy.typing import ArrayLike, DTypeLike, NBitBase, NDArray from scipy import special +from scipy.special import gammaln, xlogy from scipy.stats import norm, poisson -from pyhf.typing import Shape, Literal - -from typing import TypeVar, Callable, Sequence, Generic, Mapping, Union, TYPE_CHECKING -from numpy.typing import ( - NDArray, - NBitBase, - ArrayLike, - DTypeLike, -) +from pyhf.typing import Literal, Shape T = TypeVar("T", bound=NBitBase) diff --git a/src/pyhf/typing.py b/src/pyhf/typing.py index fc1934bb40..ffa10d8d58 100644 --- a/src/pyhf/typing.py +++ b/src/pyhf/typing.py @@ -1,12 +1,11 @@ import os import sys -from typing import MutableSequence, Sequence, Union, Tuple, Any - +from typing import Any, MutableSequence, Sequence, Tuple, Union if sys.version_info >= (3, 8): - from typing import Literal, TypedDict, Protocol, SupportsIndex + from typing import Literal, Protocol, SupportsIndex, TypedDict else: - from typing_extensions import Literal, TypedDict, Protocol, SupportsIndex + from typing_extensions import Literal, Protocol, SupportsIndex, TypedDict __all__ = ( "PathOrStr", From 50985c9491f22e6cc6981c617b63f518d3598fd2 Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Fri, 26 Aug 2022 10:11:33 -0500 Subject: [PATCH 20/21] use full url --- src/pyhf/tensor/numpy_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyhf/tensor/numpy_backend.py b/src/pyhf/tensor/numpy_backend.py index 9ad28dc879..c54b1e411d 100644 --- a/src/pyhf/tensor/numpy_backend.py +++ b/src/pyhf/tensor/numpy_backend.py @@ -337,7 +337,7 @@ def percentile( NumPy ndarray: The value of the :math:`q`-th percentile of the tensor along the specified axis. """ - # see numpy/numpy#22125 + # see https://github.com/numpy/numpy/issues/22125 return np.percentile(tensor_in, q, axis=axis, interpolation=interpolation) # type: ignore[call-overload,no-any-return] def stack(self, sequence: Sequence[Tensor[T]], axis: int = 0) -> ArrayLike: From 1f66041ba30b8b3ee714156d225e29341fc71f32 Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Fri, 26 Aug 2022 08:31:47 -0700 Subject: [PATCH 21/21] drop TYPE_CHECKING --- .coveragerc | 2 -- src/pyhf/tensor/numpy_backend.py | 7 ++----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.coveragerc b/.coveragerc index 6662eda9e1..4b634a8785 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,5 +1,3 @@ [report] -exclude_lines = - if TYPE_CHECKING: omit = src/pyhf/typing.py diff --git a/src/pyhf/tensor/numpy_backend.py b/src/pyhf/tensor/numpy_backend.py index c54b1e411d..1e623d0147 100644 --- a/src/pyhf/tensor/numpy_backend.py +++ b/src/pyhf/tensor/numpy_backend.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Callable, Generic, Mapping, Sequence, TypeVar, Union +from typing import Callable, Generic, Mapping, Sequence, TypeVar, Union import numpy as np from numpy.typing import ArrayLike, DTypeLike, NBitBase, NDArray @@ -14,10 +14,7 @@ T = TypeVar("T", bound=NBitBase) -if TYPE_CHECKING: - Tensor = Union[NDArray[np.number[T]], NDArray[np.bool_]] -else: - Tensor = Generic +Tensor = Union["NDArray[np.number[T]]", "NDArray[np.bool_]"] log = logging.getLogger(__name__)