Skip to main content

neopdf/
pdf.rs

1use numpy::{IntoPyArray, PyArray1, PyArray2};
2use pyo3::prelude::*;
3use std::sync::Mutex;
4
5use neopdf::gridpdf::ForcePositive;
6use neopdf::metadata::MetaData;
7use neopdf::pdf::PDF;
8
9use super::gridpdf::PySubGrid;
10use super::metadata::{build_lhapdf_map, PyMetaData};
11use super::uncertainty::PyUncertainty;
12
13// Type aliases
14type LazyType = Result<PDF, Box<dyn std::error::Error>>;
15type EnumeratedLazy = Box<dyn Iterator<Item = (usize, LazyType)> + Send>;
16
17/// Python wrapper for the `ForcePositive` enum.
18#[pyclass(from_py_object, name = "ForcePositive")]
19#[derive(Clone)]
20pub enum PyForcePositive {
21    /// If the calculated PDF value is negative, it is forced to 0.
22    ClipNegative,
23    /// If the calculated PDF value is less than 1e-10, it is set to 1e-10.
24    ClipSmall,
25    /// No clipping is done, value is returned as it is.
26    NoClipping,
27}
28
29impl From<PyForcePositive> for ForcePositive {
30    fn from(fmt: PyForcePositive) -> Self {
31        match fmt {
32            PyForcePositive::ClipNegative => Self::ClipNegative,
33            PyForcePositive::ClipSmall => Self::ClipSmall,
34            PyForcePositive::NoClipping => Self::NoClipping,
35        }
36    }
37}
38
39impl From<&ForcePositive> for PyForcePositive {
40    fn from(fmt: &ForcePositive) -> Self {
41        match fmt {
42            ForcePositive::ClipNegative => Self::ClipNegative,
43            ForcePositive::ClipSmall => Self::ClipSmall,
44            ForcePositive::NoClipping => Self::NoClipping,
45        }
46    }
47}
48
49/// Methods to load all the PDF members for a given set.
50#[pyclass(from_py_object, name = "LoaderMethod")]
51#[derive(Clone)]
52pub enum PyLoaderMethod {
53    /// Load the members in parallel using multi-threads.
54    Parallel,
55    /// Load the members in sequential.
56    Sequential,
57}
58
59#[pymethods]
60impl PyForcePositive {
61    fn __eq__(&self, other: &Self) -> bool {
62        std::mem::discriminant(self) == std::mem::discriminant(other)
63    }
64
65    fn __hash__(&self) -> u64 {
66        use std::collections::hash_map::DefaultHasher;
67        use std::hash::{Hash, Hasher};
68        let mut hasher = DefaultHasher::new();
69        std::mem::discriminant(self).hash(&mut hasher);
70        hasher.finish()
71    }
72}
73
74/// This enum contains the different parameters that a grid can depend on.
75#[pyclass(from_py_object, name = "GridParams")]
76#[derive(Clone)]
77pub enum PyGridParams {
78    /// The nucleon mass number A.
79    A,
80    /// The strong coupling `alpha_s`.
81    AlphaS,
82    /// The momentum fraction.
83    X,
84    /// The transverse momentum.
85    KT,
86    /// The energy scale `Q^2`.
87    Q2,
88}
89
90/// LHAPDF-compatible interface to a PDF set.
91///
92/// Provides the same API as `lhapdf.PDFSet`.
93#[pyclass(name = "PDFSet")]
94pub struct PyPDFSet {
95    name: String,
96    meta: MetaData,
97}
98
99#[pymethods]
100impl PyPDFSet {
101    /// Create a new `PDFSet` for a given set name.
102    ///
103    /// Parameters
104    /// ----------
105    /// name : str
106    ///     The name of the PDF set.
107    #[new]
108    #[must_use]
109    pub fn new(name: &str) -> Self {
110        let meta = PDF::load(name, 0).metadata().clone();
111        Self {
112            name: name.to_string(),
113            meta,
114        }
115    }
116
117    /// The name of the PDF set.
118    #[getter]
119    #[must_use]
120    pub fn name(&self) -> &str {
121        &self.name
122    }
123
124    /// Total number of members (central value + error members).
125    #[getter]
126    #[must_use]
127    pub const fn size(&self) -> u32 {
128        self.meta.num_members
129    }
130
131    /// Human-readable description of the PDF set.
132    #[getter]
133    #[must_use]
134    pub fn description(&self) -> &str {
135        &self.meta.set_desc
136    }
137
138    /// Error type string (e.g. `replicas`, `hessian`).
139    #[getter]
140    #[must_use]
141    #[pyo3(name = "errorType")]
142    pub fn error_type(&self) -> &str {
143        &self.meta.error_type
144    }
145
146    /// LHAPDF ID for member 0 of this set.
147    #[getter]
148    #[must_use]
149    #[pyo3(name = "lhapdfID")]
150    pub const fn lhapdf_id(&self) -> u32 {
151        self.meta.set_index
152    }
153
154    /// Return the metadata for this set as a `MetaData` object.
155    #[must_use]
156    pub fn info(&self) -> PyMetaData {
157        PyMetaData {
158            meta: self.meta.clone(),
159        }
160    }
161
162    /// Load a single PDF member by index.
163    ///
164    /// Parameters
165    /// ----------
166    /// member : int
167    ///     Member index. Defaults to 0 (central value).
168    #[must_use]
169    #[pyo3(signature = (member = 0))]
170    #[pyo3(name = "mkPDF")]
171    pub fn mk_pdf(&self, member: usize) -> PyPDF {
172        PyPDF::new(&self.name, member)
173    }
174
175    /// Load all members of this PDF set.
176    ///
177    /// Parameters
178    /// ----------
179    /// method : `LoaderMethod`
180    ///     Loading strategy. Defaults to `Parallel`.
181    #[must_use]
182    #[pyo3(signature = (method = &PyLoaderMethod::Parallel))]
183    #[pyo3(name = "mkPDFs")]
184    pub fn mk_pdfs(&self, method: &PyLoaderMethod) -> Vec<PyPDF> {
185        PyPDF::mkpdfs(&self.name, method)
186    }
187
188    fn __repr__(&self) -> String {
189        format!(
190            "<PDFSet '{}' ({} members)>",
191            self.name, self.meta.num_members
192        )
193    }
194
195    const fn __len__(&self) -> usize {
196        self.meta.num_members as usize
197    }
198
199    /// Confidence level (in %) at which the set's error members were constructed.
200    ///
201    /// Returns `-1.0` when the `ErrorConfLevel` field is absent from the `.info` file,
202    /// matching the LHAPDF convention.
203    #[getter]
204    #[must_use]
205    #[pyo3(name = "errorConfLevel")]
206    pub fn error_conf_level(&self) -> f64 {
207        self.meta.error_conf_level.unwrap_or(-1.0)
208    }
209
210    /// Compute PDF uncertainty from a slice of per-member values.
211    ///
212    /// Parameters
213    /// ----------
214    /// values : list[float]
215    ///     Per-member values at a given kinematic point. Element 0 is the central member;
216    ///     the remaining elements are the error members.
217    /// cl : float
218    ///     Output confidence level in %. Defaults to `CL_1_SIGMA` (~ 68.27 %).
219    /// alternative : bool
220    ///     If `True`, replica sets use a quantile-based asymmetric interval instead of
221    ///     the standard deviation. Defaults to `False`.
222    ///
223    /// # Errors
224    ///
225    /// Raises `ValueError` if `values` is empty.
226    #[pyo3(name = "uncertainty")]
227    #[pyo3(signature = (values, cl = neopdf::uncertainty::CL_1_SIGMA, alternative = false))]
228    #[allow(clippy::needless_pass_by_value)]
229    pub fn uncertainty(
230        &self,
231        values: Vec<f64>,
232        cl: f64,
233        alternative: bool,
234    ) -> PyResult<PyUncertainty> {
235        let ecl = self.meta.error_conf_level.unwrap_or(-1.0);
236        neopdf::uncertainty::uncertainty(&values, &self.meta.error_type, ecl, cl, alternative)
237            .map(|u| PyUncertainty {
238                central: u.central,
239                errminus: u.errminus,
240                errplus: u.errplus,
241            })
242            .map_err(pyo3::exceptions::PyValueError::new_err)
243    }
244
245    /// Return `True` if the given LHAPDF-canonical key is present in this set's metadata.
246    #[must_use]
247    #[pyo3(name = "has_key")]
248    pub fn has_key(&self, key: &str) -> bool {
249        build_lhapdf_map(&self.meta).contains_key(key)
250    }
251
252    /// Return the string value for an LHAPDF-canonical metadata key.
253    ///
254    /// # Errors
255    ///
256    /// Raises `KeyError` if the key is not present.
257    #[pyo3(name = "get_entry")]
258    pub fn get_entry(&self, key: &str) -> PyResult<String> {
259        build_lhapdf_map(&self.meta).remove(key).ok_or_else(|| {
260            pyo3::exceptions::PyKeyError::new_err(format!("Key '{key}' not found in metadata"))
261        })
262    }
263
264    /// Return a sorted list of all available LHAPDF-canonical metadata keys.
265    #[must_use]
266    #[pyo3(name = "keys")]
267    pub fn keys(&self) -> Vec<String> {
268        let mut ks: Vec<String> = build_lhapdf_map(&self.meta).into_keys().collect();
269        ks.sort();
270        ks
271    }
272}
273
274/// Python wrapper for the `neopdf::pdf::PDF` struct.
275///
276/// This class provides a Python-friendly interface to the core PDF
277/// interpolation functionalities of the `neopdf` Rust library.
278#[pyclass(name = "LazyPDFs")]
279pub struct PyLazyPDFs {
280    iter: Mutex<EnumeratedLazy>,
281    pdf_name: String,
282}
283
284#[pymethods]
285impl PyLazyPDFs {
286    const fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
287        slf
288    }
289
290    #[allow(clippy::needless_pass_by_value)]
291    fn __next__(slf: PyRefMut<'_, Self>) -> PyResult<Option<PyPDF>> {
292        let mut iter = slf.iter.lock().unwrap();
293        let pdf_name = slf.pdf_name.clone();
294        match iter.next() {
295            Some((member, Ok(pdf))) => Ok(Some(PyPDF {
296                pdf,
297                pdf_name,
298                member,
299            })),
300            Some((_, Err(e))) => Err(pyo3::exceptions::PyValueError::new_err(e.to_string())),
301            None => Ok(None),
302        }
303    }
304}
305
306/// Python wrapper for the `neopdf::pdf::PDF` struct.
307///
308/// This class provides a Python-friendly interface to the core PDF
309/// interpolation functionalities of the `neopdf` Rust library.
310#[pyclass(name = "PDF")]
311pub struct PyPDF {
312    pub(crate) pdf: PDF,
313    pub(crate) pdf_name: String,
314    pub(crate) member: usize,
315}
316
317#[pymethods]
318#[allow(clippy::doc_markdown)]
319impl PyPDF {
320    /// Creates a new `PDF` instance for a given PDF set and member.
321    ///
322    /// This is the primary constructor for the `PDF` class.
323    ///
324    /// Parameters
325    /// ----------
326    /// pdf_name : str
327    ///     The name of the PDF set.
328    /// member : int
329    ///     The ID of the PDF member to load. Defaults to 0.
330    ///
331    /// Returns
332    /// -------
333    /// PDF
334    ///     A new `PDF` instance.
335    #[new]
336    #[must_use]
337    #[pyo3(signature = (pdf_name, member = 0))]
338    pub fn new(pdf_name: &str, member: usize) -> Self {
339        Self {
340            pdf: PDF::load(pdf_name, member),
341            pdf_name: pdf_name.to_string(),
342            member,
343        }
344    }
345
346    /// Loads a given member of the PDF set.
347    ///
348    /// This is an alternative constructor for convenience, equivalent
349    /// to `PDF(pdf_name, member)`.
350    ///
351    /// Parameters
352    /// ----------
353    /// pdf_name : str
354    ///     The name of the PDF set.
355    /// member : int
356    ///     The ID of the PDF member. Defaults to 0.
357    ///
358    /// Returns
359    /// -------
360    /// PDF
361    ///     A new `PDF` instance.
362    #[must_use]
363    #[staticmethod]
364    #[pyo3(name = "mkPDF")]
365    #[pyo3(signature = (pdf_name, member = 0))]
366    pub fn mkpdf(pdf_name: &str, member: usize) -> Self {
367        Self::new(pdf_name, member)
368    }
369
370    /// Loads a PDF member by its LHAPDF ID (LHAID).
371    ///
372    /// The set name and member index are resolved by fetching the LHAPDF set
373    /// index from `https://lhapdfsets.web.cern.ch/current/pdfsets.index`.
374    ///
375    /// Parameters
376    /// ----------
377    /// lhaid : int
378    ///     The LHAPDF ID uniquely identifying both the PDF set and the member.
379    ///
380    /// Returns
381    /// -------
382    /// PDF
383    ///     A new `PDF` instance for the set and member encoded in `lhaid`.
384    #[must_use]
385    #[staticmethod]
386    #[pyo3(name = "mkPDF_lhaid")]
387    pub fn mkpdf_lhaid(lhaid: u32) -> Self {
388        let pdf = PDF::load_by_lhaid(lhaid);
389        let member = lhaid.saturating_sub(pdf.metadata().set_index) as usize;
390
391        Self {
392            pdf,
393            pdf_name: String::new(),
394            member,
395        }
396    }
397
398    /// Loads a PDF member from a specific LHAPDF `.dat` file path.
399    ///
400    /// Parameters
401    /// ----------
402    /// path : str
403    ///     The path to the LHAPDF `.dat` file.
404    ///
405    /// Returns
406    /// -------
407    /// PDF
408    ///     A new `PDF` instance for the specified file.
409    #[must_use]
410    #[staticmethod]
411    #[pyo3(name = "mkPDF_lhapdf_file")]
412    pub fn mkpdf_lhapdf_file(path: &str) -> Self {
413        Self {
414            pdf: PDF::load_lhapdf_by_file(path),
415            pdf_name: path.to_string(),
416            member: 0,
417        }
418    }
419
420    /// Loads all members of the PDF set.
421    ///
422    /// This function loads all available members for a given PDF set,
423    /// returning a list of `PDF` instances.
424    ///
425    /// Parameters
426    /// ----------
427    /// pdf_name : str
428    ///     The name of the PDF set.
429    ///
430    /// Returns
431    /// -------
432    /// list[PDF]
433    ///     A list of `PDF` instances, one for each member.
434    #[must_use]
435    #[staticmethod]
436    #[pyo3(name = "mkPDFs")]
437    #[pyo3(signature = (pdf_name, method = &PyLoaderMethod::Parallel))]
438    pub fn mkpdfs(pdf_name: &str, method: &PyLoaderMethod) -> Vec<Self> {
439        let loader_method = match method {
440            PyLoaderMethod::Parallel => PDF::load_pdfs,
441            PyLoaderMethod::Sequential => PDF::load_pdfs_seq,
442        };
443
444        loader_method(pdf_name)
445            .into_iter()
446            .enumerate()
447            .map(|(i, pdfobj)| Self {
448                pdf: pdfobj,
449                pdf_name: pdf_name.to_string(),
450                member: i,
451            })
452            .collect()
453    }
454
455    /// Creates an iterator that loads PDF members lazily.
456    ///
457    /// This function is suitable for `.neopdf.lz4` files, which support lazy loading.
458    /// It returns an iterator that yields `PDF` instances on demand, which is useful
459    /// for reducing memory consumption when working with large PDF sets.
460    ///
461    /// # Arguments
462    ///
463    /// * `pdf_name` - The name of the PDF set (must end with `.neopdf.lz4`).
464    ///
465    /// # Returns
466    ///
467    /// An iterator over `Result<PDF, Box<dyn std::error::Error>>`.
468    #[must_use]
469    #[staticmethod]
470    #[pyo3(name = "mkPDFs_lazy")]
471    pub fn mkpdfs_lazy(pdf_name: &str) -> PyLazyPDFs {
472        PyLazyPDFs {
473            iter: Mutex::new(Box::new(PDF::load_pdfs_lazy(pdf_name).enumerate())),
474            pdf_name: pdf_name.to_string(),
475        }
476    }
477
478    /// Returns the list of `PID` values.
479    ///
480    /// Returns
481    /// -------
482    /// list[int]
483    ///     The PID values.
484    #[must_use]
485    pub fn pids(&self) -> Vec<i32> {
486        self.pdf.pids().to_vec()
487    }
488
489    /// Returns the list of `Subgrid` objects.
490    ///
491    /// Returns
492    /// -------
493    /// list[PySubgrid]
494    ///     The subgrids.
495    #[must_use]
496    pub fn subgrids(&self) -> Vec<PySubGrid> {
497        self.pdf
498            .subgrids()
499            .iter()
500            .map(|subgrid| PySubGrid {
501                subgrid: subgrid.clone(),
502            })
503            .collect()
504    }
505
506    /// Returns the subgrid knots of a parameter for a given subgrid index.
507    ///
508    /// The parameter could be the nucleon numbers `A`, the strong coupling
509    /// `alphas`, the momentum fraction `x`, or the momentum scale `Q2`.
510    ///
511    /// # Panics
512    ///
513    /// This panics if the parameter is not valid.
514    ///
515    /// Returns
516    /// -------
517    /// list[float]
518    ///     The subgrid knots for a given parameter.
519    #[must_use]
520    pub fn subgrid_knots(&self, param: &PyGridParams, subgrid_index: usize) -> Vec<f64> {
521        match param {
522            PyGridParams::AlphaS => self.pdf.subgrid(subgrid_index).alphas.to_vec(),
523            PyGridParams::X => self.pdf.subgrid(subgrid_index).xs.to_vec(),
524            PyGridParams::Q2 => self.pdf.subgrid(subgrid_index).q2s.to_vec(),
525            PyGridParams::A => self.pdf.subgrid(subgrid_index).nucleons.to_vec(),
526            PyGridParams::KT => self.pdf.subgrid(subgrid_index).kts.to_vec(),
527        }
528    }
529
530    /// Clip the negative or small values for the `PDF` object.
531    ///
532    /// Parameters
533    /// ----------
534    /// id : PyFrocePositive
535    ///     The clipping method use to handle negative or small values.
536    pub fn set_force_positive(&mut self, option: PyForcePositive) {
537        self.pdf.set_force_positive(option.into());
538    }
539
540    /// Clip the negative or small values for all the `PDF` objects.
541    ///
542    /// Parameters
543    /// ----------
544    /// pdfs : list[PDF]
545    ///     A list of `PDF` instances.
546    /// option : PyForcePositive
547    ///     The clipping method use to handle negative or small values.
548    #[staticmethod]
549    #[pyo3(name = "set_force_positive_members")]
550    #[allow(clippy::needless_pass_by_value)]
551    pub fn set_force_positive_members(pdfs: Vec<PyRefMut<Self>>, option: PyForcePositive) {
552        for mut pypdf in pdfs {
553            pypdf.set_force_positive(option.clone());
554        }
555    }
556
557    /// Returns the clipping method used for a single `PDF` object.
558    ///
559    /// Returns
560    /// -------
561    /// PyForcePositive
562    ///     The clipping method used for the `PDF` object.
563    #[must_use]
564    pub fn is_force_positive(&self) -> PyForcePositive {
565        self.pdf.is_force_positive().into()
566    }
567
568    /// Retrieves the minimum x-value for this PDF set.
569    ///
570    /// Returns
571    /// -------
572    /// float
573    ///     The minimum x-value.
574    #[must_use]
575    pub fn x_min(&self) -> f64 {
576        self.pdf.param_ranges().x.min
577    }
578
579    /// Retrieves the maximum x-value for this PDF set.
580    ///
581    /// Returns
582    /// -------
583    /// float
584    ///     The maximum x-value.
585    #[must_use]
586    pub fn x_max(&self) -> f64 {
587        self.pdf.param_ranges().x.max
588    }
589
590    /// Retrieves the minimum Q2-value for this PDF set.
591    ///
592    /// Returns
593    /// -------
594    /// float
595    ///     The minimum Q2-value.
596    #[must_use]
597    pub fn q2_min(&self) -> f64 {
598        self.pdf.param_ranges().q2.min
599    }
600
601    /// Retrieves the maximum Q2-value for this PDF set.
602    ///
603    /// Returns
604    /// -------
605    /// float
606    ///     The maximum Q2-value.
607    #[must_use]
608    pub fn q2_max(&self) -> f64 {
609        self.pdf.param_ranges().q2.max
610    }
611
612    /// Retrieves the flavour PIDs for the PDF set.
613    ///
614    /// Returns
615    /// -------
616    /// list(int)
617    ///     The flavour PID values.
618    #[must_use]
619    pub fn flavour_pids(&self) -> Vec<i32> {
620        self.pdf.metadata().flavors.clone()
621    }
622
623    /// Interpolates the PDF value (xf) for a given flavor, x, and Q2.
624    ///
625    /// Parameters
626    /// ----------
627    /// id : int
628    ///     The flavor ID (e.g., 21 for gluon, 1 for d-quark).
629    /// x : float
630    ///     The momentum fraction.
631    /// q2 : float
632    ///     The energy scale squared.
633    ///
634    /// Returns
635    /// -------
636    /// float
637    ///     The interpolated PDF value. Returns 0.0 if extrapolation is
638    ///     attempted and not allowed.
639    #[must_use]
640    #[pyo3(name = "xfxQ2")]
641    pub fn xfxq2(&self, id: i32, x: f64, q2: f64) -> f64 {
642        self.pdf.xfxq2(id, &[x, q2])
643    }
644
645    /// Interpolates the PDF value (xf) for a given set of parameters.
646    ///
647    /// Parameters
648    /// ----------
649    /// id : int
650    ///     The flavor ID (e.g., 21 for gluon, 1 for d-quark).
651    /// params: list[float]
652    ///     A list of parameters that the grids depends on. If the PDF
653    ///     grid only contains `x` and `Q2` dependence then its value is
654    ///     `[x, q2]`; if it contains either the `A` and `alpha_s`
655    ///     dependence, then its value is `[A, x, q2]` or `[alpha_s, x, q2]`
656    ///     respectively; if it contains both, then `[A, alpha_s, x, q2]`.
657    ///
658    /// Returns
659    /// -------
660    /// float
661    ///     The interpolated PDF value. Returns 0.0 if extrapolation is
662    ///     attempted and not allowed.
663    #[must_use]
664    #[pyo3(name = "xfxQ2_ND")]
665    #[allow(clippy::needless_pass_by_value)]
666    pub fn xfxq2_nd(&self, id: i32, params: Vec<f64>) -> f64 {
667        self.pdf.xfxq2(id, &params)
668    }
669
670    /// Evaluates all requested flavors at a single kinematic point.
671    ///
672    /// Parameters
673    /// ----------
674    /// pids : list[int]
675    ///     A list of flavor IDs.
676    /// x : float
677    ///     The momentum fraction.
678    /// q2 : float
679    ///     The energy scale squared.
680    ///
681    /// Returns
682    /// -------
683    /// numpy.ndarray
684    ///     A 1D NumPy array containing the interpolated PDF values for each PID.
685    #[must_use]
686    #[pyo3(name = "xfxQ2_allpids")]
687    #[allow(clippy::needless_pass_by_value)]
688    pub fn xfxq2_allpids<'py>(
689        &self,
690        pids: Vec<i32>,
691        x: f64,
692        q2: f64,
693        py: Python<'py>,
694    ) -> Bound<'py, PyArray1<f64>> {
695        let mut out = vec![0.0; pids.len()];
696        self.pdf.xfxq2_allpids(&pids, &[x, q2], &mut out);
697        out.into_pyarray(py)
698    }
699
700    /// Evaluates all requested flavors at a single kinematic point.
701    ///
702    /// Parameters
703    /// ----------
704    /// pids : list[int]
705    ///     A list of flavor IDs.
706    /// params : list[float]
707    ///     A list of parameters (e.g., [kT, x, q2]).
708    ///
709    /// Returns
710    /// -------
711    /// numpy.ndarray
712    ///     A 1D NumPy array containing the interpolated PDF values for each PID.
713    #[must_use]
714    #[pyo3(name = "xfxQ2_allpids_ND")]
715    #[allow(clippy::needless_pass_by_value)]
716    pub fn xfxq2_allpids_nd<'py>(
717        &self,
718        pids: Vec<i32>,
719        params: Vec<f64>,
720        py: Python<'py>,
721    ) -> Bound<'py, PyArray1<f64>> {
722        let mut out = vec![0.0; pids.len()];
723        self.pdf.xfxq2_allpids(&pids, &params, &mut out);
724        out.into_pyarray(py)
725    }
726
727    /// Interpolates the PDF value (xf) for a list containg a set of parameters.
728    ///
729    /// Parameters
730    /// ----------
731    /// id : int
732    ///     The flavor ID (e.g., 21 for gluon, 1 for d-quark).
733    /// params: list[list[float]]
734    ///     A list containing the list of points. Each element in the list
735    ///     is in turn a list containing the parameters that the grids depends
736    ///     on. If the PDF grid only contains `x` and `Q2` dependence then its
737    ///     value is `[x, q2]`; if it contains either the `A` and `alpha_s`
738    ///     dependence, then its value is `[A, x, q2]` or `[alpha_s, x, q2]`
739    ///     respectively; if it contains both, then `[A, alpha_s, x, q2]`.
740    ///
741    /// Returns
742    /// -------
743    /// float
744    ///     The interpolated PDF value. Returns 0.0 if extrapolation is
745    ///     attempted and not allowed.
746    #[must_use]
747    #[pyo3(name = "xfxQ2_Chebyshev_batch")]
748    #[allow(clippy::needless_pass_by_value)]
749    pub fn xfxq2_cheby_batch(&self, id: i32, params: Vec<Vec<f64>>) -> Vec<f64> {
750        let slices: Vec<&[f64]> = params.iter().map(Vec::as_slice).collect();
751        self.pdf.xfxq2_cheby_batch(id, &slices)
752    }
753
754    /// Interpolates the PDF value (xf) for lists of flavors, x-values,
755    /// and Q2-values.
756    ///
757    /// Parameters
758    /// ----------
759    /// id : list[int]
760    ///     A list of flavor IDs.
761    /// xs : list[float]
762    ///     A list of momentum fractions.
763    /// q2s : list[float]
764    ///     A list of energy scales squared.
765    ///
766    /// Returns
767    /// -------
768    /// numpy.ndarray
769    ///     A 2D NumPy array containing the interpolated PDF values.
770    #[must_use]
771    #[pyo3(name = "xfxQ2s")]
772    #[allow(clippy::needless_pass_by_value)]
773    pub fn xfxq2s<'py>(
774        &self,
775        pids: Vec<i32>,
776        xs: Vec<f64>,
777        q2s: Vec<f64>,
778        py: Python<'py>,
779    ) -> Bound<'py, PyArray2<f64>> {
780        let flatten_points: Vec<Vec<f64>> = xs
781            .iter()
782            .flat_map(|&x| q2s.iter().map(move |&q2| vec![x, q2]))
783            .collect();
784        let points_interp: Vec<&[f64]> = flatten_points.iter().map(Vec::as_slice).collect();
785        let slice_points: &[&[f64]] = &points_interp;
786
787        self.pdf.xfxq2s(pids, slice_points).into_pyarray(py)
788    }
789
790    /// Computes the alpha_s value at a given Q2.
791    ///
792    /// Parameters
793    /// ----------
794    /// q2 : float
795    ///     The energy scale squared.
796    ///
797    /// Returns
798    /// -------
799    /// float
800    ///     The interpolated alpha_s value.
801    #[must_use]
802    #[pyo3(name = "alphasQ2")]
803    pub fn alphas_q2(&self, q2: f64) -> f64 {
804        self.pdf.alphas_q2(q2)
805    }
806
807    /// Returns the metadata associated with this PDF set.
808    ///
809    /// Provides access to the metadata describing the PDF set, including information
810    /// such as the set description, number of members, parameter ranges, and other
811    /// relevant details.
812    ///
813    /// Returns
814    /// -------
815    /// MetaData
816    ///     The metadata for this PDF set as a `MetaData` Python object.
817    #[must_use]
818    #[pyo3(name = "metadata")]
819    pub fn metadata(&self) -> PyMetaData {
820        PyMetaData {
821            meta: self.pdf.metadata().clone(),
822        }
823    }
824
825    // ------------------ LHAPDF-compatible API ------------------
826
827    /// Evaluate xf(x, Q) for a single flavor.
828    #[must_use]
829    #[pyo3(name = "xfxQ")]
830    pub fn xfxq(&self, id: i32, x: f64, q: f64) -> f64 {
831        self.xfxq2(id, x, q * q)
832    }
833
834    /// Evaluate alpha_s(Q).
835    #[must_use]
836    #[pyo3(name = "alphasQ")]
837    pub fn alphas_q(&self, q: f64) -> f64 {
838        self.alphas_q2(q * q)
839    }
840
841    /// Return the list of available flavor PIDs (LHAPDF name for `pids`).
842    #[must_use]
843    pub fn flavors(&self) -> Vec<i32> {
844        self.pdf.metadata().flavors.clone()
845    }
846
847    /// Return `True` if the given PID is available in this set.
848    #[must_use]
849    #[pyo3(name = "hasFlavor")]
850    pub fn has_flavor(&self, id: i32) -> bool {
851        self.pdf.metadata().flavors.contains(&id)
852    }
853
854    /// Return `True` if `x` lies within the grid x-range.
855    #[must_use]
856    #[pyo3(name = "inRangeX")]
857    pub fn in_range_x(&self, x: f64) -> bool {
858        let r = self.pdf.param_ranges().x;
859        x >= r.min && x <= r.max
860    }
861
862    /// Return `True` if `Q` (not Q²) lies within the grid Q-range.
863    #[must_use]
864    #[pyo3(name = "inRangeQ")]
865    pub fn in_range_q(&self, q: f64) -> bool {
866        self.in_range_q2(q * q)
867    }
868
869    /// Return `True` if `Q²` lies within the grid Q²-range.
870    #[must_use]
871    #[pyo3(name = "inRangeQ2")]
872    pub fn in_range_q2(&self, q2: f64) -> bool {
873        let r = self.pdf.param_ranges().q2;
874        q2 >= r.min && q2 <= r.max
875    }
876
877    /// Return `True` if both `x` and `Q` are within their respective ranges.
878    #[must_use]
879    #[pyo3(name = "inRangeXQ")]
880    pub fn in_range_xq(&self, x: f64, q: f64) -> bool {
881        self.in_range_x(x) && self.in_range_q(q)
882    }
883
884    /// Return `True` if both `x` and `Q²` are within their respective ranges.
885    #[must_use]
886    #[pyo3(name = "inRangeXQ2")]
887    pub fn in_range_xq2(&self, x: f64, q2: f64) -> bool {
888        self.in_range_x(x) && self.in_range_q2(q2)
889    }
890
891    /// Index of this member within its PDF set (0 = central value).
892    #[getter]
893    #[must_use]
894    #[pyo3(name = "memberID")]
895    pub const fn member_id(&self) -> usize {
896        self.member
897    }
898
899    /// LHAPDF ID of this specific member (set base ID + member index).
900    #[getter]
901    #[must_use]
902    #[pyo3(name = "lhapdfID")]
903    pub fn lhapdf_id(&self) -> u32 {
904        self.pdf.metadata().set_index + self.member as u32
905    }
906
907    /// QCD perturbative order used for this PDF set.
908    #[getter]
909    #[must_use]
910    #[pyo3(name = "orderQCD")]
911    pub fn order_qcd(&self) -> u32 {
912        self.pdf.metadata().order_qcd
913    }
914
915    /// Maximum x value in the grid (camelCase LHAPDF alias).
916    #[getter]
917    #[must_use]
918    #[pyo3(name = "xMax")]
919    pub fn x_max_lhapdf(&self) -> f64 {
920        self.x_max()
921    }
922
923    /// Minimum x value in the grid (camelCase LHAPDF alias).
924    #[getter]
925    #[must_use]
926    #[pyo3(name = "xMin")]
927    pub fn x_min_lhapdf(&self) -> f64 {
928        self.x_min()
929    }
930
931    /// Maximum Q² value in the grid (camelCase LHAPDF alias).
932    #[getter]
933    #[must_use]
934    #[pyo3(name = "q2Max")]
935    pub fn q2_max_lhapdf(&self) -> f64 {
936        self.q2_max()
937    }
938
939    /// Minimum Q² value in the grid (camelCase LHAPDF alias).
940    #[getter]
941    #[must_use]
942    #[pyo3(name = "q2Min")]
943    pub fn q2_min_lhapdf(&self) -> f64 {
944        self.q2_min()
945    }
946
947    /// Pole mass of the quark with the given flavor ID.
948    ///
949    /// Returns 0.0 for unknown flavor IDs.
950    #[must_use]
951    #[pyo3(name = "quarkMass")]
952    pub fn quark_mass(&self, id: i32) -> f64 {
953        let m = self.pdf.metadata();
954        match id.abs() {
955            1 => m.m_down,
956            2 => m.m_up,
957            3 => m.m_strange,
958            4 => m.m_charm,
959            5 => m.m_bottom,
960            6 => m.m_top,
961            _ => 0.0,
962        }
963    }
964
965    /// Flavor threshold for the given quark ID (equal to the quark mass).
966    #[must_use]
967    #[pyo3(name = "quarkThreshold")]
968    pub fn quark_threshold(&self, id: i32) -> f64 {
969        self.quark_mass(id)
970    }
971
972    /// Human-readable description of this PDF set.
973    #[getter]
974    #[must_use]
975    pub fn description(&self) -> String {
976        self.pdf.metadata().set_desc.clone()
977    }
978
979    /// Return the `PDFSet` this member belongs to.
980    ///
981    /// Only available when the PDF was loaded by name; returns `None` when
982    /// loaded via LHAID or file path.
983    #[getter]
984    #[must_use]
985    pub fn set(&self) -> Option<PyPDFSet> {
986        if self.pdf_name.is_empty() {
987            None
988        } else {
989            Some(PyPDFSet::new(&self.pdf_name))
990        }
991    }
992
993    /// Return `True` if the given LHAPDF-canonical key is present in this PDF's metadata.
994    #[must_use]
995    #[pyo3(name = "has_key")]
996    pub fn has_key(&self, key: &str) -> bool {
997        build_lhapdf_map(self.pdf.metadata()).contains_key(key)
998    }
999
1000    /// Return the string value for an LHAPDF-canonical metadata key.
1001    ///
1002    /// # Errors
1003    ///
1004    /// Raises `KeyError` if the key is not present.
1005    #[pyo3(name = "get_entry")]
1006    pub fn get_entry(&self, key: &str) -> PyResult<String> {
1007        build_lhapdf_map(self.pdf.metadata())
1008            .remove(key)
1009            .ok_or_else(|| {
1010                pyo3::exceptions::PyKeyError::new_err(format!("Key '{key}' not found in metadata"))
1011            })
1012    }
1013
1014    /// Return a sorted list of all available LHAPDF-canonical metadata keys.
1015    #[must_use]
1016    #[pyo3(name = "keys")]
1017    pub fn keys(&self) -> Vec<String> {
1018        let mut ks: Vec<String> = build_lhapdf_map(self.pdf.metadata()).into_keys().collect();
1019        ks.sort();
1020        ks
1021    }
1022}
1023
1024/// Registers the `pdf` submodule with the parent Python module.
1025///
1026/// This function is typically called during the initialization of the
1027/// `neopdf` Python package to expose the `PDF` class.
1028///
1029/// Parameters
1030/// ----------
1031/// `parent_module` : pyo3.Bound[pyo3.types.PyModule]
1032///     The parent Python module to which the `pdf` submodule will be added.
1033///
1034/// Returns
1035/// -------
1036/// pyo3.PyResult<()>
1037///     `Ok(())` if the registration is successful, or an error if the submodule
1038///     cannot be created or added.
1039///
1040/// # Errors
1041///
1042/// Raises an error if the (sub)module is not found or cannot be registered.
1043pub fn register(parent_module: &Bound<'_, PyModule>) -> PyResult<()> {
1044    let m = PyModule::new(parent_module.py(), "pdf")?;
1045    m.setattr(pyo3::intern!(m.py(), "__doc__"), "Interface for PDF.")?;
1046    pyo3::py_run!(
1047        parent_module.py(),
1048        m,
1049        "import sys; sys.modules['neopdf.pdf'] = m"
1050    );
1051    m.add_class::<PyPDF>()?;
1052    m.add_class::<PyPDFSet>()?;
1053    m.add_class::<PyLazyPDFs>()?;
1054    m.add_class::<PyForcePositive>()?;
1055    m.add_class::<PyGridParams>()?;
1056    m.add_class::<PyLoaderMethod>()?;
1057    parent_module.add_class::<PyPDFSet>()?;
1058    parent_module.add_submodule(&m)
1059}