managed_lhapdf/
lib.rs

1#![warn(clippy::all, clippy::cargo, clippy::nursery, clippy::pedantic)]
2#![warn(missing_docs)]
3
4//! (Unofficial) Rust wrapper for the [LHAPDF](https://lhapdf.hepforge.org) C++ library.
5
6mod error;
7mod ffi;
8#[cfg(feature = "managed")]
9mod manager;
10mod unmanaged;
11
12#[cfg(not(feature = "managed"))]
13mod manager {
14    pub use super::unmanaged::*;
15}
16
17use cxx::{let_cxx_string, CxxVector, UniquePtr};
18use std::fmt::{self, Formatter};
19
20pub use error::{Error, Result};
21pub use ffi::PdfUncertainty;
22
23/// CL percentage for a Gaussian 1-sigma.
24pub const CL_1_SIGMA: f64 = 68.268_949_213_708_58;
25
26/// Convert an LHAID to an LHAPDF set name and member ID.
27#[must_use]
28pub fn lookup_pdf(lhaid: i32) -> Option<(String, i32)> {
29    manager::pdf_name_and_member_via_lhaid(lhaid)
30}
31
32/// Convenient way to set the verbosity level.
33pub fn set_verbosity(verbosity: i32) {
34    manager::set_verbosity(verbosity);
35}
36
37/// Convenient way to get the current verbosity level.
38#[must_use]
39pub fn verbosity() -> i32 {
40    manager::verbosity()
41}
42
43/// Wrapper to an LHAPDF object of the type `LHAPDF::PDF`.
44pub struct Pdf {
45    ptr: UniquePtr<ffi::PDF>,
46}
47
48impl fmt::Debug for Pdf {
49    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
50        // TODO: not all PDFs have an LHAID
51        f.debug_struct("Pdf")
52            .field("lhaid", &self.ptr.lhapdfID())
53            .finish()
54    }
55}
56
57impl Pdf {
58    /// Constructor. Create a new PDF with the given `lhaid` ID code.
59    ///
60    /// # Errors
61    ///
62    /// TODO
63    pub fn with_lhaid(lhaid: i32) -> Result<Self> {
64        let Some((setname, member)) = lookup_pdf(lhaid) else {
65            return Err(Error::General(format!(
66                "did not find PDF with LHAID = {lhaid}"
67            )));
68        };
69
70        Self::with_setname_and_member(&setname, member)
71    }
72
73    /// Constructor. Create a new PDF with the given PDF `setname` and `member` ID.
74    ///
75    /// # Errors
76    ///
77    /// TODO
78    pub fn with_setname_and_member(setname: &str, member: i32) -> Result<Self> {
79        manager::pdf_with_setname_and_member(setname, member).map(|ptr| Self { ptr })
80    }
81
82    /// Create a new PDF with the given PDF set name and member ID as a single string.
83    ///
84    /// The format of the `setname_nmem` string is `<setname>/<nmem>` where `<nmem>` must be
85    /// parseable as a positive integer. The `/` character is not permitted in set names due to
86    /// clashes with Unix filesystem path syntax.
87    ///
88    /// If no `/<nmem>` is given, member number 0 will be used.
89    ///
90    /// # Errors
91    ///
92    /// TODO
93    pub fn with_setname_and_nmem(setname_nmem: &str) -> Result<Self> {
94        let (setname, member) = setname_nmem.split_once('/').map_or(
95            Ok::<_, Error>((setname_nmem, 0)),
96            |(setname, nmem)| {
97                Ok((
98                    setname,
99                    nmem.parse().map_err(|err| {
100                        {
101                            Error::General(format!(
102                                "problem while parsing member index = {nmem}: '{err}'"
103                            ))
104                        }
105                    })?,
106                ))
107            },
108        )?;
109
110        Self::with_setname_and_member(setname, member)
111    }
112
113    /// Get the PDF `x * f(x)` value at `x` and `q2` for the given PDG ID.
114    ///
115    /// # Panics
116    ///
117    /// If the value of either `x` or `q2` is not within proper boundaries this method will panic.
118    #[must_use]
119    pub fn xfx_q2(&self, id: i32, x: f64, q2: f64) -> f64 {
120        self.ptr.xfxQ2(id, x, q2).unwrap()
121    }
122
123    /// Value of of the strong coupling at `q2` used by this PDF.
124    ///
125    /// # Panics
126    ///
127    /// If the value of `q2` is not within proper boundaries this method will panic.
128    #[must_use]
129    pub fn alphas_q2(&self, q2: f64) -> f64 {
130        self.ptr.alphasQ2(q2).unwrap()
131    }
132
133    /// Get the info class that actually stores and handles the metadata.
134    #[must_use]
135    pub fn set(&self) -> PdfSet {
136        let_cxx_string!(setname = "");
137        ffi::pdf_setname(&self.ptr, setname.as_mut());
138        // UNWRAP: if `setname` contains any non-UTF8 bytes there's an error somewhere else
139        let setname = setname.to_str().unwrap_or_else(|_| unreachable!());
140
141        // UNWRAP: if a `PDF` doesn't have a `PDFSet` there's a bug somewhere
142        PdfSet::new(setname).unwrap_or_else(|_| unreachable!())
143    }
144
145    /// Minimum valid x value for this PDF.
146    #[must_use]
147    pub fn x_min(&mut self) -> f64 {
148        self.ptr.pin_mut().xMin()
149    }
150
151    /// Maximum valid x value for this PDF.
152    #[must_use]
153    pub fn x_max(&mut self) -> f64 {
154        self.ptr.pin_mut().xMax()
155    }
156
157    /// Set whether the PDF will only return positive (definite) values or not.
158    pub fn set_force_positive(&mut self, mode: i32) {
159        self.ptr.pin_mut().setForcePositive(mode);
160    }
161
162    /// Check whether the PDF is set to only return positive (definite) values or not.
163    ///
164    /// This is to avoid overshooting in to negative values when interpolating/extrapolating PDFs
165    /// that sharply decrease towards zero. 0 = unforced, 1 = forced positive, 2 = forced positive
166    /// definite (>= 1e-10).
167    #[must_use]
168    pub fn force_positive(&mut self) -> i32 {
169        self.ptr.pin_mut().forcePositive()
170    }
171
172    /// List of flavours defined by this [`Pdf`] set.
173    #[must_use]
174    pub fn flavors(&self) -> Vec<i32> {
175        self.ptr.flavors().iter().copied().collect()
176    }
177
178    /// Manually set/override the list of flavours defined by this [`Pdf`] set.
179    pub fn set_flavors(&mut self, flavors: &[i32]) {
180        let mut vector = CxxVector::new();
181
182        flavors
183            .iter()
184            .for_each(|&flavor| vector.pin_mut().push(flavor));
185
186        self.ptr.pin_mut().setFlavors(&vector);
187    }
188}
189
190unsafe impl Send for Pdf {}
191unsafe impl Sync for Pdf {}
192
193/// Class for PDF set metadata and manipulation.
194pub struct PdfSet {
195    ptr: UniquePtr<ffi::PDFSet>,
196}
197
198impl fmt::Debug for PdfSet {
199    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
200        // TODO: a PDF set may not have an LHAPDF ID
201        f.debug_struct("PdfSet")
202            .field("lhaid", &self.ptr.lhapdfID())
203            .finish()
204    }
205}
206
207impl PdfSet {
208    /// Constructor from a set name.
209    ///
210    /// # Errors
211    ///
212    /// If the PDF set with the specified name was not found an error is returned.
213    pub fn new(setname: &str) -> Result<Self> {
214        manager::pdfset_new(setname).map(|ptr| Self { ptr })
215    }
216
217    /// Retrieve a metadata string by key name.
218    #[must_use]
219    pub fn entry(&self, key: &str) -> Option<String> {
220        let_cxx_string!(cxx_key = key);
221
222        if self.ptr.has_key(&cxx_key) {
223            Some(self.ptr.get_entry(&cxx_key).to_string_lossy().into_owned())
224        } else {
225            None
226        }
227    }
228
229    /// Get the type of PDF errors in this set (replicas, symmhessian, hessian, custom, etc.).
230    #[must_use]
231    pub fn error_type(&self) -> String {
232        let_cxx_string!(string = "");
233
234        ffi::get_pdfset_error_type(&self.ptr, string.as_mut());
235        string.to_string_lossy().into_owned()
236    }
237
238    /// Make all the PDFs in this set.
239    ///
240    /// # Errors
241    ///
242    /// TODO
243    pub fn mk_pdfs(&self) -> Result<Vec<Pdf>> {
244        let setname = self.name();
245
246        // UNWRAP: if we can't convert a `usize` to an `i32`, then we probably got too many members
247        // indicating a bug somewher
248        (0..i32::try_from(self.ptr.size()).unwrap_or_else(|_| unreachable!()))
249            .map(|member| Pdf::with_setname_and_member(&setname, member))
250            .collect::<Result<Vec<_>>>()
251    }
252
253    /// PDF set name.
254    #[must_use]
255    pub fn name(&self) -> String {
256        let_cxx_string!(setname = "");
257        ffi::pdfset_setname(&self.ptr, setname.as_mut());
258        // UNWRAP: if `setname` contains any non-UTF8 bytes there's an error somewhere else
259        let setname = setname.to_str().unwrap_or_else(|_| unreachable!());
260
261        setname.to_owned()
262    }
263
264    /// Calculate central value and error from vector values with appropriate formulae for this
265    /// set.
266    ///
267    /// Warning: The values vector corresponds to the members of this PDF set and must be ordered
268    /// accordingly.
269    ///
270    /// In the Hessian approach, the central value is the best-fit "values\[0\]" and the uncertainty
271    /// is given by either the symmetric or asymmetric formula using eigenvector PDF sets.
272    ///
273    /// If the PDF set is given in the form of replicas, by default, the central value is given by
274    /// the mean and is not necessarily "values\[0]\" for quantities with a non-linear dependence on
275    /// PDFs, while the uncertainty is given by the standard deviation.
276    ///
277    /// The argument `cl` is used to rescale uncertainties to a particular confidence level (in
278    /// percent); a negative number will rescale to the default CL for this set. The default value
279    /// in LHAPDF is `100*erf(1/sqrt(2))=68.268949213709`, corresponding to 1-sigma uncertainties.
280    ///
281    /// If the PDF set is given in the form of replicas, then the argument `alternative` equal to
282    /// `true` (default in LHAPDF: `false`) will construct a confidence interval from the
283    /// probability distribution of replicas, with the central value given by the median.
284    ///
285    /// For a combined set, a breakdown of the separate PDF and parameter variation uncertainties
286    /// is available. The parameter variation uncertainties are computed from the last `2*n`
287    /// members of the set, with `n` the number of parameters.
288    ///
289    /// # Errors
290    ///
291    /// TODO
292    pub fn uncertainty(
293        &self,
294        values: &[f64],
295        cl: f64,
296        alternative: bool,
297    ) -> Result<PdfUncertainty> {
298        Ok(ffi::pdf_uncertainty(&self.ptr, values, cl, alternative)?)
299    }
300}
301
302#[cfg(test)]
303mod test {
304    use super::*;
305
306    #[test]
307    fn set_verbosity() {
308        super::set_verbosity(0);
309        assert_eq!(verbosity(), 0);
310    }
311
312    #[test]
313    fn check_lookup_pdf() {
314        assert!(matches!(lookup_pdf(324900), Some((name, member))
315            if (name == "NNPDF31_nlo_as_0118_luxqed") && (member == 0)));
316        assert!(matches!(lookup_pdf(324901), Some((name, member))
317            if (name == "NNPDF31_nlo_as_0118_luxqed") && (member == 1)));
318        assert!(matches!(lookup_pdf(-1), None));
319    }
320
321    #[test]
322    fn debug_pdf() -> Result<()> {
323        let pdf = Pdf::with_setname_and_member("NNPDF31_nlo_as_0118_luxqed", 0)?;
324
325        assert_eq!(format!("{:?}", pdf), "Pdf { lhaid: 324900 }");
326
327        Ok(())
328    }
329
330    #[test]
331    fn check_pdf() -> Result<()> {
332        let mut pdf_0 = Pdf::with_setname_and_member("NNPDF31_nlo_as_0118_luxqed", 0)?;
333        let mut pdf_1 = Pdf::with_lhaid(324900)?;
334
335        let value_0 = pdf_0.xfx_q2(2, 0.5, 90.0 * 90.0);
336        let value_1 = pdf_1.xfx_q2(2, 0.5, 90.0 * 90.0);
337
338        assert_ne!(value_0, 0.0);
339        assert_eq!(value_0, value_1);
340
341        let value_0 = pdf_0.alphas_q2(90.0 * 90.0);
342        let value_1 = pdf_1.alphas_q2(90.0 * 90.0);
343
344        assert_ne!(value_0, 0.0);
345        assert_eq!(value_0, value_1);
346
347        assert_eq!(
348            Pdf::with_setname_and_member("NNPDF31_nlo_as_0118_luxqed", 10000)
349                .unwrap_err()
350                .to_string(),
351            "PDF NNPDF31_nlo_as_0118_luxqed/10000 is out of the member range of set NNPDF31_nlo_as_0118_luxqed"
352        );
353
354        assert_eq!(
355            Pdf::with_lhaid(0).unwrap_err().to_string(),
356            "did not find PDF with LHAID = 0"
357        );
358
359        assert_eq!(pdf_0.x_min(), 1e-9);
360        assert_eq!(pdf_0.x_max(), 1.0);
361        assert_eq!(pdf_1.x_min(), 1e-9);
362        assert_eq!(pdf_1.x_max(), 1.0);
363
364        Ok(())
365    }
366
367    #[test]
368    fn check_setname_and_nmem() -> Result<()> {
369        let pdf_0 = Pdf::with_setname_and_member("NNPDF31_nlo_as_0118_luxqed", 1)?;
370        let pdf_1 = Pdf::with_setname_and_nmem("NNPDF31_nlo_as_0118_luxqed/1")?;
371
372        let value_0 = pdf_0.xfx_q2(2, 0.5, 90.0 * 90.0);
373        let value_1 = pdf_1.xfx_q2(2, 0.5, 90.0 * 90.0);
374
375        assert_ne!(value_0, 0.0);
376        assert_eq!(value_0, value_1);
377
378        let value_0 = pdf_0.alphas_q2(90.0 * 90.0);
379        let value_1 = pdf_1.alphas_q2(90.0 * 90.0);
380
381        assert_ne!(value_0, 0.0);
382        assert_eq!(value_0, value_1);
383
384        assert_eq!(
385            Pdf::with_setname_and_nmem("foobar/0")
386                .unwrap_err()
387                .to_string(),
388            "Info file not found for PDF set 'foobar'"
389        );
390
391        assert_eq!(
392            Pdf::with_setname_and_nmem("NNPDF31_nlo_as_0118_luxqed/x")
393                .unwrap_err()
394                .to_string(),
395            "problem while parsing member index = x: 'invalid digit found in string'"
396        );
397
398        Ok(())
399    }
400
401    #[test]
402    fn check_pdf_set() -> Result<()> {
403        let pdf_set = PdfSet::new("NNPDF31_nlo_as_0118_luxqed")?;
404
405        assert!(matches!(pdf_set.entry("Particle"), Some(value) if value == "2212"));
406        assert!(matches!(pdf_set.entry("Flavors"), Some(value)
407            if value == "[-5, -4, -3, -2, -1, 21, 1, 2, 3, 4, 5, 22]"));
408        assert_eq!(pdf_set.entry("idontexist"), None);
409
410        assert_eq!(pdf_set.error_type(), "replicas");
411        assert_eq!(pdf_set.name(), "NNPDF31_nlo_as_0118_luxqed");
412
413        assert_eq!(
414            PdfSet::new("IDontExist").unwrap_err().to_string(),
415            "Info file not found for PDF set 'IDontExist'"
416        );
417
418        assert_eq!(pdf_set.mk_pdfs().unwrap().len(), 101);
419
420        let uncertainty = pdf_set.uncertainty(&[0.0; 101], 68.268949213709, false)?;
421
422        assert_eq!(uncertainty.central, 0.0);
423        assert_eq!(uncertainty.central, 0.0);
424        assert_eq!(uncertainty.errplus, 0.0);
425        assert_eq!(uncertainty.errminus, 0.0);
426        assert_eq!(uncertainty.errsymm, 0.0);
427        //assert_eq!(uncertainty.scale, 1.0);
428        assert_eq!(uncertainty.errplus_pdf, 0.0);
429        assert_eq!(uncertainty.errminus_pdf, 0.0);
430        assert_eq!(uncertainty.errsymm_pdf, 0.0);
431        assert_eq!(uncertainty.err_par, 0.0);
432
433        Ok(())
434    }
435
436    #[test]
437    fn debug_pdf_set() -> Result<()> {
438        let pdf_set = PdfSet::new("NNPDF31_nlo_as_0118_luxqed")?;
439
440        assert_eq!(format!("{:?}", pdf_set), "PdfSet { lhaid: 324900 }");
441
442        Ok(())
443    }
444
445    #[test]
446    fn check_pdf_pdfset() -> Result<()> {
447        let pdf_set0 = PdfSet::new("NNPDF31_nlo_as_0118_luxqed")?;
448        let pdf_set1 = Pdf::with_setname_and_member("NNPDF31_nlo_as_0118_luxqed", 0)?.set();
449
450        assert_eq!(pdf_set0.entry("Particle"), pdf_set1.entry("Particle"));
451        assert_eq!(pdf_set0.entry("NumMembers"), pdf_set1.entry("NumMembers"));
452
453        Ok(())
454    }
455
456    #[test]
457    fn force_positive() -> Result<()> {
458        let mut pdf = Pdf::with_setname_and_member("NNPDF31_nlo_as_0118_luxqed", 1)?;
459
460        assert_eq!(pdf.force_positive(), 0);
461
462        pdf.set_force_positive(1);
463        assert_eq!(pdf.force_positive(), 1);
464
465        Ok(())
466    }
467
468    #[test]
469    fn set_flavors() {
470        let mut pdf = Pdf::with_setname_and_member("NNPDF31_nlo_as_0118_luxqed", 0).unwrap();
471
472        assert_eq!(pdf.flavors(), &[-5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 21, 22]);
473
474        pdf.set_flavors(&[-5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 21]);
475
476        assert_eq!(pdf.flavors(), &[-5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 21]);
477    }
478
479    #[test]
480    fn download_pdf_set() {
481        let _ = Pdf::with_setname_and_member("CT10", 0).unwrap();
482    }
483}