pineappl/
fk_table.rs

1//! Provides the [`FkTable`] type.
2
3use super::boc::Order;
4use super::convolutions::{Convolution, LumiCache};
5use super::grid::{Grid, GridError};
6use super::subgrid::Subgrid;
7use float_cmp::approx_eq;
8use ndarray::Array4;
9use std::collections::HashMap;
10use std::fmt::{self, Display, Formatter};
11use std::io::Write;
12use std::str::FromStr;
13use thiserror::Error;
14
15/// Structure implementing FK tables. These are special [`Grid`]s, for which the following
16/// additional guarantees are given:
17///
18/// - all subgrids of the grid evaluate the PDFs at a single factorization scale given by
19///   [`FkTable::muf2`].
20/// - all subgrids, for both hadronic initial states (if both initial states are hadronic), share
21///   the same `x` grid. See [`FkTable::x_grid`].
22/// - the channel definitions are *simple*, meaning that every entry consists of a single pair of
23///   partons with trivial factor `1.0`, and all tuples are distinct from each other. See
24///   [`Grid::channels`].
25/// - the FK table's grid contains only a single [`Order`], whose exponents are all zero.
26#[repr(transparent)]
27pub struct FkTable {
28    grid: Grid,
29}
30
31/// The error type returned when a conversion of a [`Grid`] to an [`FkTable`] fails.
32#[derive(Debug, Error)]
33pub enum TryFromGridError {
34    /// Error if the grid contains multiple scales instead of a single one.
35    #[error("multiple scales detected")]
36    MultipleScales,
37    /// Error if the channels are not simple.
38    #[error("complicated channel function detected")]
39    InvalidChannel,
40    /// Error if the order of the grid was not a single one with all zeros in the exponents.
41    #[error("multiple orders detected")]
42    NonTrivialOrder,
43}
44
45/// The optimization assumptions for an [`FkTable`], needed for [`FkTable::optimize`]. Since FK
46/// tables are typically stored at very small `Q2 = Q0`, the PDFs `f(x,Q0)` of heavy quarks are
47/// typically set to zero at this scale or set to the same value as their anti-quark PDF. This is
48/// used to optimize the size of FK tables.
49#[derive(Debug, Clone, Copy, Eq, PartialEq)]
50pub enum FkAssumptions {
51    /// All quark PDFs are non-zero at the FK table scale and completely independent.
52    Nf6Ind,
53    /// Like [`Nf6Ind`](Self::Nf6Ind), but the PDFs of top and anti-top quarks are the same at FK
54    /// table scale.
55    Nf6Sym,
56    /// Like [`Nf6Ind`](Self::Nf6Ind), but the PDFs of top and anti-top quarks are zero at FK table
57    /// scale.
58    Nf5Ind,
59    /// Like [`Nf5Ind`](Self::Nf5Ind), but the PDFs of bottom and anti-bottom quarks are the same
60    /// at FK table scale.
61    Nf5Sym,
62    /// Like [`Nf5Ind`](Self::Nf5Ind), but the PDFs of bottom and anti-bottom quarks are zero at FK
63    /// table scale.
64    Nf4Ind,
65    /// Like [`Nf4Ind`](Self::Nf4Ind), but the PDFs of charm and anti-charm quarks are the same at
66    /// FK table scale. PDF sets that make this assumption are NNPDF4.0 and NNPDF3.1 at fitting
67    /// scale.
68    Nf4Sym,
69    /// Like [`Nf4Ind`](Self::Nf4Ind), but the PDFs of charm and anti-charm quarks are zero at FK
70    /// table scale. PDF sets that make this assumption are MSHT20 and NNPDF3.0 at fitting scale.
71    Nf3Ind,
72    /// Like [`Nf3Ind`](Self::Nf3Ind), but the PDFs of strange and anti-strange are the same at FK
73    /// table scale. A PDF set that makes this assumption is CT18 at fitting scale.
74    Nf3Sym,
75}
76
77/// Error type when trying to construct [`FkAssumptions`] with a string.
78#[derive(Debug, Eq, Error, PartialEq)]
79#[error("unknown variant for FkAssumptions: {variant}")]
80pub struct UnknownFkAssumption {
81    variant: String,
82}
83
84impl Display for FkAssumptions {
85    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
86        write!(
87            f,
88            "{}",
89            match self {
90                Self::Nf6Ind => "Nf6Ind",
91                Self::Nf6Sym => "Nf6Sym",
92                Self::Nf5Ind => "Nf5Ind",
93                Self::Nf5Sym => "Nf5Sym",
94                Self::Nf4Ind => "Nf4Ind",
95                Self::Nf4Sym => "Nf4Sym",
96                Self::Nf3Ind => "Nf3Ind",
97                Self::Nf3Sym => "Nf3Sym",
98            }
99        )
100    }
101}
102
103impl FromStr for FkAssumptions {
104    type Err = UnknownFkAssumption;
105
106    fn from_str(s: &str) -> Result<Self, Self::Err> {
107        Ok(match s {
108            "Nf6Ind" => Self::Nf6Ind,
109            "Nf6Sym" => Self::Nf6Sym,
110            "Nf5Ind" => Self::Nf5Ind,
111            "Nf5Sym" => Self::Nf5Sym,
112            "Nf4Ind" => Self::Nf4Ind,
113            "Nf4Sym" => Self::Nf4Sym,
114            "Nf3Ind" => Self::Nf3Ind,
115            "Nf3Sym" => Self::Nf3Sym,
116            _ => {
117                return Err(UnknownFkAssumption {
118                    variant: s.to_owned(),
119                });
120            }
121        })
122    }
123}
124
125impl FkTable {
126    /// Returns the [`Grid`] object for this `FkTable`.
127    #[must_use]
128    pub const fn grid(&self) -> &Grid {
129        &self.grid
130    }
131
132    // TODO: when trying to convert the following function to `const` as per clippy's suggestion,
133    // the compiler errors out with: 'the destructor for this type cannot be evaluated in constant
134    // functions'
135
136    /// Converts the `FkTable` back to a [`Grid`].
137    #[must_use]
138    pub fn into_grid(self) -> Grid {
139        self.grid
140    }
141
142    /// Returns the FK table represented as a four-dimensional array indexed by `bin`, `channel`,
143    /// `x1` and `x2`, in this order.
144    ///
145    /// # Panics
146    ///
147    /// TODO
148    #[must_use]
149    pub fn table(&self) -> Array4<f64> {
150        let has_pdf1 = self.grid.convolutions()[0] != Convolution::None;
151        let has_pdf2 = self.grid.convolutions()[1] != Convolution::None;
152        let x_grid = self.x_grid();
153
154        let mut result = Array4::zeros((
155            self.bins(),
156            self.grid.channels().len(),
157            if has_pdf1 { x_grid.len() } else { 1 },
158            if has_pdf2 { x_grid.len() } else { 1 },
159        ));
160
161        for ((_, bin, channel), subgrid) in self.grid().subgrids().indexed_iter() {
162            let indices1 = if has_pdf1 {
163                subgrid
164                    .x1_grid()
165                    .iter()
166                    .map(|&s| x_grid.iter().position(|&x| approx_eq!(f64, s, x, ulps = 2)))
167                    .collect::<Option<_>>()
168                    .unwrap()
169            } else {
170                vec![0]
171            };
172            let indices2 = if has_pdf2 {
173                subgrid
174                    .x2_grid()
175                    .iter()
176                    .map(|&s| x_grid.iter().position(|&x| approx_eq!(f64, s, x, ulps = 2)))
177                    .collect::<Option<_>>()
178                    .unwrap()
179            } else {
180                vec![0]
181            };
182
183            for ((_, ix1, ix2), value) in subgrid.indexed_iter() {
184                result[[bin, channel, indices1[ix1], indices2[ix2]]] = value;
185            }
186        }
187
188        result
189    }
190
191    /// Returns the number of bins for this `FkTable`.
192    #[must_use]
193    pub fn bins(&self) -> usize {
194        self.grid.bin_info().bins()
195    }
196
197    /// Extract the normalizations for each bin.
198    #[must_use]
199    pub fn bin_normalizations(&self) -> Vec<f64> {
200        self.grid.bin_info().normalizations()
201    }
202
203    /// Extract the number of dimensions for bins.
204    #[must_use]
205    pub fn bin_dimensions(&self) -> usize {
206        self.grid.bin_info().dimensions()
207    }
208
209    /// Extract the left edges of a specific bin dimension.
210    #[must_use]
211    pub fn bin_left(&self, dimension: usize) -> Vec<f64> {
212        self.grid.bin_info().left(dimension)
213    }
214
215    /// Extract the right edges of a specific bin dimension.
216    #[must_use]
217    pub fn bin_right(&self, dimension: usize) -> Vec<f64> {
218        self.grid.bin_info().right(dimension)
219    }
220
221    /// Access meta data
222    #[must_use]
223    pub const fn key_values(&self) -> Option<&HashMap<String, String>> {
224        self.grid.key_values()
225    }
226
227    /// Return the channel definition for this `FkTable`. All factors are `1.0`.
228    #[must_use]
229    pub fn channels(&self) -> Vec<(i32, i32)> {
230        self.grid
231            .channels()
232            .iter()
233            .map(|entry| (entry.entry()[0].0, entry.entry()[0].1))
234            .collect()
235    }
236
237    /// Returns the single `muf2` scale of this `FkTable`.
238    #[must_use]
239    pub fn muf2(&self) -> f64 {
240        if let &[muf2] = &self.grid.evolve_info(&[true]).fac1[..] {
241            muf2
242        } else {
243            // every `FkTable` has only a single factorization scale
244            unreachable!()
245        }
246    }
247
248    /// Returns the x grid that all subgrids for all hadronic initial states share.
249    #[must_use]
250    pub fn x_grid(&self) -> Vec<f64> {
251        self.grid.evolve_info(&[true]).x1
252    }
253
254    /// Propagate write to grid
255    ///
256    /// # Errors
257    ///
258    /// TODO
259    pub fn write(&self, writer: impl Write) -> Result<(), GridError> {
260        self.grid.write(writer)
261    }
262
263    /// Propagate `write_lz4` to `Grid`.
264    ///
265    /// # Errors
266    ///
267    /// See [`Grid::write_lz4`].
268    pub fn write_lz4(&self, writer: impl Write) -> Result<(), GridError> {
269        self.grid.write_lz4(writer)
270    }
271
272    /// Convolve the FK-table. This method has fewer arguments than [`Grid::convolve`], because
273    /// FK-tables have all orders merged together and do not support scale variations.
274    pub fn convolve(
275        &self,
276        lumi_cache: &mut LumiCache,
277        bin_indices: &[usize],
278        channel_mask: &[bool],
279    ) -> Vec<f64> {
280        self.grid
281            .convolve(lumi_cache, &[], bin_indices, channel_mask, &[(1.0, 1.0)])
282    }
283
284    /// Set a metadata key-value pair
285    pub fn set_key_value(&mut self, key: &str, value: &str) {
286        self.grid.set_key_value(key, value);
287    }
288
289    /// Optimizes the storage of FK tables based of assumptions of the PDFs at the FK table's
290    /// scale.
291    ///
292    /// # Panics
293    ///
294    /// TODO
295    pub fn optimize(&mut self, assumptions: FkAssumptions) {
296        let mut add = Vec::new();
297
298        match assumptions {
299            FkAssumptions::Nf6Ind => {
300                // nothing to do here
301            }
302            FkAssumptions::Nf6Sym => {
303                add.push((235, 200));
304            }
305            FkAssumptions::Nf5Ind => {
306                add.extend_from_slice(&[(235, 200), (135, 100)]);
307            }
308            FkAssumptions::Nf5Sym => {
309                add.extend_from_slice(&[(235, 200), (135, 100), (224, 200)]);
310            }
311            FkAssumptions::Nf4Ind => {
312                add.extend_from_slice(&[(235, 200), (135, 100), (224, 200), (124, 100)]);
313            }
314            FkAssumptions::Nf4Sym => {
315                add.extend_from_slice(&[
316                    (235, 200),
317                    (135, 100),
318                    (224, 200),
319                    (124, 100),
320                    (215, 200),
321                ]);
322            }
323            FkAssumptions::Nf3Ind => {
324                add.extend_from_slice(&[
325                    (235, 200),
326                    (135, 100),
327                    (224, 200),
328                    (124, 100),
329                    (215, 200),
330                    (115, 100),
331                ]);
332            }
333            FkAssumptions::Nf3Sym => {
334                add.extend_from_slice(&[
335                    (235, 200),
336                    (135, 100),
337                    (224, 200),
338                    (124, 100),
339                    (215, 200),
340                    (115, 100),
341                    (208, 200),
342                ]);
343            }
344        }
345
346        self.grid.rewrite_channels(&add, &[]);
347
348        // store the assumption so that we can check it later on
349        self.grid
350            .set_key_value("fk_assumptions", &assumptions.to_string());
351        self.grid.optimize();
352    }
353}
354
355impl TryFrom<Grid> for FkTable {
356    type Error = TryFromGridError;
357
358    fn try_from(grid: Grid) -> Result<Self, Self::Error> {
359        let mut muf2 = -1.0;
360
361        if grid.orders()
362            != [Order {
363                alphas: 0,
364                alpha: 0,
365                logxir: 0,
366                logxif: 0,
367            }]
368        {
369            return Err(TryFromGridError::NonTrivialOrder);
370        }
371
372        for subgrid in grid.subgrids() {
373            if subgrid.is_empty() {
374                continue;
375            }
376
377            let mu2_grid = subgrid.mu2_grid();
378
379            if mu2_grid.len() > 1 {
380                return Err(TryFromGridError::MultipleScales);
381            }
382
383            if muf2 < 0.0 {
384                muf2 = mu2_grid[0].fac;
385            } else if muf2 != mu2_grid[0].fac {
386                return Err(TryFromGridError::MultipleScales);
387            }
388        }
389
390        for channel in grid.channels() {
391            let entry = channel.entry();
392
393            if entry.len() != 1 || entry[0].2 != 1.0 {
394                return Err(TryFromGridError::InvalidChannel);
395            }
396        }
397
398        if (1..grid.channels().len())
399            .any(|i| grid.channels()[i..].contains(&grid.channels()[i - 1]))
400        {
401            return Err(TryFromGridError::InvalidChannel);
402        }
403
404        Ok(Self { grid })
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411
412    #[test]
413    fn fk_assumptions_try_from() {
414        assert_eq!(FkAssumptions::from_str("Nf6Ind"), Ok(FkAssumptions::Nf6Ind));
415        assert_eq!(FkAssumptions::from_str("Nf6Sym"), Ok(FkAssumptions::Nf6Sym));
416        assert_eq!(FkAssumptions::from_str("Nf5Ind"), Ok(FkAssumptions::Nf5Ind));
417        assert_eq!(FkAssumptions::from_str("Nf5Sym"), Ok(FkAssumptions::Nf5Sym));
418        assert_eq!(FkAssumptions::from_str("Nf4Ind"), Ok(FkAssumptions::Nf4Ind));
419        assert_eq!(FkAssumptions::from_str("Nf4Sym"), Ok(FkAssumptions::Nf4Sym));
420        assert_eq!(FkAssumptions::from_str("Nf3Ind"), Ok(FkAssumptions::Nf3Ind));
421        assert_eq!(FkAssumptions::from_str("Nf3Sym"), Ok(FkAssumptions::Nf3Sym));
422        assert_eq!(
423            FkAssumptions::from_str("XXXXXX"),
424            Err(UnknownFkAssumption {
425                variant: "XXXXXX".to_owned()
426            })
427        );
428    }
429
430    #[test]
431    fn fk_assumptions_display() {
432        assert_eq!(format!("{}", FkAssumptions::Nf6Ind), "Nf6Ind");
433        assert_eq!(format!("{}", FkAssumptions::Nf6Sym), "Nf6Sym");
434        assert_eq!(format!("{}", FkAssumptions::Nf5Ind), "Nf5Ind");
435        assert_eq!(format!("{}", FkAssumptions::Nf5Sym), "Nf5Sym");
436        assert_eq!(format!("{}", FkAssumptions::Nf4Ind), "Nf4Ind");
437        assert_eq!(format!("{}", FkAssumptions::Nf4Sym), "Nf4Sym");
438        assert_eq!(format!("{}", FkAssumptions::Nf3Ind), "Nf3Ind");
439        assert_eq!(format!("{}", FkAssumptions::Nf3Sym), "Nf3Sym");
440    }
441}