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
3 changes: 2 additions & 1 deletion docs/design-and-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`,
Expand Down
1 change: 1 addition & 0 deletions neopdf/src/alphas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ mod tests {
xi_max: 0.0,
delta_min: 0.0,
delta_max: 0.0,
error_conf_level: None,
}
}

Expand Down
1 change: 1 addition & 0 deletions neopdf/src/gridpdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
13 changes: 12 additions & 1 deletion neopdf/src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<f64>,
}

impl MetaDataV2 {
Expand Down Expand Up @@ -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(())
}
}

Expand Down Expand Up @@ -258,6 +268,7 @@ impl From<neopdf_legacy::metadata::MetaData> for MetaData {
xi_max: 0.0,
delta_min: 0.0,
delta_max: 0.0,
error_conf_level: None,
}
}
}
1 change: 1 addition & 0 deletions neopdf/src/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions neopdf_capi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,7 @@ fn process_metadata(meta: *const NeoPDFMetaData) -> Option<MetaData> {
xi_max: 1.0,
delta_min: 0.0,
delta_max: 0.0,
error_conf_level: None,
};

Some(metadata)
Expand Down Expand Up @@ -1213,6 +1214,7 @@ fn process_metadata_v2(meta: *const NeoPDFMetaDataV2) -> Option<MetaData> {
xi_max: meta.xi_max,
delta_min: meta.delta_min,
delta_max: meta.delta_max,
error_conf_level: None,
};

Some(metadata)
Expand Down
1 change: 1 addition & 0 deletions neopdf_cli/src/tmd_converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ pub fn convert_tmd(input_path: &str, output_path: &str) -> Result<(), Box<dyn st
xi_max: 0.0,
delta_min: 0.0,
delta_max: 0.0,
error_conf_level: None,
};

GridArrayCollection::compress(&member_grid_refs, &meta, output_path)?;
Expand Down
148 changes: 148 additions & 0 deletions neopdf_pyapi/src/metadata.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashMap;

use pyo3::prelude::*;

use neopdf::metadata::{InterpolatorType, MetaData, SetType};
Expand Down Expand Up @@ -197,6 +199,123 @@ impl Default for PyPhysicsParameters {
}
}

fn format_list<T: ToString>(v: &[T]) -> String {
format!(
"[{}]",
v.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)
}

fn insert_mandatory_fields(map: &mut HashMap<String, String>, 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<String, String>, 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<String, String>, 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<String, String>, 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<String, String> {
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)]
Expand Down Expand Up @@ -296,6 +415,7 @@ impl PyMetaData {
xi_max: xsi_max,
delta_min,
delta_max,
error_conf_level: None,
},
}
}
Expand Down Expand Up @@ -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<String> {
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<String> {
let mut keys: Vec<String> = 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 {
Expand Down
107 changes: 106 additions & 1 deletion neopdf_pyapi/src/pdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PDF, Box<dyn std::error::Error>>;
Expand Down Expand Up @@ -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<f64>,
cl: f64,
alternative: bool,
) -> PyResult<PyUncertainty> {
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<String> {
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<String> {
let mut ks: Vec<String> = build_lhapdf_map(&self.meta).into_keys().collect();
ks.sort();
ks
}
}

/// Python wrapper for the `neopdf::pdf::PDF` struct.
Expand Down Expand Up @@ -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<String> {
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<String> {
let mut ks: Vec<String> = build_lhapdf_map(self.pdf.metadata()).into_keys().collect();
ks.sort();
ks
}
}

/// Registers the `pdf` submodule with the parent Python module.
Expand Down
Loading
Loading