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, ¶ms)
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, ¶ms, &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}