use anyhow::{Context as _, Result, anyhow};
use enum_dispatch::enum_dispatch;
use lhapdf::PdfSet;
use neopdf::gridpdf::ForcePositive as NeoPDFForcePositive;
use neopdf::pdf::PDF;
use std::str::FromStr;
pub use neopdf::uncertainty::CL_1_SIGMA;
#[derive(Clone, Copy, Debug)]
pub enum ForcePositive {
None,
ClipNegative,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Backend {
#[default]
Lhapdf,
Neopdf,
}
impl FromStr for Backend {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"lhapdf" => Ok(Self::Lhapdf),
"neopdf" => Ok(Self::Neopdf),
_ => Err(anyhow!(
"unknown PDF backend '{s}'; must be 'lhapdf' or 'neopdf'"
)),
}
}
}
#[enum_dispatch]
pub trait PdfBackend: Send {
fn alphas_q2(&self, q2: f64) -> f64;
fn particle_id(&self) -> i32;
fn set_force_positive(&mut self, method: ForcePositive);
fn x_max(&mut self) -> f64;
fn x_min(&mut self) -> f64;
fn xfx_q2(&self, id: i32, x: f64, q2: f64) -> f64;
}
#[expect(
clippy::large_enum_variant,
reason = "LhapdfPdf is artificially small due to indirection"
)]
#[enum_dispatch(PdfBackend)]
pub enum ConvFunBackend {
LhapdfPdf,
NeopdfPdf,
}
#[enum_dispatch]
pub trait PdfSetBackend {
fn mk_pdfs(&self) -> Result<Vec<ConvFunBackend>>;
fn uncertainty(&self, values: &[f64], cl: f64, alternative: bool) -> Result<Uncertainty>;
}
#[enum_dispatch(PdfSetBackend)]
pub enum ConvFunSetBackend {
LhapdfSet,
NeopdfSet,
}
#[derive(Clone, Debug)]
pub struct Uncertainty {
pub central: f64,
pub errminus: f64,
pub errplus: f64,
}
pub struct LhapdfPdf {
pdf: lhapdf::Pdf,
}
impl LhapdfPdf {
pub fn with_lhaid(lhaid: i32) -> Result<Self> {
Ok(Self {
pdf: lhapdf::Pdf::with_lhaid(lhaid)
.with_context(|| format!("failed to load LHAPDF with LHAID {lhaid}"))?,
})
}
pub fn with_setname_and_member(setname: &str, member: i32) -> Result<Self> {
Ok(Self {
pdf: lhapdf::Pdf::with_setname_and_member(setname, member).with_context(|| {
format!("failed to load LHAPDF set '{setname}' member {member}")
})?,
})
}
}
impl PdfBackend for LhapdfPdf {
fn alphas_q2(&self, q2: f64) -> f64 {
self.pdf.alphas_q2(q2)
}
fn particle_id(&self) -> i32 {
self.pdf
.set()
.entry("Particle")
.map_or(Ok(2212), |s| s.parse())
.unwrap_or(2212)
}
fn set_force_positive(&mut self, method: ForcePositive) {
match method {
ForcePositive::None => self.pdf.set_force_positive(0),
ForcePositive::ClipNegative => self.pdf.set_force_positive(1),
}
}
fn x_max(&mut self) -> f64 {
self.pdf.x_max()
}
fn x_min(&mut self) -> f64 {
self.pdf.x_min()
}
fn xfx_q2(&self, id: i32, x: f64, q2: f64) -> f64 {
self.pdf.xfx_q2(id, x, q2)
}
}
pub struct LhapdfSet {
set: lhapdf::PdfSet,
}
impl LhapdfSet {
pub fn new(setname: &str) -> Result<Self> {
Ok(Self {
set: PdfSet::new(setname)
.with_context(|| format!("failed to load LHAPDF set '{setname}'"))?,
})
}
}
impl PdfSetBackend for LhapdfSet {
fn mk_pdfs(&self) -> Result<Vec<ConvFunBackend>> {
let pdfs = self.set.mk_pdfs()?;
Ok(pdfs
.into_iter()
.map(|pdf| LhapdfPdf { pdf }.into())
.collect())
}
fn uncertainty(&self, values: &[f64], cl: f64, alternative: bool) -> Result<Uncertainty> {
let unc = self.set.uncertainty(values, cl, alternative)?;
Ok(Uncertainty {
central: unc.central,
errminus: unc.errminus,
errplus: unc.errplus,
})
}
}
pub struct NeopdfPdf {
pdf: PDF,
}
impl NeopdfPdf {
#[must_use]
pub fn load(pdf_name: &str, member: usize) -> Self {
Self {
pdf: PDF::load(pdf_name, member),
}
}
#[must_use]
pub fn load_by_lhaid(lhaid: u32) -> Self {
Self {
pdf: PDF::load_by_lhaid(lhaid),
}
}
}
impl PdfBackend for NeopdfPdf {
fn alphas_q2(&self, q2: f64) -> f64 {
self.pdf.alphas_q2(q2)
}
fn particle_id(&self) -> i32 {
self.pdf.metadata().hadron_pid
}
fn set_force_positive(&mut self, method: ForcePositive) {
match method {
ForcePositive::None => {
self.pdf.set_force_positive(NeoPDFForcePositive::NoClipping);
}
ForcePositive::ClipNegative => {
self.pdf
.set_force_positive(NeoPDFForcePositive::ClipNegative);
}
}
}
fn x_max(&mut self) -> f64 {
self.pdf.metadata().x_max
}
fn x_min(&mut self) -> f64 {
self.pdf.metadata().x_min
}
fn xfx_q2(&self, id: i32, x: f64, q2: f64) -> f64 {
self.pdf.xfxq2(id, &[x, q2])
}
}
pub struct NeopdfSet {
pdf_name: String,
error_type: String,
}
impl NeopdfSet {
#[must_use]
pub fn new(pdf_name: &str) -> Self {
let pdf0 = PDF::load(pdf_name, 0);
let metadata = pdf0.metadata();
Self {
pdf_name: pdf_name.to_owned(),
error_type: metadata.error_type.clone(),
}
}
}
impl PdfSetBackend for NeopdfSet {
fn mk_pdfs(&self) -> Result<Vec<ConvFunBackend>> {
let pdfs = PDF::load_pdfs(&self.pdf_name);
Ok(pdfs
.into_iter()
.map(|pdf| NeopdfPdf { pdf }.into())
.collect())
}
fn uncertainty(&self, values: &[f64], cl: f64, alternative: bool) -> Result<Uncertainty> {
let unc =
neopdf::uncertainty::uncertainty(values, &self.error_type, CL_1_SIGMA, cl, alternative)
.map_err(|e| anyhow::anyhow!(e))?;
Ok(Uncertainty {
central: unc.central,
errminus: unc.errminus,
errplus: unc.errplus,
})
}
}
pub fn create_pdf(name: &str, member: usize, backend: Backend) -> Result<ConvFunBackend> {
match backend {
Backend::Lhapdf => {
if let Ok(lhaid) = name.parse::<i32>() {
Ok(LhapdfPdf::with_lhaid(lhaid)?.into())
} else {
Ok(LhapdfPdf::with_setname_and_member(name, member.try_into().unwrap())?.into())
}
}
Backend::Neopdf => {
if let Ok(lhaid) = name.parse::<u32>() {
Ok(NeopdfPdf::load_by_lhaid(lhaid).into())
} else {
Ok(NeopdfPdf::load(name, member).into())
}
}
}
}
pub fn create_pdf_set(name: &str, backend: Backend) -> Result<ConvFunSetBackend> {
match backend {
Backend::Lhapdf => {
let setname = if let Ok(lhaid) = name.parse::<i32>() {
lhapdf::lookup_pdf(lhaid)
.map(|(set, _)| set)
.ok_or_else(|| anyhow!("no convolution function for LHAID = `{lhaid}` found"))?
} else {
name.to_owned()
};
Ok(LhapdfSet::new(&setname)?.into())
}
Backend::Neopdf => Ok(NeopdfSet::new(name).into()),
}
}