diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 9dd25a3..e98ceab 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -2,3 +2,5 @@ b42067f6776dcd763827000d585a88e071b78af3 # applied prettier - https://github.com/glass-dev/glass/pull/227 f9bac62f497a7288aa71fd4dbd948945c37f854e +# applied ruff-linting - https://github.com/glass-dev/glass/pull/232 +e58d8616c03b8447aba90996905a98b42f18ba0a diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3180103..5d3d9f0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,6 +27,8 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.6.3 hooks: + - id: ruff + args: ["--fix", "--show-fixes"] - id: ruff-format - repo: https://github.com/pappasam/toml-sort rev: v0.23.1 diff --git a/docs/conf.py b/docs/conf.py index 498c744..4392b0b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,7 +12,7 @@ project = "GLASS" author = "GLASS developers" -copyright = f"2022-{datetime.date.today().year} {author}" +copyright = f"2022-{datetime.date.today().year} {author}" # noqa: A001, DTZ011 version = metadata.version("glass") release = version diff --git a/examples/1-basic/density.ipynb b/examples/1-basic/density.ipynb index e489d1f..f1e778c 100644 --- a/examples/1-basic/density.ipynb +++ b/examples/1-basic/density.ipynb @@ -34,8 +34,8 @@ }, "outputs": [], "source": [ - "import numpy as np\n", "import matplotlib.pyplot as plt\n", + "import numpy as np\n", "from matplotlib.colors import LogNorm\n", "\n", "# use the CAMB cosmology that generated the matter power spectra\n", @@ -45,7 +45,6 @@ "# import GLASS for matter, random fields, random points, galaxies\n", "import glass\n", "\n", - "\n", "# cosmology for the simulation\n", "h = 0.7\n", "Oc = 0.25\n", @@ -56,7 +55,10 @@ "\n", "# set up CAMB parameters for matter angular power spectrum\n", "pars = camb.set_params(\n", - " H0=100 * h, omch2=Oc * h**2, ombh2=Ob * h**2, NonLinear=camb.model.NonLinear_both\n", + " H0=100 * h,\n", + " omch2=Oc * h**2,\n", + " ombh2=Ob * h**2,\n", + " NonLinear=camb.model.NonLinear_both,\n", ")\n", "\n", "# get the cosmology from CAMB\n", @@ -169,8 +171,10 @@ " z1 = gal_z * np.cos(np.deg2rad(gal_lon)) * np.cos(np.deg2rad(gal_lat))\n", " z2 = gal_z * np.sin(np.deg2rad(gal_lon)) * np.cos(np.deg2rad(gal_lat))\n", " z3 = gal_z * np.sin(np.deg2rad(gal_lat))\n", - " (i, j, k), c = np.unique(\n", - " np.searchsorted(zcub[1:], [z1, z2, z3]), axis=1, return_counts=True\n", + " (i, j, k), c = np.unique( # noqa: PLW2901\n", + " np.searchsorted(zcub[1:], [z1, z2, z3]),\n", + " axis=1,\n", + " return_counts=True,\n", " )\n", " cube[i, j, k] += c" ] diff --git a/examples/1-basic/lensing.ipynb b/examples/1-basic/lensing.ipynb index 15d2b56..e32ff2a 100644 --- a/examples/1-basic/lensing.ipynb +++ b/examples/1-basic/lensing.ipynb @@ -38,9 +38,9 @@ }, "outputs": [], "source": [ - "import numpy as np\n", "import healpy as hp\n", "import matplotlib.pyplot as plt\n", + "import numpy as np\n", "\n", "# use the CAMB cosmology that generated the matter power spectra\n", "import camb\n", @@ -49,7 +49,6 @@ "# import GLASS\n", "import glass\n", "\n", - "\n", "# cosmology for the simulation\n", "h = 0.7\n", "Oc = 0.25\n", @@ -60,7 +59,10 @@ "\n", "# set up CAMB parameters for matter angular power spectrum\n", "pars = camb.set_params(\n", - " H0=100 * h, omch2=Oc * h**2, ombh2=Ob * h**2, NonLinear=camb.model.NonLinear_both\n", + " H0=100 * h,\n", + " omch2=Oc * h**2,\n", + " ombh2=Ob * h**2,\n", + " NonLinear=camb.model.NonLinear_both,\n", ")\n", "\n", "# get the cosmology from CAMB\n", @@ -253,14 +255,17 @@ "source": [ "# get the angular power spectra of the lensing maps\n", "sim_cls = hp.anafast(\n", - " [kappa_bar, gamm1_bar, gamm2_bar], pol=True, lmax=lmax, use_pixel_weights=True\n", + " [kappa_bar, gamm1_bar, gamm2_bar],\n", + " pol=True,\n", + " lmax=lmax,\n", + " use_pixel_weights=True,\n", ")\n", "\n", "# get the expected cls from CAMB\n", "pars.min_l = 1\n", "pars.set_for_lmax(lmax)\n", "pars.SourceWindows = [\n", - " camb.sources.SplinedSourceWindow(z=z, W=dndz, source_type=\"lensing\")\n", + " camb.sources.SplinedSourceWindow(z=z, W=dndz, source_type=\"lensing\"),\n", "]\n", "theory_cls = camb.get_results(pars).get_source_cls_dict(lmax=lmax, raw_cl=True)\n", "\n", @@ -268,7 +273,7 @@ "pw = hp.pixwin(nside, lmax=lmax)\n", "\n", "# plot the realised and expected cls\n", - "l = np.arange(lmax + 1)\n", + "l = np.arange(lmax + 1) # noqa: E741\n", "plt.plot(l, sim_cls[0], \"-k\", lw=2, label=\"simulation\")\n", "plt.plot(l, theory_cls[\"W1xW1\"] * pw**2, \"-r\", lw=2, label=\"expectation\")\n", "plt.xscale(\"symlog\", linthresh=10, linscale=0.5, subs=[2, 3, 4, 5, 6, 7, 8, 9])\n", diff --git a/examples/1-basic/matter.ipynb b/examples/1-basic/matter.ipynb index 65c0372..45585bc 100644 --- a/examples/1-basic/matter.ipynb +++ b/examples/1-basic/matter.ipynb @@ -34,9 +34,9 @@ }, "outputs": [], "source": [ - "import numpy as np\n", "import healpy as hp\n", "import matplotlib.pyplot as plt\n", + "import numpy as np\n", "\n", "# uses the CAMB cosmology which produced the cls\n", "import camb\n", @@ -45,7 +45,6 @@ "# import GLASS for matter and random fields\n", "import glass\n", "\n", - "\n", "# cosmology for the simulation\n", "h = 0.7\n", "Oc = 0.25\n", @@ -57,7 +56,10 @@ "\n", "# set up CAMB parameters for matter angular power spectrum\n", "pars = camb.set_params(\n", - " H0=100 * h, omch2=Oc * h**2, ombh2=Ob * h**2, NonLinear=camb.model.NonLinear_both\n", + " H0=100 * h,\n", + " omch2=Oc * h**2,\n", + " ombh2=Ob * h**2,\n", + " NonLinear=camb.model.NonLinear_both,\n", ")\n", "\n", "# get the cosmology from CAMB\n", @@ -135,7 +137,7 @@ " theta, phi = hp.vec2ang(np.transpose([x[g] / zmax, y[g] / zmax, zg]))\n", " grid[g] = hp.get_interp_val(delta_i, theta, phi)\n", " ax.add_patch(\n", - " plt.Circle((0, 0), zmax / zend, fc=\"none\", ec=\"k\", lw=0.5, alpha=0.5, zorder=1)\n", + " plt.Circle((0, 0), zmax / zend, fc=\"none\", ec=\"k\", lw=0.5, alpha=0.5, zorder=1),\n", " )\n", "\n", "# show the grid of shells\n", diff --git a/examples/1-basic/photoz.ipynb b/examples/1-basic/photoz.ipynb index 1fe5d6f..9e500cb 100644 --- a/examples/1-basic/photoz.ipynb +++ b/examples/1-basic/photoz.ipynb @@ -35,8 +35,8 @@ }, "outputs": [], "source": [ - "import numpy as np\n", "import matplotlib.pyplot as plt\n", + "import numpy as np\n", "\n", "# import GLASS for matter shells, galaxies, random points, and observational\n", "import glass\n", @@ -44,7 +44,6 @@ "# how many arcmin2 over the entire sphere\n", "from glass.core.constants import ARCMIN2_SPHERE\n", "\n", - "\n", "# galaxy density\n", "n_arcmin2 = 1e-4\n", "\n", diff --git a/examples/1-basic/shells.ipynb b/examples/1-basic/shells.ipynb index a9bd11b..f284077 100644 --- a/examples/1-basic/shells.ipynb +++ b/examples/1-basic/shells.ipynb @@ -38,15 +38,15 @@ }, "outputs": [], "source": [ - "import numpy as np\n", "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", "import camb\n", "from cosmology import Cosmology\n", "\n", "import glass\n", "import glass.ext.camb\n", "\n", - "\n", "# cosmology for the simulation\n", "h = 0.7\n", "Oc = 0.25\n", @@ -57,7 +57,10 @@ "\n", "# set up CAMB parameters for matter angular power spectrum\n", "pars = camb.set_params(\n", - " H0=100 * h, omch2=Oc * h**2, ombh2=Ob * h**2, NonLinear=camb.model.NonLinear_both\n", + " H0=100 * h,\n", + " omch2=Oc * h**2,\n", + " ombh2=Ob * h**2,\n", + " NonLinear=camb.model.NonLinear_both,\n", ")\n", "\n", "# get the cosmology from CAMB\n", diff --git a/examples/2-advanced/cosmic_shear.ipynb b/examples/2-advanced/cosmic_shear.ipynb index acce207..146ddee 100644 --- a/examples/2-advanced/cosmic_shear.ipynb +++ b/examples/2-advanced/cosmic_shear.ipynb @@ -33,18 +33,18 @@ }, "outputs": [], "source": [ - "import numpy as np\n", "import healpy as hp\n", "import matplotlib.pyplot as plt\n", + "import numpy as np\n", "\n", "# use the CAMB cosmology that generated the matter power spectra\n", "import camb\n", "from cosmology import Cosmology\n", "\n", + "# GLASS modules: cosmology and everything in the glass namespace\n", "import glass\n", "from glass.core.constants import ARCMIN2_SPHERE\n", "\n", - "\n", "# cosmology for the simulation\n", "h = 0.7\n", "Oc = 0.25\n", @@ -55,7 +55,10 @@ "\n", "# set up CAMB parameters for matter angular power spectrum\n", "pars = camb.set_params(\n", - " H0=100 * h, omch2=Oc * h**2, ombh2=Ob * h**2, NonLinear=camb.model.NonLinear_both\n", + " H0=100 * h,\n", + " omch2=Oc * h**2,\n", + " ombh2=Ob * h**2,\n", + " NonLinear=camb.model.NonLinear_both,\n", ")\n", "\n", "# get the cosmology from CAMB\n", @@ -281,12 +284,12 @@ "pars.min_l = 1\n", "pars.set_for_lmax(lmax)\n", "pars.SourceWindows = [\n", - " camb.sources.SplinedSourceWindow(z=z, W=dndz, source_type=\"lensing\")\n", + " camb.sources.SplinedSourceWindow(z=z, W=dndz, source_type=\"lensing\"),\n", "]\n", "theory_cls = camb.get_results(pars).get_source_cls_dict(lmax=lmax, raw_cl=True)\n", "\n", "# factor transforming convergence to shear\n", - "l = np.arange(lmax + 1)\n", + "l = np.arange(lmax + 1) # noqa: E741\n", "fl = (l + 2) * (l + 1) * l * (l - 1) / np.clip(l**2 * (l + 1) ** 2, 1, None)\n", "\n", "# the noise level from discrete observations with shape noise\n", diff --git a/examples/2-advanced/stage_4_galaxies.ipynb b/examples/2-advanced/stage_4_galaxies.ipynb index e95a356..7f2269b 100644 --- a/examples/2-advanced/stage_4_galaxies.ipynb +++ b/examples/2-advanced/stage_4_galaxies.ipynb @@ -36,18 +36,18 @@ }, "outputs": [], "source": [ - "import numpy as np\n", "import healpy as hp\n", "import matplotlib.pyplot as plt\n", + "import numpy as np\n", "\n", "# use the CAMB cosmology that generated the matter power spectra\n", "import camb\n", "from cosmology import Cosmology\n", "\n", + "# GLASS modules: cosmology and everything in the glass namespace\n", "import glass\n", "import glass.ext.camb\n", "\n", - "\n", "# cosmology for the simulation\n", "h = 0.7\n", "Oc = 0.25\n", @@ -58,7 +58,10 @@ "\n", "# set up CAMB parameters for matter angular power spectrum\n", "pars = camb.set_params(\n", - " H0=100 * h, omch2=Oc * h**2, ombh2=Ob * h**2, NonLinear=camb.model.NonLinear_both\n", + " H0=100 * h,\n", + " omch2=Oc * h**2,\n", + " ombh2=Ob * h**2,\n", + " NonLinear=camb.model.NonLinear_both,\n", ")\n", "\n", "# get the cosmology from CAMB\n", diff --git a/glass/__init__.py b/glass/__init__.py index 4f06d29..5fdd889 100644 --- a/glass/__init__.py +++ b/glass/__init__.py @@ -1,70 +1,71 @@ +"""GLASS package.""" + +import contextlib from importlib.metadata import PackageNotFoundError -try: +with contextlib.suppress(PackageNotFoundError): from ._version import __version__, __version_tuple__ -except PackageNotFoundError: - pass from glass.fields import ( - iternorm, cls2cov, - multalm, - transform_cls, + effective_cls, gaussian_gls, - lognormal_gls, generate_gaussian, generate_lognormal, getcl, - effective_cls, + iternorm, + lognormal_gls, + multalm, + transform_cls, ) from glass.galaxies import ( - redshifts, - redshifts_from_nz, galaxy_shear, gaussian_phz, + redshifts, + redshifts_from_nz, ) from glass.lensing import ( - from_convergence, - shear_from_convergence, MultiPlaneConvergence, + deflect, + from_convergence, multi_plane_matrix, multi_plane_weights, - deflect, + shear_from_convergence, ) from glass.observations import ( - vmap_galactic_ecliptic, + equal_dens_zbins, + fixed_zbins, gaussian_nz, smail_nz, - fixed_zbins, - equal_dens_zbins, tomo_nz_gausserr, + vmap_galactic_ecliptic, ) from glass.points import ( effective_bias, linear_bias, loglinear_bias, + position_weights, positions_from_delta, uniform_positions, - position_weights, ) from glass.shapes import ( - triaxial_axis_ratio, - ellipticity_ryden04, ellipticity_gaussian, ellipticity_intnorm, + ellipticity_ryden04, + triaxial_axis_ratio, ) from glass.shells import ( - distance_weight, - volume_weight, + RadialWindow, + combine, + cubic_windows, density_weight, - tophat_windows, + distance_grid, + distance_weight, linear_windows, - cubic_windows, - restrict, partition, redshift_grid, - distance_grid, - combine, - RadialWindow, + restrict, + tophat_windows, + volume_weight, ) -from glass.user import save_cls, load_cls, write_catalog +from glass.user import load_cls, save_cls, write_catalog diff --git a/glass/core/__init__.py b/glass/core/__init__.py index f80f402..0e4bf9d 100644 --- a/glass/core/__init__.py +++ b/glass/core/__init__.py @@ -7,4 +7,4 @@ The :mod:`glass.core` module contains core functionality for developing GLASS modules. -""" +""" # noqa: D205, D400, D415 diff --git a/glass/core/algorithm.py b/glass/core/algorithm.py index 62117d7..88c310a 100644 --- a/glass/core/algorithm.py +++ b/glass/core/algorithm.py @@ -1,9 +1,13 @@ -"""core module for algorithms""" +"""Core module for algorithms.""" from __future__ import annotations +from typing import TYPE_CHECKING + import numpy as np -from numpy.typing import ArrayLike + +if TYPE_CHECKING: + from numpy.typing import ArrayLike def nnls( @@ -13,7 +17,8 @@ def nnls( tol: float = 0.0, maxiter: int | None = None, ) -> ArrayLike: - """Compute a non-negative least squares solution. + """ + Compute a non-negative least squares solution. Implementation of the algorithm due to [1]_ as described in [2]_. @@ -26,16 +31,18 @@ def nnls( Chemometrics, 11, 393-401. """ - a = np.asanyarray(a) b = np.asanyarray(b) if a.ndim != 2: - raise ValueError("input `a` is not a matrix") + msg = "input `a` is not a matrix" + raise ValueError(msg) if b.ndim != 1: - raise ValueError("input `b` is not a vector") + msg = "input `b` is not a vector" + raise ValueError(msg) if a.shape[0] != b.shape[0]: - raise ValueError("the shapes of `a` and `b` do not match") + msg = "the shapes of `a` and `b` do not match" + raise ValueError(msg) _, n = a.shape @@ -43,9 +50,9 @@ def nnls( maxiter = 3 * n index = np.arange(n) - p = np.full(n, False) + p = np.full(n, fill_value=False) x = np.zeros(n) - for i in range(maxiter): + for _ in range(maxiter): if np.all(p): break w = np.dot(b - a @ x, a) diff --git a/glass/core/array.py b/glass/core/array.py index 1ea2793..ff19ba2 100644 --- a/glass/core/array.py +++ b/glass/core/array.py @@ -1,25 +1,26 @@ -"""module for array utilities""" +"""Module for array utilities.""" -import numpy as np from functools import partial +import numpy as np + def broadcast_first(*arrays): """Broadcast arrays, treating the first axis as common.""" arrays = tuple(np.moveaxis(a, 0, -1) if np.ndim(a) else a for a in arrays) arrays = np.broadcast_arrays(*arrays) - arrays = tuple(np.moveaxis(a, -1, 0) if np.ndim(a) else a for a in arrays) - return arrays + return tuple(np.moveaxis(a, -1, 0) if np.ndim(a) else a for a in arrays) def broadcast_leading_axes(*args): - """Broadcast all but the last N axes. + """ + Broadcast all but the last N axes. Returns the shape of the broadcast dimensions, and all input arrays with leading axes matching that shape. - Example - ------- + Examples + -------- Broadcast all dimensions of ``a``, all except the last dimension of ``b``, and all except the last two dimensions of ``c``. @@ -37,7 +38,6 @@ def broadcast_leading_axes(*args): (3, 4, 5, 6) """ - shapes, trails = [], [] for a, n in args: s = np.shape(a) @@ -49,19 +49,25 @@ def broadcast_leading_axes(*args): return (dims, *arrs) -def ndinterp(x, xp, fp, axis=-1, left=None, right=None, period=None): - """interpolate multi-dimensional array over axis""" +def ndinterp(x, xp, fp, axis=-1, left=None, right=None, period=None): # noqa: PLR0913 + """Interpolate multi-dimensional array over axis.""" return np.apply_along_axis( - partial(np.interp, x, xp), axis, fp, left=left, right=right, period=period + partial(np.interp, x, xp), + axis, + fp, + left=left, + right=right, + period=period, ) def trapz_product(f, *ff, axis=-1): - """trapezoidal rule for a product of functions""" + """Trapezoidal rule for a product of functions.""" x, _ = f for x_, _ in ff: x = np.union1d( - x[(x >= x_[0]) & (x <= x_[-1])], x_[(x_ >= x[0]) & (x_ <= x[-1])] + x[(x >= x_[0]) & (x <= x_[-1])], + x_[(x_ >= x[0]) & (x_ <= x[-1])], ) y = np.interp(x, *f) for f_ in ff: @@ -70,8 +76,7 @@ def trapz_product(f, *ff, axis=-1): def cumtrapz(f, x, dtype=None, out=None): - """cumulative trapezoidal rule along last axis""" - + """Cumulative trapezoidal rule along last axis.""" if out is None: out = np.empty_like(f, dtype=dtype) diff --git a/glass/core/constants.py b/glass/core/constants.py index 5e97555..5d7882e 100644 --- a/glass/core/constants.py +++ b/glass/core/constants.py @@ -1,4 +1,4 @@ -"""module for constants""" +"""Module for constants.""" PI = 3.1415926535897932384626433832795028841971693993751 diff --git a/glass/ext/__init__.py b/glass/ext/__init__.py index 8d471cf..df401e1 100644 --- a/glass/ext/__init__.py +++ b/glass/ext/__init__.py @@ -1,4 +1,5 @@ -"""Namespace loader for GLASS extension packages. +""" +Namespace loader for GLASS extension packages. Uses the pkgutil namespace mechanism to find "ext" submodules of packages that provide a "glass" module. @@ -6,14 +7,17 @@ """ -def _extend_path(path, name): +def _extend_path(path, name) -> list: import os.path from pkgutil import extend_path _pkg, _, _mod = name.partition(".") return list( - filter(os.path.isdir, (os.path.join(p, _mod) for p in extend_path(path, _pkg))) + filter( + os.path.isdir, + (os.path.join(p, _mod) for p in extend_path(path, _pkg)), # noqa: PTH118 + ) ) diff --git a/glass/fields.py b/glass/fields.py index 6fdea25..394f2b4 100644 --- a/glass/fields.py +++ b/glass/fields.py @@ -22,15 +22,19 @@ .. autofunction:: getcl -""" +""" # noqa: D205, D400, D415 + +from __future__ import annotations import warnings -import numpy as np -import healpy as hp -from gaussiancl import gaussiancl # typing -from typing import Any, Union, Tuple, Generator, Optional, Sequence, Callable, Iterable +from typing import Any, Callable, Generator, Iterable, Optional, Sequence, Tuple, Union + +import healpy as hp +import numpy as np +import numpy.typing as npt +from gaussiancl import gaussiancl # types Array = np.ndarray @@ -42,11 +46,12 @@ def iternorm( - k: int, cov: Iterable[Array], size: Size = None + k: int, + cov: Iterable[Array], + size: Size = None, ) -> Generator[Iternorm, None, None]: - """return the vector a and variance sigma^2 for iterative normal sampling""" - - n: Tuple[int, ...] + """Return the vector a and variance sigma^2 for iterative normal sampling.""" + n: tuple[int, ...] if size is None: n = () elif isinstance(size, int): @@ -61,14 +66,13 @@ def iternorm( j = 0 if k > 0 else None for i, x in enumerate(cov): - x = np.asanyarray(x) + x = np.asanyarray(x) # noqa: PLW2901 if x.shape != q: try: - x = np.broadcast_to(x, q) + x = np.broadcast_to(x, q) # noqa: PLW2901 except ValueError: - raise TypeError( - f"covariance row {i}: shape {x.shape} cannot be broadcast to {q}" - ) from None + msg = f"covariance row {i}: shape {x.shape} cannot be broadcast to {q}" + raise TypeError(msg) from None # only need to update matrix A if there are correlations if j is not None: @@ -95,7 +99,8 @@ def iternorm( # compute new standard deviation s = x[..., 0] - np.einsum("...i,...i", a, a) if np.any(s < 0): - raise ValueError("covariance matrix is not positive definite") + msg = "covariance matrix is not positive definite" + raise ValueError(msg) s = np.sqrt(s) # yield the next index, vector a, and standard deviation s @@ -113,7 +118,8 @@ def cls2cov(cls: Cls, nl: int, nf: int, nc: int) -> Generator[Array, None, None] cov[:, i] = 0 else: if i == 0 and np.any(np.less(cl, 0)): - raise ValueError("negative values in cl") + msg = "negative values in cl" + raise ValueError(msg) n = len(cl) cov[:n, i] = cl cov[n:, i] = 0 @@ -121,30 +127,26 @@ def cls2cov(cls: Cls, nl: int, nf: int, nc: int) -> Generator[Array, None, None] yield cov -def multalm(alm: Alms, bl: Array, inplace: bool = False) -> Alms: - """multiply alm by bl""" +def multalm(alm: Alms, bl: Array, *, inplace: bool = False) -> Alms: + """Multiply alm by bl.""" n = len(bl) - if inplace: - out = np.asanyarray(alm) - else: - out = np.copy(alm) + out = np.asanyarray(alm) if inplace else np.copy(alm) for m in range(n): out[m * n - m * (m - 1) // 2 : (m + 1) * n - m * (m + 1) // 2] *= bl[m:] return out -def transform_cls(cls: Cls, tfm: ClTransform, pars: Tuple[Any, ...] = ()) -> Cls: +def transform_cls(cls: Cls, tfm: ClTransform, pars: tuple[Any, ...] = ()) -> Cls: """Transform Cls to Gaussian Cls.""" gls = [] for cl in cls: if cl is not None and len(cl) > 0: - if cl[0] == 0: - monopole = 0.0 - else: - monopole = None - gl, info, err, niter = gaussiancl(cl, tfm, pars, monopole=monopole) + monopole = 0.0 if cl[0] == 0 else None + gl, info, _, _ = gaussiancl(cl, tfm, pars, monopole=monopole) if info == 0: - warnings.warn("Gaussian cl did not converge, inexact transform") + warnings.warn( + "Gaussian cl did not converge, inexact transform", stacklevel=2 + ) else: gl = [] gls.append(gl) @@ -154,11 +156,12 @@ def transform_cls(cls: Cls, tfm: ClTransform, pars: Tuple[Any, ...] = ()) -> Cls def gaussian_gls( cls: Cls, *, - lmax: Optional[int] = None, - ncorr: Optional[int] = None, - nside: Optional[int] = None, + lmax: int | None = None, + ncorr: int | None = None, + nside: int | None = None, ) -> Cls: - """Compute Gaussian Cls for a Gaussian random field. + """ + Compute Gaussian Cls for a Gaussian random field. Depending on the given arguments, this truncates the angular power spectra to ``lmax``, removes all but ``ncorr`` correlations between fields, and @@ -166,11 +169,11 @@ def gaussian_gls( arguments are given, no action is performed. """ - if ncorr is not None: n = int((2 * len(cls)) ** 0.5) if n * (n + 1) // 2 != len(cls): - raise ValueError("length of cls array is not a triangle number") + msg = "length of cls array is not a triangle number" + raise ValueError(msg) cls = [ cls[i * (i + 1) // 2 + j] if j <= ncorr else [] for i in range(n) @@ -184,10 +187,10 @@ def gaussian_gls( for cl in cls: if cl is not None and len(cl) > 0: if lmax is not None: - cl = cl[: lmax + 1] + cl = cl[: lmax + 1] # noqa: PLW2901 if nside is not None: n = min(len(cl), len(pw)) - cl = cl[:n] * pw[:n] ** 2 + cl = cl[:n] * pw[:n] ** 2 # noqa: PLW2901 gls.append(cl) return gls @@ -196,9 +199,9 @@ def lognormal_gls( cls: Cls, shift: float = 1.0, *, - lmax: Optional[int] = None, - ncorr: Optional[int] = None, - nside: Optional[int] = None, + lmax: int | None = None, + ncorr: int | None = None, + nside: int | None = None, ) -> Cls: """Compute Gaussian Cls for a lognormal random field.""" gls = gaussian_gls(cls, lmax=lmax, ncorr=ncorr, nside=nside) @@ -209,10 +212,11 @@ def generate_gaussian( gls: Cls, nside: int, *, - ncorr: Optional[int] = None, - rng: Optional[np.random.Generator] = None, + ncorr: int | None = None, + rng: np.random.Generator | None = None, ) -> Generator[Array, None, None]: - """Iteratively sample Gaussian random fields from Cls. + """ + Sample Gaussian random fields from Cls iteratively. A generator that iteratively samples HEALPix maps of Gaussian random fields with the given angular power spectra ``gls`` and resolution parameter @@ -234,7 +238,6 @@ def generate_gaussian( Missing entries can be set to ``None``. """ - # get the default RNG if not given if rng is None: rng = np.random.default_rng() @@ -250,7 +253,8 @@ def generate_gaussian( # number of modes n = max((len(gl) for gl in gls if gl is not None), default=0) if n == 0: - raise ValueError("all gls are empty") + msg = "all gls are empty" + raise ValueError(msg) # generates the covariance matrix for the iterative sampler cov = cls2cov(gls, n, ngrf, ncorr) @@ -294,10 +298,10 @@ def generate_lognormal( nside: int, shift: float = 1.0, *, - ncorr: Optional[int] = None, - rng: Optional[np.random.Generator] = None, + ncorr: int | None = None, + rng: np.random.Generator | None = None, ) -> Generator[Array, None, None]: - """Iterative sample lognormal random fields from Gaussian Cls.""" + """Sample lognormal random fields from Gaussian Cls iteratively.""" for i, m in enumerate(generate_gaussian(gls, nside, ncorr=ncorr, rng=rng)): # compute the variance of the auto-correlation gl = gls[i * (i + 1) // 2] @@ -305,21 +309,22 @@ def generate_lognormal( var = np.sum((2 * ell + 1) * gl) / (4 * np.pi) # fix mean of the Gaussian random field for lognormal transformation - m -= var / 2 + m -= var / 2 # noqa: PLW2901 # exponentiate values in place and subtract 1 in one operation np.expm1(m, out=m) # lognormal shift, unless unity if shift != 1: - m *= shift + m *= shift # noqa: PLW2901 # yield the lognormal map yield m def getcl(cls, i, j, lmax=None): - """Return a specific angular power spectrum from an array. + """ + Return a specific angular power spectrum from an array. Return the angular power spectrum for indices *i* and *j* from an array in *GLASS* ordering. @@ -350,8 +355,11 @@ def getcl(cls, i, j, lmax=None): return cl -def effective_cls(cls, weights1, weights2=None, *, lmax=None): - """Compute effective angular power spectra from weights. +def effective_cls( + cls, weights1, weights2=None, *, lmax=None +) -> npt.NDArray[np.float64]: + r""" + Compute effective angular power spectra from weights. Computes a linear combination of the angular power spectra *cls* using the factors provided by *weights1* and *weights2*. Additional @@ -382,7 +390,8 @@ def effective_cls(cls, weights1, weights2=None, *, lmax=None): # this is the number of fields n = int((2 * len(cls)) ** 0.5) if n * (n + 1) // 2 != len(cls): - raise ValueError("length of cls is not a triangle number") + msg = "length of cls is not a triangle number" + raise ValueError(msg) # find lmax if not given if lmax is None: @@ -395,7 +404,8 @@ def effective_cls(cls, weights1, weights2=None, *, lmax=None): shape1, shape2 = weights1.shape, weights2.shape for i, shape in enumerate((shape1, shape2)): if not shape or shape[0] != n: - raise ValueError(f"shape mismatch between fields and weights{i+1}") + msg = f"shape mismatch between fields and weights{i+1}" + raise ValueError(msg) # get the iterator over leading weight axes # auto-spectra do not repeat identical computations diff --git a/glass/galaxies.py b/glass/galaxies.py index a7f9794..e226d4d 100644 --- a/glass/galaxies.py +++ b/glass/galaxies.py @@ -15,23 +15,31 @@ .. autofunction:: galaxy_shear .. autofunction:: gaussian_phz -""" +""" # noqa: D205, D400, D415 from __future__ import annotations -import numpy as np -import healpix +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from numpy.typing import ArrayLike -from numpy.typing import ArrayLike + from glass.shells import RadialWindow + +import healpix +import numpy as np from glass.core.array import broadcast_leading_axes, cumtrapz -from glass.shells import RadialWindow def redshifts( - n: int | ArrayLike, w: RadialWindow, *, rng: np.random.Generator | None = None + n: int | ArrayLike, + w: RadialWindow, + *, + rng: np.random.Generator | None = None, ) -> np.ndarray: - """Sample redshifts from a radial window function. + """ + Sample redshifts from a radial window function. This function samples *n* redshifts from a distribution that follows the given radial window function *w*. @@ -62,7 +70,8 @@ def redshifts_from_nz( *, rng: np.random.Generator | None = None, ) -> np.ndarray: - """Generate galaxy redshifts from a source distribution. + """ + Generate galaxy redshifts from a source distribution. The function supports sampling from multiple populations of redshifts if *count* is an array or if there are additional axes in @@ -90,7 +99,6 @@ def redshifts_from_nz( samples from all populations. """ - # get default RNG if not given if rng is None: rng = np.random.default_rng() @@ -112,16 +120,18 @@ def redshifts_from_nz( # sample redshifts and store result redshifts[total : total + count[k]] = np.interp( - rng.uniform(0, 1, size=count[k]), cdf, z[k] + rng.uniform(0, 1, size=count[k]), + cdf, + z[k], ) total += count[k] - assert total == redshifts.size + assert total == redshifts.size # noqa: S101 return redshifts -def galaxy_shear( +def galaxy_shear( # noqa: PLR0913 lon: np.ndarray, lat: np.ndarray, eps: np.ndarray, @@ -131,7 +141,8 @@ def galaxy_shear( *, reduced_shear: bool = True, ) -> np.ndarray: - """Observed galaxy shears from weak lensing. + """ + Observed galaxy shears from weak lensing. Takes lensing maps for convergence and shear and produces a lensed ellipticity (shear) for each intrinsic galaxy ellipticity. @@ -154,7 +165,6 @@ def galaxy_shear( Array of complex-valued observed galaxy shears (lensed ellipticities). """ - nside = healpix.npix2nside(np.broadcast(kappa, gamma1, gamma2).shape[-1]) size = np.broadcast(lon, lat, eps).size @@ -192,7 +202,8 @@ def gaussian_phz( upper: ArrayLike | None = None, rng: np.random.Generator | None = None, ) -> np.ndarray: - r"""Photometric redshifts assuming a Gaussian error. + r""" + Photometric redshifts assuming a Gaussian error. A simple toy model of photometric redshift errors that assumes a Gaussian error with redshift-dependent standard deviation @@ -237,7 +248,6 @@ def gaussian_phz( See the :doc:`/examples/1-basic/photoz` example. """ - # get default RNG if not given if rng is None: rng = np.random.default_rng() @@ -253,7 +263,8 @@ def gaussian_phz( upper = np.inf if not np.all(lower < upper): - raise ValueError("requires lower < upper") + msg = "requires lower < upper" + raise ValueError(msg) if not dims: while zphot < lower or zphot > upper: diff --git a/glass/lensing.py b/glass/lensing.py index e5fe926..b25cb7f 100644 --- a/glass/lensing.py +++ b/glass/lensing.py @@ -27,31 +27,36 @@ .. autofunction:: deflect -""" +""" # noqa: D205, D400, D415 -import numpy as np -import healpy as hp +from __future__ import annotations # typing support -from typing import Optional, Sequence, Tuple, TYPE_CHECKING -from numpy.typing import NDArray, ArrayLike +from typing import TYPE_CHECKING, Sequence + +import healpy as hp +import numpy as np if TYPE_CHECKING: # to prevent circular dependencies, only import these for type checking + from numpy.typing import ArrayLike, NDArray + from cosmology import Cosmology + from glass.shells import RadialWindow -def from_convergence( +def from_convergence( # noqa: PLR0913 kappa: NDArray, - lmax: Optional[int] = None, + lmax: int | None = None, *, potential: bool = False, deflection: bool = False, shear: bool = False, discretized: bool = True, -) -> Tuple[NDArray, ...]: - r"""Compute other weak lensing maps from the convergence. +) -> tuple[NDArray, ...]: + r""" + Compute other weak lensing maps from the convergence. Takes a weak lensing convergence map and returns one or more of deflection potential, deflection, and shear maps. The maps are @@ -65,6 +70,8 @@ def from_convergence( Maximum angular mode number to use in the transform. potential, deflection, shear : bool, optional Which lensing maps to return. + discretized : bool + Correct the pixel window function in output maps. Returns ------- @@ -150,7 +157,6 @@ def from_convergence( doi:10.21105/astro.2302.01942 """ - # no output means no computation, return empty tuple if not (potential or deflection or shear): return () @@ -164,7 +170,7 @@ def from_convergence( alm = hp.map2alm(kappa, lmax=lmax, pol=False, use_pixel_weights=True) # mode number; all conversions are factors of this - l = np.arange(lmax + 1) + l = np.arange(lmax + 1) # noqa: E741 # this tuple will be returned results = () @@ -187,7 +193,8 @@ def from_convergence( # compute deflection alms in place fl = np.sqrt(l * (l + 1)) - # TODO: missing spin-1 pixel window function here + # TODO(ntessore): missing spin-1 pixel window function here # noqa: FIX002 + # https://github.com/glass-dev/glass/issues/243 hp.almxfl(alm, fl, inplace=True) # if deflection is requested, compute spin-1 maps and add to output @@ -219,9 +226,13 @@ def from_convergence( def shear_from_convergence( - kappa: np.ndarray, lmax: Optional[int] = None, *, discretized: bool = True + kappa: np.ndarray, + lmax: int | None = None, + *, + discretized: bool = True, ) -> np.ndarray: - r"""Weak lensing shear from convergence. + r""" + Weak lensing shear from convergence. .. deprecated:: 2023.6 Use the more general :func:`from_convergence` function instead. @@ -230,7 +241,6 @@ def shear_from_convergence( transform. """ - nside = hp.get_nside(kappa) if lmax is None: lmax = 3 * nside - 1 @@ -242,7 +252,7 @@ def shear_from_convergence( blm = np.zeros_like(alm) # factor to convert convergence alm to shear alm - l = np.arange(lmax + 1) + l = np.arange(lmax + 1) # noqa: E741 fl = np.sqrt((l + 2) * (l + 1) * l * (l - 1)) fl /= np.clip(l * (l + 1), 1, None) fl *= -1 @@ -262,7 +272,7 @@ def shear_from_convergence( class MultiPlaneConvergence: """Compute convergence fields iteratively from multiple matter planes.""" - def __init__(self, cosmo: "Cosmology") -> None: + def __init__(self, cosmo: Cosmology) -> None: """Create a new instance to iteratively compute the convergence.""" self.cosmo = cosmo @@ -273,17 +283,17 @@ def __init__(self, cosmo: "Cosmology") -> None: self.w3: float = 0.0 self.r23: float = 1.0 self.delta3: np.ndarray = np.array(0.0) - self.kappa2: Optional[np.ndarray] = None - self.kappa3: Optional[np.ndarray] = None + self.kappa2: np.ndarray | None = None + self.kappa3: np.ndarray | None = None - def add_window(self, delta: np.ndarray, w: "RadialWindow") -> None: - """Add a mass plane from a window function to the convergence. + def add_window(self, delta: np.ndarray, w: RadialWindow) -> None: + """ + Add a mass plane from a window function to the convergence. The lensing weight is computed from the window function, and the source plane redshift is the effective redshift of the window. """ - zsrc = w.zeff lens_weight = np.trapz(w.wa, w.za) / np.interp(zsrc, w.za, w.wa) @@ -291,9 +301,9 @@ def add_window(self, delta: np.ndarray, w: "RadialWindow") -> None: def add_plane(self, delta: np.ndarray, zsrc: float, wlens: float = 1.0) -> None: """Add a mass plane at redshift ``zsrc`` to the convergence.""" - if zsrc <= self.z3: - raise ValueError("source redshift must be increasing") + msg = "source redshift must be increasing" + raise ValueError(msg) # cycle mass plane, ... delta2, self.delta3 = self.delta3, delta @@ -338,7 +348,7 @@ def zsrc(self) -> float: return self.z3 @property - def kappa(self) -> Optional[np.ndarray]: + def kappa(self) -> np.ndarray | None: """The current convergence plane.""" return self.kappa3 @@ -354,8 +364,8 @@ def wlens(self) -> float: def multi_plane_matrix( - shells: Sequence["RadialWindow"], - cosmo: "Cosmology", + shells: Sequence[RadialWindow], + cosmo: Cosmology, ) -> ArrayLike: """Compute the matrix of lensing contributions from each shell.""" mpc = MultiPlaneConvergence(cosmo) @@ -368,10 +378,11 @@ def multi_plane_matrix( def multi_plane_weights( weights: ArrayLike, - shells: Sequence["RadialWindow"], - cosmo: "Cosmology", + shells: Sequence[RadialWindow], + cosmo: Cosmology, ) -> ArrayLike: - """Compute effective weights for multi-plane convergence. + """ + Compute effective weights for multi-plane convergence. Converts an array *weights* of relative weights for each shell into the equivalent array of relative lensing weights. @@ -399,7 +410,8 @@ def multi_plane_weights( # ensure shape of weights ends with the number of shells shape = np.shape(weights) if not shape or shape[0] != len(shells): - raise ValueError("shape mismatch between weights and shells") + msg = "shape mismatch between weights and shells" + raise ValueError(msg) # normalise weights weights = weights / np.sum(weights, axis=0) # combine weights and the matrix of lensing contributions @@ -408,7 +420,8 @@ def multi_plane_weights( def deflect(lon: ArrayLike, lat: ArrayLike, alpha: ArrayLike) -> NDArray: - """Apply deflections to positions. + r""" + Apply deflections to positions. Takes an array of :term:`deflection` values and applies them to the given positions. @@ -437,7 +450,6 @@ def deflect(lon: ArrayLike, lat: ArrayLike, alpha: ArrayLike) -> NDArray: exponential map. """ - alpha = np.asanyarray(alpha) if np.iscomplexobj(alpha): alpha1, alpha2 = alpha.real, alpha.imag diff --git a/glass/observations.py b/glass/observations.py index 740ee55..0814fb6 100644 --- a/glass/observations.py +++ b/glass/observations.py @@ -24,24 +24,29 @@ .. autofunction:: vmap_galactic_ecliptic -""" +""" # noqa: D205, D400, D415 + +from __future__ import annotations -import numpy as np -import healpy as hp import math +from typing import TYPE_CHECKING -from typing import Optional, Tuple, List -from numpy.typing import ArrayLike +import healpy as hp +import numpy as np from glass.core.array import cumtrapz +if TYPE_CHECKING: + from numpy.typing import ArrayLike + def vmap_galactic_ecliptic( nside: int, - galactic: Tuple[float, float] = (30, 90), - ecliptic: Tuple[float, float] = (20, 80), + galactic: tuple[float, float] = (30, 90), + ecliptic: tuple[float, float] = (20, 80), ) -> np.ndarray: - """visibility map masking galactic and ecliptic plane + """ + Visibility map masking galactic and ecliptic plane. This function returns a :term:`visibility map` that blocks out stripes for the galactic and ecliptic planes. The location of the stripes is set with @@ -67,17 +72,17 @@ def vmap_galactic_ecliptic( """ if np.ndim(galactic) != 1 or len(galactic) != 2: - raise TypeError("galactic stripe must be a pair of numbers") + msg = "galactic stripe must be a pair of numbers" + raise TypeError(msg) if np.ndim(ecliptic) != 1 or len(ecliptic) != 2: - raise TypeError("ecliptic stripe must be a pair of numbers") + msg = "ecliptic stripe must be a pair of numbers" + raise TypeError(msg) m = np.ones(hp.nside2npix(nside)) m[hp.query_strip(nside, *galactic)] = 0 m = hp.Rotator(coord="GC").rotate_map_pixel(m) m[hp.query_strip(nside, *ecliptic)] = 0 - m = hp.Rotator(coord="CE").rotate_map_pixel(m) - - return m + return hp.Rotator(coord="CE").rotate_map_pixel(m) def gaussian_nz( @@ -85,9 +90,10 @@ def gaussian_nz( mean: ArrayLike, sigma: ArrayLike, *, - norm: Optional[ArrayLike] = None, + norm: ArrayLike | None = None, ) -> np.ndarray: - r"""Gaussian redshift distribution. + r""" + Gaussian redshift distribution. The redshift follows a Gaussian distribution with the given mean and standard deviation. @@ -99,7 +105,7 @@ def gaussian_nz( ---------- z : array_like Redshift values of the distribution. - mode : float or array_like + mean : float or array_like Mean(s) of the redshift distribution. sigma : float or array_like Standard deviation(s) of the redshift distribution. @@ -130,9 +136,10 @@ def smail_nz( alpha: ArrayLike, beta: ArrayLike, *, - norm: Optional[ArrayLike] = None, + norm: ArrayLike | None = None, ) -> np.ndarray: - r"""Redshift distribution following Smail et al. (1994). + r""" + Redshift distribution following Smail et al. (1994). The redshift follows the Smail et al. [1]_ redshift distribution. @@ -186,9 +193,14 @@ def smail_nz( def fixed_zbins( - zmin: float, zmax: float, *, nbins: Optional[int] = None, dz: Optional[float] = None -) -> List[Tuple[float, float]]: - """tomographic redshift bins of fixed size + zmin: float, + zmax: float, + *, + nbins: int | None = None, + dz: float | None = None, +) -> list[tuple[float, float]]: + """ + Tomographic redshift bins of fixed size. This function creates contiguous tomographic redshift bins of fixed size. It takes either the number or size of the bins. @@ -206,22 +218,26 @@ def fixed_zbins( ------- zbins : list of tuple of float List of redshift bin edges. - """ + """ if nbins is not None and dz is None: zbinedges = np.linspace(zmin, zmax, nbins + 1) if nbins is None and dz is not None: zbinedges = np.arange(zmin, zmax, dz) else: - raise ValueError("exactly one of nbins and dz must be given") + msg = "exactly one of nbins and dz must be given" + raise ValueError(msg) return list(zip(zbinedges, zbinedges[1:])) def equal_dens_zbins( - z: np.ndarray, nz: np.ndarray, nbins: int -) -> List[Tuple[float, float]]: - """equal density tomographic redshift bins + z: np.ndarray, + nz: np.ndarray, + nbins: int, +) -> list[tuple[float, float]]: + """ + Equal density tomographic redshift bins. This function subdivides a source redshift distribution into ``nbins`` tomographic redshift bins with equal density. @@ -251,9 +267,13 @@ def equal_dens_zbins( def tomo_nz_gausserr( - z: np.ndarray, nz: np.ndarray, sigma_0: float, zbins: List[Tuple[float, float]] + z: np.ndarray, + nz: np.ndarray, + sigma_0: float, + zbins: list[tuple[float, float]], ) -> np.ndarray: - """tomographic redshift bins with a Gaussian redshift error + """ + Tomographic redshift bins with a Gaussian redshift error. This function takes a _true_ overall source redshift distribution ``z``, ``nz`` and returns tomographic source redshift distributions for the diff --git a/glass/points.py b/glass/points.py index 0640a68..02a7cbb 100644 --- a/glass/points.py +++ b/glass/points.py @@ -27,17 +27,18 @@ .. autofunction:: linear_bias .. autofunction:: loglinear_bias -""" +""" # noqa: D205, D400, D415 -import numpy as np import healpix +import numpy as np from glass.core.array import broadcast_first, broadcast_leading_axes, trapz_product from glass.core.constants import ARCMIN2_SPHERE def effective_bias(z, bz, w): - """Effective bias parameter from a redshift-dependent bias function. + r""" + Effective bias parameter from a redshift-dependent bias function. This function takes a redshift-dependent bias function :math:`b(z)` and computes an effective bias parameter :math:`\\bar{b}` for a @@ -71,19 +72,19 @@ def effective_bias(z, bz, w): def linear_bias(delta, b): - """linear bias model :math:`\\delta_g = b \\, \\delta`""" + r"""Linear bias model :math:`\\delta_g = b \\, \\delta`.""" return b * delta def loglinear_bias(delta, b): - """log-linear bias model :math:`\\ln(1 + \\delta_g) = b \\ln(1 + \\delta)`""" + r"""log-linear bias model :math:`\\ln(1 + \\delta_g) = b \\ln(1 + \\delta)`.""" delta_g = np.log1p(delta) delta_g *= b np.expm1(delta_g, out=delta_g) return delta_g -def positions_from_delta( +def positions_from_delta( # noqa: PLR0912, PLR0913, PLR0915 ngal, delta, bias=None, @@ -94,7 +95,8 @@ def positions_from_delta( batch=1_000_000, rng=None, ): - """Generate positions tracing a density contrast. + """ + Generate positions tracing a density contrast. The map of expected number counts is constructed from the number density, density contrast, an optional bias model, and an optional @@ -150,7 +152,6 @@ def positions_from_delta( dimensions is returned. """ - # get default RNG if not given if rng is None: rng = np.random.default_rng() @@ -159,7 +160,8 @@ def positions_from_delta( if isinstance(bias_model, str): bias_model = globals()[f"{bias_model}_bias"] elif not callable(bias_model): - raise ValueError("bias_model must be string or callable") + msg = "bias_model must be string or callable" + raise TypeError(msg) # broadcast inputs to common shape of extra dimensions inputs = [(ngal, 0), (delta, 1)] @@ -176,10 +178,7 @@ def positions_from_delta( # iterate the leading dimensions for k in np.ndindex(dims): # compute density contrast from bias model, or copy - if bias is None: - n = np.copy(delta[k]) - else: - n = bias_model(delta[k], bias[k]) + n = np.copy(delta[k]) if bias is None else bias_model(delta[k], bias[k]) # remove monopole if asked to if remove_monopole: @@ -244,11 +243,12 @@ def positions_from_delta( yield lon, lat, ipix.size * cmask # make sure that the correct number of pixels was sampled - assert np.sum(n[stop:]) == 0 + assert np.sum(n[stop:]) == 0 # noqa: S101 def uniform_positions(ngal, *, rng=None): - """Generate positions uniformly over the sphere. + """ + Generate positions uniformly over the sphere. The function supports array input for the ``ngal`` parameter. @@ -268,7 +268,6 @@ def uniform_positions(ngal, *, rng=None): counts with the same shape is returned. """ - # get default RNG if not given if rng is None: rng = np.random.default_rng() @@ -299,7 +298,8 @@ def uniform_positions(ngal, *, rng=None): def position_weights(densities, bias=None): - """Compute relative weights for angular clustering. + r""" + Compute relative weights for angular clustering. Takes an array *densities* of densities in arbitrary units and returns the relative weight of each shell. If *bias* is given, a diff --git a/glass/shapes.py b/glass/shapes.py index affbcc6..a922ba1 100644 --- a/glass/shapes.py +++ b/glass/shapes.py @@ -20,16 +20,21 @@ .. autofunction:: triaxial_axis_ratio -""" +""" # noqa: D205, D400, D415 from __future__ import annotations +from typing import TYPE_CHECKING + import numpy as np -from numpy.typing import ArrayLike, NDArray + +if TYPE_CHECKING: + from numpy.typing import ArrayLike, NDArray def triaxial_axis_ratio(zeta, xi, size=None, *, rng=None): - r"""axis ratio of a randomly projected triaxial ellipsoid + r""" + Axis ratio of a randomly projected triaxial ellipsoid. Given the two axis ratios `1 >= zeta >= xi` of a randomly oriented triaxial ellipsoid, computes the axis ratio `q` of the projection. @@ -60,7 +65,6 @@ def triaxial_axis_ratio(zeta, xi, size=None, *, rng=None): .. [1] Binney J., 1985, MNRAS, 212, 767. doi:10.1093/mnras/212.4.767 """ - # default RNG if not provided if rng is None: rng = np.random.default_rng() @@ -83,20 +87,19 @@ def triaxial_axis_ratio(zeta, xi, size=None, *, rng=None): x2 = np.square(xi) # eq. (11) multiplied by xi^2 zeta^2 - A = (1 + z2m1 * sin2_phi) * cos2_theta + x2 * sin2_theta - B2 = 4 * z2m1**2 * cos2_theta * sin2_phi * cos2_phi - C = 1 + z2m1 * cos2_phi + A = (1 + z2m1 * sin2_phi) * cos2_theta + x2 * sin2_theta # noqa: N806 + B2 = 4 * z2m1**2 * cos2_theta * sin2_phi * cos2_phi # noqa: N806 + C = 1 + z2m1 * cos2_phi # noqa: N806 # eq. (12) - q = np.sqrt( - (A + C - np.sqrt((A - C) ** 2 + B2)) / (A + C + np.sqrt((A - C) ** 2 + B2)) + return np.sqrt( + (A + C - np.sqrt((A - C) ** 2 + B2)) / (A + C + np.sqrt((A - C) ** 2 + B2)), ) - return q - -def ellipticity_ryden04(mu, sigma, gamma, sigma_gamma, size=None, *, rng=None): - r"""ellipticity distribution following Ryden (2004) +def ellipticity_ryden04(mu, sigma, gamma, sigma_gamma, size=None, *, rng=None): # noqa: PLR0913 + r""" + Ellipticity distribution following Ryden (2004). The ellipticities are sampled by randomly projecting a 3D ellipsoid with principal axes :math:`A > B > C` [1]_. The distribution of :math:`\log(1 - @@ -131,7 +134,6 @@ def ellipticity_ryden04(mu, sigma, gamma, sigma_gamma, size=None, *, rng=None): .. [2] Padilla N. D., Strauss M. A., 2008, MNRAS, 388, 1321. """ - # default RNG if not provided if rng is None: rng = np.random.default_rng() @@ -165,9 +167,13 @@ def ellipticity_ryden04(mu, sigma, gamma, sigma_gamma, size=None, *, rng=None): def ellipticity_gaussian( - count: int | ArrayLike, sigma: ArrayLike, *, rng: np.random.Generator | None = None + count: int | ArrayLike, + sigma: ArrayLike, + *, + rng: np.random.Generator | None = None, ) -> NDArray: - r"""Sample Gaussian galaxy ellipticities. + r""" + Sample Gaussian galaxy ellipticities. The ellipticities are sampled from a normal distribution with standard deviation ``sigma`` for each component. Samples where the @@ -190,7 +196,6 @@ def ellipticity_gaussian( Array of galaxy :term:`ellipticity`. """ - # default RNG if not provided if rng is None: rng = np.random.default_rng() @@ -219,9 +224,13 @@ def ellipticity_gaussian( def ellipticity_intnorm( - count: int | ArrayLike, sigma: ArrayLike, *, rng: np.random.Generator | None = None + count: int | ArrayLike, + sigma: ArrayLike, + *, + rng: np.random.Generator | None = None, ) -> NDArray: - r"""Sample galaxy ellipticities with intrinsic normal distribution. + r""" + Sample galaxy ellipticities with intrinsic normal distribution. The ellipticities are sampled from an intrinsic normal distribution with standard deviation ``sigma`` for each component. @@ -241,7 +250,6 @@ def ellipticity_intnorm( Array of galaxy :term:`ellipticity`. """ - # default RNG if not provided if rng is None: rng = np.random.default_rng() @@ -250,8 +258,9 @@ def ellipticity_intnorm( count, sigma = np.broadcast_arrays(count, sigma) # make sure sigma is admissible - if not np.all((0 <= sigma) & (sigma < 0.5**0.5)): - raise ValueError("sigma must be between 0 and sqrt(0.5)") + if not np.all((sigma >= 0) & (sigma < 0.5**0.5)): + msg = "sigma must be between 0 and sqrt(0.5)" + raise ValueError(msg) # convert to sigma_eta using fit sigma_eta = sigma * ((8 + 5 * sigma**2) / (2 - 4 * sigma**2)) ** 0.5 diff --git a/glass/shells.py b/glass/shells.py index 49bd021..e020ae5 100644 --- a/glass/shells.py +++ b/glass/shells.py @@ -40,44 +40,48 @@ .. autofunction:: distance_weight .. autofunction:: volume_weight .. autofunction:: density_weight +""" # noqa: D205, D400, D415 -""" +from __future__ import annotations import warnings -from collections import namedtuple + +# type checking +from typing import TYPE_CHECKING, Callable, NamedTuple, Sequence, Union + import numpy as np from glass.core.array import ndinterp -# type checking -from typing import Union, Sequence, List, Tuple, Optional, Callable, TYPE_CHECKING -from numpy.typing import ArrayLike - if TYPE_CHECKING: + from numpy.typing import ArrayLike + from cosmology import Cosmology + # types ArrayLike1D = Union[Sequence[float], np.ndarray] WeightFunc = Callable[[ArrayLike1D], np.ndarray] -def distance_weight(z: ArrayLike, cosmo: "Cosmology") -> np.ndarray: +def distance_weight(z: ArrayLike, cosmo: Cosmology) -> np.ndarray: """Uniform weight in comoving distance.""" return 1 / cosmo.ef(z) -def volume_weight(z: ArrayLike, cosmo: "Cosmology") -> np.ndarray: +def volume_weight(z: ArrayLike, cosmo: Cosmology) -> np.ndarray: """Uniform weight in comoving volume.""" return cosmo.xm(z) ** 2 / cosmo.ef(z) -def density_weight(z: ArrayLike, cosmo: "Cosmology") -> np.ndarray: +def density_weight(z: ArrayLike, cosmo: Cosmology) -> np.ndarray: """Uniform weight in matter density.""" return cosmo.rho_m_z(z) * cosmo.xm(z) ** 2 / cosmo.ef(z) -RadialWindow = namedtuple("RadialWindow", "za, wa, zeff") -RadialWindow.__doc__ = """A radial window, defined by a window function. +class RadialWindow(NamedTuple): + """ + A radial window, defined by a window function. The radial window is defined by a window function in redshift, which is given by a pair of arrays ``za``, ``wa``. @@ -106,9 +110,9 @@ def density_weight(z: ArrayLike, cosmo: "Cosmology") -> np.ndarray: Attributes ---------- - za : (N,) array_like + za : Sequence[float] Redshift array; the abscissae of the window function. - wa : (N,) array_like + wa : Sequence[float] Weight array; the values (ordinates) of the window function. zeff : float Effective redshift of the window. @@ -118,17 +122,19 @@ def density_weight(z: ArrayLike, cosmo: "Cosmology") -> np.ndarray: _replace """ -RadialWindow.za.__doc__ = """Redshift array; the abscissae of the window function.""" -RadialWindow.wa.__doc__ = ( - """Weight array; the values (ordinates) of the window function.""" -) -RadialWindow.zeff.__doc__ = """Effective redshift of the window.""" + + za: Sequence[float] + wa: Sequence[float] + zeff: float def tophat_windows( - zbins: ArrayLike1D, dz: float = 1e-3, weight: Optional[WeightFunc] = None -) -> List[RadialWindow]: - """Tophat window functions from the given redshift bin edges. + zbins: ArrayLike1D, + dz: float = 1e-3, + weight: WeightFunc | None = None, +) -> list[RadialWindow]: + """ + Tophat window functions from the given redshift bin edges. Uses the *N+1* given redshifts as bin edges to construct *N* tophat window functions. The redshifts of the windows have linear spacing @@ -162,16 +168,15 @@ def tophat_windows( """ if len(zbins) < 2: - raise ValueError("zbins must have at least two entries") + msg = "zbins must have at least two entries" + raise ValueError(msg) if zbins[0] != 0: - warnings.warn("first tophat window does not start at redshift zero") + warnings.warn( + "first tophat window does not start at redshift zero", stacklevel=2 + ) wht: WeightFunc - if weight is not None: - wht = weight - else: - wht = np.ones_like - + wht = weight if weight is not None else np.ones_like ws = [] for zmin, zmax in zip(zbins, zbins[1:]): n = max(round((zmax - zmin) / dz), 2) @@ -185,9 +190,10 @@ def tophat_windows( def linear_windows( zgrid: ArrayLike1D, dz: float = 1e-3, - weight: Optional[WeightFunc] = None, -) -> List[RadialWindow]: - """Linear interpolation window functions. + weight: WeightFunc | None = None, +) -> list[RadialWindow]: + """ + Linear interpolation window functions. Uses the *N+2* given redshifts as nodes to construct *N* triangular window functions between the first and last node. These correspond @@ -221,19 +227,20 @@ def linear_windows( """ if len(zgrid) < 3: - raise ValueError("nodes must have at least 3 entries") + msg = "nodes must have at least 3 entries" + raise ValueError(msg) if zgrid[0] != 0: - warnings.warn("first triangular window does not start at z=0") + warnings.warn("first triangular window does not start at z=0", stacklevel=2) ws = [] for zmin, zmid, zmax in zip(zgrid, zgrid[1:], zgrid[2:]): n = max(round((zmid - zmin) / dz), 2) - 1 m = max(round((zmax - zmid) / dz), 2) z = np.concatenate( - [np.linspace(zmin, zmid, n, endpoint=False), np.linspace(zmid, zmax, m)] + [np.linspace(zmin, zmid, n, endpoint=False), np.linspace(zmid, zmax, m)], ) w = np.concatenate( - [np.linspace(0.0, 1.0, n, endpoint=False), np.linspace(1.0, 0.0, m)] + [np.linspace(0.0, 1.0, n, endpoint=False), np.linspace(1.0, 0.0, m)], ) if weight is not None: w *= weight(z) @@ -244,9 +251,10 @@ def linear_windows( def cubic_windows( zgrid: ArrayLike1D, dz: float = 1e-3, - weight: Optional[WeightFunc] = None, -) -> List[RadialWindow]: - """Cubic interpolation window functions. + weight: WeightFunc | None = None, +) -> list[RadialWindow]: + """ + Cubic interpolation window functions. Uses the *N+2* given redshifts as nodes to construct *N* cubic Hermite spline window functions between the first and last node. @@ -281,16 +289,17 @@ def cubic_windows( """ if len(zgrid) < 3: - raise ValueError("nodes must have at least 3 entries") + msg = "nodes must have at least 3 entries" + raise ValueError(msg) if zgrid[0] != 0: - warnings.warn("first cubic spline window does not start at z=0") + warnings.warn("first cubic spline window does not start at z=0", stacklevel=2) ws = [] for zmin, zmid, zmax in zip(zgrid, zgrid[1:], zgrid[2:]): n = max(round((zmid - zmin) / dz), 2) - 1 m = max(round((zmax - zmid) / dz), 2) z = np.concatenate( - [np.linspace(zmin, zmid, n, endpoint=False), np.linspace(zmid, zmax, m)] + [np.linspace(zmin, zmid, n, endpoint=False), np.linspace(zmid, zmax, m)], ) u = np.linspace(0.0, 1.0, n, endpoint=False) v = np.linspace(1.0, 0.0, m) @@ -302,9 +311,12 @@ def cubic_windows( def restrict( - z: ArrayLike1D, f: ArrayLike1D, w: RadialWindow -) -> Tuple[np.ndarray, np.ndarray]: - """Restrict a function to a redshift window. + z: ArrayLike1D, + f: ArrayLike1D, + w: RadialWindow, +) -> tuple[np.ndarray, np.ndarray]: + """ + Restrict a function to a redshift window. Multiply the function :math:`f(z)` by a window function :math:`w(z)` to produce :math:`w(z) f(z)` over the support of :math:`w`. @@ -333,7 +345,6 @@ def restrict( The restricted function. """ - z_ = np.compress(np.greater(z, w.za[0]) & np.less(z, w.za[-1]), z) zr = np.union1d(w.za, z_) fr = ndinterp(zr, z, f, left=0.0, right=0.0) * ndinterp(zr, w.za, w.wa) @@ -347,7 +358,8 @@ def partition( *, method: str = "nnls", ) -> ArrayLike: - """Partition a function by a sequence of windows. + r""" + Partition a function by a sequence of windows. Returns a vector of weights :math:`x_1, x_2, \\ldots` such that the weighted sum of normalised radial window functions :math:`x_1 \\, @@ -444,7 +456,8 @@ def partition( try: partition_method = globals()[f"partition_{method}"] except KeyError: - raise ValueError(f"invalid method: {method}") from None + msg = f"invalid method: {method}" + raise ValueError(msg) from None return partition_method(z, fz, shells) @@ -456,10 +469,8 @@ def partition_lstsq( sumtol: float = 0.01, ) -> ArrayLike: """Least-squares partition.""" - # make sure nothing breaks - if sumtol < 1e-4: - sumtol = 1e-4 + sumtol = max(sumtol, 1e-4) # compute the union of all given redshift grids zp = z @@ -474,7 +485,7 @@ def partition_lstsq( # create the window function matrix a = [np.interp(zp, za, wa, left=0.0, right=0.0) for za, wa, _ in shells] - a = a / np.trapz(a, zp, axis=-1)[..., None] + a /= np.trapz(a, zp, axis=-1)[..., None] a = a * dz # create the target vector of distribution values @@ -493,9 +504,7 @@ def partition_lstsq( x = np.linalg.lstsq(a.T, b.reshape(-1, zp.size + 1).T, rcond=None)[0] x = x.T.reshape(*dims, len(shells)) # roll the last axis of size len(shells) to the front - x = np.moveaxis(x, -1, 0) - # all done - return x + return np.moveaxis(x, -1, 0) def partition_nnls( @@ -505,18 +514,17 @@ def partition_nnls( *, sumtol: float = 0.01, ) -> ArrayLike: - """Non-negative least-squares partition. + """ + Non-negative least-squares partition. Uses the ``nnls()`` algorithm from ``scipy.optimize`` and thus requires SciPy. """ - from glass.core.algorithm import nnls # make sure nothing breaks - if sumtol < 1e-4: - sumtol = 1e-4 + sumtol = max(sumtol, 1e-4) # compute the union of all given redshift grids zp = z @@ -531,7 +539,7 @@ def partition_nnls( # create the window function matrix a = [np.interp(zp, za, wa, left=0.0, right=0.0) for za, wa, _ in shells] - a = a / np.trapz(a, zp, axis=-1)[..., None] + a /= np.trapz(a, zp, axis=-1)[..., None] a = a * dz # create the target vector of distribution values @@ -552,9 +560,9 @@ def partition_nnls( y = np.einsum("ji,...j", q, b) # for each dim, find non-negative weights x such that y == r @ x - x = np.empty([len(shells)] + dims) + x = np.empty([len(shells), *dims]) for i in np.ndindex(*dims): - x[(...,) + i] = nnls(r, y[i]) + x[(..., *i)] = nnls(r, y[i]) # all done return x @@ -566,7 +574,6 @@ def partition_restrict( shells: Sequence[RadialWindow], ) -> ArrayLike: """Partition by restriction and integration.""" - part = np.empty((len(shells),) + np.shape(fz)[:-1]) for i, w in enumerate(shells): zr, fr = restrict(z, fz, w) @@ -581,7 +588,8 @@ def redshift_grid(zmin, zmax, *, dz=None, num=None): elif dz is None and num is not None: z = np.linspace(zmin, zmax, num + 1) else: - raise ValueError('exactly one of "dz" or "num" must be given') + msg = 'exactly one of "dz" or "num" must be given' + raise ValueError(msg) return z @@ -593,7 +601,8 @@ def distance_grid(cosmo, zmin, zmax, *, dx=None, num=None): elif dx is None and num is not None: x = np.linspace(xmin, xmax, num + 1) else: - raise ValueError('exactly one of "dx" or "num" must be given') + msg = 'exactly one of "dx" or "num" must be given' + raise ValueError(msg) return cosmo.dc_inv(x) @@ -602,7 +611,8 @@ def combine( weights: ArrayLike, shells: Sequence[RadialWindow], ) -> ArrayLike: - """Evaluate a linear combination of window functions. + r""" + Evaluate a linear combination of window functions. Takes a vector of weights :math:`x_1, x_2, \\ldots` and computes the weighted sum of normalised radial window functions :math:`f(z) = x_1 diff --git a/glass/user.py b/glass/user.py index 63076a2..7996e78 100644 --- a/glass/user.py +++ b/glass/user.py @@ -15,32 +15,33 @@ .. autofunction:: load_cls .. autofunction:: write_catalog -""" +""" # noqa: D205, D400, D415 -import numpy as np from contextlib import contextmanager +import numpy as np + -def save_cls(filename, cls): - """Save a list of Cls to file. +def save_cls(filename, cls) -> None: + """ + Save a list of Cls to file. Uses :func:`numpy.savez` internally. The filename should therefore have a ``.npz`` suffix, or it will be given one. """ - split = np.cumsum([len(cl) if cl is not None else 0 for cl in cls[:-1]]) values = np.concatenate([cl for cl in cls if cl is not None]) np.savez(filename, values=values, split=split) def load_cls(filename): - """Load a list of Cls from file. + """ + Load a list of Cls from file. Uses :func:`numpy.load` internally. """ - with np.load(filename) as npz: values = npz["values"] split = npz["split"] @@ -48,16 +49,19 @@ def load_cls(filename): class _FitsWriter: - """Writer that creates a FITS file. Initialised with the fits object and extension name.""" + """ + Writer that creates a FITS file. + + Initialised with the fits object and extension name. + """ - def __init__(self, fits, ext=None): + def __init__(self, fits, ext=None) -> None: """Create a new, uninitialised writer.""" self.fits = fits self.ext = ext - def _append(self, data, names=None): - """Internal method where the FITS writing is done""" - + def _append(self, data, names=None) -> None: + """Write the FITS file.""" if self.ext is None or self.ext not in self.fits: self.fits.write_table(data, names=names, extname=self.ext) if self.ext is None: @@ -67,11 +71,13 @@ def _append(self, data, names=None): # not using hdu.append here because of incompatibilities hdu.write(data, names=names, firstrow=hdu.get_nrows()) - def write(self, data=None, /, **columns): - """Writes to FITS by calling the internal _append method. - Pass either a positional variable (data) - or multiple named arguments (**columns)""" + def write(self, data=None, /, **columns) -> None: + """ + Write to FITS by calling the internal _append method. + Pass either a positional variable (data) + or multiple named arguments (**columns) + """ # if data is given, write it as it is if data is not None: self._append(data) @@ -85,8 +91,10 @@ def write(self, data=None, /, **columns): @contextmanager def write_catalog(filename, *, ext=None): """ - Write a catalogue into a FITS file, where *ext* is the optional - name of the extension. To be used as a context manager:: + Write a catalogue into a FITS file. + + *ext* is the optional name of the extension. + To be used as a context manager:: # create the catalogue writer with write_catalog("catalog.fits") as out: @@ -102,5 +110,4 @@ def write_catalog(filename, *, ext=None): with fitsio.FITS(filename, "rw", clobber=True) as fits: fits.write(None) - writer = _FitsWriter(fits, ext) - yield writer + yield _FitsWriter(fits, ext) diff --git a/pyproject.toml b/pyproject.toml index 96d6f4e..8acc4db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,6 +94,60 @@ force-exclude = true src = [ "glass", ] +lint.ignore = [ + "ANN001", # TODO: missing-type-function-argument + "ANN002", # TODO: missing-type-args + "ANN003", # TODO: missing-type-kwargs + "ANN201", # TODO: missing-return-type-undocumented-public-function + "COM812", # missing-trailing-comma (ruff-format recommended) + "D203", # one-blank-line-before-class + "D212", # blank-line-before-class + "ERA001", # TODO: commented-out-code + "ISC001", # single-line-implicit-string-concatenation (ruff-format recommended) + "NPY002", # TODO: numpy-legacy-random + "NPY201", # TODO: numpy2-deprecation + "RUF003", # ambiguous-unicode-character-comment +] +lint.isort = {known-first-party = [ + "glass", +], section-order = [ + "future", + "standard-library", + "third-party", + "cosmo", + "first-party", + "local-folder", +], sections = {"cosmo" = [ + "camb", + "cosmology", +]}} +lint.per-file-ignores = {"__init__.py" = [ + "F401", # unused-import +], "docs*" = [ + "D100", # undocumented-public-module + "INP001", # implicit-namespace-package, +], "examples*" = [ + "PLR2004", # magic-value-comparison + "T201", # print +], "glass*" = [ + "PLR2004", # TODO: magic-value-comparison +], "tests*" = [ + "ANN001", # TODO: missing-type-function-argument + "ANN201", # TODO: issing-return-type-undocumented-public-function + "ANN202", # TODO: missing-return-type-private-function + "D100", # undocumented-public-module + "D103", # TODO: undocumented-public-function + "D104", # undocumented-public-package + "INP001", # implicit-namespace-package + "PLR2004", # magic-value-comparison + "PT011", # TODO: pytest-raises-too-broad + "S101", # assert +]} +lint.select = [ + "ALL", +] +lint.mccabe.max-complexity = 18 [tool.tomlsort] overrides."project.classifiers".inline_arrays = false +overrides."tool.ruff.lint.isort.section-order".inline_arrays = false diff --git a/tests/core/test_algorithm.py b/tests/core/test_algorithm.py index a25b1d3..baad3a6 100644 --- a/tests/core/test_algorithm.py +++ b/tests/core/test_algorithm.py @@ -14,6 +14,7 @@ def test_nnls(): import numpy as np from scipy.optimize import nnls as nnls_scipy + from glass.core.algorithm import nnls as nnls_glass a = np.random.randn(100, 20) diff --git a/tests/core/test_array.py b/tests/core/test_array.py index e406531..8268d80 100644 --- a/tests/core/test_array.py +++ b/tests/core/test_array.py @@ -58,7 +58,9 @@ def test_ndinterp(): y = ndinterp(x, xp, yp) assert np.shape(y) == (2, 2, 2) npt.assert_allclose( - y, [[[1.15, 1.25], [1.35, 1.45]], [[2.15, 2.25], [2.35, 2.45]]], atol=1e-15 + y, + [[[1.15, 1.25], [1.35, 1.45]], [[2.15, 2.25], [2.35, 2.45]]], + atol=1e-15, ) # test nd interpolation in middle axis @@ -74,7 +76,9 @@ def test_ndinterp(): y = ndinterp(x, xp, yp, axis=1) assert np.shape(y) == (2, 3, 1) npt.assert_allclose( - y, [[[1.15], [1.25], [1.35]], [[2.15], [2.25], [2.35]]], atol=1e-15 + y, + [[[1.15], [1.25], [1.35]], [[2.15], [2.25], [2.35]]], + atol=1e-15, ) x = [[0.5, 1.5, 2.5, 3.5], [3.5, 2.5, 1.5, 0.5], [0.5, 3.5, 1.5, 2.5]] diff --git a/tests/test_fits.py b/tests/test_fits.py index 055c0b3..2a14edf 100644 --- a/tests/test_fits.py +++ b/tests/test_fits.py @@ -1,19 +1,16 @@ -import pytest - # check if fitsio is available for testing import importlib.util -if importlib.util.find_spec("fitsio") is not None: - HAVE_FITSIO = True -else: - HAVE_FITSIO = False - -import glass.user as user import numpy as np +import pytest + +from glass import user + +HAVE_FITSIO = importlib.util.find_spec("fitsio") is not None def _test_append(fits, data, names): - """Write routine for FITS test cases""" + """Write routine for FITS test cases.""" cat_name = "CATALOG" if cat_name not in fits: fits.write_table(data, names=names, extname=cat_name) @@ -38,9 +35,11 @@ def test_basic_write(tmp_path): filename_tfits = "tfits.fits" # file created on the fly to test against with user.write_catalog(d / filename_gfits, ext="CATALOG") as out, fitsio.FITS( - d / filename_tfits, "rw", clobber=True - ) as myFits: - for i in range(0, my_max): + d / filename_tfits, + "rw", + clobber=True, + ) as myFits: # noqa: N806 + for i in range(my_max): array = np.arange(i, i + 1, delta) # array of size 1/delta array2 = np.arange(i + 1, i + 2, delta) # array of size 1/delta out.write(RA=array, RB=array2) @@ -49,7 +48,7 @@ def test_basic_write(tmp_path): _test_append(myFits, arrays, names) with fitsio.FITS(d / filename_gfits) as g_fits, fitsio.FITS( - d / filename_tfits + d / filename_tfits, ) as t_fits: glass_data = g_fits[1].read() test_data = t_fits[1].read() @@ -64,14 +63,15 @@ def test_write_exception(tmp_path): try: with user.write_catalog(d / filename, ext="CATALOG") as out: - for i in range(0, my_max): + for i in range(my_max): if i == except_int: - raise Exception("Unhandled exception") + msg = "Unhandled exception" + raise Exception(msg) # noqa: TRY002, TRY301 array = np.arange(i, i + 1, delta) # array of size 1/delta array2 = np.arange(i + 1, i + 2, delta) # array of size 1/delta out.write(RA=array, RB=array2) - except Exception: + except Exception: # noqa: BLE001 import fitsio with fitsio.FITS(d / filename) as hdul: @@ -79,11 +79,13 @@ def test_write_exception(tmp_path): assert data["RA"].size == except_int / delta assert data["RB"].size == except_int / delta - fitsMat = data["RA"].reshape(except_int, int(1 / delta)) - fitsMat2 = data["RB"].reshape(except_int, int(1 / delta)) - for i in range(0, except_int): + fitsMat = data["RA"].reshape(except_int, int(1 / delta)) # noqa: N806 + fitsMat2 = data["RB"].reshape(except_int, int(1 / delta)) # noqa: N806 + for i in range(except_int): array = np.arange( - i, i + 1, delta + i, + i + 1, + delta, ) # re-create array to compare to read data array2 = np.arange(i + 1, i + 2, delta) assert array.tolist() == fitsMat[i].tolist() @@ -91,9 +93,9 @@ def test_write_exception(tmp_path): @pytest.mark.skipif(not HAVE_FITSIO, reason="test requires fitsio") -def test_out_filename(tmp_path): +def test_out_filename(): import fitsio fits = fitsio.FITS(filename, "rw", clobber=True) - writer = user._FitsWriter(fits) - assert writer.fits._filename == filename + writer = user._FitsWriter(fits) # noqa: SLF001 + assert writer.fits._filename == filename # noqa: SLF001 diff --git a/tests/test_galaxies.py b/tests/test_galaxies.py index 3520df8..554f1c1 100644 --- a/tests/test_galaxies.py +++ b/tests/test_galaxies.py @@ -3,7 +3,9 @@ def test_redshifts(): from unittest.mock import Mock + import numpy as np + from glass.galaxies import redshifts # create a mock radial window function @@ -14,7 +16,8 @@ def test_redshifts(): # sample redshifts (scalar) z = redshifts(13, w) assert z.shape == (13,) - assert z.min() >= 0.0 and z.max() <= 1.0 + assert z.min() >= 0.0 + assert z.max() <= 1.0 # sample redshifts (array) z = redshifts([[1, 2], [3, 4]], w) @@ -23,18 +26,19 @@ def test_redshifts(): def test_redshifts_from_nz(): import numpy as np + from glass.galaxies import redshifts_from_nz # test sampling redshifts = redshifts_from_nz(10, [0, 1, 2, 3, 4], [1, 0, 0, 0, 0]) - assert np.all((0 <= redshifts) & (redshifts <= 1)) + assert np.all((0 <= redshifts) & (redshifts <= 1)) # noqa: SIM300 redshifts = redshifts_from_nz(10, [0, 1, 2, 3, 4], [0, 0, 1, 0, 0]) - assert np.all((1 <= redshifts) & (redshifts <= 3)) + assert np.all((1 <= redshifts) & (redshifts <= 3)) # noqa: SIM300 redshifts = redshifts_from_nz(10, [0, 1, 2, 3, 4], [0, 0, 0, 0, 1]) - assert np.all((3 <= redshifts) & (redshifts <= 4)) + assert np.all((3 <= redshifts) & (redshifts <= 4)) # noqa: SIM300 redshifts = redshifts_from_nz(10, [0, 1, 2, 3, 4], [0, 0, 1, 1, 1]) assert not np.any(redshifts <= 1) @@ -50,7 +54,7 @@ def test_redshifts_from_nz(): redshifts = redshifts_from_nz(count, z, nz) assert redshifts.shape == (count,) - assert np.all((0 <= redshifts) & (redshifts <= 1)) + assert np.all((0 <= redshifts) & (redshifts <= 1)) # noqa: SIM300 # case: extra dimensions from count @@ -94,6 +98,7 @@ def test_redshifts_from_nz(): def test_gaussian_phz(): import numpy as np + from glass.galaxies import gaussian_phz # test sampling diff --git a/tests/test_lensing.py b/tests/test_lensing.py index d1c6027..08edca9 100644 --- a/tests/test_lensing.py +++ b/tests/test_lensing.py @@ -7,7 +7,7 @@ def shells(): from glass.shells import RadialWindow - shells = [ + return [ RadialWindow([0.0, 1.0, 2.0], [0.0, 1.0, 0.0], 1.0), RadialWindow([1.0, 2.0, 3.0], [0.0, 1.0, 0.0], 2.0), RadialWindow([2.0, 3.0, 4.0], [0.0, 1.0, 0.0], 3.0), @@ -15,8 +15,6 @@ def shells(): RadialWindow([4.0, 5.0, 6.0], [0.0, 1.0, 0.0], 5.0), ] - return shells - @pytest.fixture def cosmo(): @@ -31,8 +29,7 @@ def ef(self, z): def xm(self, z, z2=None): if z2 is None: return np.array(z) * 1000 - else: - return (np.array(z2) - np.array(z)) * 1000 + return (np.array(z2) - np.array(z)) * 1000 return MockCosmology() @@ -72,6 +69,7 @@ def alpha(re, im): def test_deflect_many(): import healpix + from glass.lensing import deflect n = 1000 diff --git a/tests/test_points.py b/tests/test_points.py index 9247ec0..f24de0f 100644 --- a/tests/test_points.py +++ b/tests/test_points.py @@ -111,11 +111,13 @@ def test_position_weights(): if bias is not None: if np.ndim(bias) > np.ndim(expected): expected = np.expand_dims( - expected, tuple(range(np.ndim(expected), np.ndim(bias))) + expected, + tuple(range(np.ndim(expected), np.ndim(bias))), ) else: bias = np.expand_dims( - bias, tuple(range(np.ndim(bias), np.ndim(expected))) + bias, + tuple(range(np.ndim(bias), np.ndim(expected))), ) expected = bias * expected diff --git a/tests/test_shells.py b/tests/test_shells.py index ff92497..86abbd7 100644 --- a/tests/test_shells.py +++ b/tests/test_shells.py @@ -22,7 +22,7 @@ def test_tophat_windows(): def test_restrict(): - from glass.shells import restrict, RadialWindow + from glass.shells import RadialWindow, restrict # Gaussian test function z = np.linspace(0.0, 5.0, 1000) @@ -33,7 +33,8 @@ def test_restrict(): zr, fr = restrict(z, f, w) - assert zr[0] == w.za[0] and zr[-1] == w.za[-1] + assert zr[0] == w.za[0] + assert zr[-1] == w.za[-1] assert fr[0] == fr[-1] == 0.0 @@ -52,6 +53,7 @@ def test_restrict(): @pytest.mark.parametrize("method", ["lstsq", "nnls", "restrict"]) def test_partition(method): import numpy as np + from glass.shells import RadialWindow, partition shells = [