Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/regenerate-figures.yml
Original file line number Diff line number Diff line change
@@ -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 }}
2 changes: 1 addition & 1 deletion .github/workflows/run-python.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Python Versions 🐍
name: Check Python Versions 🐍

on: [push]

Expand Down
5 changes: 5 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion neopdf_pyapi/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
123 changes: 77 additions & 46 deletions neopdf_pyapi/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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
25 changes: 12 additions & 13 deletions neopdf_pyapi/tests/test_converter.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
39 changes: 19 additions & 20 deletions neopdf_pyapi/tests/test_gridpdf.py
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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)
Expand All @@ -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(
Expand All @@ -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)

Expand Down
Loading
Loading