diff --git a/.github/workflows/regenerate-figures.yml b/.github/workflows/regenerate-figures.yml new file mode 100644 index 0000000..d09acd4 --- /dev/null +++ b/.github/workflows/regenerate-figures.yml @@ -0,0 +1,48 @@ +name: Regenerate MPL baseline figures 📊 + +on: + pull_request: + types: [labeled] + +permissions: + contents: write + packages: read + +jobs: + regenerate: + if: github.event.label.name == 'regenerate-figures' + runs-on: ubuntu-22.04 + container: ghcr.io/qcdlab/neopdf-container:latest + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Python package 🐍 + run: | + cd neopdf_pyapi + python3 -m venv --system-site-packages myenv + . myenv/bin/activate + pip install maturin==1.12.4 + maturin develop --extras test + + - name: Regenerate baseline figures 🖼️ + run: | + cd neopdf_pyapi + . myenv/bin/activate + export NEOPDF_DATA_PATH=/usr/share/LHAPDF + pytest --mpl-generate-path=tests/baseline tests/test_interpolations.py + + - name: Commit and push updated baselines + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config --global --add safe.directory "${GITHUB_WORKSPACE}" + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" + git add neopdf_pyapi/tests/baseline/ + git diff --staged --quiet || git commit -m "Regenerate MPL baseline figures" + git push origin HEAD:${{ github.head_ref }} diff --git a/.github/workflows/run-python.yml b/.github/workflows/run-python.yml index 21ff27a..524ca02 100644 --- a/.github/workflows/run-python.yml +++ b/.github/workflows/run-python.yml @@ -1,4 +1,4 @@ -name: Python Versions 🐍 +name: Check Python Versions 🐍 on: [push] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 19f1b97..76c8ad0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,6 +18,11 @@ repos: args: [--fix] - id: ruff-format args: [] + - repo: https://github.com/astral-sh/ty-pre-commit + rev: v0.0.49 + hooks: + - id: ty + args: [--no-python-downloads, --no-cache, --project, neopdf_pyapi] - repo: local hooks: - id: fmt diff --git a/neopdf_pyapi/pyproject.toml b/neopdf_pyapi/pyproject.toml index 78d544d..53d3c0c 100644 --- a/neopdf_pyapi/pyproject.toml +++ b/neopdf_pyapi/pyproject.toml @@ -22,7 +22,7 @@ dynamic = ["version"] readme = "README.md" [project.optional-dependencies] -test = ["pytest", "pytest-cov", "pytest-mpl", "matplotlib"] +test = ["pytest", "pytest-cov", "pytest-mpl", "matplotlib", "pydantic>=2.0"] [project.urls] homepage = "https://github.com/Radonirinaunimi/neopdf" @@ -42,3 +42,12 @@ addopts = [ '--strict-markers', ] markers = ["multipleversions: test Python API with multiple versions"] + +[tool.ty.environment] +python-version = "3.13" + +[tool.ty.rules] +# neopdf and lhapdf are PyO3/C extensions without stubs; surface real +# annotation issues without blocking on unresolvable module paths. +unresolved-import = "warn" +unresolved-attribute = "warn" diff --git a/neopdf_pyapi/tests/baseline/test_interpolation_visual_-2-ubar.png b/neopdf_pyapi/tests/baseline/test_interpolation_visual_-2-ubar.png index 07606af..e99dad4 100644 Binary files a/neopdf_pyapi/tests/baseline/test_interpolation_visual_-2-ubar.png and b/neopdf_pyapi/tests/baseline/test_interpolation_visual_-2-ubar.png differ diff --git a/neopdf_pyapi/tests/baseline/test_interpolation_visual_21-gluon.png b/neopdf_pyapi/tests/baseline/test_interpolation_visual_21-gluon.png index 73cb31d..bfb7fde 100644 Binary files a/neopdf_pyapi/tests/baseline/test_interpolation_visual_21-gluon.png and b/neopdf_pyapi/tests/baseline/test_interpolation_visual_21-gluon.png differ diff --git a/neopdf_pyapi/tests/conftest.py b/neopdf_pyapi/tests/conftest.py index dac32fd..66e74cd 100644 --- a/neopdf_pyapi/tests/conftest.py +++ b/neopdf_pyapi/tests/conftest.py @@ -1,79 +1,110 @@ -import lhapdf -import pytest +from __future__ import annotations + +import lhapdf # type: ignore[import-untyped] import numpy as np +import pytest + +from neopdf.pdf import PDF as NeoPDF # type: ignore[import-untyped] +from pydantic import BaseModel, Field +from typing import Any, Iterator, Protocol + + +class XQ2Range(BaseModel): + xmin: float = Field(gt=0) + xmax: float = Field(gt=0, le=1) + q2min: float = Field(gt=0) + q2max: float = Field(gt=0) + + +class PDFFactory(Protocol): + """Factory that loads (and caches) a single PDF member by set name.""" + + def __call__(self, pdfname: str) -> Any: ... + + +class PDFsFactory(Protocol): + """Factory that loads (and caches) all PDF members by set name.""" + + def __call__(self, pdfname: str) -> list[Any]: ... + + +class LazyPDFsFactory(Protocol): + """Factory that lazily streams PDF members from a compressed archive.""" + + def __call__(self, pdfname: str) -> Iterator[Any]: ... + + +class XQ2PointsFactory(Protocol): + """Factory that produces log-spaced (x, Q²) grid points from a range.""" -from neopdf.pdf import PDF as NeoPDF -from neopdf.pdf import LazyPDFs -from typing import List, Dict, Iterator + def __call__(self, r: XQ2Range) -> tuple[np.ndarray, np.ndarray]: ... @pytest.fixture(scope="session") -def neo_pdf(): - cached_pdf = {} +def neo_pdf() -> PDFFactory: + cached: dict[str, Any] = {} - def _init_pdf(pdfname: str) -> Dict[str, NeoPDF]: - if pdfname not in cached_pdf: - cached_pdf[pdfname] = NeoPDF(pdfname) - return cached_pdf[pdfname] + def _init(pdfname: str) -> Any: + if pdfname not in cached: + cached[pdfname] = NeoPDF(pdfname) + return cached[pdfname] - return _init_pdf + return _init @pytest.fixture(scope="session") -def neo_pdfs(): - cached_pdf = {} +def neo_pdfs() -> PDFsFactory: + cached: dict[str, list[Any]] = {} - def _init_pdf(pdfname: str) -> Dict[str, List[NeoPDF]]: - if pdfname not in cached_pdf: - cached_pdf[pdfname] = NeoPDF.mkPDFs(pdfname) - return cached_pdf[pdfname] + def _init(pdfname: str) -> list[Any]: + if pdfname not in cached: + cached[pdfname] = NeoPDF.mkPDFs(pdfname) + return cached[pdfname] - return _init_pdf + return _init @pytest.fixture(scope="session") -def neo_pdfs_lazy(): - cached_pdf = {} +def neo_pdfs_lazy() -> LazyPDFsFactory: + cached: dict[str, Iterator[Any]] = {} - def _init_pdf(pdfname: str) -> Dict[str, Iterator[LazyPDFs]]: - if pdfname not in cached_pdf: - cached_pdf[pdfname] = NeoPDF.mkPDFs_lazy(pdfname) - return cached_pdf[pdfname] + def _init(pdfname: str) -> Iterator[Any]: + if pdfname not in cached: + cached[pdfname] = NeoPDF.mkPDFs_lazy(pdfname) + return cached[pdfname] - return _init_pdf + return _init @pytest.fixture(scope="session") -def lha_pdf(): - cached_pdf = {} +def lha_pdf() -> PDFFactory: + cached: dict[str, Any] = {} - def _init_pdf(pdfname: str) -> Dict[str, lhapdf.PDF]: - if pdfname not in cached_pdf: - cached_pdf[pdfname] = lhapdf.mkPDF(pdfname) - return cached_pdf[pdfname] + def _init(pdfname: str) -> Any: + if pdfname not in cached: + cached[pdfname] = lhapdf.mkPDF(pdfname) # type: ignore[attr-defined] + return cached[pdfname] - return _init_pdf + return _init @pytest.fixture(scope="session") -def lha_pdfs(): - cached_pdf = {} +def lha_pdfs() -> PDFsFactory: + cached: dict[str, list[Any]] = {} - def _init_pdf(pdfname: str) -> Dict[str, List[lhapdf.PDF]]: - if pdfname not in cached_pdf: - cached_pdf[pdfname] = lhapdf.mkPDFs(pdfname) - return cached_pdf[pdfname] + def _init(pdfname: str) -> list[Any]: + if pdfname not in cached: + cached[pdfname] = lhapdf.mkPDFs(pdfname) # type: ignore[attr-defined] + return cached[pdfname] - return _init_pdf + return _init @pytest.fixture(scope="session") -def xq2_points(): - def _xq2_points( - xmin: float, xmax: float, q2min: float, q2max: float - ) -> tuple[np.ndarray, np.ndarray]: - xs = np.geomspace(xmin, xmax, num=200) - q2s = np.geomspace(q2min, q2max, num=200) +def xq2_points() -> XQ2PointsFactory: + def _xq2_points(r: XQ2Range) -> tuple[np.ndarray, np.ndarray]: + xs = np.geomspace(r.xmin, r.xmax, num=200) + q2s = np.geomspace(r.q2min, r.q2max, num=200) return xs, q2s return _xq2_points diff --git a/neopdf_pyapi/tests/test_converter.py b/neopdf_pyapi/tests/test_converter.py index 72fbb78..d62d560 100644 --- a/neopdf_pyapi/tests/test_converter.py +++ b/neopdf_pyapi/tests/test_converter.py @@ -1,10 +1,14 @@ +from __future__ import annotations + +import os import neopdf import pytest -import os + +from pathlib import Path @pytest.fixture -def create_dummy_files(tmp_path): +def create_dummy_files(tmp_path: Path) -> str: dummy_content = """ --- Some contents @@ -13,18 +17,13 @@ def create_dummy_files(tmp_path): lhapdf_dir = tmp_path / "LHAPDF" lhapdf_dir.mkdir() base_name = "NNPDF40_nnlo_as_01180" - files = [f"{base_name}.info", f"{base_name}_0001.dat"] - - for filename in files: - set_path = lhapdf_dir / filename - set_path.write_text(dummy_content) - + for filename in [f"{base_name}.info", f"{base_name}_0001.dat"]: + (lhapdf_dir / filename).write_text(dummy_content) return str(tmp_path) -def test_convert_lhapdf(create_dummy_files): - lhapdf_path = create_dummy_files - os.environ["LHAPDF_DATA_PATH"] = lhapdf_path - output_path = os.path.join(lhapdf_path, "test.neopdf.lz4") - neopdf.converter.convert_lhapdf("NNPDF40_nnlo_as_01180", output_path) +def test_convert_lhapdf(create_dummy_files: str) -> None: + os.environ["LHAPDF_DATA_PATH"] = create_dummy_files + output_path = os.path.join(create_dummy_files, "test.neopdf.lz4") + neopdf.converter.convert_lhapdf("NNPDF40_nnlo_as_01180", output_path) # type: ignore[attr-defined] assert os.path.exists(output_path) diff --git a/neopdf_pyapi/tests/test_gridpdf.py b/neopdf_pyapi/tests/test_gridpdf.py index 53fe990..89a2743 100644 --- a/neopdf_pyapi/tests/test_gridpdf.py +++ b/neopdf_pyapi/tests/test_gridpdf.py @@ -1,15 +1,20 @@ +from __future__ import annotations + import numpy as np -from neopdf.gridpdf import SubGrid, GridArray +from conftest import XQ2Range, XQ2PointsFactory +from neopdf.gridpdf import SubGrid, GridArray # type: ignore[import-untyped] + + +XQ2_RANGE = XQ2Range(xmin=1e-5, xmax=1.0, q2min=1.65, q2max=1.0e8) class TestGridPDF: - def test_subgrid(self, xq2_points): - xmin, xmax, q2min, q2max = (1e-5, 1.0, 1.65, 1.0e8) - xs, q2s = xq2_points(xmin, xmax, q2min, q2max) + def test_subgrid(self, xq2_points: XQ2PointsFactory) -> None: + xs, q2s = xq2_points(XQ2_RANGE) kts = [0.5, 1.0] - xis = [0.0] # dummy values - deltas = [0.0] # dummy values + xis = [0.0] + deltas = [0.0] nucleons = [1.0, 2.0] alphas = [0.118, 0.120] grid = np.random.rand( @@ -22,7 +27,6 @@ def test_subgrid(self, xq2_points): len(q2s), 1, ) - subgrid = SubGrid(xs, q2s, kts, xis, deltas, nucleons, alphas, grid) assert subgrid.alphas_range() == (0.118, 0.120) @@ -39,12 +43,11 @@ def test_subgrid(self, xq2_points): 1, ] - def test_gridarray(self, xq2_points): - xmin, xmax, q2min, q2max = (1e-5, 1.0, 1.65, 1.0e8) - xs, q2s = xq2_points(xmin, xmax, q2min, q2max) + def test_gridarray(self, xq2_points: XQ2PointsFactory) -> None: + xs, q2s = xq2_points(XQ2_RANGE) kts = [0.5, 1.0] - xis = [0.0] # dummy values - deltas = [0.0] # dummy values + xis = [0.0] + deltas = [0.0] nucleons = [1.0, 2.0] alphas = [0.118, 0.120] grid = np.random.rand( @@ -57,25 +60,21 @@ def test_gridarray(self, xq2_points): len(q2s), 1, ) - - subgrid1 = SubGrid(xs, q2s, kts, xis, deltas, nucleons, alphas, grid) - subgrid2 = SubGrid(xs, q2s, kts, xis, deltas, nucleons, alphas, grid) + subgrid = SubGrid(xs, q2s, kts, xis, deltas, nucleons, alphas, grid) pids = [21, -2, -1, 1, 2] - grid_array = GridArray(pids, [subgrid1, subgrid2]) + grid_array = GridArray(pids, [subgrid, subgrid]) assert grid_array.pids() == pids assert len(grid_array.subgrids()) == 2 - def test_subgrid_extra_ranges(self, xq2_points): - xmin, xmax, q2min, q2max = (1e-5, 1.0, 1.65, 1.0e8) - xs, q2s = xq2_points(xmin, xmax, q2min, q2max) + def test_subgrid_extra_ranges(self, xq2_points: XQ2PointsFactory) -> None: + xs, q2s = xq2_points(XQ2_RANGE) kts = [0.5, 1.0] xis = [0.1, 0.2] deltas = [0.3, 0.4] nucleons = [1.0, 2.0] alphas = [0.118, 0.120] - grid = np.random.rand(2, 2, 2, 2, 2, len(xs), len(q2s), 1) subgrid = SubGrid(xs, q2s, kts, xis, deltas, nucleons, alphas, grid) diff --git a/neopdf_pyapi/tests/test_interpolations.py b/neopdf_pyapi/tests/test_interpolations.py index 56e7b8b..9f110c7 100644 --- a/neopdf_pyapi/tests/test_interpolations.py +++ b/neopdf_pyapi/tests/test_interpolations.py @@ -1,16 +1,25 @@ +from __future__ import annotations + import numpy as np import pytest + import matplotlib.pyplot as plt from pathlib import Path -from typing import Union, List, Optional, Dict, Any +from matplotlib.figure import Figure +from pydantic import BaseModel, Field -from neopdf.pdf import PDF as NEOPDF -from neopdf.writer import compress -from neopdf.gridpdf import GridArray, SubGrid -from neopdf.metadata import InterpolatorType, SetType, MetaData, PhysicsParameters +from neopdf.gridpdf import GridArray, SubGrid # type: ignore[import-untyped] +from neopdf.metadata import ( # type: ignore[import-untyped] + InterpolatorType, + MetaData, + PhysicsParameters, + SetType, +) +from neopdf.pdf import PDF as NEOPDF # type: ignore[import-untyped] +from neopdf.writer import compress # type: ignore[import-untyped] -def f_ubar_abmp16_nnlo(x: Union[float, np.ndarray]) -> Union[float, np.ndarray]: +def f_ubar_abmp16_nnlo(x: float | np.ndarray) -> float | np.ndarray: """Analytic u-bar parametrization from ABMP16 NNLO.""" return ( 0.0703 @@ -19,61 +28,65 @@ def f_ubar_abmp16_nnlo(x: Union[float, np.ndarray]) -> Union[float, np.ndarray]: ) -def f_gluon_herapdf20_nlo(x: Union[float, np.ndarray]) -> Union[float, np.ndarray]: +def f_gluon_herapdf20_nlo(x: float | np.ndarray) -> float | np.ndarray: """Analytic gluon parametrization from HERAPDF 2.0 NLO.""" - return ( - 4.34 * x ** (-0.015) * (1 - x) ** 9.11 - 1.048 * x ** (-0.167) * (1 - x) ** 25.0 - ) + return 4.34 * x**-0.015 * (1 - x) ** 9.11 - 1.048 * x**-0.167 * (1 - x) ** 25.0 def remove_near_nodes( x_samples: np.ndarray, nodes: np.ndarray, rel_tol: float = 1e-1, - abs_tol: Optional[float] = None, + abs_tol: float | None = None, ) -> np.ndarray: - """Remove points from x_samples that are too close to grid - nodes to avoid trivial matches. + """Remove points from *x_samples* that are too close to grid nodes. + + Prevents trivial (on-node) matches that would inflate accuracy metrics. """ x = np.asarray(x_samples) - nodes = np.sort(np.asarray(nodes)) - diffs = np.diff(nodes) + sorted_nodes = np.sort(np.asarray(nodes)) + diffs = np.diff(sorted_nodes) valid_diffs = diffs[diffs > 0] - min_spacing = np.min(valid_diffs) if valid_diffs.size > 0 else np.min(np.abs(nodes)) + min_spacing = ( + np.min(valid_diffs) if valid_diffs.size > 0 else np.min(np.abs(sorted_nodes)) + ) if abs_tol is None: abs_tol = max(min_spacing * rel_tol, np.finfo(float).eps) - keep_mask = np.ones_like(x, dtype=bool) - for n in nodes: - keep_mask &= np.abs(x - n) > abs_tol - return x[keep_mask] + keep = np.ones_like(x, dtype=bool) + for n in sorted_nodes: + keep &= np.abs(x - n) > abs_tol + return x[keep] def create_cheby_grid(n_points: int, x_min: float, x_max: float) -> np.ndarray: - """Create a Chebyshev grid with logarithmic spacing.""" - u_min = np.log(x_min) - u_max = np.log(x_max) - grid_points = [] - for j in range(n_points): - t_j = np.cos(np.pi * (n_points - 1 - j) / (n_points - 1)) - u_j = u_min + (u_max - u_min) * (t_j + 1.0) / 2.0 - grid_points.append(np.exp(u_j)) - return np.array(grid_points) + """Chebyshev nodes mapped to [log(x_min), log(x_max)].""" + u_min, u_max = np.log(x_min), np.log(x_max) + ts = np.cos(np.pi * np.arange(n_points - 1, -1, -1) / (n_points - 1)) + return np.exp(u_min + (u_max - u_min) * (ts + 1.0) / 2.0) + + +class InterpolationSetup(BaseModel): + """Holds the temporary paths and grid parameters created by the fixture.""" + + model_config = {"arbitrary_types_allowed": True} + + path: Path + x_nodes: np.ndarray = Field(repr=False) + n_cheb_nodes: int = Field(gt=0) + n_cheb_nodes_b: int = Field(gt=0) class TestInterpolations: @pytest.fixture(scope="class") def interpolation_sets( - self, - tmp_path_factory: pytest.TempPathFactory, - ) -> Dict[str, Any]: - """Fixture to generate toy NeoPDF sets with different interpolation - types. - """ + self, tmp_path_factory: pytest.TempPathFactory + ) -> InterpolationSetup: + """Generate toy NeoPDF sets with each supported interpolation type.""" tmp_dir = tmp_path_factory.mktemp("interp_sets") N_NODES = 100 - N_CHEB_NODES = 50 - N_CHEB_NODES_B = 25 + N_CHEB = 50 + N_CHEB_B = 25 q2_values = np.logspace(1, 6, 40) x_values = np.logspace(-6, 0, N_NODES) @@ -93,57 +106,54 @@ def interpolation_sets( ) x_cheb = [ - create_cheby_grid(N_CHEB_NODES, 1e-6, 0.2), - create_cheby_grid(N_CHEB_NODES, 0.2, 1.0), + create_cheby_grid(N_CHEB, 1e-6, 0.2), + create_cheby_grid(N_CHEB, 0.2, 1.0), ] self._generate_pdf( tmp_dir, - f"logchebyshev_{N_CHEB_NODES}", + f"logchebyshev_{N_CHEB}", InterpolatorType.LogChebyshev, x_cheb, q2_values, ) x_cheb_b = [ - create_cheby_grid(N_CHEB_NODES_B, 1e-6, 0.2), - create_cheby_grid(N_CHEB_NODES_B, 0.2, 1.0), + create_cheby_grid(N_CHEB_B, 1e-6, 0.2), + create_cheby_grid(N_CHEB_B, 0.2, 1.0), ] self._generate_pdf( tmp_dir, - f"logchebyshev_{N_CHEB_NODES_B}", + f"logchebyshev_{N_CHEB_B}", InterpolatorType.LogChebyshev, x_cheb_b, q2_values, ) - - return { - "path": tmp_dir, - "x_nodes": x_values, - "N_CHEB_NODES": N_CHEB_NODES, - "N_CHEB_NODES_B": N_CHEB_NODES_B, - } + return InterpolationSetup( + path=tmp_dir, + x_nodes=x_values, + n_cheb_nodes=N_CHEB, + n_cheb_nodes_b=N_CHEB_B, + ) def _generate_pdf( self, path: Path, name: str, interp_type: InterpolatorType, - x_vals_list: List[np.ndarray], + x_vals_list: list[np.ndarray], q2_vals: np.ndarray, ) -> None: - global_x_min = min(xv.min() for xv in x_vals_list) - global_x_max = max(xv.max() for xv in x_vals_list) - - meta = self._get_metadata(interp_type, global_x_min, global_x_max, q2_vals) + x_min = min(xv.min() for xv in x_vals_list) + x_max = max(xv.max() for xv in x_vals_list) + meta = self._build_metadata(interp_type, x_min, x_max, q2_vals) subgrids = [self._create_subgrid(xv, q2_vals) for xv in x_vals_list] - grid_member = GridArray(pids=[-2, 21], subgrids=subgrids) compress( - grids=[grid_member], + grids=[GridArray(pids=[-2, 21], subgrids=subgrids)], metadata=meta, path=str(path / f"interp_test_{name}.neopdf.lz4"), ) - def _get_metadata( + def _build_metadata( self, interp_type: InterpolatorType, x_min: float, @@ -156,23 +166,22 @@ def _get_metadata( alphas_order_qcd=2, m_z=91.1876, ) - _as_q = np.geomspace(np.sqrt(q2_vals.min()), np.sqrt(q2_vals.max()), 6) - + q_vals = np.geomspace(np.sqrt(q2_vals.min()), np.sqrt(q2_vals.max()), 6) return MetaData( set_desc="Toy NeoPDF set", set_index=123456, num_members=1, x_min=x_min, x_max=x_max, - q_min=np.sqrt(q2_vals.min()), - q_max=np.sqrt(q2_vals.max()), + q_min=float(np.sqrt(q2_vals.min())), + q_max=float(np.sqrt(q2_vals.max())), xsi_min=0.0, xsi_max=0.0, delta_min=0.0, delta_max=0.0, flavors=[-2, 21], format="neopdf", - alphas_q_values=_as_q, + alphas_q_values=q_vals, alphas_vals=np.random.uniform(0.1, 0.2, 6), polarised=False, set_type=SetType.SpaceLike, @@ -182,14 +191,10 @@ def _get_metadata( def _create_subgrid(self, x_vals: np.ndarray, q2_vals: np.ndarray) -> SubGrid: pids = [-2, 21] - xq2_flavors = [] - for pid in pids: - xf_func = f_gluon_herapdf20_nlo if pid == 21 else f_ubar_abmp16_nnlo - xq2_array = np.zeros((x_vals.size, q2_vals.size)) - for i, x in enumerate(x_vals): - xq2_array[i, :] = xf_func(x) - xq2_flavors.append(xq2_array) - + xf_funcs = {21: f_gluon_herapdf20_nlo, -2: f_ubar_abmp16_nnlo} + xq2_flavors = [ + np.outer(xf_funcs[pid](x_vals), np.ones(q2_vals.size)) for pid in pids + ] grid = np.array(xq2_flavors).reshape( 1, 1, @@ -214,36 +219,30 @@ def _create_subgrid(self, x_vals: np.ndarray, q2_vals: np.ndarray) -> SubGrid: @pytest.mark.mpl_image_compare(baseline_dir="baseline", tolerance=10) @pytest.mark.parametrize("pid, label", [(21, "gluon"), (-2, "ubar")]) def test_interpolation_visual( - self, interpolation_sets: Dict[str, Any], pid: int, label: str - ) -> plt.Figure: - """Generates error plots and checks numerical consistency.""" - base_path = interpolation_sets["path"] - - pdf_lin = NEOPDF(str(base_path / "interp_test_logbilinear.neopdf.lz4")) - pdf_cub = NEOPDF(str(base_path / "interp_test_logbicubic.neopdf.lz4")) + self, interpolation_sets: InterpolationSetup, pid: int, label: str + ) -> Figure: + """Generate relative-error plots and check numerical consistency.""" + p = interpolation_sets.path + n_cheb = interpolation_sets.n_cheb_nodes + n_cheb_b = interpolation_sets.n_cheb_nodes_b - n_cheb = interpolation_sets["N_CHEB_NODES"] - n_cheb_b = interpolation_sets["N_CHEB_NODES_B"] - pdf_cheb = NEOPDF( - str(base_path / f"interp_test_logchebyshev_{n_cheb}.neopdf.lz4") - ) - pdf_cheb_b = NEOPDF( - str(base_path / f"interp_test_logchebyshev_{n_cheb_b}.neopdf.lz4") - ) + pdf_lin = NEOPDF(str(p / "interp_test_logbilinear.neopdf.lz4")) + pdf_cub = NEOPDF(str(p / "interp_test_logbicubic.neopdf.lz4")) + pdf_cheb = NEOPDF(str(p / f"interp_test_logchebyshev_{n_cheb}.neopdf.lz4")) + pdf_cheb_b = NEOPDF(str(p / f"interp_test_logchebyshev_{n_cheb_b}.neopdf.lz4")) x_tests = np.logspace(-6, 0, 250) - x_clean = remove_near_nodes(x_tests, interpolation_sets["x_nodes"]) - - xf_ref_func = f_gluon_herapdf20_nlo if pid == 21 else f_ubar_abmp16_nnlo - ref = xf_ref_func(x_clean) + x_clean = remove_near_nodes(x_tests, interpolation_sets.x_nodes) q2_test = 1e2 + xf_ref = f_gluon_herapdf20_nlo if pid == 21 else f_ubar_abmp16_nnlo + ref = xf_ref(x_clean) + res_lin = np.array([pdf_lin.xfxQ2(pid, x, q2_test) for x in x_clean]) res_cub = np.array([pdf_cub.xfxQ2(pid, x, q2_test) for x in x_clean]) res_cheb = np.array([pdf_cheb.xfxQ2(pid, x, q2_test) for x in x_clean]) res_cheb_b = np.array([pdf_cheb_b.xfxQ2(pid, x, q2_test) for x in x_clean]) - # Plotting fig, ax = plt.subplots(figsize=(5.6, 3.9)) ax.loglog( x_clean, @@ -280,19 +279,17 @@ def test_interpolation_visual( color="C1", alpha=0.7, ) - ax.set_xlabel("x") ax.set_ylabel(r"|f_interp / f_ref - 1|") ax.set_title(f"Interpolation Error - {label.upper()}") ax.set_xlim(1e-6, 1) ax.set_ylim(1e-16, 1) - shift_ylegend = 0.20 if label == "ubar" else 0.35 + shift = 0.20 if label == "ubar" else 0.35 ax.legend( fontsize=10, ncols=2, loc="center", frameon=False, - bbox_to_anchor=(0.52, shift_ylegend), + bbox_to_anchor=(0.52, shift), ) - return fig diff --git a/neopdf_pyapi/tests/test_lhapdf_compatibility.py b/neopdf_pyapi/tests/test_lhapdf_compatibility.py index 9e47e17..c49a9f7 100644 --- a/neopdf_pyapi/tests/test_lhapdf_compatibility.py +++ b/neopdf_pyapi/tests/test_lhapdf_compatibility.py @@ -1,187 +1,180 @@ +from __future__ import annotations + + +import lhapdf # type: ignore[import-untyped] +import neopdf import numpy as np import pytest -import lhapdf -import neopdf -from neopdf.uncertainty import uncertainty, CL_1_SIGMA +from neopdf.uncertainty import CL_1_SIGMA, uncertainty # type: ignore[import-untyped] +from typing import Any, TypedDict PDF_SET = "NNPDF40_nnlo_as_01180" MEMBER = 7 @pytest.fixture(scope="module") -def neo_ps(): +def neo_ps() -> Any: return neopdf.getPDFSet(PDF_SET) @pytest.fixture(scope="module") -def lha_ps(): - return lhapdf.getPDFSet(PDF_SET) +def lha_ps() -> Any: + return lhapdf.getPDFSet(PDF_SET) # type: ignore[attr-defined] @pytest.fixture(scope="module") -def neo_p(): +def neo_p() -> Any: return neopdf.mkPDF(PDF_SET, MEMBER) @pytest.fixture(scope="module") -def lha_p(): - return lhapdf.mkPDF(PDF_SET, MEMBER) +def lha_p() -> Any: + return lhapdf.mkPDF(PDF_SET, MEMBER) # type: ignore[attr-defined] class TestModuleLevelFunctions: - def test_verbosity_roundtrip(self): + def test_verbosity_roundtrip(self) -> None: neopdf.setVerbosity(3) assert neopdf.verbosity() == 3 neopdf.setVerbosity(0) assert neopdf.verbosity() == 0 - def test_get_pdf_set_returns_pdfset(self): - ps = neopdf.getPDFSet(PDF_SET) - assert isinstance(ps, neopdf.PDFSet) + def test_get_pdf_set_returns_pdfset(self) -> None: + assert isinstance(neopdf.getPDFSet(PDF_SET), neopdf.PDFSet) - def test_mk_pdf_returns_pdf(self): - p = neopdf.mkPDF(PDF_SET, 0) - assert isinstance(p, neopdf.pdf.PDF) + def test_mk_pdf_returns_pdf(self) -> None: + assert isinstance(neopdf.mkPDF(PDF_SET, 0), neopdf.pdf.PDF) - def test_mk_pdf_default_member(self): - p = neopdf.mkPDF(PDF_SET) - assert p.memberID == 0 + def test_mk_pdf_default_member(self) -> None: + assert neopdf.mkPDF(PDF_SET).memberID == 0 - def test_mk_pdfs_length_matches_lhapdf(self): - neo = neopdf.mkPDFs(PDF_SET) - lha = lhapdf.mkPDFs(PDF_SET) - assert len(neo) == len(lha) + def test_mk_pdfs_length_matches_lhapdf(self) -> None: + assert len(neopdf.mkPDFs(PDF_SET)) == len(lhapdf.mkPDFs(PDF_SET)) # type: ignore[attr-defined] - def test_available_pdf_sets_is_list_of_str(self): - sets = neopdf.availablePDFSets() + def test_available_pdf_sets_is_list_of_str(self) -> None: + sets: list[Any] = neopdf.availablePDFSets() assert isinstance(sets, list) assert all(isinstance(s, str) for s in sets) - def test_available_pdf_sets_contains_test_set(self): + def test_available_pdf_sets_contains_test_set(self) -> None: assert PDF_SET in neopdf.availablePDFSets() - def test_paths_returns_list_of_str(self): - ps = neopdf.paths() + def test_paths_returns_list_of_str(self) -> None: + ps: list[Any] = neopdf.paths() assert isinstance(ps, list) assert len(ps) >= 1 assert all(isinstance(p, str) for p in ps) - def test_set_paths_no_op(self): + def test_set_paths_no_op(self) -> None: neopdf.setPaths(["/tmp/fake"]) - def test_paths_append_no_op(self): + def test_paths_append_no_op(self) -> None: neopdf.pathsAppend("/tmp/fake") - def test_paths_prepend_no_op(self): + def test_paths_prepend_no_op(self) -> None: neopdf.pathsPrepend("/tmp/fake") class TestPDFSetClass: - def test_name(self, neo_ps, lha_ps): + def test_name(self, neo_ps: Any, lha_ps: Any) -> None: assert neo_ps.name == lha_ps.name - def test_size(self, neo_ps, lha_ps): + def test_size(self, neo_ps: Any, lha_ps: Any) -> None: assert neo_ps.size == lha_ps.size - def test_lhapdf_id(self, neo_ps, lha_ps): + def test_lhapdf_id(self, neo_ps: Any, lha_ps: Any) -> None: assert neo_ps.lhapdfID == lha_ps.lhapdfID - def test_error_type(self, neo_ps, lha_ps): + def test_error_type(self, neo_ps: Any, lha_ps: Any) -> None: assert neo_ps.errorType == lha_ps.errorType - def test_description_non_empty(self, neo_ps): + def test_description_non_empty(self, neo_ps: Any) -> None: assert len(neo_ps.description) > 0 - def test_len(self, neo_ps, lha_ps): + def test_len(self, neo_ps: Any, lha_ps: Any) -> None: assert len(neo_ps) == len(lha_ps) - def test_mkpdf_member_id(self, neo_ps, lha_ps): - neo_p = neo_ps.mkPDF(MEMBER) - lha_p = lha_ps.mkPDF(MEMBER) - assert neo_p.memberID == lha_p.memberID + def test_mkpdf_member_id(self, neo_ps: Any, lha_ps: Any) -> None: + assert neo_ps.mkPDF(MEMBER).memberID == lha_ps.mkPDF(MEMBER).memberID - def test_mkpdf_lhapdf_id(self, neo_ps, lha_ps): - neo_p = neo_ps.mkPDF(MEMBER) - lha_p = lha_ps.mkPDF(MEMBER) - assert neo_p.lhapdfID == lha_p.lhapdfID + def test_mkpdf_lhapdf_id(self, neo_ps: Any, lha_ps: Any) -> None: + assert neo_ps.mkPDF(MEMBER).lhapdfID == lha_ps.mkPDF(MEMBER).lhapdfID - def test_mkpdfs_count(self, neo_ps, lha_ps): - neo_members = neo_ps.mkPDFs() - lha_members = lha_ps.mkPDFs() - assert len(neo_members) == len(lha_members) + def test_mkpdfs_count(self, neo_ps: Any, lha_ps: Any) -> None: + assert len(neo_ps.mkPDFs()) == len(lha_ps.mkPDFs()) - def test_mkpdfs_member_ids(self, neo_ps): - members = neo_ps.mkPDFs() - for i, m in enumerate(members): + def test_mkpdfs_member_ids(self, neo_ps: Any) -> None: + for i, m in enumerate(neo_ps.mkPDFs()): assert m.memberID == i @pytest.mark.parametrize( - "x,q,pid", - [(1e-4, 10.0, 21), (0.1, 100.0, 1), (0.5, 1000.0, -5)], + "x,q,pid", [(1e-4, 10.0, 21), (0.1, 100.0, 1), (0.5, 1000.0, -5)] ) class TestPDFXfxQ: - def test_xfxq_equals_xfxq2(self, x, q, pid, neo_p): + def test_xfxq_equals_xfxq2(self, x: float, q: float, pid: int, neo_p: Any) -> None: """xfxQ(id, x, Q) must equal xfxQ2(id, x, Q²).""" assert neo_p.xfxQ(pid, x, q) == neo_p.xfxQ2(pid, x, q * q) - def test_xfxq_matches_lhapdf(self, x, q, pid, neo_p, lha_p): + def test_xfxq_matches_lhapdf( + self, x: float, q: float, pid: int, neo_p: Any, lha_p: Any + ) -> None: np.testing.assert_equal(neo_p.xfxQ(pid, x, q), lha_p.xfxQ(pid, x, q)) class TestPDFAlphaS: @pytest.mark.parametrize("q", [2.0, 10.0, 91.2, 1000.0]) - def test_alphasq_equals_alphasq2(self, q, neo_p): + def test_alphasq_equals_alphasq2(self, q: float, neo_p: Any) -> None: assert neo_p.alphasQ(q) == neo_p.alphasQ2(q * q) @pytest.mark.parametrize("q", [2.0, 10.0, 91.2, 1000.0]) - def test_alphasq_matches_lhapdf(self, q, neo_p, lha_p): + def test_alphasq_matches_lhapdf(self, q: float, neo_p: Any, lha_p: Any) -> None: np.testing.assert_equal(neo_p.alphasQ(q), lha_p.alphasQ(q)) class TestPDFIdentity: - def test_member_id(self, neo_p, lha_p): + def test_member_id(self, neo_p: Any, lha_p: Any) -> None: assert neo_p.memberID == lha_p.memberID - def test_lhapdf_id(self, neo_p, lha_p): + def test_lhapdf_id(self, neo_p: Any, lha_p: Any) -> None: assert neo_p.lhapdfID == lha_p.lhapdfID - def test_order_qcd(self, neo_p, lha_p): + def test_order_qcd(self, neo_p: Any, lha_p: Any) -> None: assert neo_p.orderQCD == lha_p.orderQCD - def test_quark_mass_charm(self, neo_p, lha_p): + def test_quark_mass_charm(self, neo_p: Any, lha_p: Any) -> None: np.testing.assert_allclose(neo_p.quarkMass(4), lha_p.quarkMass(4)) - def test_quark_mass_bottom(self, neo_p, lha_p): + def test_quark_mass_bottom(self, neo_p: Any, lha_p: Any) -> None: np.testing.assert_allclose(neo_p.quarkMass(5), lha_p.quarkMass(5)) - def test_quark_threshold_equals_mass(self, neo_p): + def test_quark_threshold_equals_mass(self, neo_p: Any) -> None: assert neo_p.quarkThreshold(4) == neo_p.quarkMass(4) assert neo_p.quarkThreshold(5) == neo_p.quarkMass(5) - def test_x_max(self, neo_p, lha_p): + def test_x_max(self, neo_p: Any, lha_p: Any) -> None: np.testing.assert_allclose(neo_p.xMax, lha_p.xMax) - def test_q2_min(self, neo_p, lha_p): + def test_q2_min(self, neo_p: Any, lha_p: Any) -> None: np.testing.assert_allclose(neo_p.q2Min, lha_p.q2Min) - def test_q2_max(self, neo_p, lha_p): + def test_q2_max(self, neo_p: Any, lha_p: Any) -> None: np.testing.assert_allclose(neo_p.q2Max, lha_p.q2Max) class TestPDFFlavors: - def test_flavors_match_lhapdf(self, neo_p, lha_p): + def test_flavors_match_lhapdf(self, neo_p: Any, lha_p: Any) -> None: assert sorted(neo_p.flavors()) == sorted(lha_p.flavors()) - def test_has_flavor_gluon(self, neo_p, lha_p): + def test_has_flavor_gluon(self, neo_p: Any, lha_p: Any) -> None: assert neo_p.hasFlavor(21) == lha_p.hasFlavor(21) - def test_has_flavor_unknown(self, neo_p, lha_p): + def test_has_flavor_unknown(self, neo_p: Any, lha_p: Any) -> None: assert neo_p.hasFlavor(99) == lha_p.hasFlavor(99) @pytest.mark.parametrize("pid", [-5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 21]) - def test_has_flavor_all_standard(self, pid, neo_p): + def test_has_flavor_all_standard(self, pid: int, neo_p: Any) -> None: assert neo_p.hasFlavor(pid) is True @@ -195,98 +188,96 @@ class TestPDFRangeChecks: (1e-4, 1e13, False), ], ) - def test_in_range_xq2_matches_lhapdf(self, x, q2, expected, neo_p, lha_p): + def test_in_range_xq2_matches_lhapdf( + self, x: float, q2: float, expected: bool, neo_p: Any, lha_p: Any + ) -> None: assert neo_p.inRangeXQ2(x, q2) == lha_p.inRangeXQ2(x, q2) - def test_in_range_xq_matches_lhapdf(self, neo_p, lha_p): + def test_in_range_xq_matches_lhapdf(self, neo_p: Any, lha_p: Any) -> None: for x, q in [(1e-4, 10.0), (0.5, 100.0), (2.0, 10.0)]: assert neo_p.inRangeXQ(x, q) == lha_p.inRangeXQ(x, q) - def test_in_range_x_matches_lhapdf(self, neo_p, lha_p): + def test_in_range_x_matches_lhapdf(self, neo_p: Any, lha_p: Any) -> None: for x in [1e-6, 0.1, 0.9, 1.5]: assert neo_p.inRangeX(x) == lha_p.inRangeX(x) - def test_in_range_q2_matches_lhapdf(self, neo_p, lha_p): + def test_in_range_q2_matches_lhapdf(self, neo_p: Any, lha_p: Any) -> None: for q2 in [1.0, 100.0, 1e10, 1e13]: assert neo_p.inRangeQ2(q2) == lha_p.inRangeQ2(q2) - def test_in_range_q_matches_lhapdf(self, neo_p, lha_p): + def test_in_range_q_matches_lhapdf(self, neo_p: Any, lha_p: Any) -> None: for q in [1.0, 10.0, 1e5, 1e7]: assert neo_p.inRangeQ(q) == lha_p.inRangeQ(q) class TestPDFSetProperty: - def test_set_property_not_none(self, neo_p): + def test_set_property_not_none(self, neo_p: Any) -> None: assert neo_p.set is not None - def test_set_property_is_pdfset(self, neo_p): + def test_set_property_is_pdfset(self, neo_p: Any) -> None: assert isinstance(neo_p.set, neopdf.PDFSet) - def test_set_property_name(self, neo_p): + def test_set_property_name(self, neo_p: Any) -> None: assert neo_p.set.name == PDF_SET - def test_set_property_none_when_loaded_by_lhaid(self): - from neopdf.pdf import PDF + def test_set_property_none_when_loaded_by_lhaid(self) -> None: + from neopdf.pdf import PDF # type: ignore[import-untyped] + + assert PDF.mkPDF_lhaid(14400).set is None + - p = PDF.mkPDF_lhaid(14400) - assert p.set is None +class PDFSetSummary(TypedDict): + name: str + size: int + lhapdfID: int + errorType: str -def _compute_xfxQ2_central(backend, pdfname, pid, x, q2): - pdf = backend.mkPDF(pdfname, 0) - return pdf.xfxQ2(pid, x, q2) +def _compute_xfxq2_central( + backend: Any, pdfname: str, pid: int, x: float, q2: float +) -> float: + return backend.mkPDF(pdfname, 0).xfxQ2(pid, x, q2) -def _collect_all_member_values(backend, pdfname, pid, x, q2): - pdfs = backend.mkPDFs(pdfname) - return [p.xfxQ2(pid, x, q2) for p in pdfs] +def _collect_all_member_values( + backend: Any, pdfname: str, pid: int, x: float, q2: float +) -> list[float]: + return [p.xfxQ2(pid, x, q2) for p in backend.mkPDFs(pdfname)] -def _inspect_set(backend, pdfname): +def _inspect_set(backend: Any, pdfname: str) -> PDFSetSummary: ps = backend.getPDFSet(pdfname) - return { - "name": ps.name, - "size": ps.size, - "lhapdfID": ps.lhapdfID, - "errorType": ps.errorType, - } + return PDFSetSummary( + name=ps.name, size=ps.size, lhapdfID=ps.lhapdfID, errorType=ps.errorType + ) -def _check_kinematics(backend, pdfname, x, q): - pdf = backend.mkPDF(pdfname, 0) - return pdf.inRangeXQ(x, q) +def _check_kinematics(backend: Any, pdfname: str, x: float, q: float) -> bool: + return backend.mkPDF(pdfname, 0).inRangeXQ(x, q) -@pytest.mark.parametrize( - "pid,x,q2", - [(21, 1e-4, 100.0), (1, 0.1, 1e4), (-3, 0.3, 1e6)], -) +@pytest.mark.parametrize("pid,x,q2", [(21, 1e-4, 100.0), (1, 0.1, 1e4), (-3, 0.3, 1e6)]) class TestBackendSwap: - def test_central_member_xfxq2(self, pid, x, q2): - lha_val = _compute_xfxQ2_central(lhapdf, PDF_SET, pid, x, q2) - neo_val = _compute_xfxQ2_central(neopdf, PDF_SET, pid, x, q2) + def test_central_member_xfxq2(self, pid: int, x: float, q2: float) -> None: + lha_val = _compute_xfxq2_central(lhapdf, PDF_SET, pid, x, q2) + neo_val = _compute_xfxq2_central(neopdf, PDF_SET, pid, x, q2) np.testing.assert_equal(neo_val, lha_val) - def test_all_member_values(self, pid, x, q2): + def test_all_member_values(self, pid: int, x: float, q2: float) -> None: lha_vals = _collect_all_member_values(lhapdf, PDF_SET, pid, x, q2) neo_vals = _collect_all_member_values(neopdf, PDF_SET, pid, x, q2) np.testing.assert_array_equal(neo_vals, lha_vals) class TestBackendSwapMetadata: - def test_set_metadata(self): - lha_meta = _inspect_set(lhapdf, PDF_SET) - neo_meta = _inspect_set(neopdf, PDF_SET) - assert neo_meta == lha_meta + def test_set_metadata(self) -> None: + assert _inspect_set(neopdf, PDF_SET) == _inspect_set(lhapdf, PDF_SET) - def test_kinematics_in_range(self): + def test_kinematics_in_range(self) -> None: for x, q in [(1e-3, 10.0), (0.5, 300.0), (2.0, 10.0)]: - assert _check_kinematics( - neopdf, - PDF_SET, - x, - q, - ) == _check_kinematics(lhapdf, PDF_SET, x, q) + assert _check_kinematics(neopdf, PDF_SET, x, q) == _check_kinematics( + lhapdf, PDF_SET, x, q + ) @pytest.mark.parametrize("pdfname", ["CT18NNLO_as_0118", "MSHT20qed_an3lo"]) @@ -302,60 +293,57 @@ class TestHasKeyGetEntry: "Format", ] - def test_has_key_mandatory(self, pdfname): + def test_has_key_mandatory(self, pdfname: str) -> None: neo_ps = neopdf.getPDFSet(pdfname) for key in self.MANDATORY_KEYS: assert neo_ps.has_key(key), f"PDFSet missing mandatory key '{key}'" - def test_has_key_absent(self, pdfname): - neo_ps = neopdf.getPDFSet(pdfname) - assert not neo_ps.has_key("NonExistentKey_xyz") + def test_has_key_absent(self, pdfname: str) -> None: + assert not neopdf.getPDFSet(pdfname).has_key("NonExistentKey_xyz") - def test_get_entry_num_members(self, pdfname): - lha_ps = lhapdf.getPDFSet(pdfname) + def test_get_entry_num_members(self, pdfname: str) -> None: + lha_ps = lhapdf.getPDFSet(pdfname) # type: ignore[attr-defined] neo_ps = neopdf.getPDFSet(pdfname) assert int(neo_ps.get_entry("NumMembers")) == int( lha_ps.get_entry("NumMembers") ) - def test_get_entry_error_type(self, pdfname): - lha_ps = lhapdf.getPDFSet(pdfname) + def test_get_entry_error_type(self, pdfname: str) -> None: + lha_ps = lhapdf.getPDFSet(pdfname) # type: ignore[attr-defined] neo_ps = neopdf.getPDFSet(pdfname) assert neo_ps.get_entry("ErrorType") == lha_ps.get_entry("ErrorType") - def test_get_entry_raises_key_error(self, pdfname): - neo_ps = neopdf.getPDFSet(pdfname) + def test_get_entry_raises_key_error(self, pdfname: str) -> None: with pytest.raises(KeyError): - neo_ps.get_entry("NonExistentKey_xyz") + neopdf.getPDFSet(pdfname).get_entry("NonExistentKey_xyz") - def test_keys_contains_mandatory(self, pdfname): - neo_ps = neopdf.getPDFSet(pdfname) - ks = neo_ps.keys() + def test_keys_contains_mandatory(self, pdfname: str) -> None: + ks: list[str] = neopdf.getPDFSet(pdfname).keys() assert isinstance(ks, list) for key in self.MANDATORY_KEYS: assert key in ks, f"key '{key}' missing from keys()" - def test_error_conf_level_when_key_present(self, pdfname): + def test_error_conf_level_when_key_present(self, pdfname: str) -> None: neo_ps = neopdf.getPDFSet(pdfname) - lha_ps = lhapdf.getPDFSet(pdfname) + lha_ps = lhapdf.getPDFSet(pdfname) # type: ignore[attr-defined] if not lha_ps.has_key("ErrorConfLevel"): assert neo_ps.errorConfLevel == -1.0 else: assert neo_ps.errorConfLevel == lha_ps.errorConfLevel - def test_has_key_error_conf_level(self, pdfname): + def test_has_key_error_conf_level(self, pdfname: str) -> None: neo_ps = neopdf.getPDFSet(pdfname) - lha_ps = lhapdf.getPDFSet(pdfname) + lha_ps = lhapdf.getPDFSet(pdfname) # type: ignore[attr-defined] assert neo_ps.has_key("ErrorConfLevel") == lha_ps.has_key("ErrorConfLevel") - def test_metadata_has_key(self, pdfname): + def test_metadata_has_key(self, pdfname: str) -> None: neo_p = neopdf.mkPDF(pdfname, 0) for key in self.MANDATORY_KEYS: assert neo_p.has_key(key), f"PDF missing mandatory key '{key}'" - def test_metadata_get_entry(self, pdfname): + def test_metadata_get_entry(self, pdfname: str) -> None: neo_p = neopdf.mkPDF(pdfname, 0) - lha_ps = lhapdf.getPDFSet(pdfname) + lha_ps = lhapdf.getPDFSet(pdfname) # type: ignore[attr-defined] assert int(neo_p.get_entry("NumMembers")) == int(lha_ps.get_entry("NumMembers")) @@ -364,56 +352,62 @@ def test_metadata_get_entry(self, pdfname): "pid,x,q", [(21, 0.1, 100.0), (2, 0.01, 10.0), (-3, 0.3, 1000.0)] ) class TestPDFSetUncertainty: - def _values(self, pdfname: str, pid: int, x: float, q: float): - pdfs = neopdf.mkPDFs(pdfname) + def _values(self, pdfname: str, pid: int, x: float, q: float) -> list[float]: + pdfs: list[Any] = neopdf.mkPDFs(pdfname) if len(pdfs) <= 1: pytest.skip(f"{pdfname} has only {len(pdfs)} member(s) installed") return [p.xfxQ(pid, x, q) for p in pdfs] - def test_uncertainty_returns_object(self, pdfname, pid, x, q): - from neopdf.uncertainty import Uncertainty + def test_uncertainty_returns_object( + self, pdfname: str, pid: int, x: float, q: float + ) -> None: + from neopdf.uncertainty import Uncertainty # type: ignore[import-untyped] neo_ps = neopdf.getPDFSet(pdfname) - values = self._values(pdfname, pid, x, q) - unc = neo_ps.uncertainty(values) + unc = neo_ps.uncertainty(self._values(pdfname, pid, x, q)) assert isinstance(unc, Uncertainty) assert abs(unc.central) >= 0 - def test_uncertainty_matches_standalone(self, pdfname, pid, x, q): + def test_uncertainty_matches_standalone( + self, pdfname: str, pid: int, x: float, q: float + ) -> None: neo_ps = neopdf.getPDFSet(pdfname) values = np.array(self._values(pdfname, pid, x, q)) - ecl = neo_ps.errorConfLevel + ecl: float = neo_ps.errorConfLevel unc_set = neo_ps.uncertainty(list(values), CL_1_SIGMA, False) unc_fn = uncertainty( - values, - neo_ps.errorType, - error_conf_level=ecl, - cl=CL_1_SIGMA, + values, neo_ps.errorType, error_conf_level=ecl, cl=CL_1_SIGMA ) np.testing.assert_allclose(unc_set.central, unc_fn.central, rtol=1e-10) np.testing.assert_allclose(unc_set.errminus, unc_fn.errminus, rtol=1e-10) np.testing.assert_allclose(unc_set.errplus, unc_fn.errplus, rtol=1e-10) - def test_uncertainty_central_matches_lhapdf(self, pdfname, pid, x, q): + def test_uncertainty_central_matches_lhapdf( + self, pdfname: str, pid: int, x: float, q: float + ) -> None: values = self._values(pdfname, pid, x, q) neo_ps = neopdf.getPDFSet(pdfname) - lha_ps = lhapdf.getPDFSet(pdfname) + lha_ps = lhapdf.getPDFSet(pdfname) # type: ignore[attr-defined] neo_unc = neo_ps.uncertainty(values, CL_1_SIGMA, False) lha_unc = lha_ps.uncertainty(values, CL_1_SIGMA, False) np.testing.assert_allclose(neo_unc.central, lha_unc.central, rtol=1e-10) - def test_uncertainty_errminus_matches_lhapdf(self, pdfname, pid, x, q): + def test_uncertainty_errminus_matches_lhapdf( + self, pdfname: str, pid: int, x: float, q: float + ) -> None: values = self._values(pdfname, pid, x, q) neo_ps = neopdf.getPDFSet(pdfname) - lha_ps = lhapdf.getPDFSet(pdfname) + lha_ps = lhapdf.getPDFSet(pdfname) # type: ignore[attr-defined] neo_unc = neo_ps.uncertainty(values, CL_1_SIGMA, False) lha_unc = lha_ps.uncertainty(values, CL_1_SIGMA, False) np.testing.assert_allclose(neo_unc.errminus, lha_unc.errminus, rtol=1e-2) - def test_uncertainty_errplus_matches_lhapdf(self, pdfname, pid, x, q): + def test_uncertainty_errplus_matches_lhapdf( + self, pdfname: str, pid: int, x: float, q: float + ) -> None: values = self._values(pdfname, pid, x, q) neo_ps = neopdf.getPDFSet(pdfname) - lha_ps = lhapdf.getPDFSet(pdfname) + lha_ps = lhapdf.getPDFSet(pdfname) # type: ignore[attr-defined] neo_unc = neo_ps.uncertainty(values, CL_1_SIGMA, False) lha_unc = lha_ps.uncertainty(values, CL_1_SIGMA, False) np.testing.assert_allclose(neo_unc.errplus, lha_unc.errplus, rtol=1e-3) diff --git a/neopdf_pyapi/tests/test_manage.py b/neopdf_pyapi/tests/test_manage.py index ff8714d..dd5a857 100644 --- a/neopdf_pyapi/tests/test_manage.py +++ b/neopdf_pyapi/tests/test_manage.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +from typing import Any + import neopdf import pytest @@ -5,28 +9,24 @@ @pytest.fixture -def manage_data(tmp_path): - pdf_format = neopdf.manage.PdfSetFormat.Lhapdf - return neopdf.manage.ManageData(SETNAME, pdf_format) +def manage_data() -> Any: + pdf_format = neopdf.manage.PdfSetFormat.Lhapdf # type: ignore[attr-defined] + return neopdf.manage.ManageData(SETNAME, pdf_format) # type: ignore[attr-defined] -def test_manage_data(manage_data): +def test_manage_data(manage_data: Any) -> None: assert manage_data.is_pdf_installed() - set_name = manage_data.set_name() - assert set_name == SETNAME - set_path = manage_data.set_path() - data_path = manage_data.data_path() - assert set_path == f"{data_path}/{SETNAME}" + assert manage_data.set_name() == SETNAME + assert manage_data.set_path() == f"{manage_data.data_path()}/{SETNAME}" -def test_manage_data_status(manage_data): - status = manage_data.is_pdf_installed() - assert isinstance(status, bool) +def test_manage_data_status(manage_data: Any) -> None: + assert isinstance(manage_data.is_pdf_installed(), bool) -def test_manage_data_paths(manage_data): - d_path = manage_data.data_path() - s_path = manage_data.set_path() +def test_manage_data_paths(manage_data: Any) -> None: + d_path: str = manage_data.data_path() + s_path: str = manage_data.set_path() assert isinstance(d_path, str) assert isinstance(s_path, str) assert SETNAME in s_path diff --git a/neopdf_pyapi/tests/test_metadata.py b/neopdf_pyapi/tests/test_metadata.py index 27b0710..d44b8f5 100644 --- a/neopdf_pyapi/tests/test_metadata.py +++ b/neopdf_pyapi/tests/test_metadata.py @@ -1,23 +1,26 @@ -from math import sqrt -import neopdf.metadata as metadata +from __future__ import annotations + import numpy as np import pytest +from math import sqrt +import neopdf.metadata as metadata # type: ignore[import-untyped] +from typing import Any + class TestMetaData: @pytest.mark.parametrize("pdfname", ["NNPDF40_nnlo_as_01180", "MSHT20qed_an3lo"]) - def test_metadata_fields(self, neo_pdf, lha_pdf, pdfname): - neopdf = neo_pdf(pdfname) - lhapdf = lha_pdf(pdfname) - - neopdf_meta = neopdf.metadata() - np.testing.assert_equal(neopdf_meta.x_min(), lhapdf.xMin) - np.testing.assert_equal(neopdf_meta.x_max(), lhapdf.xMax) - np.testing.assert_equal(neopdf_meta.q_min(), sqrt(lhapdf.q2Min)) - np.testing.assert_equal(neopdf_meta.q_max(), sqrt(lhapdf.q2Max)) - np.testing.assert_equal(neopdf_meta.set_index(), lhapdf.lhapdfID) + def test_metadata_fields(self, neo_pdf: Any, lha_pdf: Any, pdfname: str) -> None: + neopdf_obj = neo_pdf(pdfname) + lhapdf_obj = lha_pdf(pdfname) + meta = neopdf_obj.metadata() + np.testing.assert_equal(meta.x_min(), lhapdf_obj.xMin) + np.testing.assert_equal(meta.x_max(), lhapdf_obj.xMax) + np.testing.assert_equal(meta.q_min(), sqrt(lhapdf_obj.q2Min)) + np.testing.assert_equal(meta.q_max(), sqrt(lhapdf_obj.q2Max)) + np.testing.assert_equal(meta.set_index(), lhapdf_obj.lhapdfID) - def test_metadata_creation(self): + def test_metadata_creation(self) -> None: phys_params = metadata.PhysicsParameters( flavor_scheme="test_scheme", order_qcd=2, @@ -31,7 +34,6 @@ def test_metadata_creation(self): m_bottom=4.18, m_top=172.9, ) - meta = metadata.MetaData( set_desc="test_desc", set_index=1, @@ -55,7 +57,6 @@ def test_metadata_creation(self): hadron_pid=2212, phys_params=phys_params, ) - assert meta.set_desc() == "test_desc" assert meta.set_index() == 1 assert meta.number_sets() == 1 @@ -77,11 +78,8 @@ def test_metadata_creation(self): assert meta.error_type() == "replicas" assert meta.hadron_pid() == 2212 - def test_metadata_to_dict(self): - phys_params = metadata.PhysicsParameters( - flavor_scheme="variable", - order_qcd=2, - ) + def test_metadata_to_dict(self) -> None: + phys_params = metadata.PhysicsParameters(flavor_scheme="variable", order_qcd=2) meta = metadata.MetaData( set_desc="test_desc", set_index=1, @@ -98,13 +96,12 @@ def test_metadata_to_dict(self): format="test_format", phys_params=phys_params, ) - - d = meta.to_dict() + d: dict[str, Any] = meta.to_dict() assert d["set_desc"] == "test_desc" assert d["set_index"] == 1 assert d["flavor_scheme"] == "variable" assert d["order_qcd"] == 2 - pd = phys_params.to_dict() + pd: dict[str, Any] = phys_params.to_dict() assert pd["flavor_scheme"] == "variable" assert pd["order_qcd"] == 2 diff --git a/neopdf_pyapi/tests/test_parser.py b/neopdf_pyapi/tests/test_parser.py index c6c7001..e9cbed4 100644 --- a/neopdf_pyapi/tests/test_parser.py +++ b/neopdf_pyapi/tests/test_parser.py @@ -1,9 +1,13 @@ -import neopdf.parser as parser +from __future__ import annotations + +import neopdf.parser as parser # type: ignore[import-untyped] import pytest +from typing import Any + @pytest.mark.parametrize("pdf_name", ["NNPDF40_nnlo_as_01180", "MSHT20qed_an3lo"]) -def test_lhapdf_set(pdf_name): - lhapdf_set = parser.LhapdfSet(pdf_name) +def test_lhapdf_set(pdf_name: str) -> None: + lhapdf_set: Any = parser.LhapdfSet(pdf_name) assert lhapdf_set.info() is not None assert len(lhapdf_set.members()) > 0 diff --git a/neopdf_pyapi/tests/test_pdfs.py b/neopdf_pyapi/tests/test_pdfs.py index bba441c..55a7149 100644 --- a/neopdf_pyapi/tests/test_pdfs.py +++ b/neopdf_pyapi/tests/test_pdfs.py @@ -1,19 +1,28 @@ -import pytest -import numpy as np +from __future__ import annotations + import os from pathlib import Path +import numpy as np +import pytest +from conftest import ( + XQ2Range, + PDFFactory, + PDFsFactory, + LazyPDFsFactory, + XQ2PointsFactory, +) from itertools import product -from neopdf.pdf import ForcePositive +from neopdf.pdf import ForcePositive # type: ignore[import-untyped] @pytest.mark.parametrize("pdfname", ["NNPDF40_nnlo_as_01180", "MSHT20qed_an3lo"]) class TestPDF: - def test_mkpdfs(self, neo_pdfs, pdfname): + def test_mkpdfs(self, neo_pdfs: PDFsFactory, pdfname: str) -> None: pdfs = neo_pdfs(pdfname) assert len(pdfs) > 0 - def test_pdf_methods(self, neo_pdf, pdfname): + def test_pdf_methods(self, neo_pdf: PDFFactory, pdfname: str) -> None: neopdf = neo_pdf(pdfname) assert len(neopdf.pids()) > 0 assert len(neopdf.subgrids()) > 0 @@ -35,177 +44,170 @@ class TestPDFInterpolations: ], ) @pytest.mark.parametrize("pid", [nf for nf in range(-5, 6) if nf != 0]) - def test_xfxq2(self, neo_pdf, lha_pdf, xq2_points, pdfname, pid): + def test_xfxq2( + self, + neo_pdf: PDFFactory, + lha_pdf: PDFFactory, + xq2_points: XQ2PointsFactory, + pdfname: str, + pid: int, + ) -> None: neopdf = neo_pdf(pdfname) lhapdf = lha_pdf(pdfname) - - params_range = { - "xmin": lhapdf.xMin, - "xmax": lhapdf.xMax, - "q2min": lhapdf.q2Min, - "q2max": lhapdf.q2Max, - } - xs, q2s = xq2_points(**params_range) - + xs, q2s = xq2_points( + XQ2Range( + xmin=lhapdf.xMin, + xmax=lhapdf.xMax, + q2min=lhapdf.q2Min, + q2max=lhapdf.q2Max, + ) + ) for x, q2 in product(xs, q2s): - ref = lhapdf.xfxQ2(pid, x, q2) - res = neopdf.xfxQ2(pid, x, q2) - np.testing.assert_equal(res, ref) + np.testing.assert_equal(neopdf.xfxQ2(pid, x, q2), lhapdf.xfxQ2(pid, x, q2)) @pytest.mark.parametrize("pdfname", ["NNPDF40_nnlo_as_01180", "MSHT20qed_an3lo"]) @pytest.mark.parametrize("pid", [21]) - def test_xfxq2s(self, neo_pdf, lha_pdf, xq2_points, pdfname, pid): + def test_xfxq2s( + self, + neo_pdf: PDFFactory, + lha_pdf: PDFFactory, + xq2_points: XQ2PointsFactory, + pdfname: str, + pid: int, + ) -> None: neopdf = neo_pdf(pdfname) lhapdf = lha_pdf(pdfname) - - params_range = { - "xmin": lhapdf.xMin, - "xmax": lhapdf.xMax, - "q2min": lhapdf.q2Min, - "q2max": lhapdf.q2Max, - } - xs, q2s = xq2_points(**params_range) - + xs, q2s = xq2_points( + XQ2Range( + xmin=lhapdf.xMin, + xmax=lhapdf.xMax, + q2min=lhapdf.q2Min, + q2max=lhapdf.q2Max, + ) + ) res = neopdf.xfxQ2s([pid], xs, q2s) ref = [lhapdf.xfxQ2(pid, x, q2) for x, q2 in product(xs, q2s)] np.testing.assert_equal(res, [ref]) @pytest.mark.parametrize("pdfname", ["NNPDF40_nnlo_as_01180", "MSHT20qed_an3lo"]) - def test_xfxq2_allpids(self, neo_pdf, lha_pdf, xq2_points, pdfname): + def test_xfxq2_allpids( + self, + neo_pdf: PDFFactory, + lha_pdf: PDFFactory, + xq2_points: XQ2PointsFactory, + pdfname: str, + ) -> None: neopdf = neo_pdf(pdfname) lhapdf = lha_pdf(pdfname) - - params_range = { - "xmin": lhapdf.xMin, - "xmax": lhapdf.xMax, - "q2min": lhapdf.q2Min, - "q2max": lhapdf.q2Max, - } - xs, q2s = xq2_points(**params_range) - pids = neopdf.pids() - + xs, q2s = xq2_points( + XQ2Range( + xmin=lhapdf.xMin, + xmax=lhapdf.xMax, + q2min=lhapdf.q2Min, + q2max=lhapdf.q2Max, + ) + ) + pids: list[int] = neopdf.pids() for x, q2 in product(xs, q2s): - res = neopdf.xfxQ2_allpids(pids, x, q2) ref = [lhapdf.xfxQ2(pid, x, q2) for pid in pids] - np.testing.assert_allclose(res, ref) - - res_nd = neopdf.xfxQ2_allpids_ND(pids, [x, q2]) - np.testing.assert_allclose(res_nd, ref) + np.testing.assert_allclose(neopdf.xfxQ2_allpids(pids, x, q2), ref) + np.testing.assert_allclose(neopdf.xfxQ2_allpids_ND(pids, [x, q2]), ref) class TestAlphaSInterpolations: @pytest.mark.parametrize("pdfname", ["NNPDF40_nnlo_as_01180", "MSHT20qed_an3lo"]) - def test_alphasQ2(self, neo_pdf, lha_pdf, pdfname): + def test_alphasQ2( + self, neo_pdf: PDFFactory, lha_pdf: PDFFactory, pdfname: str + ) -> None: neopdf = neo_pdf(pdfname) lhapdf = lha_pdf(pdfname) - qs = neopdf.metadata().alphas_q() + qs: list[float] = neopdf.metadata().alphas_q() q2_points = [q * q for q in np.linspace(qs[0], qs[-1], num=300)] - - for q2_point in q2_points: - ref = lhapdf.alphasQ2(q2_point) - res = neopdf.alphasQ2(q2_point) - np.testing.assert_equal(res, ref) + for q2 in q2_points: + np.testing.assert_equal(neopdf.alphasQ2(q2), lhapdf.alphasQ2(q2)) @pytest.mark.parametrize( "pdfname", ["ABMP16_5_nnlo", "ABMP16als118_5_nnlo", "MSHT20nlo_as_smallrange_nf4"], ) - def test_alphasQ2_member(self, neo_pdfs, lha_pdfs, pdfname): + def test_alphasQ2_member( + self, neo_pdfs: PDFsFactory, lha_pdfs: PDFsFactory, pdfname: str + ) -> None: neopdf = neo_pdfs(pdfname) lhapdf = lha_pdfs(pdfname) - for idx in range(len(neopdf)): - qs = neopdf[idx].metadata().alphas_q() + qs: list[float] = neopdf[idx].metadata().alphas_q() q2_points = [q * q for q in np.linspace(qs[0], qs[-1], num=100)] - - for q2_point in q2_points: - ref = lhapdf[idx].alphasQ2(q2_point) - res = neopdf[idx].alphasQ2(q2_point) - np.testing.assert_equal(res, ref) + for q2 in q2_points: + np.testing.assert_equal( + neopdf[idx].alphasQ2(q2), lhapdf[idx].alphasQ2(q2) + ) class TestLazyLoader: @pytest.mark.parametrize("pdfname", ["NNPDF40_nnlo_as_01180.neopdf.lz4"]) - def test_lazy_loader(self, neo_pdfs_lazy, pdfname): - neopdfs = neo_pdfs_lazy(pdfname) - - for pdf in neopdfs: - res = pdf.xfxQ2(21, 1e-5, 1e2) - assert isinstance(res, float) + def test_lazy_loader(self, neo_pdfs_lazy: LazyPDFsFactory, pdfname: str) -> None: + for pdf in neo_pdfs_lazy(pdfname): + assert isinstance(pdf.xfxQ2(21, 1e-5, 1e2), float) class TestLHAID: - def test_mkpdf_lhaid_base(self, neo_pdf): - # NNPDF40_nnlo_as_01180 has base LHAID 331100; member 0 = LHAID 331100 - from neopdf.pdf import PDF + def test_mkpdf_lhaid_base(self, neo_pdf: PDFFactory) -> None: + from neopdf.pdf import PDF # type: ignore[import-untyped] + # NNPDF40_nnlo_as_01180 has base LHAID 331100; member 0 = LHAID 331100 pdf_by_id = PDF.mkPDF_lhaid(331100) pdf_by_name = neo_pdf("NNPDF40_nnlo_as_01180") - x, q2, pid = 1e-4, 100.0, 21 np.testing.assert_equal( - pdf_by_id.xfxQ2(pid, x, q2), - pdf_by_name.xfxQ2(pid, x, q2), - ) - np.testing.assert_equal( - pdf_by_id.alphasQ2(q2), - pdf_by_name.alphasQ2(q2), + pdf_by_id.xfxQ2(pid, x, q2), pdf_by_name.xfxQ2(pid, x, q2) ) + np.testing.assert_equal(pdf_by_id.alphasQ2(q2), pdf_by_name.alphasQ2(q2)) - def test_mkpdf_lhaid_member_offset(self, neo_pdfs): - # ABMP16als118_5_nnlo owns range [42780, 42810); member 10 = LHAID 42790 - from neopdf.pdf import PDF + def test_mkpdf_lhaid_member_offset(self, neo_pdfs: PDFsFactory) -> None: + from neopdf.pdf import PDF # type: ignore[import-untyped] + # ABMP16als118_5_nnlo owns range [42780, 42810); member 10 = LHAID 42790 pdf_by_id = PDF.mkPDF_lhaid(42790) pdf_by_name = neo_pdfs("ABMP16als118_5_nnlo")[10] - x, q2, pid = 1e-3, 10.0, 1 np.testing.assert_equal( - pdf_by_id.xfxQ2(pid, x, q2), - pdf_by_name.xfxQ2(pid, x, q2), - ) - np.testing.assert_equal( - pdf_by_id.alphasQ2(q2), - pdf_by_name.alphasQ2(q2), + pdf_by_id.xfxQ2(pid, x, q2), pdf_by_name.xfxQ2(pid, x, q2) ) + np.testing.assert_equal(pdf_by_id.alphasQ2(q2), pdf_by_name.alphasQ2(q2)) class TestLoadFromFile: - def test_mkpdf_lhapdf_by_file(self): - from neopdf.pdf import PDF + def test_mkpdf_lhapdf_by_file(self) -> None: + from neopdf.pdf import PDF # type: ignore[import-untyped] neopdf_data_path = os.environ.get("NEOPDF_DATA_PATH") if not neopdf_data_path: pytest.skip("NEOPDF_DATA_PATH environment variable not set.") + assert neopdf_data_path - path = Path(neopdf_data_path).joinpath( - "NNPDF40_nnlo_as_01180/NNPDF40_nnlo_as_01180_0000.dat" + path = ( + Path(neopdf_data_path) + / "NNPDF40_nnlo_as_01180" + / "NNPDF40_nnlo_as_01180_0000.dat" ) - if not path.exists(): pytest.skip(f"Data file not found at {path}") - pdf = PDF.mkPDF_lhapdf_file(str(path)) - - xf = pdf.xfxQ2(21, 1e-9, 1.65 * 1.65) + xf = PDF.mkPDF_lhapdf_file(str(path)).xfxQ2(21, 1e-9, 1.65 * 1.65) np.testing.assert_allclose(xf, 0.14844111, rtol=1e-8) class TestForcePositive: - def test_force_positive(self, neo_pdf): + def test_force_positive(self, neo_pdf: PDFFactory) -> None: neopdf = neo_pdf("nNNPDF30_nlo_as_0118_A56_Z26") assert neopdf.is_force_positive() == ForcePositive.NoClipping - neg_interp = neopdf.xfxQ2(21, 0.9, 1e2) - assert neg_interp < 0.0 + assert neopdf.xfxQ2(21, 0.9, 1e2) < 0.0 - # Clip negative values to zero neopdf.set_force_positive(ForcePositive.ClipNegative) assert neopdf.is_force_positive() == ForcePositive.ClipNegative - zero_interp = neopdf.xfxQ2(21, 0.9, 1e2) - assert zero_interp == 0.0 + assert neopdf.xfxQ2(21, 0.9, 1e2) == 0.0 - # Clip negative values to small positive definite neopdf.set_force_positive(ForcePositive.ClipSmall) assert neopdf.is_force_positive() == ForcePositive.ClipSmall - small_interp = neopdf.xfxQ2(21, 0.9, 1e2) - assert small_interp == 1e-10 + assert neopdf.xfxQ2(21, 0.9, 1e2) == 1e-10 diff --git a/neopdf_pyapi/tests/test_uncertainty.py b/neopdf_pyapi/tests/test_uncertainty.py index 43f8588..5ecd4f4 100644 --- a/neopdf_pyapi/tests/test_uncertainty.py +++ b/neopdf_pyapi/tests/test_uncertainty.py @@ -1,27 +1,29 @@ """Tests for neopdf.uncertainty, benchmarked against LHAPDF.""" +from __future__ import annotations + + +import lhapdf # type: ignore[import-untyped] import numpy as np import pytest -import lhapdf -from neopdf.pdf import PDF as NeoPDF -from neopdf.uncertainty import CL_1_SIGMA, CL_2_SIGMA, uncertainty, Uncertainty +from neopdf.pdf import PDF as NeoPDF # type: ignore[import-untyped] +from neopdf.uncertainty import CL_1_SIGMA, CL_2_SIGMA, Uncertainty, uncertainty # type: ignore[import-untyped] +from typing import Any, NamedTuple def lha_values(pdfname: str, pid: int, x: float, q: float) -> np.ndarray: - """Return an array of xfxQ values (all members) from LHAPDF.""" - pdfs = lhapdf.mkPDFs(pdfname) - return np.array([p.xfxQ(pid, x, q) for p in pdfs]) + """Return xfxQ values for all members from LHAPDF.""" + return np.array([p.xfxQ(pid, x, q) for p in lhapdf.mkPDFs(pdfname)]) # type: ignore[attr-defined] def neo_values(pdfname: str, pid: int, x: float, q: float) -> np.ndarray: - """Return an array of xfxQ2 values (all members) from NeoPDF.""" - pdfs = NeoPDF.mkPDFs(pdfname) - return np.array([p.xfxQ2(pid, x, q * q) for p in pdfs]) + """Return xfxQ2 values for all members from NeoPDF.""" + return np.array([p.xfxQ2(pid, x, q * q) for p in NeoPDF.mkPDFs(pdfname)]) class TestUncertaintyClass: - def test_attributes(self): + def test_attributes(self) -> None: values = np.array([1.0, 0.9, 1.1, 0.95, 1.05]) unc = uncertainty(values, "replicas", cl=CL_1_SIGMA) assert hasattr(unc, "central") @@ -29,29 +31,29 @@ def test_attributes(self): assert hasattr(unc, "errplus") assert isinstance(unc, Uncertainty) - def test_central_replicas_is_mean(self): + def test_central_replicas_is_mean(self) -> None: values = np.array([5.0, 4.0, 6.0, 4.5, 5.5]) unc = uncertainty(values, "replicas", cl=CL_1_SIGMA) assert unc.central == np.mean(values[1:]) - def test_errsymm(self): + def test_errsymm(self) -> None: values = np.array([1.0, 0.8, 1.2, 0.9, 1.1]) unc = uncertainty(values, "hessian", cl=CL_1_SIGMA) assert np.isclose(unc.errsymm(), (unc.errminus + unc.errplus) / 2.0) - def test_repr(self): + def test_repr(self) -> None: values = np.array([1.0, 0.9, 1.1]) unc = uncertainty(values, "replicas", cl=CL_1_SIGMA) assert "Uncertainty" in repr(unc) assert "central" in repr(unc) - def test_raises_on_empty(self): + def test_raises_on_empty(self) -> None: with pytest.raises(Exception): uncertainty(np.array([]), "replicas", cl=CL_1_SIGMA) class TestUncertaintyAlgorithms: - def test_replicas_symmetric(self): + def test_replicas_symmetric(self) -> None: rng = np.random.default_rng(42) replicas = rng.normal(loc=0.0, scale=1.0, size=100) values = np.concatenate([[0.0], replicas]) @@ -60,7 +62,7 @@ def test_replicas_symmetric(self): assert np.isclose(unc.errminus, expected_std, rtol=1e-10) assert np.isclose(unc.errplus, expected_std, rtol=1e-10) - def test_replicas_alternative_asymmetric(self): + def test_replicas_alternative_asymmetric(self) -> None: rng = np.random.default_rng(0) replicas = rng.exponential(scale=1.0, size=1000) values = np.concatenate([[np.mean(replicas)], replicas]) @@ -69,33 +71,33 @@ def test_replicas_alternative_asymmetric(self): assert not np.isclose(unc_alt.errminus, unc_alt.errplus, rtol=1e-3) assert np.isclose(unc_sym.errminus, unc_sym.errplus, rtol=1e-10) - def test_hessian_symmetric_pairs(self): + def test_hessian_symmetric_pairs(self) -> None: values = np.array([1.0, 1.1, 0.9, 1.2, 0.8]) unc = uncertainty(values, "hessian", cl=CL_1_SIGMA) expected = np.sqrt(0.1**2 + 0.2**2) assert np.isclose(unc.errminus, expected, rtol=1e-10) assert np.isclose(unc.errplus, expected, rtol=1e-10) - def test_symmhessian_same_as_hessian(self): + def test_symmhessian_same_as_hessian(self) -> None: values = np.array([1.0, 1.1, 0.9, 1.3, 0.7]) unc_h = uncertainty(values, "hessian", cl=CL_1_SIGMA) unc_s = uncertainty(values, "symmhessian", cl=CL_1_SIGMA) assert np.isclose(unc_h.errminus, unc_s.errminus) assert np.isclose(unc_h.errplus, unc_s.errplus) - def test_asymhessian(self): + def test_asymhessian(self) -> None: values = np.array([1.0, 1.2, 0.8]) unc = uncertainty(values, "asymhessian", cl=CL_1_SIGMA) assert np.isclose(unc.errplus, 0.2, rtol=1e-10) assert np.isclose(unc.errminus, 0.2, rtol=1e-10) - def test_cl_rescaling_hessian(self): + def test_cl_rescaling_hessian(self) -> None: values = np.array([1.0, 1.1, 0.9, 1.2, 0.8]) unc_1s = uncertainty(values, "hessian", cl=68.27) unc_2s = uncertainty(values, "hessian", cl=95.45) assert np.isclose(unc_2s.errminus / unc_1s.errminus, 2.0, rtol=1e-2) - def test_cl_rescaling_replicas(self): + def test_cl_rescaling_replicas(self) -> None: rng = np.random.default_rng(7) replicas = rng.normal(0.0, 1.0, 500) values = np.concatenate([[0.0], replicas]) @@ -103,94 +105,119 @@ def test_cl_rescaling_replicas(self): unc_2s = uncertainty(values, "replicas", cl=95.45) assert np.isclose(unc_2s.errminus / unc_1s.errminus, 2.0, rtol=1e-2) - def test_error_conf_level_ct18_convention(self): + def test_error_conf_level_ct18_convention(self) -> None: values = np.array([1.0, 1.2, 0.8, 1.3, 0.7]) unc_68 = uncertainty( - values, - "hessian", - error_conf_level=CL_1_SIGMA, - cl=CL_1_SIGMA, - ) - unc_90 = uncertainty( - values, - "hessian", - error_conf_level=90.0, - cl=CL_1_SIGMA, + values, "hessian", error_conf_level=CL_1_SIGMA, cl=CL_1_SIGMA ) + unc_90 = uncertainty(values, "hessian", error_conf_level=90.0, cl=CL_1_SIGMA) assert unc_90.errminus < unc_68.errminus assert np.isclose( - unc_90.errminus / unc_68.errminus, - 1.0 / (1.6449 / 1.0), - rtol=1e-2, + unc_90.errminus / unc_68.errminus, 1.0 / (1.6449 / 1.0), rtol=1e-2 ) +class _UncertaintyContext(NamedTuple): + """Bundles the data needed to run a single uncertainty comparison test.""" + + pdfset: Any + values: np.ndarray + error_type: str + ecl: float + + @pytest.mark.parametrize( - "pdfname", - ["NNPDF40_nnlo_as_01180", "CT18NNLO_as_0118", "MSHT20qed_an3lo"], + "pdfname", ["NNPDF40_nnlo_as_01180", "CT18NNLO_as_0118", "MSHT20qed_an3lo"] ) @pytest.mark.parametrize( - "pid,x,q", - [(21, 0.1, 100.0), (2, 0.01, 10.0), (-3, 0.3, 1000.0)], + "pid,x,q", [(21, 0.1, 100.0), (2, 0.01, 10.0), (-3, 0.3, 1000.0)] ) class TestAgainstLhapdf: - def _setup(self, pdfname, pid, x, q): - pdfset = lhapdf.getPDFSet(pdfname) + def _setup(self, pdfname: str, pid: int, x: float, q: float) -> _UncertaintyContext: + pdfset = lhapdf.getPDFSet(pdfname) # type: ignore[attr-defined] values = lha_values(pdfname, pid, x, q) - if len(values) <= 1: pytest.skip(f"{pdfname} has only {len(values)} member(s) installed") - pdf0 = NeoPDF(pdfname, 0) - error_type = pdf0.metadata().error_type() - ecl = float(pdfset.errorConfLevel) # -1.0 when not set in the info file - return pdfset, values, error_type, ecl - - def test_central(self, pdfname, pid, x, q): - pdfset, values, error_type, ecl = self._setup(pdfname, pid, x, q) - lha = pdfset.uncertainty(values, CL_1_SIGMA, False) - neo = uncertainty(values, error_type, error_conf_level=ecl, cl=CL_1_SIGMA) + return _UncertaintyContext( + pdfset=pdfset, + values=values, + error_type=pdf0.metadata().error_type(), + ecl=float(pdfset.errorConfLevel), + ) + + def test_central(self, pdfname: str, pid: int, x: float, q: float) -> None: + ctx = self._setup(pdfname, pid, x, q) + lha = ctx.pdfset.uncertainty(ctx.values, CL_1_SIGMA, False) + neo = uncertainty( + ctx.values, + ctx.error_type, + error_conf_level=ctx.ecl, + cl=CL_1_SIGMA, + ) np.testing.assert_allclose(neo.central, lha.central, rtol=1e-10) - def test_errminus(self, pdfname, pid, x, q): - pdfset, values, error_type, ecl = self._setup(pdfname, pid, x, q) - lha = pdfset.uncertainty(values, CL_1_SIGMA, False) - neo = uncertainty(values, error_type, error_conf_level=ecl, cl=CL_1_SIGMA) + def test_errminus(self, pdfname: str, pid: int, x: float, q: float) -> None: + ctx = self._setup(pdfname, pid, x, q) + lha = ctx.pdfset.uncertainty(ctx.values, CL_1_SIGMA, False) + neo = uncertainty( + ctx.values, + ctx.error_type, + error_conf_level=ctx.ecl, + cl=CL_1_SIGMA, + ) # TODO: To be investigated further np.testing.assert_allclose(neo.errminus, lha.errminus, rtol=1e-2) - def test_errplus(self, pdfname, pid, x, q): - pdfset, values, error_type, ecl = self._setup(pdfname, pid, x, q) - lha = pdfset.uncertainty(values, CL_1_SIGMA, False) - neo = uncertainty(values, error_type, error_conf_level=ecl, cl=CL_1_SIGMA) + def test_errplus(self, pdfname: str, pid: int, x: float, q: float) -> None: + ctx = self._setup(pdfname, pid, x, q) + lha = ctx.pdfset.uncertainty(ctx.values, CL_1_SIGMA, False) + neo = uncertainty( + ctx.values, + ctx.error_type, + error_conf_level=ctx.ecl, + cl=CL_1_SIGMA, + ) np.testing.assert_allclose(neo.errplus, lha.errplus, rtol=1e-3) - def test_alternative_prescription(self, pdfname, pid, x, q): + def test_alternative_prescription( + self, pdfname: str, pid: int, x: float, q: float + ) -> None: if pdfname != "NNPDF40_nnlo_as_01180": pytest.skip("alternative prescription only applies to replica sets") - pdfset, values, error_type, ecl = self._setup(pdfname, pid, x, q) - neo = uncertainty( - values, - error_type, - error_conf_level=ecl, + ctx = self._setup(pdfname, pid, x, q) + neo_alt = uncertainty( + ctx.values, + ctx.error_type, + error_conf_level=ctx.ecl, cl=CL_1_SIGMA, alternative=True, ) neo_std = uncertainty( - values, - error_type, - error_conf_level=ecl, + ctx.values, + ctx.error_type, + error_conf_level=ctx.ecl, cl=CL_1_SIGMA, ) - assert neo.errminus != neo.errplus, "expected asymmetric interval" - assert neo.errminus > 0 - assert neo.errplus > 0 - assert neo.errminus < 3 * neo_std.errminus - assert neo.errplus < 3 * neo_std.errplus - - def test_higher_cl(self, pdfname, pid, x, q): - pdfset, values, error_type, ecl = self._setup(pdfname, pid, x, q) - neo_1s = uncertainty(values, error_type, error_conf_level=ecl, cl=CL_1_SIGMA) - neo_2s = uncertainty(values, error_type, error_conf_level=ecl, cl=CL_2_SIGMA) + assert neo_alt.errminus != neo_alt.errplus, "expected asymmetric interval" + assert neo_alt.errminus > 0 + assert neo_alt.errplus > 0 + assert neo_alt.errminus < 3 * neo_std.errminus + assert neo_alt.errplus < 3 * neo_std.errplus + + def test_higher_cl(self, pdfname: str, pid: int, x: float, q: float) -> None: + ctx = self._setup(pdfname, pid, x, q) + neo_1s = uncertainty( + ctx.values, + ctx.error_type, + error_conf_level=ctx.ecl, + cl=CL_1_SIGMA, + ) + neo_2s = uncertainty( + ctx.values, + ctx.error_type, + error_conf_level=ctx.ecl, + cl=CL_2_SIGMA, + ) assert neo_2s.errminus >= neo_1s.errminus assert neo_2s.errplus >= neo_1s.errplus diff --git a/neopdf_pyapi/tests/test_writer.py b/neopdf_pyapi/tests/test_writer.py index 2c25970..6f76664 100644 --- a/neopdf_pyapi/tests/test_writer.py +++ b/neopdf_pyapi/tests/test_writer.py @@ -1,20 +1,25 @@ -import neopdf.writer as writer -import neopdf.parser as parser +from __future__ import annotations + +from pathlib import Path import pytest +from typing import Any + +import neopdf.parser as parser # type: ignore[import-untyped] +import neopdf.writer as writer # type: ignore[import-untyped] @pytest.mark.parametrize("pdf_name", ["NNPDF40_nnlo_as_01180"]) -def test_writer(pdf_name, tmp_path): +def test_writer(pdf_name: str, tmp_path: Path) -> None: lhapdf_set = parser.LhapdfSet(pdf_name) - members = lhapdf_set.members() - metadata = lhapdf_set.info() + members: list[Any] = lhapdf_set.members() + metadata: Any = lhapdf_set.info() grids = [m[1] for m in members] output_path = tmp_path / f"{pdf_name}.neopdf.lz4" writer.compress(grids, metadata, str(output_path)) - decompressed_members = writer.decompress(str(output_path)) + decompressed_members: list[Any] = writer.decompress(str(output_path)) assert len(decompressed_members) == len(members) - extracted_metadata = writer.extract_metadata(str(output_path)) + extracted_metadata: Any = writer.extract_metadata(str(output_path)) assert extracted_metadata.set_index() == metadata.set_index() diff --git a/pixi.lock b/pixi.lock index 2365d5b..eb287e3 100644 --- a/pixi.lock +++ b/pixi.lock @@ -12,6 +12,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.15.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/backrefs-5.8-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils-2.44-h4852527_1.conda @@ -165,6 +166,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.4-py313h843e2db_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pymdown-extensions-10.16.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda @@ -172,6 +175,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-mpl-0.19.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.12-hc97d973_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda @@ -190,6 +194,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py313h07c4f96_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.14.1-h4440ef1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda @@ -225,6 +231,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda - pypi: https://files.pythonhosted.org/packages/1f/d4/6fe9c62bec56212b96607bdc26ee1072518213414d77c4ff878c17c7889c/mkdocs_roamlinks_plugin-0.3.2-py3-none-any.whl osx-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/backrefs-5.8-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-1.2.0-hf139dec_1.conda @@ -346,12 +353,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/pthread-stubs-0.4-h00291cd_1002.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pydantic-core-2.46.4-py313h23ec8f2_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pymdown-extensions-10.16.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-mpl-0.19.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.12-h894a449_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda @@ -370,6 +380,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.5-py313hf59fe81_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.14.1-h4440ef1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda @@ -384,6 +396,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h8210216_2.conda - pypi: https://files.pythonhosted.org/packages/1f/d4/6fe9c62bec56212b96607bdc26ee1072518213414d77c4ff878c17c7889c/mkdocs_roamlinks_plugin-0.3.2-py3-none-any.whl osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/backrefs-5.8-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda @@ -508,12 +521,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.46.4-py313h212e517_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pymdown-extensions-10.16.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-mpl-0.19.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.12-h20e6be0_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda @@ -532,6 +548,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py313h0997733_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.14.1-h4440ef1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda @@ -547,6 +565,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/1f/d4/6fe9c62bec56212b96607bdc26ee1072518213414d77c4ff878c17c7889c/mkdocs_roamlinks_plugin-0.3.2-py3-none-any.whl win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/backrefs-5.8-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/binutils_impl_win-64-2.44-h095e170_1.conda @@ -668,6 +687,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-h0e40799_1002.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.46.4-py313hfbe8231_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pymdown-extensions-10.16.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda @@ -675,6 +696,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-mpl-0.19.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.12-h09917c8_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda @@ -692,6 +714,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py313h5ea7bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.14.1-h4440ef1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda @@ -759,6 +783,18 @@ packages: purls: [] size: 584660 timestamp: 1768327524772 +- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + sha256: e0ea1ba78fbb64f17062601edda82097fcf815012cf52bb704150a2668110d48 + md5: 2934f256a8acfe48f6ebb4fce6cde29c + depends: + - python >=3.9 + - typing-extensions >=4.0.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/annotated-types?source=hash-mapping + size: 18074 + timestamp: 1733247158254 - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda sha256: 1c656a35800b7f57f7371605bc6507c8d3ad60fbaaec65876fce7f73df1fc8ac md5: 0a01c169f0ab0f91b26e77a3301fbfe4 @@ -6289,6 +6325,88 @@ packages: - pkg:pypi/pycparser?source=hash-mapping size: 110100 timestamp: 1733195786147 +- conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + sha256: 69700e31165df070e9716315e042196aa92525dae5deb5107785847ab9f4189f + md5: 729843edafc0899b3348bd3f19525b9d + depends: + - typing-inspection >=0.4.2 + - typing_extensions >=4.14.1 + - python >=3.10 + - annotated-types >=0.6.0 + - pydantic-core ==2.46.4 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydantic?source=hash-mapping + size: 346511 + timestamp: 1778103405862 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.4-py313h843e2db_0.conda + sha256: d6705962afa2655cc22edcd075ce1f7e67c4407c005137d6d63c0dc5eaa76f47 + md5: ef0f9efec6ec28b17e22f787c7672c67 + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python_abi 3.13.* *_cp313 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydantic-core?source=hash-mapping + size: 1893749 + timestamp: 1778084222642 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pydantic-core-2.46.4-py313h23ec8f2_0.conda + sha256: 66b1f30f0968f00b748710f7ee9ac410e98edccb6a53ae590aa1bd53f145a631 + md5: c6f73413ba12c55c2a0b6d2f3c1474d0 + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - __osx >=11.0 + - python_abi 3.13.* *_cp313 + constrains: + - __osx >=10.13 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydantic-core?source=hash-mapping + size: 1879854 + timestamp: 1778084387529 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.46.4-py313h212e517_0.conda + sha256: 5bcc20f2c21d8a20d8f2821caf31d8c0ef2fa3bcdaf7a9bdce62e37be819f1d7 + md5: 32bb0d4dbb1be2064c06248a1190aa96 + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - python 3.13.* *_cp313 + - __osx >=11.0 + - python_abi 3.13.* *_cp313 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydantic-core?source=hash-mapping + size: 1720475 + timestamp: 1778084300413 +- conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.46.4-py313hfbe8231_0.conda + sha256: 6466b7e441adb71e29308de2a87c9787d5d0100601ede2d02ed4f85c251699d6 + md5: 9981bc46be6341306a5473b290b2f366 + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydantic-core?source=hash-mapping + size: 1888029 + timestamp: 1778084253466 - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda sha256: 5577623b9f6685ece2697c6eb7511b4c9ac5fb607c9babc2646c811b428fd46a md5: 6b6ece66ebcae2d5f326c77ef2c5a066 @@ -6431,6 +6549,22 @@ packages: - pkg:pypi/pytest-cov?source=hash-mapping size: 28216 timestamp: 1749778064293 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-mpl-0.19.0-pyhd8ed1ab_0.conda + sha256: 31d19101973885455efd75760f7499bf1807e55b667689b9dec3684d114ab06b + md5: db89279e1501c5709d414176149ecc0a + depends: + - jinja2 >=2.10.2 + - matplotlib-base >=3.5.3 + - packaging >=22.0.0 + - pillow >=8.1.1 + - pytest >=6.2.5 + - python >=3.10 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/pytest-mpl?source=hash-mapping + size: 30401 + timestamp: 1774897789748 - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.12-hc97d973_100_cp313.conda build_number: 100 sha256: 8a08fe5b7cb5a28aa44e2994d18dbf77f443956990753a4ca8173153ffb6eb56 @@ -7090,6 +7224,29 @@ packages: - pkg:pypi/tornado?source=hash-mapping size: 883689 timestamp: 1774358224157 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.14.1-h4440ef1_0.conda + sha256: 349951278fa8d0860ec6b61fcdc1e6f604e6fce74fabf73af2e39a37979d0223 + md5: 75be1a943e0a7f99fcf118309092c635 + depends: + - typing_extensions ==4.14.1 pyhe01879c_0 + license: PSF-2.0 + license_family: PSF + purls: [] + size: 90486 + timestamp: 1751643513473 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda + sha256: 8b90d2f19f9458b8c58a55e1fcdc1d90c1603a847a47654d8a454549413ba60a + md5: 53f5409c5cfd6c5a66417d68e3f0a864 + depends: + - python >=3.10 + - typing_extensions >=4.12.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/typing-inspection?source=hash-mapping + size: 20935 + timestamp: 1777105465795 - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda sha256: 4f52390e331ea8b9019b87effaebc4f80c6466d09f68453f52d5cdc2a3e1194f md5: e523f4f1e980ed7a4240d7e27e9ec81f diff --git a/pixi.toml b/pixi.toml index d39b6ea..ff34d01 100644 --- a/pixi.toml +++ b/pixi.toml @@ -17,6 +17,7 @@ NEOPDF_DATA_PATH = "$PWD/neopdf-data" maturin = "*" numpy = "*" matplotlib = "*" +pydantic = "*" pytest = "*" pytest-cov = "*" pytest-mpl = "*"