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