Skip to content
Open
3 changes: 2 additions & 1 deletion .github/actions/install_conda_pip/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ inputs:
nnpdf-extras:
required: true
description: "Which extras to install"
default: "[qed,tests,hyperopt]"
default: "[qed,tests,hyperopt,neopdf]"


runs:
Expand All @@ -44,3 +44,4 @@ runs:
shell: bash -l {0}
run: |
pip install -e .${{ inputs.nnpdf-extras }}
- uses: ./.github/actions/set_neopdf_datapath
12 changes: 12 additions & 0 deletions .github/actions/set_neopdf_datapath/action.yml
Original file line number Diff line number Diff line change
@@ -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
8 changes: 6 additions & 2 deletions .github/workflows/all_tests_nnpdf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ jobs:
runs-on: ubuntu-latest
env:
KERAS_BACKEND: torch
NNPDF_PDF_BACKEND: neopdf
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
Expand All @@ -129,9 +130,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: |
Expand All @@ -151,6 +153,7 @@ jobs:
runs-on: ubuntu-latest
env:
KERAS_BACKEND: jax
NNPDF_PDF_BACKEND: neopdf
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
Expand All @@ -159,9 +162,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: |
Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"]
Expand Down
115 changes: 101 additions & 14 deletions validphys2/src/validphys/lhapdf_compatibility.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -25,17 +30,44 @@

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
set by validphys
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):
Expand Down Expand Up @@ -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:
-----------
Expand All @@ -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:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw, to get the information from the profile just get_nnpdf_profile() is enough (from nnpdf_data::utils)

I'm not sure right now whether one can add variables at will without changing that function (in any case it would be trivial). The important thing is, don't add a new default to get_nnpdf_profile for pdf_backend but rather add explicitly to validphys/nnprofile_default.yaml pdf_backend: lhapdf.

The goal I have in mind is that people can start using neopdf there if they like and in a few months we try to swap the default and see what happens.

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))]
Expand Down
3 changes: 3 additions & 0 deletions validphys2/src/validphys/nnprofile_default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading
Loading