neopdf/
pdf.rs

1use numpy::{IntoPyArray, 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 all members of the PDF set.
175    ///
176    /// This function loads all available members for a given PDF set,
177    /// returning a list of `PDF` instances.
178    ///
179    /// Parameters
180    /// ----------
181    /// pdf_name : str
182    ///     The name of the PDF set.
183    ///
184    /// Returns
185    /// -------
186    /// list[PDF]
187    ///     A list of `PDF` instances, one for each member.
188    #[must_use]
189    #[staticmethod]
190    #[pyo3(name = "mkPDFs")]
191    #[pyo3(signature = (pdf_name, method = &PyLoaderMethod::Parallel))]
192    pub fn mkpdfs(pdf_name: &str, method: &PyLoaderMethod) -> Vec<Self> {
193        let loader_method = match method {
194            PyLoaderMethod::Parallel => PDF::load_pdfs,
195            PyLoaderMethod::Sequential => PDF::load_pdfs_seq,
196        };
197
198        loader_method(pdf_name)
199            .into_iter()
200            .map(move |pdfobj| Self { pdf: pdfobj })
201            .collect()
202    }
203
204    /// Creates an iterator that loads PDF members lazily.
205    ///
206    /// This function is suitable for `.neopdf.lz4` files, which support lazy loading.
207    /// It returns an iterator that yields `PDF` instances on demand, which is useful
208    /// for reducing memory consumption when working with large PDF sets.
209    ///
210    /// # Arguments
211    ///
212    /// * `pdf_name` - The name of the PDF set (must end with `.neopdf.lz4`).
213    ///
214    /// # Returns
215    ///
216    /// An iterator over `Result<PDF, Box<dyn std::error::Error>>`.
217    #[must_use]
218    #[staticmethod]
219    #[pyo3(name = "mkPDFs_lazy")]
220    pub fn mkpdfs_lazy(pdf_name: &str) -> PyLazyPDFs {
221        PyLazyPDFs {
222            iter: Mutex::new(Box::new(PDF::load_pdfs_lazy(pdf_name))),
223        }
224    }
225
226    /// Returns the list of `PID` values.
227    ///
228    /// Returns
229    /// -------
230    /// list[int]
231    ///     The PID values.
232    #[must_use]
233    pub fn pids(&self) -> Vec<i32> {
234        self.pdf.pids().to_vec()
235    }
236
237    /// Returns the list of `Subgrid` objects.
238    ///
239    /// Returns
240    /// -------
241    /// list[PySubgrid]
242    ///     The subgrids.
243    #[must_use]
244    pub fn subgrids(&self) -> Vec<PySubGrid> {
245        self.pdf
246            .subgrids()
247            .iter()
248            .map(|subgrid| PySubGrid {
249                subgrid: subgrid.clone(),
250            })
251            .collect()
252    }
253
254    /// Returns the subgrid knots of a parameter for a given subgrid index.
255    ///
256    /// The parameter could be the nucleon numbers `A`, the strong coupling
257    /// `alphas`, the momentum fraction `x`, or the momentum scale `Q2`.
258    ///
259    /// # Panics
260    ///
261    /// This panics if the parameter is not valid.
262    ///
263    /// Returns
264    /// -------
265    /// list[float]
266    ///     The subgrid knots for a given parameter.
267    #[must_use]
268    pub fn subgrid_knots(&self, param: &PyGridParams, subgrid_index: usize) -> Vec<f64> {
269        match param {
270            PyGridParams::AlphaS => self.pdf.subgrid(subgrid_index).alphas.to_vec(),
271            PyGridParams::X => self.pdf.subgrid(subgrid_index).xs.to_vec(),
272            PyGridParams::Q2 => self.pdf.subgrid(subgrid_index).q2s.to_vec(),
273            PyGridParams::A => self.pdf.subgrid(subgrid_index).nucleons.to_vec(),
274            PyGridParams::KT => self.pdf.subgrid(subgrid_index).kts.to_vec(),
275        }
276    }
277
278    /// Clip the negative or small values for the `PDF` object.
279    ///
280    /// Parameters
281    /// ----------
282    /// id : PyFrocePositive
283    ///     The clipping method use to handle negative or small values.
284    pub fn set_force_positive(&mut self, option: PyForcePositive) {
285        self.pdf.set_force_positive(option.into());
286    }
287
288    /// Clip the negative or small values for all the `PDF` objects.
289    ///
290    /// Parameters
291    /// ----------
292    /// pdfs : list[PDF]
293    ///     A list of `PDF` instances.
294    /// option : PyForcePositive
295    ///     The clipping method use to handle negative or small values.
296    #[staticmethod]
297    #[pyo3(name = "set_force_positive_members")]
298    #[allow(clippy::needless_pass_by_value)]
299    pub fn set_force_positive_members(pdfs: Vec<PyRefMut<Self>>, option: PyForcePositive) {
300        for mut pypdf in pdfs {
301            pypdf.set_force_positive(option.clone());
302        }
303    }
304
305    /// Returns the clipping method used for a single `PDF` object.
306    ///
307    /// Returns
308    /// -------
309    /// PyForcePositive
310    ///     The clipping method used for the `PDF` object.
311    #[must_use]
312    pub fn is_force_positive(&self) -> PyForcePositive {
313        self.pdf.is_force_positive().into()
314    }
315
316    /// Retrieves the minimum x-value for this PDF set.
317    ///
318    /// Returns
319    /// -------
320    /// float
321    ///     The minimum x-value.
322    #[must_use]
323    pub fn x_min(&self) -> f64 {
324        self.pdf.param_ranges().x.min
325    }
326
327    /// Retrieves the maximum x-value for this PDF set.
328    ///
329    /// Returns
330    /// -------
331    /// float
332    ///     The maximum x-value.
333    #[must_use]
334    pub fn x_max(&self) -> f64 {
335        self.pdf.param_ranges().x.max
336    }
337
338    /// Retrieves the minimum Q2-value for this PDF set.
339    ///
340    /// Returns
341    /// -------
342    /// float
343    ///     The minimum Q2-value.
344    #[must_use]
345    pub fn q2_min(&self) -> f64 {
346        self.pdf.param_ranges().q2.min
347    }
348
349    /// Retrieves the maximum Q2-value for this PDF set.
350    ///
351    /// Returns
352    /// -------
353    /// float
354    ///     The maximum Q2-value.
355    #[must_use]
356    pub fn q2_max(&self) -> f64 {
357        self.pdf.param_ranges().q2.max
358    }
359
360    /// Retrieves the flavour PIDs for the PDF set.
361    ///
362    /// Returns
363    /// -------
364    /// list(int)
365    ///     The flavour PID values.
366    #[must_use]
367    pub fn flavour_pids(&self) -> Vec<i32> {
368        self.pdf.metadata().flavors.clone()
369    }
370
371    /// Interpolates the PDF value (xf) for a given flavor, x, and Q2.
372    ///
373    /// Parameters
374    /// ----------
375    /// id : int
376    ///     The flavor ID (e.g., 21 for gluon, 1 for d-quark).
377    /// x : float
378    ///     The momentum fraction.
379    /// q2 : float
380    ///     The energy scale squared.
381    ///
382    /// Returns
383    /// -------
384    /// float
385    ///     The interpolated PDF value. Returns 0.0 if extrapolation is
386    ///     attempted and not allowed.
387    #[must_use]
388    #[pyo3(name = "xfxQ2")]
389    pub fn xfxq2(&self, id: i32, x: f64, q2: f64) -> f64 {
390        self.pdf.xfxq2(id, &[x, q2])
391    }
392
393    /// Interpolates the PDF value (xf) for a given set of parameters.
394    ///
395    /// Parameters
396    /// ----------
397    /// id : int
398    ///     The flavor ID (e.g., 21 for gluon, 1 for d-quark).
399    /// params: list[float]
400    ///     A list of parameters that the grids depends on. If the PDF
401    ///     grid only contains `x` and `Q2` dependence then its value is
402    ///     `[x, q2]`; if it contains either the `A` and `alpha_s`
403    ///     dependence, then its value is `[A, x, q2]` or `[alpha_s, x, q2]`
404    ///     respectively; if it contains both, then `[A, alpha_s, x, q2]`.
405    ///
406    /// Returns
407    /// -------
408    /// float
409    ///     The interpolated PDF value. Returns 0.0 if extrapolation is
410    ///     attempted and not allowed.
411    #[must_use]
412    #[pyo3(name = "xfxQ2_ND")]
413    #[allow(clippy::needless_pass_by_value)]
414    pub fn xfxq2_nd(&self, id: i32, params: Vec<f64>) -> f64 {
415        self.pdf.xfxq2(id, &params)
416    }
417
418    /// Interpolates the PDF value (xf) for a list containg a set of parameters.
419    ///
420    /// Parameters
421    /// ----------
422    /// id : int
423    ///     The flavor ID (e.g., 21 for gluon, 1 for d-quark).
424    /// params: list[list[float]]
425    ///     A list containing the list of points. Each element in the list
426    ///     is in turn a list containing the parameters that the grids depends
427    ///     on. If the PDF grid only contains `x` and `Q2` dependence then its
428    ///     value is `[x, q2]`; if it contains either the `A` and `alpha_s`
429    ///     dependence, then its value is `[A, x, q2]` or `[alpha_s, x, q2]`
430    ///     respectively; if it contains both, then `[A, alpha_s, x, q2]`.
431    ///
432    /// Returns
433    /// -------
434    /// float
435    ///     The interpolated PDF value. Returns 0.0 if extrapolation is
436    ///     attempted and not allowed.
437    #[must_use]
438    #[pyo3(name = "xfxQ2_Chebyshev_batch")]
439    #[allow(clippy::needless_pass_by_value)]
440    pub fn xfxq2_cheby_batch(&self, id: i32, params: Vec<Vec<f64>>) -> Vec<f64> {
441        let slices: Vec<&[f64]> = params.iter().map(Vec::as_slice).collect();
442        self.pdf.xfxq2_cheby_batch(id, &slices)
443    }
444
445    /// Interpolates the PDF value (xf) for lists of flavors, x-values,
446    /// and Q2-values.
447    ///
448    /// Parameters
449    /// ----------
450    /// id : list[int]
451    ///     A list of flavor IDs.
452    /// xs : list[float]
453    ///     A list of momentum fractions.
454    /// q2s : list[float]
455    ///     A list of energy scales squared.
456    ///
457    /// Returns
458    /// -------
459    /// numpy.ndarray
460    ///     A 2D NumPy array containing the interpolated PDF values.
461    #[must_use]
462    #[pyo3(name = "xfxQ2s")]
463    #[allow(clippy::needless_pass_by_value)]
464    pub fn xfxq2s<'py>(
465        &self,
466        pids: Vec<i32>,
467        xs: Vec<f64>,
468        q2s: Vec<f64>,
469        py: Python<'py>,
470    ) -> Bound<'py, PyArray2<f64>> {
471        let flatten_points: Vec<Vec<f64>> = xs
472            .iter()
473            .flat_map(|&x| q2s.iter().map(move |&q2| vec![x, q2]))
474            .collect();
475        let points_interp: Vec<&[f64]> = flatten_points.iter().map(Vec::as_slice).collect();
476        let slice_points: &[&[f64]] = &points_interp;
477
478        self.pdf.xfxq2s(pids, slice_points).into_pyarray(py)
479    }
480
481    /// Computes the alpha_s value at a given Q2.
482    ///
483    /// Parameters
484    /// ----------
485    /// q2 : float
486    ///     The energy scale squared.
487    ///
488    /// Returns
489    /// -------
490    /// float
491    ///     The interpolated alpha_s value.
492    #[must_use]
493    #[pyo3(name = "alphasQ2")]
494    pub fn alphas_q2(&self, q2: f64) -> f64 {
495        self.pdf.alphas_q2(q2)
496    }
497
498    /// Returns the metadata associated with this PDF set.
499    ///
500    /// Provides access to the metadata describing the PDF set, including information
501    /// such as the set description, number of members, parameter ranges, and other
502    /// relevant details.
503    ///
504    /// Returns
505    /// -------
506    /// MetaData
507    ///     The metadata for this PDF set as a `MetaData` Python object.
508    #[must_use]
509    #[pyo3(name = "metadata")]
510    pub fn metadata(&self) -> PyMetaData {
511        PyMetaData {
512            meta: self.pdf.metadata().clone(),
513        }
514    }
515}
516
517/// Registers the `pdf` submodule with the parent Python module.
518///
519/// This function is typically called during the initialization of the
520/// `neopdf` Python package to expose the `PDF` class.
521///
522/// Parameters
523/// ----------
524/// `parent_module` : pyo3.Bound[pyo3.types.PyModule]
525///     The parent Python module to which the `pdf` submodule will be added.
526///
527/// Returns
528/// -------
529/// pyo3.PyResult<()>
530///     `Ok(())` if the registration is successful, or an error if the submodule
531///     cannot be created or added.
532///
533/// # Errors
534///
535/// Raises an error if the (sub)module is not found or cannot be registered.
536pub fn register(parent_module: &Bound<'_, PyModule>) -> PyResult<()> {
537    let m = PyModule::new(parent_module.py(), "pdf")?;
538    m.setattr(pyo3::intern!(m.py(), "__doc__"), "Interface for PDF.")?;
539    pyo3::py_run!(
540        parent_module.py(),
541        m,
542        "import sys; sys.modules['neopdf.pdf'] = m"
543    );
544    m.add_class::<PyPDF>()?;
545    m.add_class::<PyLazyPDFs>()?;
546    m.add_class::<PyForcePositive>()?;
547    m.add_class::<PyGridParams>()?;
548    m.add_class::<PyLoaderMethod>()?;
549    parent_module.add_submodule(&m)
550}