diff --git a/.github/actions/install_conda_pip/action.yml b/.github/actions/install_conda_pip/action.yml index 5e758408b2..a43477bf76 100644 --- a/.github/actions/install_conda_pip/action.yml +++ b/.github/actions/install_conda_pip/action.yml @@ -27,7 +27,7 @@ inputs: nnpdf-extras: required: true description: "Which extras to install" - default: "[qed,tests,hyperopt]" + default: "[qed,tests,hyperopt,neopdf]" runs: @@ -44,3 +44,4 @@ runs: shell: bash -l {0} run: | pip install -e .${{ inputs.nnpdf-extras }} + - uses: ./.github/actions/set_neopdf_datapath diff --git a/.github/actions/set_neopdf_datapath/action.yml b/.github/actions/set_neopdf_datapath/action.yml new file mode 100644 index 0000000000..3bda94a787 --- /dev/null +++ b/.github/actions/set_neopdf_datapath/action.yml @@ -0,0 +1,12 @@ +name: Set NEOPDF_DATA_PATH + +# Point NeoPDF at the directory where lhapdf-management stores PDF sets, +# so it reads local files instead of downloading from the LHAPDF servers. + +runs: + using: "composite" + steps: + - name: Point NeoPDF at the LHAPDF data directory + shell: bash -l {0} + run: | + echo "NEOPDF_DATA_PATH=$(python -c 'from lhapdf_management.configuration import environment; print(environment.datapath)')" >> $GITHUB_ENV diff --git a/.github/workflows/all_tests_nnpdf.yml b/.github/workflows/all_tests_nnpdf.yml index d5c29293c9..1528b38a92 100644 --- a/.github/workflows/all_tests_nnpdf.yml +++ b/.github/workflows/all_tests_nnpdf.yml @@ -119,6 +119,7 @@ jobs: runs-on: ubuntu-latest env: KERAS_BACKEND: torch + NNPDF_PDF_BACKEND: neopdf steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v5 @@ -134,9 +135,10 @@ jobs: # at import on CPU-only Linux + CPython 3.13. Pinning torch<2.12 also # pins triton to 3.6.x. Remove this pin once the upstream issue is # resolved (see https://github.com/nascheme/triton/issues/43). - pip install ".[nolha,torch]" "torch<2.12" + pip install ".[neopdf,torch]" "torch<2.12" # Since there is no LHAPDF in the system, initialize the folder and download pdfsets.index lhapdf-management update --init + - uses: ./.github/actions/set_neopdf_datapath - name: Test we can run one runcard shell: bash -l {0} run: | @@ -157,6 +159,7 @@ jobs: runs-on: ubuntu-latest env: KERAS_BACKEND: jax + NNPDF_PDF_BACKEND: neopdf steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v5 @@ -165,9 +168,10 @@ jobs: - name: Install nnpdf without LHAPDF shell: bash -l {0} run: | - pip install .[nolha,jax] + pip install .[neopdf,jax] # Since there is no LHAPDF in the system, initialize the folder and download pdfsets.index lhapdf-management update --init + - uses: ./.github/actions/set_neopdf_datapath - name: Test we can run one runcard shell: bash -l {0} run: | diff --git a/pyproject.toml b/pyproject.toml index 42c6c3e07b..38164952bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,6 +98,8 @@ fiatlux = {version = "*", optional = true} # without lhapdf pdfflow = {version = "^1.2.1", optional = true} lhapdf-management = {version = "^0.6", optional = true} +# neopdf interpolation backend +neopdf-hep = {version = "^0.3", optional = true} # torch torch = {version = "*", optional = true} # jax @@ -110,7 +112,9 @@ pymongo = {version = "<4", optional = true} tests = ["pytest", "pytest-mpl", "hypothesis", "pytest-cov"] docs = ["sphinxcontrib-bibtex", "sphinx-rtd-theme", "sphinx", "tabulate"] qed = ["fiatlux"] -nolha = ["pdfflow", "lhapdf-management"] +pdfflow = ["pdfflow", "lhapdf-management"] +neopdf = ["neopdf-hep", "lhapdf-management"] +nolha = ["neopdf-hep", "lhapdf-management"] torch = ["torch"] jax = ["jax"] hyperopt = ["hyperopt", "setuptools"] diff --git a/validphys2/src/validphys/lhapdf_compatibility.py b/validphys2/src/validphys/lhapdf_compatibility.py index 33c355a71e..93ec2a1393 100644 --- a/validphys2/src/validphys/lhapdf_compatibility.py +++ b/validphys2/src/validphys/lhapdf_compatibility.py @@ -1,13 +1,18 @@ """ - Module for LHAPDF compatibility backends +Module for LHAPDF compatibility backends - If LHAPDF is installed, the module will transparently hand over everything to LHAPDF - if LHAPDF is not available, it will try to use a combination of the packages - `lhapdf-management` and `pdfflow` - which cover all the features of LHAPDF used during the fit (and likely most of validphys) +If LHAPDF is installed, the module will transparently hand over everything to LHAPDF. +If LHAPDF is not available, it will try to use a combination of the packages + `lhapdf-management` and `pdfflow` +which cover all the features of LHAPDF used during the fit (and likely most of validphys). + +The NeoPDF interpolation library can be selected by setting ``pdf_backend: neopdf`` +in the NNPDF profile (``nnprofile.yaml``), or via the ``NNPDF_PDF_BACKEND`` environment +variable which takes precedence over the profile. """ from functools import cached_property +import os import numpy as np @@ -25,6 +30,31 @@ USING_LHAPDF = False +_BACKEND_ENV_VAR = "NNPDF_PDF_BACKEND" +_VALID_BACKENDS = ("lhapdf", "pdfflow", "neopdf") + + +class InvalidPDFBackend(Exception): + pass + + +def _active_backend(): + """Return the active PDF backend. + + Resolution order (highest priority first): + 1. ``NNPDF_PDF_BACKEND`` environment variable + 2. ``pdf_backend`` key in the NNPDF profile (``nnprofile.yaml``) + """ + backend = os.environ.get(_BACKEND_ENV_VAR) + if backend is None: + try: + from nnpdf_data.utils import get_nnpdf_profile + + backend = get_nnpdf_profile().get("pdf_backend", "lhapdf") + except Exception: + backend = "lhapdf" + return backend.lower() + class _PDFFlowPDF: """Wrapper around the PDFFlow PDF so that it can be used as an LHAPDF @@ -32,10 +62,12 @@ class _PDFFlowPDF: Takes as input a pdf_meta object (which is a PDFset from lhapdf_management and which knows where the PDF needs to be loaded from) and a single member - Loading the PDF is done in a lazy manner since most of the time only a few members are needed. + Loading the PDF is done in a lazy manner since most of the time only a few + members are needed. - Since PDFFlow is only utilized to load the PDF for interpolation, the import is delayed until - the first call to `mkPDF`. This allows the usage of most of validphys without tensorflow. + Since PDFFlow is only utilized to load the PDF for interpolation, the import + is delayed until the first call to `mkPDF`. This allows the usage of most of + validphys without tensorflow. """ def __init__(self, pdf_meta, member): @@ -96,13 +128,54 @@ def xfxQ2(self, a, b, c=None): return self.xfxQ(a, b, np.sqrt(c)) +class _NeoPDFPDF: + """Thin wrapper around a single NeoPDF member exposing the LHAPDF-compatible interface.""" + + def __init__(self, neo_member): + self._member = neo_member + self._pids = neo_member.pids() + + def flavors(self): + return self._pids + + def _xfxQ_all_pid(self, x, q): + scalar_input = np.ndim(x) == 0 and np.ndim(q) == 0 + x = np.atleast_1d(x) + q = np.atleast_1d(q) + vals = np.array( + [ + self._member.xfxQ2_allpids(self._pids, float(xi), float(qi) ** 2) + for xi, qi in zip(x, q) + ] + ) # (n_points, n_pids) + if scalar_input: + return dict(zip(self._pids, vals[0])) + return dict(zip(self._pids, vals.T)) + + def xfxQ(self, a, b, c=None): + if c is None: + return self._xfxQ_all_pid(a, b) + ret_dict = self.xfxQ(b, c) + zeros = np.zeros_like(b) + if isinstance(a, int): + return ret_dict.get(a, zeros) + return np.array([ret_dict.get(i, zeros) for i in a]).T + + def xfxQ2(self, a, b, c=None): + if c is None: + return self.xfxQ(a, np.sqrt(b)) + return self.xfxQ(a, b, np.sqrt(c)) + + def make_pdf(pdf_name, member=None): - """Load a PDF - if member is given, load the single member otherwise, load the entire set as a list + """Load a single member if specified, otherwise load the entire set as a list. - if LHAPDF is provided, it returns LHAPDF PDF instances - otherwise it returns and object which is _compatible_ with LHAPDF - for lhapdf functions for the selected backend + If LHAPDF is provided, it returns LHAPDF PDF instances otherwise it returns and + object which is _compatible_ with LHAPDF for lhapdf functions for the selected + backend. + + The backend can be overridden by setting the ``NNPDF_PDF_BACKEND`` environment + variable to ``neopdf`` to use the NeoPDF interpolation library. Parameters: ----------- @@ -115,11 +188,25 @@ def make_pdf(pdf_name, member=None): -------- list(pdf_sets) """ - if USING_LHAPDF: + backend = _active_backend() + + if backend not in _VALID_BACKENDS: + raise InvalidPDFBackend(f"Unknown backend {backend!r}. Options are: {_VALID_BACKENDS}") + + if backend == "lhapdf": if member is None: return lhapdf.mkPDFs(pdf_name) return [lhapdf.mkPDF(pdf_name, member)] + if backend == "neopdf": + from neopdf.pdf import PDF as _NeoPDF + + members = _NeoPDF.mkPDFs(pdf_name) + if member is None: + return [_NeoPDFPDF(m) for m in members] + return [_NeoPDFPDF(members[member])] + + # backend == "pdfflow" pdf_meta = lhapdf.load_pdf_meta(pdf_name) if member is None: return [_PDFFlowPDF(pdf_meta, m) for m in range(len(pdf_meta))] diff --git a/validphys2/src/validphys/nnprofile_default.yaml b/validphys2/src/validphys/nnprofile_default.yaml index f09ffe9559..a83fdda131 100644 --- a/validphys2/src/validphys/nnprofile_default.yaml +++ b/validphys2/src/validphys/nnprofile_default.yaml @@ -67,6 +67,9 @@ photon_qed_urls: photon_qed_index: 'photondata.json' +# PDF interpolation backend: "lhapdf" (default), "pdfflow", or "neopdf" +pdf_backend: lhapdf + lhapdf_urls: - 'http://lhapdfsets.web.cern.ch/lhapdfsets/current/' nnpdf_pdfs_urls: diff --git a/validphys2/src/validphys/tests/test_neopdfset.py b/validphys2/src/validphys/tests/test_neopdfset.py new file mode 100644 index 0000000000..7034676512 --- /dev/null +++ b/validphys2/src/validphys/tests/test_neopdfset.py @@ -0,0 +1,315 @@ +""" +Tests for the NeoPDF interpolation backend. +""" + +import os + +import numpy as np +import pytest + +# Skip the whole module if neopdf is not importable +neopdf = pytest.importorskip("neopdf", reason="neopdf not installed") + +# In view of making LHAPDF Optional in the future. +try: + import lhapdf as _lhapdf + + _lhapdf.setVerbosity(0) + HAS_LHAPDF = True +except ModuleNotFoundError: + HAS_LHAPDF = False + +requires_lhapdf = pytest.mark.skipif(not HAS_LHAPDF, reason="lhapdf not installed") + +PDF_NAME = "NNPDF40_nnlo_as_01180" + +PIDS = [-5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 21] + +XGRID = np.array([1e-5, 1e-3, 1e-2, 0.1, 0.3, 0.5, 0.7, 0.9]) +QGRID = np.array([1.7, 5.0, 10.0, 100.0]) # GeV + +X_VALUE = 0.1 +Q_VALUE = 10.0 + + +@pytest.fixture(scope="module") +def neo_pdfset(): + """LHAPDFSet loaded with the NeoPDF backend.""" + from validphys.lhapdfset import LHAPDFSet + + os.environ["NNPDF_PDF_BACKEND"] = "neopdf" + pdfset = LHAPDFSet(PDF_NAME, "replicas") + del os.environ["NNPDF_PDF_BACKEND"] + return pdfset + + +@pytest.fixture(scope="module") +def neo_pdfset_t0(): + """LHAPDFSet in t0 mode loaded with the NeoPDF backend.""" + from validphys.lhapdfset import LHAPDFSet + + os.environ["NNPDF_PDF_BACKEND"] = "neopdf" + pdfset = LHAPDFSet(PDF_NAME, "t0") + del os.environ["NNPDF_PDF_BACKEND"] + return pdfset + + +@pytest.fixture(scope="module") +def lha_pdfset(): + from validphys.lhapdfset import LHAPDFSet + + return LHAPDFSet(PDF_NAME, "replicas") + + +@pytest.fixture(scope="module") +def lha_pdfset_t0(): + from validphys.lhapdfset import LHAPDFSet + + return LHAPDFSet(PDF_NAME, "t0") + + +class TestT0Mode: + """Focused tests on t0 semantics for the NeoPDF backend.""" + + def test_t0_members_slice_length(self, neo_pdfset_t0): + assert len(neo_pdfset_t0.members) == 1 + + def test_t0_central_member_identity(self, neo_pdfset_t0): + assert neo_pdfset_t0.central_member is neo_pdfset_t0.members[0] + + def test_t0_xfxQ_index_zero(self, neo_pdfset_t0): + val = neo_pdfset_t0.xfxQ(X_VALUE, Q_VALUE, n=0, fl=21) + assert isinstance(val, float) + assert np.isfinite(val) + + def test_t0_xfxQ_out_of_range_member_raises(self, neo_pdfset_t0): + """Requesting member index >= 1 in t0 mode must raise IndexError.""" + with pytest.raises(IndexError): + neo_pdfset_t0.xfxQ(X_VALUE, Q_VALUE, n=1, fl=21) + + @requires_lhapdf + def test_t0_xfxQ_agrees_with_lhapdf(self, neo_pdfset_t0, lha_pdfset_t0): + for fl in [21, 1, 2, -1]: + if fl not in neo_pdfset_t0.flavors: + continue + neo_val = neo_pdfset_t0.xfxQ(X_VALUE, Q_VALUE, n=0, fl=fl) + lha_val = lha_pdfset_t0.xfxQ(X_VALUE, Q_VALUE, n=0, fl=fl) + np.testing.assert_equal(neo_val, lha_val) + + +class TestNeoPDFSetInterface: + """The following simpy checks that that ``NeoPDFSet`` exposes the same + interface as ``LHAPDFSet``. + """ + + def test_is_t0_false_for_replicas(self, neo_pdfset): + assert neo_pdfset.is_t0 is False + + def test_is_t0_true_for_t0(self, neo_pdfset_t0): + assert neo_pdfset_t0.is_t0 is True + + def test_n_members_positive(self, neo_pdfset): + assert neo_pdfset.n_members > 0 + + def test_n_members_matches_members_length(self, neo_pdfset): + assert neo_pdfset.n_members == len(neo_pdfset.members) + + def test_t0_has_exactly_one_member(self, neo_pdfset_t0): + assert neo_pdfset_t0.n_members == 1 + + def test_central_member_is_member_zero(self, neo_pdfset): + assert neo_pdfset.central_member is neo_pdfset.members[0] + + def test_flavors_is_list(self, neo_pdfset): + assert isinstance(neo_pdfset.flavors, list) + + def test_flavors_contains_gluon(self, neo_pdfset): + assert 21 in neo_pdfset.flavors + + def test_flavors_contains_quarks(self, neo_pdfset): + for pid in [-2, -1, 1, 2]: + assert pid in neo_pdfset.flavors + + def test_flavors_cached(self, neo_pdfset): + assert neo_pdfset.flavors is neo_pdfset.flavors + + def test_xfxQ_returns_float(self, neo_pdfset): + val = neo_pdfset.xfxQ(X_VALUE, Q_VALUE, n=0, fl=21) + assert isinstance(val, float) + + def test_xfxQ_absent_flavour_returns_zero(self, neo_pdfset): + if 6 not in neo_pdfset.flavors: + assert neo_pdfset.xfxQ(X_VALUE, Q_VALUE, n=0, fl=6) == 0.0 + + def test_grid_values_shape(self, neo_pdfset): + nx, nq, _ = len(XGRID), len(QGRID), len(PIDS) + pids = [p for p in PIDS if p in neo_pdfset.flavors] + result = neo_pdfset.grid_values(np.array(pids), XGRID, QGRID) + assert result.shape == (neo_pdfset.n_members, len(pids), nx, nq) + + def test_grid_values_dtype_is_float(self, neo_pdfset): + pids = [p for p in PIDS if p in neo_pdfset.flavors] + result = neo_pdfset.grid_values(np.array(pids), XGRID, QGRID) + assert np.issubdtype(result.dtype, np.floating) + + def test_grid_values_finite(self, neo_pdfset): + pids = [p for p in PIDS if p in neo_pdfset.flavors] + result = neo_pdfset.grid_values(np.array(pids), XGRID, QGRID) + assert np.all(np.isfinite(result)) + + def test_grid_values_single_x_single_q(self, neo_pdfset): + pids = [21] + result = neo_pdfset.grid_values(np.array(pids), np.array([0.1]), np.array([10.0])) + assert result.shape == (neo_pdfset.n_members, 1, 1, 1) + + def test_t0_grid_values_shape(self, neo_pdfset_t0): + pids = [p for p in PIDS if p in neo_pdfset_t0.flavors] + result = neo_pdfset_t0.grid_values(np.array(pids), XGRID, QGRID) + assert result.shape == (1, len(pids), len(XGRID), len(QGRID)) + + +@requires_lhapdf +class TestNumericalAgreement: + """ + NeoPDF and LHAPDF must produce identical values (bit-for-bit) for every + call that both backends support. + """ + + @pytest.mark.parametrize("fl", [21, 1, -1, 2, -2, 3]) + def test_xfxQ_member0(self, neo_pdfset, lha_pdfset, fl): + if fl not in neo_pdfset.flavors: + pytest.skip(f"pid {fl} not in set") + neo_val = neo_pdfset.xfxQ(X_VALUE, Q_VALUE, n=0, fl=fl) + lha_val = lha_pdfset.xfxQ(X_VALUE, Q_VALUE, n=0, fl=fl) + np.testing.assert_equal(neo_val, lha_val) + + @pytest.mark.parametrize("fl", [21, 2]) + def test_xfxQ_all_members(self, neo_pdfset, lha_pdfset, fl): + if fl not in neo_pdfset.flavors: + pytest.skip(f"pid {fl} not in set") + for n in range(neo_pdfset.n_members): + neo_val = neo_pdfset.xfxQ(X_VALUE, Q_VALUE, n=n, fl=fl) + lha_val = lha_pdfset.xfxQ(X_VALUE, Q_VALUE, n=n, fl=fl) + np.testing.assert_equal(neo_val, lha_val) + + @pytest.mark.parametrize("x", [1e-5, 1e-3, 0.1, 0.5, 0.9]) + @pytest.mark.parametrize("Q", [1.7, 10.0, 100.0]) + def test_xfxQ_gluon_phase_space(self, neo_pdfset, lha_pdfset, x, Q): + neo_val = neo_pdfset.xfxQ(x, Q, n=0, fl=21) + lha_val = lha_pdfset.xfxQ(x, Q, n=0, fl=21) + np.testing.assert_equal(neo_val, lha_val) + + @pytest.mark.parametrize("x", [1e-5, 0.1, 0.9]) + @pytest.mark.parametrize("Q", [1.7, 100.0]) + @pytest.mark.parametrize("fl", [6, -6]) + def test_xfxQ_absent_flavour_zero(self, neo_pdfset, fl, x, Q): + """Absent flavours must return 0.0.""" + assert fl not in neo_pdfset.flavors + assert neo_pdfset.xfxQ(x, Q, n=0, fl=fl) == 0.0 + + def test_grid_values_shape_matches_lhapdf(self, neo_pdfset, lha_pdfset): + pids = np.array([p for p in PIDS if p in neo_pdfset.flavors]) + neo_result = neo_pdfset.grid_values(pids, XGRID, QGRID) + lha_result = lha_pdfset.grid_values(pids, XGRID, QGRID) + assert neo_result.shape == lha_result.shape + + @pytest.mark.parametrize("fl", [21, 1, 2]) + def test_grid_values_single_flavour(self, neo_pdfset, lha_pdfset, fl): + if fl not in neo_pdfset.flavors: + pytest.skip(f"pid {fl} not in set") + pids = np.array([fl]) + neo_result = neo_pdfset.grid_values(pids, XGRID, QGRID) + lha_result = lha_pdfset.grid_values(pids, XGRID, QGRID) + np.testing.assert_array_equal(neo_result, lha_result) + + def test_grid_values_all_flavours(self, neo_pdfset, lha_pdfset): + pids = np.array([p for p in PIDS if p in neo_pdfset.flavors]) + neo_result = neo_pdfset.grid_values(pids, XGRID, QGRID) + lha_result = lha_pdfset.grid_values(pids, XGRID, QGRID) + np.testing.assert_array_equal(neo_result, lha_result) + + def test_grid_values_member0_equals_xfxQ_scalar(self, neo_pdfset): + """With nq=1 the grid cell directly maps to the scalar xfxQ value.""" + fl = 21 + x_idx = 2 + x = XGRID[x_idx] + Q_single = np.array([Q_VALUE]) + grid = neo_pdfset.grid_values(np.array([fl]), XGRID, Q_single) + scalar = neo_pdfset.xfxQ(x, Q_VALUE, n=0, fl=fl) + np.testing.assert_equal(grid[0, 0, x_idx, 0], scalar) + + def test_t0_central_value(self, neo_pdfset_t0, lha_pdfset_t0): + pids = np.array([p for p in PIDS if p in neo_pdfset_t0.flavors]) + neo_result = neo_pdfset_t0.grid_values(pids, XGRID, QGRID) + lha_result = lha_pdfset_t0.grid_values(pids, XGRID, QGRID) + np.testing.assert_array_equal(neo_result, lha_result) + + def test_t0_same_as_replica_member0(self, neo_pdfset, neo_pdfset_t0): + """t0 set must return the same values as member 0 of the replica set.""" + pids = np.array([p for p in PIDS if p in neo_pdfset.flavors]) + full = neo_pdfset.grid_values(pids, XGRID, QGRID) + t0 = neo_pdfset_t0.grid_values(pids, XGRID, QGRID) + np.testing.assert_array_equal(full[0:1], t0) + + def test_flavors_match_lhapdf(self, neo_pdfset, lha_pdfset): + assert sorted(neo_pdfset.flavors) == sorted(lha_pdfset.flavors) + + def test_t0_flavors_match_lhapdf(self, neo_pdfset_t0, lha_pdfset_t0): + assert sorted(neo_pdfset_t0.flavors) == sorted(lha_pdfset_t0.flavors) + + def test_n_members_matches_lhapdf(self, neo_pdfset, lha_pdfset): + assert neo_pdfset.n_members == lha_pdfset.n_members + + +class TestBackendFactory: + """Verify that ``make_pdf`` in ``lhapdf_compatibility`` selects the correct + backend based on the ``NNPDF_PDF_BACKEND`` environment variable. + """ + + def test_default_returns_lhapdf_members(self, monkeypatch): + monkeypatch.delenv("NNPDF_PDF_BACKEND", raising=False) + from validphys.lhapdf_compatibility import _NeoPDFPDF, make_pdf + + members = make_pdf(PDF_NAME) + assert not isinstance(members[0], _NeoPDFPDF) + + def test_env_var_neopdf_returns_neopdf_members(self, monkeypatch): + monkeypatch.setenv("NNPDF_PDF_BACKEND", "neopdf") + from validphys.lhapdf_compatibility import _NeoPDFPDF, make_pdf + + members = make_pdf(PDF_NAME) + assert all(isinstance(m, _NeoPDFPDF) for m in members) + + def test_env_var_lhapdf_returns_non_neopdf_members(self, monkeypatch): + monkeypatch.setenv("NNPDF_PDF_BACKEND", "lhapdf") + from validphys.lhapdf_compatibility import _NeoPDFPDF, make_pdf + + members = make_pdf(PDF_NAME) + assert not isinstance(members[0], _NeoPDFPDF) + + def test_invalid_env_var_raises(self, monkeypatch): + monkeypatch.setenv("NNPDF_PDF_BACKEND", "pdfflow_is_not_valid_here") + from validphys.lhapdf_compatibility import InvalidPDFBackend, make_pdf + + with pytest.raises(InvalidPDFBackend, match="Unknown backend"): + make_pdf(PDF_NAME) + + def test_neopdf_single_member_returns_neopdf_member(self, monkeypatch): + monkeypatch.setenv("NNPDF_PDF_BACKEND", "neopdf") + from validphys.lhapdf_compatibility import _NeoPDFPDF, make_pdf + + members = make_pdf(PDF_NAME, member=0) + assert len(members) == 1 + assert isinstance(members[0], _NeoPDFPDF) + + @requires_lhapdf + def test_both_backends_return_same_member_count(self, monkeypatch): + from validphys.lhapdf_compatibility import make_pdf + + monkeypatch.setenv("NNPDF_PDF_BACKEND", "neopdf") + neo_members = make_pdf(PDF_NAME) + + monkeypatch.setenv("NNPDF_PDF_BACKEND", "lhapdf") + lha_members = make_pdf(PDF_NAME) + + assert len(neo_members) == len(lha_members)