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::pdf::PDF;
7
8use super::gridpdf::PySubGrid;
9use super::metadata::PyMetaData;
10
11// Type aliases
12type LazyType = Result<PDF, Box<dyn std::error::Error>>;
13
14/// Python wrapper for the `ForcePositive` enum.
15#[pyclass(name = "ForcePositive")]
16#[derive(Clone)]
17pub enum PyForcePositive {
18    /// If the calculated PDF value is negative, it is forced to 0.
19    ClipNegative,
20    /// If the calculated PDF value is less than 1e-10, it is set to 1e-10.
21    ClipSmall,
22    /// No clipping is done, value is returned as it is.
23    NoClipping,
24}
25
26impl From<PyForcePositive> for ForcePositive {
27    fn from(fmt: PyForcePositive) -> Self {
28        match fmt {
29            PyForcePositive::ClipNegative => Self::ClipNegative,
30            PyForcePositive::ClipSmall => Self::ClipSmall,
31            PyForcePositive::NoClipping => Self::NoClipping,
32        }
33    }
34}
35
36impl From<&ForcePositive> for PyForcePositive {
37    fn from(fmt: &ForcePositive) -> Self {
38        match fmt {
39            ForcePositive::ClipNegative => Self::ClipNegative,
40            ForcePositive::ClipSmall => Self::ClipSmall,
41            ForcePositive::NoClipping => Self::NoClipping,
42        }
43    }
44}
45
46/// Methods to load all the PDF members for a given set.
47#[pyclass(name = "LoaderMethod")]
48#[derive(Clone)]
49pub enum PyLoaderMethod {
50    /// Load the members in parallel using multi-threads.
51    Parallel,
52    /// Load the members in sequential.
53    Sequential,
54}
55
56#[pymethods]
57impl PyForcePositive {
58    fn __eq__(&self, other: &Self) -> bool {
59        std::mem::discriminant(self) == std::mem::discriminant(other)
60    }
61
62    fn __hash__(&self) -> u64 {
63        use std::collections::hash_map::DefaultHasher;
64        use std::hash::{Hash, Hasher};
65        let mut hasher = DefaultHasher::new();
66        std::mem::discriminant(self).hash(&mut hasher);
67        hasher.finish()
68    }
69}
70
71/// This enum contains the different parameters that a grid can depend on.
72#[pyclass(name = "GridParams")]
73#[derive(Clone)]
74pub enum PyGridParams {
75    /// The nucleon mass number A.
76    A,
77    /// The strong coupling `alpha_s`.
78    AlphaS,
79    /// The momentum fraction.
80    X,
81    /// The transverse momentum.
82    KT,
83    /// The energy scale `Q^2`.
84    Q2,
85}
86
87/// Python wrapper for the `neopdf::pdf::PDF` struct.
88///
89/// This class provides a Python-friendly interface to the core PDF
90/// interpolation functionalities of the `neopdf` Rust library.
91#[pyclass(name = "LazyPDFs")]
92pub struct PyLazyPDFs {
93    iter: Mutex<Box<dyn Iterator<Item = LazyType> + Send>>,
94}
95
96#[pymethods]
97impl PyLazyPDFs {
98    const fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
99        slf
100    }
101
102    #[allow(clippy::needless_pass_by_value)]
103    fn __next__(slf: PyRefMut<'_, Self>) -> PyResult<Option<PyPDF>> {
104        let mut iter = slf.iter.lock().unwrap();
105        match iter.next() {
106            Some(Ok(pdf)) => Ok(Some(PyPDF { pdf })),
107            Some(Err(e)) => Err(pyo3::exceptions::PyValueError::new_err(e.to_string())),
108            None => Ok(None),
109        }
110    }
111}
112
113/// Python wrapper for the `neopdf::pdf::PDF` struct.
114///
115/// This class provides a Python-friendly interface to the core PDF
116/// interpolation functionalities of the `neopdf` Rust library.
117#[pyclass(name = "PDF")]
118#[repr(transparent)]
119pub struct PyPDF {
120    pub(crate) pdf: PDF,
121}
122
123#[pymethods]
124#[allow(clippy::doc_markdown)]
125impl PyPDF {
126    /// Creates a new `PDF` instance for a given PDF set and member.
127    ///
128    /// This is the primary constructor for the `PDF` class.
129    ///
130    /// Parameters
131    /// ----------
132    /// pdf_name : str
133    ///     The name of the PDF set.
134    /// member : int
135    ///     The ID of the PDF member to load. Defaults to 0.
136    ///
137    /// Returns
138    /// -------
139    /// PDF
140    ///     A new `PDF` instance.
141    #[new]
142    #[must_use]
143    #[pyo3(signature = (pdf_name, member = 0))]
144    pub fn new(pdf_name: &str, member: usize) -> Self {
145        Self {
146            pdf: PDF::load(pdf_name, member),
147        }
148    }
149
150    /// Loads a given member of the PDF set.
151    ///
152    /// This is an alternative constructor for convenience, equivalent
153    /// to `PDF(pdf_name, member)`.
154    ///
155    /// Parameters
156    /// ----------
157    /// pdf_name : str
158    ///     The name of the PDF set.
159    /// member : int
160    ///     The ID of the PDF member. Defaults to 0.
161    ///
162    /// Returns
163    /// -------
164    /// PDF
165    ///     A new `PDF` instance.
166    #[must_use]
167    #[staticmethod]
168    #[pyo3(name = "mkPDF")]
169    #[pyo3(signature = (pdf_name, member = 0))]
170    pub fn mkpdf(pdf_name: &str, member: usize) -> Self {
171        Self::new(pdf_name, member)
172    }
173
174    /// Loads a PDF member by its LHAPDF ID (LHAID).
175    ///
176    /// The set name and member index are resolved by fetching the LHAPDF set
177    /// index from `https://lhapdfsets.web.cern.ch/current/pdfsets.index`.
178    ///
179    /// Parameters
180    /// ----------
181    /// lhaid : int
182    ///     The LHAPDF ID uniquely identifying both the PDF set and the member.
183    ///
184    /// Returns
185    /// -------
186    /// PDF
187    ///     A new `PDF` instance for the set and member encoded in `lhaid`.
188    #[must_use]
189    #[staticmethod]
190    #[pyo3(name = "mkPDF_lhaid")]
191    pub fn mkpdf_lhaid(lhaid: u32) -> Self {
192        Self {
193            pdf: PDF::load_by_lhaid(lhaid),
194        }
195    }
196
197    /// Loads a PDF member from a specific LHAPDF `.dat` file path.
198    ///
199    /// Parameters
200    /// ----------
201    /// path : str
202    ///     The path to the LHAPDF `.dat` file.
203    ///
204    /// Returns
205    /// -------
206    /// PDF
207    ///     A new `PDF` instance for the specified file.
208    #[must_use]
209    #[staticmethod]
210    #[pyo3(name = "mkPDF_lhapdf_file")]
211    pub fn mkpdf_lhapdf_file(path: &str) -> Self {
212        Self {
213            pdf: PDF::load_lhapdf_by_file(path),
214        }
215    }
216
217    /// Loads all members of the PDF set.
218    ///
219    /// This function loads all available members for a given PDF set,
220    /// returning a list of `PDF` instances.
221    ///
222    /// Parameters
223    /// ----------
224    /// pdf_name : str
225    ///     The name of the PDF set.
226    ///
227    /// Returns
228    /// -------
229    /// list[PDF]
230    ///     A list of `PDF` instances, one for each member.
231    #[must_use]
232    #[staticmethod]
233    #[pyo3(name = "mkPDFs")]
234    #[pyo3(signature = (pdf_name, method = &PyLoaderMethod::Parallel))]
235    pub fn mkpdfs(pdf_name: &str, method: &PyLoaderMethod) -> Vec<Self> {
236        let loader_method = match method {
237            PyLoaderMethod::Parallel => PDF::load_pdfs,
238            PyLoaderMethod::Sequential => PDF::load_pdfs_seq,
239        };
240
241        loader_method(pdf_name)
242            .into_iter()
243            .map(move |pdfobj| Self { pdf: pdfobj })
244            .collect()
245    }
246
247    /// Creates an iterator that loads PDF members lazily.
248    ///
249    /// This function is suitable for `.neopdf.lz4` files, which support lazy loading.
250    /// It returns an iterator that yields `PDF` instances on demand, which is useful
251    /// for reducing memory consumption when working with large PDF sets.
252    ///
253    /// # Arguments
254    ///
255    /// * `pdf_name` - The name of the PDF set (must end with `.neopdf.lz4`).
256    ///
257    /// # Returns
258    ///
259    /// An iterator over `Result<PDF, Box<dyn std::error::Error>>`.
260    #[must_use]
261    #[staticmethod]
262    #[pyo3(name = "mkPDFs_lazy")]
263    pub fn mkpdfs_lazy(pdf_name: &str) -> PyLazyPDFs {
264        PyLazyPDFs {
265            iter: Mutex::new(Box::new(PDF::load_pdfs_lazy(pdf_name))),
266        }
267    }
268
269    /// Returns the list of `PID` values.
270    ///
271    /// Returns
272    /// -------
273    /// list[int]
274    ///     The PID values.
275    #[must_use]
276    pub fn pids(&self) -> Vec<i32> {
277        self.pdf.pids().to_vec()
278    }
279
280    /// Returns the list of `Subgrid` objects.
281    ///
282    /// Returns
283    /// -------
284    /// list[PySubgrid]
285    ///     The subgrids.
286    #[must_use]
287    pub fn subgrids(&self) -> Vec<PySubGrid> {
288        self.pdf
289            .subgrids()
290            .iter()
291            .map(|subgrid| PySubGrid {
292                subgrid: subgrid.clone(),
293            })
294            .collect()
295    }
296
297    /// Returns the subgrid knots of a parameter for a given subgrid index.
298    ///
299    /// The parameter could be the nucleon numbers `A`, the strong coupling
300    /// `alphas`, the momentum fraction `x`, or the momentum scale `Q2`.
301    ///
302    /// # Panics
303    ///
304    /// This panics if the parameter is not valid.
305    ///
306    /// Returns
307    /// -------
308    /// list[float]
309    ///     The subgrid knots for a given parameter.
310    #[must_use]
311    pub fn subgrid_knots(&self, param: &PyGridParams, subgrid_index: usize) -> Vec<f64> {
312        match param {
313            PyGridParams::AlphaS => self.pdf.subgrid(subgrid_index).alphas.to_vec(),
314            PyGridParams::X => self.pdf.subgrid(subgrid_index).xs.to_vec(),
315            PyGridParams::Q2 => self.pdf.subgrid(subgrid_index).q2s.to_vec(),
316            PyGridParams::A => self.pdf.subgrid(subgrid_index).nucleons.to_vec(),
317            PyGridParams::KT => self.pdf.subgrid(subgrid_index).kts.to_vec(),
318        }
319    }
320
321    /// Clip the negative or small values for the `PDF` object.
322    ///
323    /// Parameters
324    /// ----------
325    /// id : PyFrocePositive
326    ///     The clipping method use to handle negative or small values.
327    pub fn set_force_positive(&mut self, option: PyForcePositive) {
328        self.pdf.set_force_positive(option.into());
329    }
330
331    /// Clip the negative or small values for all the `PDF` objects.
332    ///
333    /// Parameters
334    /// ----------
335    /// pdfs : list[PDF]
336    ///     A list of `PDF` instances.
337    /// option : PyForcePositive
338    ///     The clipping method use to handle negative or small values.
339    #[staticmethod]
340    #[pyo3(name = "set_force_positive_members")]
341    #[allow(clippy::needless_pass_by_value)]
342    pub fn set_force_positive_members(pdfs: Vec<PyRefMut<Self>>, option: PyForcePositive) {
343        for mut pypdf in pdfs {
344            pypdf.set_force_positive(option.clone());
345        }
346    }
347
348    /// Returns the clipping method used for a single `PDF` object.
349    ///
350    /// Returns
351    /// -------
352    /// PyForcePositive
353    ///     The clipping method used for the `PDF` object.
354    #[must_use]
355    pub fn is_force_positive(&self) -> PyForcePositive {
356        self.pdf.is_force_positive().into()
357    }
358
359    /// Retrieves the minimum x-value for this PDF set.
360    ///
361    /// Returns
362    /// -------
363    /// float
364    ///     The minimum x-value.
365    #[must_use]
366    pub fn x_min(&self) -> f64 {
367        self.pdf.param_ranges().x.min
368    }
369
370    /// Retrieves the maximum x-value for this PDF set.
371    ///
372    /// Returns
373    /// -------
374    /// float
375    ///     The maximum x-value.
376    #[must_use]
377    pub fn x_max(&self) -> f64 {
378        self.pdf.param_ranges().x.max
379    }
380
381    /// Retrieves the minimum Q2-value for this PDF set.
382    ///
383    /// Returns
384    /// -------
385    /// float
386    ///     The minimum Q2-value.
387    #[must_use]
388    pub fn q2_min(&self) -> f64 {
389        self.pdf.param_ranges().q2.min
390    }
391
392    /// Retrieves the maximum Q2-value for this PDF set.
393    ///
394    /// Returns
395    /// -------
396    /// float
397    ///     The maximum Q2-value.
398    #[must_use]
399    pub fn q2_max(&self) -> f64 {
400        self.pdf.param_ranges().q2.max
401    }
402
403    /// Retrieves the flavour PIDs for the PDF set.
404    ///
405    /// Returns
406    /// -------
407    /// list(int)
408    ///     The flavour PID values.
409    #[must_use]
410    pub fn flavour_pids(&self) -> Vec<i32> {
411        self.pdf.metadata().flavors.clone()
412    }
413
414    /// Interpolates the PDF value (xf) for a given flavor, x, and Q2.
415    ///
416    /// Parameters
417    /// ----------
418    /// id : int
419    ///     The flavor ID (e.g., 21 for gluon, 1 for d-quark).
420    /// x : float
421    ///     The momentum fraction.
422    /// q2 : float
423    ///     The energy scale squared.
424    ///
425    /// Returns
426    /// -------
427    /// float
428    ///     The interpolated PDF value. Returns 0.0 if extrapolation is
429    ///     attempted and not allowed.
430    #[must_use]
431    #[pyo3(name = "xfxQ2")]
432    pub fn xfxq2(&self, id: i32, x: f64, q2: f64) -> f64 {
433        self.pdf.xfxq2(id, &[x, q2])
434    }
435
436    /// Interpolates the PDF value (xf) for a given set of parameters.
437    ///
438    /// Parameters
439    /// ----------
440    /// id : int
441    ///     The flavor ID (e.g., 21 for gluon, 1 for d-quark).
442    /// params: list[float]
443    ///     A list of parameters that the grids depends on. If the PDF
444    ///     grid only contains `x` and `Q2` dependence then its value is
445    ///     `[x, q2]`; if it contains either the `A` and `alpha_s`
446    ///     dependence, then its value is `[A, x, q2]` or `[alpha_s, x, q2]`
447    ///     respectively; if it contains both, then `[A, alpha_s, x, q2]`.
448    ///
449    /// Returns
450    /// -------
451    /// float
452    ///     The interpolated PDF value. Returns 0.0 if extrapolation is
453    ///     attempted and not allowed.
454    #[must_use]
455    #[pyo3(name = "xfxQ2_ND")]
456    #[allow(clippy::needless_pass_by_value)]
457    pub fn xfxq2_nd(&self, id: i32, params: Vec<f64>) -> f64 {
458        self.pdf.xfxq2(id, &params)
459    }
460
461    /// Evaluates all requested flavors at a single kinematic point.
462    ///
463    /// Parameters
464    /// ----------
465    /// pids : list[int]
466    ///     A list of flavor IDs.
467    /// x : float
468    ///     The momentum fraction.
469    /// q2 : float
470    ///     The energy scale squared.
471    ///
472    /// Returns
473    /// -------
474    /// numpy.ndarray
475    ///     A 1D NumPy array containing the interpolated PDF values for each PID.
476    #[must_use]
477    #[pyo3(name = "xfxQ2_allpids")]
478    #[allow(clippy::needless_pass_by_value)]
479    pub fn xfxq2_allpids<'py>(
480        &self,
481        pids: Vec<i32>,
482        x: f64,
483        q2: f64,
484        py: Python<'py>,
485    ) -> Bound<'py, PyArray1<f64>> {
486        let mut out = vec![0.0; pids.len()];
487        self.pdf.xfxq2_allpids(&pids, &[x, q2], &mut out);
488        out.into_pyarray(py)
489    }
490
491    /// Evaluates all requested flavors at a single kinematic point.
492    ///
493    /// Parameters
494    /// ----------
495    /// pids : list[int]
496    ///     A list of flavor IDs.
497    /// params : list[float]
498    ///     A list of parameters (e.g., [kT, x, q2]).
499    ///
500    /// Returns
501    /// -------
502    /// numpy.ndarray
503    ///     A 1D NumPy array containing the interpolated PDF values for each PID.
504    #[must_use]
505    #[pyo3(name = "xfxQ2_allpids_ND")]
506    #[allow(clippy::needless_pass_by_value)]
507    pub fn xfxq2_allpids_nd<'py>(
508        &self,
509        pids: Vec<i32>,
510        params: Vec<f64>,
511        py: Python<'py>,
512    ) -> Bound<'py, PyArray1<f64>> {
513        let mut out = vec![0.0; pids.len()];
514        self.pdf.xfxq2_allpids(&pids, &params, &mut out);
515        out.into_pyarray(py)
516    }
517
518    /// Interpolates the PDF value (xf) for a list containg a set of parameters.
519    ///
520    /// Parameters
521    /// ----------
522    /// id : int
523    ///     The flavor ID (e.g., 21 for gluon, 1 for d-quark).
524    /// params: list[list[float]]
525    ///     A list containing the list of points. Each element in the list
526    ///     is in turn a list containing the parameters that the grids depends
527    ///     on. If the PDF grid only contains `x` and `Q2` dependence then its
528    ///     value is `[x, q2]`; if it contains either the `A` and `alpha_s`
529    ///     dependence, then its value is `[A, x, q2]` or `[alpha_s, x, q2]`
530    ///     respectively; if it contains both, then `[A, alpha_s, x, q2]`.
531    ///
532    /// Returns
533    /// -------
534    /// float
535    ///     The interpolated PDF value. Returns 0.0 if extrapolation is
536    ///     attempted and not allowed.
537    #[must_use]
538    #[pyo3(name = "xfxQ2_Chebyshev_batch")]
539    #[allow(clippy::needless_pass_by_value)]
540    pub fn xfxq2_cheby_batch(&self, id: i32, params: Vec<Vec<f64>>) -> Vec<f64> {
541        let slices: Vec<&[f64]> = params.iter().map(Vec::as_slice).collect();
542        self.pdf.xfxq2_cheby_batch(id, &slices)
543    }
544
545    /// Interpolates the PDF value (xf) for lists of flavors, x-values,
546    /// and Q2-values.
547    ///
548    /// Parameters
549    /// ----------
550    /// id : list[int]
551    ///     A list of flavor IDs.
552    /// xs : list[float]
553    ///     A list of momentum fractions.
554    /// q2s : list[float]
555    ///     A list of energy scales squared.
556    ///
557    /// Returns
558    /// -------
559    /// numpy.ndarray
560    ///     A 2D NumPy array containing the interpolated PDF values.
561    #[must_use]
562    #[pyo3(name = "xfxQ2s")]
563    #[allow(clippy::needless_pass_by_value)]
564    pub fn xfxq2s<'py>(
565        &self,
566        pids: Vec<i32>,
567        xs: Vec<f64>,
568        q2s: Vec<f64>,
569        py: Python<'py>,
570    ) -> Bound<'py, PyArray2<f64>> {
571        let flatten_points: Vec<Vec<f64>> = xs
572            .iter()
573            .flat_map(|&x| q2s.iter().map(move |&q2| vec![x, q2]))
574            .collect();
575        let points_interp: Vec<&[f64]> = flatten_points.iter().map(Vec::as_slice).collect();
576        let slice_points: &[&[f64]] = &points_interp;
577
578        self.pdf.xfxq2s(pids, slice_points).into_pyarray(py)
579    }
580
581    /// Computes the alpha_s value at a given Q2.
582    ///
583    /// Parameters
584    /// ----------
585    /// q2 : float
586    ///     The energy scale squared.
587    ///
588    /// Returns
589    /// -------
590    /// float
591    ///     The interpolated alpha_s value.
592    #[must_use]
593    #[pyo3(name = "alphasQ2")]
594    pub fn alphas_q2(&self, q2: f64) -> f64 {
595        self.pdf.alphas_q2(q2)
596    }
597
598    /// Returns the metadata associated with this PDF set.
599    ///
600    /// Provides access to the metadata describing the PDF set, including information
601    /// such as the set description, number of members, parameter ranges, and other
602    /// relevant details.
603    ///
604    /// Returns
605    /// -------
606    /// MetaData
607    ///     The metadata for this PDF set as a `MetaData` Python object.
608    #[must_use]
609    #[pyo3(name = "metadata")]
610    pub fn metadata(&self) -> PyMetaData {
611        PyMetaData {
612            meta: self.pdf.metadata().clone(),
613        }
614    }
615}
616
617/// Registers the `pdf` submodule with the parent Python module.
618///
619/// This function is typically called during the initialization of the
620/// `neopdf` Python package to expose the `PDF` class.
621///
622/// Parameters
623/// ----------
624/// `parent_module` : pyo3.Bound[pyo3.types.PyModule]
625///     The parent Python module to which the `pdf` submodule will be added.
626///
627/// Returns
628/// -------
629/// pyo3.PyResult<()>
630///     `Ok(())` if the registration is successful, or an error if the submodule
631///     cannot be created or added.
632///
633/// # Errors
634///
635/// Raises an error if the (sub)module is not found or cannot be registered.
636pub fn register(parent_module: &Bound<'_, PyModule>) -> PyResult<()> {
637    let m = PyModule::new(parent_module.py(), "pdf")?;
638    m.setattr(pyo3::intern!(m.py(), "__doc__"), "Interface for PDF.")?;
639    pyo3::py_run!(
640        parent_module.py(),
641        m,
642        "import sys; sys.modules['neopdf.pdf'] = m"
643    );
644    m.add_class::<PyPDF>()?;
645    m.add_class::<PyLazyPDFs>()?;
646    m.add_class::<PyForcePositive>()?;
647    m.add_class::<PyGridParams>()?;
648    m.add_class::<PyLoaderMethod>()?;
649    parent_module.add_submodule(&m)
650}