From 8662bb44496f8506a842c87a0493136ffd1e180d Mon Sep 17 00:00:00 2001 From: Radonirinaunimi Date: Wed, 10 Jun 2026 16:34:49 +0200 Subject: [PATCH 1/5] Implement LHAPDF symbols for uncertainties and keys --- neopdf/src/alphas.rs | 1 + neopdf/src/gridpdf.rs | 1 + neopdf/src/metadata.rs | 13 +- neopdf/src/writer.rs | 1 + neopdf_capi/src/lib.rs | 2 + neopdf_cli/src/tmd_converter.rs | 1 + neopdf_pyapi/src/metadata.rs | 148 ++++++++++++++++++ neopdf_pyapi/src/pdf.rs | 107 ++++++++++++- .../tests/test_lhapdf_compatibility.py | 140 +++++++++++++++++ 9 files changed, 412 insertions(+), 2 deletions(-) diff --git a/neopdf/src/alphas.rs b/neopdf/src/alphas.rs index 324c7d66..4c3fd9c5 100644 --- a/neopdf/src/alphas.rs +++ b/neopdf/src/alphas.rs @@ -275,6 +275,7 @@ mod tests { xi_max: 0.0, delta_min: 0.0, delta_max: 0.0, + error_conf_level: None, } } diff --git a/neopdf/src/gridpdf.rs b/neopdf/src/gridpdf.rs index 77f3cb81..004d0a03 100644 --- a/neopdf/src/gridpdf.rs +++ b/neopdf/src/gridpdf.rs @@ -1168,6 +1168,7 @@ mod tests { xi_max: 0.0, delta_min: 0.0, delta_max: 0.0, + error_conf_level: None, }; let subgrid_data = vec![SubgridData { diff --git a/neopdf/src/metadata.rs b/neopdf/src/metadata.rs index a8eaed7b..ed49fcae 100644 --- a/neopdf/src/metadata.rs +++ b/neopdf/src/metadata.rs @@ -141,6 +141,11 @@ pub struct MetaDataV2 { /// Maximum delta-value for which the PDF is valid. #[serde(rename = "DeltaMax", default)] pub delta_max: f64, + /// Confidence level (in %) at which the error members were constructed. + /// `None` when the field is absent from the `.info` file (LHAPDF convention: + /// treat as CL_1_SIGMA ≈ 68.27 %, and return -1.0 from `errorConfLevel`). + #[serde(rename = "ErrorConfLevel", default)] + pub error_conf_level: Option, } impl MetaDataV2 { @@ -191,7 +196,12 @@ impl std::fmt::Display for MetaDataV2 { writeln!(f, "XiMin: {}", self.xi_min)?; writeln!(f, "XiMax: {}", self.xi_max)?; writeln!(f, "DeltaMin: {}", self.delta_min)?; - write!(f, "DeltaMax: {}", self.delta_max) + write!(f, "DeltaMax: {}", self.delta_max)?; + if let Some(ecl) = self.error_conf_level { + writeln!(f)?; + write!(f, "ErrorConfLevel: {ecl}")?; + } + Ok(()) } } @@ -258,6 +268,7 @@ impl From for MetaData { xi_max: 0.0, delta_min: 0.0, delta_max: 0.0, + error_conf_level: None, } } } diff --git a/neopdf/src/writer.rs b/neopdf/src/writer.rs index 918caadb..e2ed5989 100644 --- a/neopdf/src/writer.rs +++ b/neopdf/src/writer.rs @@ -641,6 +641,7 @@ mod tests { xi_max: 0.0, delta_min: 0.0, delta_max: 0.0, + error_conf_level: None, }; let test_grid = test_grid(); diff --git a/neopdf_capi/src/lib.rs b/neopdf_capi/src/lib.rs index 0a388bdd..ec2f23c6 100644 --- a/neopdf_capi/src/lib.rs +++ b/neopdf_capi/src/lib.rs @@ -1153,6 +1153,7 @@ fn process_metadata(meta: *const NeoPDFMetaData) -> Option { xi_max: 1.0, delta_min: 0.0, delta_max: 0.0, + error_conf_level: None, }; Some(metadata) @@ -1213,6 +1214,7 @@ fn process_metadata_v2(meta: *const NeoPDFMetaDataV2) -> Option { xi_max: meta.xi_max, delta_min: meta.delta_min, delta_max: meta.delta_max, + error_conf_level: None, }; Some(metadata) diff --git a/neopdf_cli/src/tmd_converter.rs b/neopdf_cli/src/tmd_converter.rs index 216d6df7..730fe3f5 100644 --- a/neopdf_cli/src/tmd_converter.rs +++ b/neopdf_cli/src/tmd_converter.rs @@ -282,6 +282,7 @@ pub fn convert_tmd(input_path: &str, output_path: &str) -> Result<(), Box(v: &[T]) -> String { + format!( + "[{}]", + v.iter() + .map(std::string::ToString::to_string) + .collect::>() + .join(", ") + ) +} + +fn insert_mandatory_fields(map: &mut HashMap, meta: &MetaData) { + let set_type = match &meta.set_type { + SetType::SpaceLike => "PDF", + SetType::TimeLike => "FragFn", + }; + let interp_type = match &meta.interpolator_type { + InterpolatorType::Bilinear => "Bilinear", + InterpolatorType::LogBilinear => "LogBilinear", + InterpolatorType::LogBicubic => "LogBicubic", + InterpolatorType::LogTricubic => "LogTricubic", + InterpolatorType::InterpNDLinear => "NDLinear", + InterpolatorType::LogChebyshev => "LogChebyshev", + InterpolatorType::LogFourCubic => "LogFourCubic", + InterpolatorType::LogFiveCubic => "LogFiveCubic", + }; + map.insert("SetDesc".to_string(), meta.set_desc.clone()); + map.insert("SetIndex".to_string(), meta.set_index.to_string()); + map.insert("NumMembers".to_string(), meta.num_members.to_string()); + map.insert("XMin".to_string(), meta.x_min.to_string()); + map.insert("XMax".to_string(), meta.x_max.to_string()); + map.insert("QMin".to_string(), meta.q_min.to_string()); + map.insert("QMax".to_string(), meta.q_max.to_string()); + map.insert("Flavors".to_string(), format_list(&meta.flavors)); + map.insert("Format".to_string(), meta.format.clone()); + map.insert("SetType".to_string(), set_type.to_string()); + map.insert("InterpolatorType".to_string(), interp_type.to_string()); + map.insert("Polarized".to_string(), meta.polarised.to_string()); + map.insert("Particle".to_string(), meta.hadron_pid.to_string()); + map.insert("OrderQCD".to_string(), meta.order_qcd.to_string()); +} + +fn insert_optional_fields(map: &mut HashMap, meta: &MetaData) { + for (key, val) in [ + ("ErrorType", meta.error_type.as_str()), + ("GitVersion", meta.git_version.as_str()), + ("CodeVersion", meta.code_version.as_str()), + ("FlavorScheme", meta.flavor_scheme.as_str()), + ("AlphaS_Type", meta.alphas_type.as_str()), + ] { + if !val.is_empty() { + map.insert(key.to_string(), val.to_string()); + } + } + for (key, vals) in [ + ("AlphaS_Qs", &meta.alphas_q_values), + ("AlphaS_Vals", &meta.alphas_vals), + ] { + if !vals.is_empty() { + map.insert(key.to_string(), format_list(vals)); + } + } + for (key, val) in [ + ("AlphaS_OrderQCD", meta.alphas_order_qcd), + ("NumFlavors", meta.number_flavors), + ] { + if val != 0 { + map.insert(key.to_string(), val.to_string()); + } + } +} + +fn insert_mass_fields(map: &mut HashMap, meta: &MetaData) { + for (key, val) in [ + ("MW", meta.m_w), + ("MZ", meta.m_z), + ("MUp", meta.m_up), + ("MDown", meta.m_down), + ("MStrange", meta.m_strange), + ("MCharm", meta.m_charm), + ("MBottom", meta.m_bottom), + ("MTop", meta.m_top), + ] { + if val != 0.0 { + map.insert(key.to_string(), val.to_string()); + } + } +} + +fn insert_tmd_fields(map: &mut HashMap, meta: &MetaData) { + for (key, val) in [ + ("XiMin", meta.xi_min), + ("XiMax", meta.xi_max), + ("DeltaMin", meta.delta_min), + ("DeltaMax", meta.delta_max), + ] { + if val != 0.0 { + map.insert(key.to_string(), val.to_string()); + } + } + if let Some(ecl) = meta.error_conf_level { + map.insert("ErrorConfLevel".to_string(), ecl.to_string()); + } +} + +/// Build a map of LHAPDF-canonical key names to their string values for a `MetaData` object. +/// +/// Keys that are absent from the original `.info` file (represented by empty strings, +/// zero values, or `None`) are not included, mirroring LHAPDF's `has_key` semantics. +pub(crate) fn build_lhapdf_map(meta: &MetaData) -> HashMap { + let mut map = HashMap::new(); + insert_mandatory_fields(&mut map, meta); + insert_optional_fields(&mut map, meta); + insert_mass_fields(&mut map, meta); + insert_tmd_fields(&mut map, meta); + map +} + /// Grid metadata. #[pyclass(from_py_object, name = "MetaData")] #[derive(Debug, Clone)] @@ -296,6 +415,7 @@ impl PyMetaData { xi_max: xsi_max, delta_min, delta_max, + error_conf_level: None, }, } } @@ -357,6 +477,34 @@ impl PyMetaData { Ok(dict.into()) } + /// Return `True` if the given LHAPDF-canonical key is present in the metadata. + #[must_use] + #[pyo3(name = "has_key")] + pub fn has_key(&self, key: &str) -> bool { + build_lhapdf_map(&self.meta).contains_key(key) + } + + /// Return the string value for an LHAPDF-canonical metadata key. + /// + /// # Errors + /// + /// Raises `KeyError` if the key is not present. + #[pyo3(name = "get_entry")] + pub fn get_entry(&self, key: &str) -> PyResult { + build_lhapdf_map(&self.meta).remove(key).ok_or_else(|| { + pyo3::exceptions::PyKeyError::new_err(format!("Key '{key}' not found in metadata")) + }) + } + + /// Return a sorted list of all available LHAPDF-canonical metadata keys. + #[must_use] + #[pyo3(name = "keys")] + pub fn keys(&self) -> Vec { + let mut keys: Vec = build_lhapdf_map(&self.meta).into_keys().collect(); + keys.sort(); + keys + } + /// The description of the set. #[must_use] pub const fn set_desc(&self) -> &String { diff --git a/neopdf_pyapi/src/pdf.rs b/neopdf_pyapi/src/pdf.rs index 5d506513..4b96b766 100644 --- a/neopdf_pyapi/src/pdf.rs +++ b/neopdf_pyapi/src/pdf.rs @@ -7,7 +7,8 @@ use neopdf::metadata::MetaData; use neopdf::pdf::PDF; use super::gridpdf::PySubGrid; -use super::metadata::PyMetaData; +use super::metadata::{build_lhapdf_map, PyMetaData}; +use super::uncertainty::PyUncertainty; // Type aliases type LazyType = Result>; @@ -194,6 +195,80 @@ impl PyPDFSet { const fn __len__(&self) -> usize { self.meta.num_members as usize } + + /// Confidence level (in %) at which the set's error members were constructed. + /// + /// Returns `-1.0` when the `ErrorConfLevel` field is absent from the `.info` file, + /// matching the LHAPDF convention. + #[getter] + #[must_use] + #[pyo3(name = "errorConfLevel")] + pub fn error_conf_level(&self) -> f64 { + self.meta.error_conf_level.unwrap_or(-1.0) + } + + /// Compute PDF uncertainty from a slice of per-member values. + /// + /// Parameters + /// ---------- + /// values : list[float] + /// Per-member values at a given kinematic point. Element 0 is the central member; + /// the remaining elements are the error members. + /// cl : float + /// Output confidence level in %. Defaults to CL_1_SIGMA (≈ 68.27 %). + /// alternative : bool + /// If `True`, replica sets use a quantile-based asymmetric interval instead of + /// the standard deviation. Defaults to `False`. + /// + /// # Errors + /// + /// Raises `ValueError` if `values` is empty. + #[pyo3(name = "uncertainty")] + #[pyo3(signature = (values, cl = neopdf::uncertainty::CL_1_SIGMA, alternative = false))] + #[allow(clippy::needless_pass_by_value)] + pub fn uncertainty( + &self, + values: Vec, + cl: f64, + alternative: bool, + ) -> PyResult { + let ecl = self.meta.error_conf_level.unwrap_or(-1.0); + neopdf::uncertainty::uncertainty(&values, &self.meta.error_type, ecl, cl, alternative) + .map(|u| PyUncertainty { + central: u.central, + errminus: u.errminus, + errplus: u.errplus, + }) + .map_err(pyo3::exceptions::PyValueError::new_err) + } + + /// Return `True` if the given LHAPDF-canonical key is present in this set's metadata. + #[must_use] + #[pyo3(name = "has_key")] + pub fn has_key(&self, key: &str) -> bool { + build_lhapdf_map(&self.meta).contains_key(key) + } + + /// Return the string value for an LHAPDF-canonical metadata key. + /// + /// # Errors + /// + /// Raises `KeyError` if the key is not present. + #[pyo3(name = "get_entry")] + pub fn get_entry(&self, key: &str) -> PyResult { + build_lhapdf_map(&self.meta).remove(key).ok_or_else(|| { + pyo3::exceptions::PyKeyError::new_err(format!("Key '{key}' not found in metadata")) + }) + } + + /// Return a sorted list of all available LHAPDF-canonical metadata keys. + #[must_use] + #[pyo3(name = "keys")] + pub fn keys(&self) -> Vec { + let mut ks: Vec = build_lhapdf_map(&self.meta).into_keys().collect(); + ks.sort(); + ks + } } /// Python wrapper for the `neopdf::pdf::PDF` struct. @@ -914,6 +989,36 @@ impl PyPDF { Some(PyPDFSet::new(&self.pdf_name)) } } + + /// Return `True` if the given LHAPDF-canonical key is present in this PDF's metadata. + #[must_use] + #[pyo3(name = "has_key")] + pub fn has_key(&self, key: &str) -> bool { + build_lhapdf_map(self.pdf.metadata()).contains_key(key) + } + + /// Return the string value for an LHAPDF-canonical metadata key. + /// + /// # Errors + /// + /// Raises `KeyError` if the key is not present. + #[pyo3(name = "get_entry")] + pub fn get_entry(&self, key: &str) -> PyResult { + build_lhapdf_map(self.pdf.metadata()) + .remove(key) + .ok_or_else(|| { + pyo3::exceptions::PyKeyError::new_err(format!("Key '{key}' not found in metadata")) + }) + } + + /// Return a sorted list of all available LHAPDF-canonical metadata keys. + #[must_use] + #[pyo3(name = "keys")] + pub fn keys(&self) -> Vec { + let mut ks: Vec = build_lhapdf_map(self.pdf.metadata()).into_keys().collect(); + ks.sort(); + ks + } } /// Registers the `pdf` submodule with the parent Python module. diff --git a/neopdf_pyapi/tests/test_lhapdf_compatibility.py b/neopdf_pyapi/tests/test_lhapdf_compatibility.py index 29c37765..1083e405 100644 --- a/neopdf_pyapi/tests/test_lhapdf_compatibility.py +++ b/neopdf_pyapi/tests/test_lhapdf_compatibility.py @@ -285,3 +285,143 @@ def test_kinematics_in_range(self): x, q, ) == _check_kinematics(lhapdf, PDF_SET, x, q) + + +@pytest.mark.parametrize( + "pdfname", ["NNPDF40_nnlo_as_01180", "CT18NNLO_as_0118", "MSHT20qed_an3lo"] +) +class TestHasKeyGetEntry: + MANDATORY_KEYS = [ + "SetDesc", + "NumMembers", + "XMin", + "XMax", + "QMin", + "QMax", + "Flavors", + "Format", + ] + + def test_has_key_mandatory(self, pdfname): + 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_get_entry_num_members(self, pdfname): + lha_ps = lhapdf.getPDFSet(pdfname) + 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) + 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) + with pytest.raises(KeyError): + neo_ps.get_entry("NonExistentKey_xyz") + + def test_keys_contains_mandatory(self, pdfname): + neo_ps = neopdf.getPDFSet(pdfname) + ks = neo_ps.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): + neo_ps = neopdf.getPDFSet(pdfname) + lha_ps = lhapdf.getPDFSet(pdfname) + if not lha_ps.has_key("ErrorConfLevel"): + # neopdf returns -1.0 when the key is absent; LHAPDF may use a heuristic default + assert neo_ps.errorConfLevel == -1.0 + else: + assert neo_ps.errorConfLevel == lha_ps.errorConfLevel + + def test_has_key_error_conf_level(self, pdfname): + neo_ps = neopdf.getPDFSet(pdfname) + lha_ps = lhapdf.getPDFSet(pdfname) + assert neo_ps.has_key("ErrorConfLevel") == lha_ps.has_key("ErrorConfLevel") + + def test_metadata_has_key(self, pdfname): + 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): + neo_p = neopdf.mkPDF(pdfname, 0) + lha_ps = lhapdf.getPDFSet(pdfname) + assert int(neo_p.get_entry("NumMembers")) == int(lha_ps.get_entry("NumMembers")) + + +@pytest.mark.parametrize( + "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)] +) +class TestPDFSetUncertainty: + def _values(self, pdfname: str, pid: int, x: float, q: float): + pdfs = 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 + + neo_ps = neopdf.getPDFSet(pdfname) + values = self._values(pdfname, pid, x, q) + unc = neo_ps.uncertainty(values) + assert isinstance(unc, Uncertainty) + assert unc.central > 0 or unc.central <= 0 # just check it's a float + + def test_uncertainty_matches_standalone(self, pdfname, pid, x, q): + from neopdf.uncertainty import uncertainty, CL_1_SIGMA + + neo_ps = neopdf.getPDFSet(pdfname) + values = np.array(self._values(pdfname, pid, x, q)) + ecl = 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 + ) + 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): + from neopdf.uncertainty import CL_1_SIGMA + + values = self._values(pdfname, pid, x, q) + neo_ps = neopdf.getPDFSet(pdfname) + lha_ps = lhapdf.getPDFSet(pdfname) + 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): + from neopdf.uncertainty import CL_1_SIGMA + + values = self._values(pdfname, pid, x, q) + neo_ps = neopdf.getPDFSet(pdfname) + lha_ps = lhapdf.getPDFSet(pdfname) + 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): + from neopdf.uncertainty import CL_1_SIGMA + + values = self._values(pdfname, pid, x, q) + neo_ps = neopdf.getPDFSet(pdfname) + lha_ps = lhapdf.getPDFSet(pdfname) + 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) From b3d4c0ddc1f2a794764173fb5fb6580a5a624150 Mon Sep 17 00:00:00 2001 From: Radonirinaunimi Date: Wed, 10 Jun 2026 16:40:44 +0200 Subject: [PATCH 2/5] Minor clean up --- neopdf_pyapi/tests/test_lhapdf_compatibility.py | 1 - 1 file changed, 1 deletion(-) diff --git a/neopdf_pyapi/tests/test_lhapdf_compatibility.py b/neopdf_pyapi/tests/test_lhapdf_compatibility.py index 1083e405..7164e914 100644 --- a/neopdf_pyapi/tests/test_lhapdf_compatibility.py +++ b/neopdf_pyapi/tests/test_lhapdf_compatibility.py @@ -339,7 +339,6 @@ def test_error_conf_level_when_key_present(self, pdfname): neo_ps = neopdf.getPDFSet(pdfname) lha_ps = lhapdf.getPDFSet(pdfname) if not lha_ps.has_key("ErrorConfLevel"): - # neopdf returns -1.0 when the key is absent; LHAPDF may use a heuristic default assert neo_ps.errorConfLevel == -1.0 else: assert neo_ps.errorConfLevel == lha_ps.errorConfLevel From f02190efc9eb9876cf09eedf95ec2973037c4f3e Mon Sep 17 00:00:00 2001 From: Radonirinaunimi Date: Wed, 10 Jun 2026 16:51:11 +0200 Subject: [PATCH 3/5] Minor cosmetic fixes --- neopdf_pyapi/tests/test_lhapdf_compatibility.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/neopdf_pyapi/tests/test_lhapdf_compatibility.py b/neopdf_pyapi/tests/test_lhapdf_compatibility.py index 7164e914..be5ab224 100644 --- a/neopdf_pyapi/tests/test_lhapdf_compatibility.py +++ b/neopdf_pyapi/tests/test_lhapdf_compatibility.py @@ -3,6 +3,8 @@ import lhapdf import neopdf +from neopdf.uncertainty import uncertainty, CL_1_SIGMA + PDF_SET = "NNPDF40_nnlo_as_01180" MEMBER = 7 @@ -379,25 +381,24 @@ def test_uncertainty_returns_object(self, pdfname, pid, x, q): values = self._values(pdfname, pid, x, q) unc = neo_ps.uncertainty(values) assert isinstance(unc, Uncertainty) - assert unc.central > 0 or unc.central <= 0 # just check it's a float + assert abs(unc.central) >= 0 def test_uncertainty_matches_standalone(self, pdfname, pid, x, q): - from neopdf.uncertainty import uncertainty, CL_1_SIGMA - neo_ps = neopdf.getPDFSet(pdfname) values = np.array(self._values(pdfname, pid, x, q)) ecl = 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): - from neopdf.uncertainty import CL_1_SIGMA - values = self._values(pdfname, pid, x, q) neo_ps = neopdf.getPDFSet(pdfname) lha_ps = lhapdf.getPDFSet(pdfname) @@ -406,8 +407,6 @@ def test_uncertainty_central_matches_lhapdf(self, pdfname, pid, x, q): np.testing.assert_allclose(neo_unc.central, lha_unc.central, rtol=1e-10) def test_uncertainty_errminus_matches_lhapdf(self, pdfname, pid, x, q): - from neopdf.uncertainty import CL_1_SIGMA - values = self._values(pdfname, pid, x, q) neo_ps = neopdf.getPDFSet(pdfname) lha_ps = lhapdf.getPDFSet(pdfname) @@ -416,8 +415,6 @@ def test_uncertainty_errminus_matches_lhapdf(self, pdfname, pid, x, q): np.testing.assert_allclose(neo_unc.errminus, lha_unc.errminus, rtol=1e-2) def test_uncertainty_errplus_matches_lhapdf(self, pdfname, pid, x, q): - from neopdf.uncertainty import CL_1_SIGMA - values = self._values(pdfname, pid, x, q) neo_ps = neopdf.getPDFSet(pdfname) lha_ps = lhapdf.getPDFSet(pdfname) From 2f0eb8f8481b1b5ebe77e5e000f688e2758f9c2b Mon Sep 17 00:00:00 2001 From: Radonirinaunimi Date: Wed, 10 Jun 2026 17:52:33 +0200 Subject: [PATCH 4/5] Update documentation --- docs/design-and-features.md | 3 ++- neopdf_pyapi/src/pdf.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/design-and-features.md b/docs/design-and-features.md index 817fe1e0..f6b01b02 100644 --- a/docs/design-and-features.md +++ b/docs/design-and-features.md @@ -206,7 +206,8 @@ The following symbols have an exact match with their LHAPDF counterparts. `availablePDFSets()`, `paths()`, `setPaths()`, `pathsAppend()`, `pathsPrepend()`. *`PDFSet` class:* -`name`, `size`, `description`, `errorType`, `lhapdfID`, `mkPDF()`, `mkPDFs()`, `info()`. +`name`, `size`, `description`, `errorType`, `lhapdfID`, `mkPDF()`, `mkPDFs()`, `info()`, +`uncertainty`, `has_key`, `get_entry`, `keys`. *`PDF` class:* `xfxQ()`, `xfxQ2()`, `alphasQ()`, `alphasQ2()`, `flavors()`, `hasFlavor()`, diff --git a/neopdf_pyapi/src/pdf.rs b/neopdf_pyapi/src/pdf.rs index 4b96b766..32177dab 100644 --- a/neopdf_pyapi/src/pdf.rs +++ b/neopdf_pyapi/src/pdf.rs @@ -215,7 +215,7 @@ impl PyPDFSet { /// Per-member values at a given kinematic point. Element 0 is the central member; /// the remaining elements are the error members. /// cl : float - /// Output confidence level in %. Defaults to CL_1_SIGMA (≈ 68.27 %). + /// Output confidence level in %. Defaults to `CL_1_SIGMA` (~ 68.27 %). /// alternative : bool /// If `True`, replica sets use a quantile-based asymmetric interval instead of /// the standard deviation. Defaults to `False`. From 5c5eb4cb2fdcbbbaefcf755ee648fc70baf50729 Mon Sep 17 00:00:00 2001 From: Radonirinaunimi Date: Thu, 11 Jun 2026 09:01:36 +0200 Subject: [PATCH 5/5] Make the tests a bit more lightweight --- neopdf_pyapi/tests/test_lhapdf_compatibility.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/neopdf_pyapi/tests/test_lhapdf_compatibility.py b/neopdf_pyapi/tests/test_lhapdf_compatibility.py index be5ab224..9e47e17e 100644 --- a/neopdf_pyapi/tests/test_lhapdf_compatibility.py +++ b/neopdf_pyapi/tests/test_lhapdf_compatibility.py @@ -289,9 +289,7 @@ def test_kinematics_in_range(self): ) == _check_kinematics(lhapdf, PDF_SET, x, q) -@pytest.mark.parametrize( - "pdfname", ["NNPDF40_nnlo_as_01180", "CT18NNLO_as_0118", "MSHT20qed_an3lo"] -) +@pytest.mark.parametrize("pdfname", ["CT18NNLO_as_0118", "MSHT20qed_an3lo"]) class TestHasKeyGetEntry: MANDATORY_KEYS = [ "SetDesc", @@ -361,9 +359,7 @@ def test_metadata_get_entry(self, pdfname): assert int(neo_p.get_entry("NumMembers")) == int(lha_ps.get_entry("NumMembers")) -@pytest.mark.parametrize( - "pdfname", ["NNPDF40_nnlo_as_01180", "CT18NNLO_as_0118", "MSHT20qed_an3lo"] -) +@pytest.mark.parametrize("pdfname", ["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)] )