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