pineappl/
fk_table.rs

1//! Provides the [`FkTable`] type.
2
3use super::boc::{Channel, Kinematics, Order};
4use super::convolutions::ConvolutionCache;
5use super::error::{Error, Result};
6use super::grid::{Grid, GridOptFlags};
7use super::pids::{OptRules, PidBasis};
8use super::subgrid::{self, EmptySubgridV1, Subgrid};
9use ndarray::{s, ArrayD};
10use std::collections::BTreeMap;
11use std::fmt::{self, Display, Formatter};
12use std::iter;
13use std::str::FromStr;
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 convolution functions at a single factorization and
19///   fragmentation scale given by [`FkTable::fac0`] and [`FkTable::frg0`], respectively.
20/// - all subgrids share the same `x` grid. See [`FkTable::x_grid`].
21/// - the channel definitions are *simple*, meaning that every entry consists of a single tuple of
22///   partons with trivial factor `1.0`, and all tuples are distinct from each other. See
23///   [`Grid::channels`].
24/// - the FK table's grid contains only a single [`Order`], whose exponents are all zero.
25#[repr(transparent)]
26pub struct FkTable {
27    grid: Grid,
28}
29
30/// The optimization assumptions for an [`FkTable`], needed for [`FkTable::optimize`]. Since FK
31/// tables are typically stored at very small `Q2 = Q0`, the PDFs `f(x,Q0)` of heavy quarks are
32/// typically set to zero at this scale or set to the same value as their anti-quark PDF. This is
33/// used to optimize the size of FK tables.
34#[repr(C)]
35#[derive(Debug, Clone, Copy, Eq, PartialEq)]
36pub enum FkAssumptions {
37    /// All quark PDFs are non-zero at the FK table scale and completely independent.
38    Nf6Ind,
39    /// Like [`Nf6Ind`](Self::Nf6Ind), but the PDFs of top and anti-top quarks are the same at FK
40    /// table scale.
41    Nf6Sym,
42    /// Like [`Nf6Ind`](Self::Nf6Ind), but the PDFs of top and anti-top quarks are zero at FK table
43    /// scale.
44    Nf5Ind,
45    /// Like [`Nf5Ind`](Self::Nf5Ind), but the PDFs of bottom and anti-bottom quarks are the same
46    /// at FK table scale.
47    Nf5Sym,
48    /// Like [`Nf5Ind`](Self::Nf5Ind), but the PDFs of bottom and anti-bottom quarks are zero at FK
49    /// table scale.
50    Nf4Ind,
51    /// Like [`Nf4Ind`](Self::Nf4Ind), but the PDFs of charm and anti-charm quarks are the same at
52    /// FK table scale. PDF sets that make this assumption are NNPDF4.0 and NNPDF3.1 at fitting
53    /// scale.
54    Nf4Sym,
55    /// Like [`Nf4Ind`](Self::Nf4Ind), but the PDFs of charm and anti-charm quarks are zero at FK
56    /// table scale. PDF sets that make this assumption are MSHT20 and NNPDF3.0 at fitting scale.
57    Nf3Ind,
58    /// Like [`Nf3Ind`](Self::Nf3Ind), but the PDFs of strange and anti-strange are the same at FK
59    /// table scale. A PDF set that makes this assumption is CT18 at fitting scale.
60    Nf3Sym,
61}
62
63impl Display for FkAssumptions {
64    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
65        write!(
66            f,
67            "{}",
68            match self {
69                Self::Nf6Ind => "Nf6Ind",
70                Self::Nf6Sym => "Nf6Sym",
71                Self::Nf5Ind => "Nf5Ind",
72                Self::Nf5Sym => "Nf5Sym",
73                Self::Nf4Ind => "Nf4Ind",
74                Self::Nf4Sym => "Nf4Sym",
75                Self::Nf3Ind => "Nf3Ind",
76                Self::Nf3Sym => "Nf3Sym",
77            }
78        )
79    }
80}
81
82impl FromStr for FkAssumptions {
83    type Err = Error;
84
85    fn from_str(s: &str) -> Result<Self> {
86        Ok(match s {
87            "Nf6Ind" => Self::Nf6Ind,
88            "Nf6Sym" => Self::Nf6Sym,
89            "Nf5Ind" => Self::Nf5Ind,
90            "Nf5Sym" => Self::Nf5Sym,
91            "Nf4Ind" => Self::Nf4Ind,
92            "Nf4Sym" => Self::Nf4Sym,
93            "Nf3Ind" => Self::Nf3Ind,
94            "Nf3Sym" => Self::Nf3Sym,
95            _ => {
96                return Err(Error::General(format!(
97                    "unknown variant for FkAssumptions: {s}",
98                )));
99            }
100        })
101    }
102}
103
104impl FkTable {
105    /// Returns the [`Grid`] object for this `FkTable`.
106    #[must_use]
107    pub const fn grid(&self) -> &Grid {
108        &self.grid
109    }
110
111    // TODO: when trying to convert the following function to `const` as per clippy's suggestion,
112    // the compiler errors out with: 'the destructor for this type cannot be evaluated in constant
113    // functions'
114
115    /// Converts the `FkTable` back to a [`Grid`].
116    #[must_use]
117    pub fn into_grid(self) -> Grid {
118        self.grid
119    }
120
121    /// Return the FK-table represented as an n-dimensional array indexed by `bin`, `channel`
122    /// and parton fractions, in this order.
123    ///
124    /// # Panics
125    ///
126    /// TODO
127    #[must_use]
128    pub fn table(&self) -> ArrayD<f64> {
129        let x_grid = self.x_grid();
130
131        let mut dim = vec![self.grid.bwfl().len(), self.grid.channels().len()];
132        dim.extend(iter::repeat(x_grid.len()).take(self.grid.convolutions().len()));
133        let mut idx = vec![0; dim.len()];
134        let mut result = ArrayD::zeros(dim);
135
136        for ((_, bin, channel), subgrid) in
137            self.grid().subgrids().indexed_iter().filter(|(_, subgrid)|
138                // skip empty subgrids, because they have no node values
139                !subgrid.is_empty())
140        {
141            let indices: Vec<Vec<_>> = self
142                .grid
143                .convolutions()
144                .iter()
145                .enumerate()
146                .map(|(index, _)| {
147                    subgrid
148                        .node_values()
149                        .iter()
150                        .zip(self.grid.kinematics())
151                        .find_map(|(node_values, kin)| {
152                            matches!(kin, Kinematics::X(i) if *i == index).then(|| {
153                                node_values
154                                    .iter()
155                                    .map(|&s| {
156                                        x_grid
157                                            .iter()
158                                            .position(|&x| subgrid::node_value_eq(s, x))
159                                            // UNWRAP: must be guaranteed by the grid constructor
160                                            .unwrap()
161                                    })
162                                    .collect()
163                            })
164                        })
165                        // UNWRAP: must be guaranteed by the grid constructor
166                        .unwrap()
167                })
168                .collect();
169
170            for (index, value) in subgrid.indexed_iter() {
171                assert_eq!(index[0], 0);
172                idx[0] = bin;
173                idx[1] = channel;
174                for i in 2..result.shape().len() {
175                    idx[i] = indices[i - 2][index[i - 1]];
176                }
177                result[idx.as_slice()] = value;
178            }
179        }
180
181        result
182    }
183
184    /// Return the channel definition for this `FkTable`. All factors are `1.0`.
185    #[must_use]
186    pub fn channels(&self) -> Vec<Vec<i32>> {
187        self.grid
188            .channels()
189            .iter()
190            .map(|entry| entry.entry()[0].0.clone())
191            .collect()
192    }
193
194    /// Return the squared factorization scale.
195    #[must_use]
196    pub fn fac0(&self) -> Option<f64> {
197        let fac1 = self.grid.evolve_info(&[true]).fac1;
198
199        if let [fac0] = fac1[..] {
200            Some(fac0)
201        } else {
202            // every `FkTable` has either a single factorization scale or none
203            assert!(fac1.is_empty());
204
205            None
206        }
207    }
208
209    /// Return the squared fragmentation scale.
210    #[must_use]
211    pub fn frg0(&self) -> Option<f64> {
212        let frg1 = self.grid.evolve_info(&[true]).frg1;
213
214        if let [frg0] = frg1[..] {
215            Some(frg0)
216        } else {
217            // every `FkTable` has either a single fragmentation scale or none
218            assert!(frg1.is_empty());
219
220            None
221        }
222    }
223
224    /// Return the metadata of this FK-table.
225    pub fn metadata_mut(&mut self) -> &mut BTreeMap<String, String> {
226        self.grid.metadata_mut()
227    }
228
229    /// Returns the x grid that all subgrids for all hadronic initial states share.
230    #[must_use]
231    pub fn x_grid(&self) -> Vec<f64> {
232        self.grid.evolve_info(&[true]).x1
233    }
234
235    /// Convolve the FK-table. This method has fewer arguments than [`Grid::convolve`], because
236    /// FK-tables have all orders merged together and do not support scale variations.
237    pub fn convolve(
238        &self,
239        convolution_cache: &mut ConvolutionCache,
240        bin_indices: &[usize],
241        channel_mask: &[bool],
242    ) -> Vec<f64> {
243        self.grid.convolve(
244            convolution_cache,
245            &[],
246            bin_indices,
247            channel_mask,
248            &[(1.0, 1.0, 1.0)],
249        )
250    }
251
252    /// Rotate the FK Table into the specified basis.
253    pub fn rotate_pid_basis(&mut self, pid_basis: PidBasis) {
254        self.grid.rotate_pid_basis(pid_basis);
255        self.grid.split_channels();
256        self.grid.merge_channel_factors();
257        self.grid.optimize_using(GridOptFlags::all());
258    }
259
260    /// Optimize the size of this FK-table by throwing away heavy quark flavors assumed to be zero
261    /// at the FK-table's scales and calling [`Grid::optimize`].
262    pub fn optimize(&mut self, assumptions: FkAssumptions) {
263        let OptRules(sum, delete) = self.grid.pid_basis().opt_rules(assumptions);
264
265        for idx in 0..self.grid.channels().len() {
266            let &[(ref pids, factor)] = self.grid.channels()[idx].entry() else {
267                // every FK-table must have a trivial channel definition
268                unreachable!()
269            };
270            let mut pids = pids.clone();
271
272            for pid in &mut pids {
273                if delete.contains(pid) {
274                    for subgrid in self.grid.subgrids_mut().slice_mut(s![.., .., idx]) {
275                        *subgrid = EmptySubgridV1.into();
276                    }
277                } else if let Some(replace) = sum
278                    .iter()
279                    .find_map(|&(search, replace)| (*pid == search).then_some(replace))
280                {
281                    *pid = replace;
282                }
283            }
284
285            self.grid.channels_mut()[idx] = Channel::new(vec![(pids, factor)]);
286        }
287
288        self.grid.optimize();
289
290        // store the assumption so that we can check it later on
291        self.grid
292            .metadata_mut()
293            .insert("fk_assumptions".to_owned(), assumptions.to_string());
294    }
295}
296
297impl TryFrom<Grid> for FkTable {
298    type Error = Error;
299
300    fn try_from(grid: Grid) -> Result<Self> {
301        let mut muf2 = -1.0;
302
303        if grid.orders()
304            != [Order {
305                alphas: 0,
306                alpha: 0,
307                logxir: 0,
308                logxif: 0,
309                logxia: 0,
310            }]
311        {
312            return Err(Error::General("multiple orders detected".to_owned()));
313        }
314
315        for subgrid in grid.subgrids() {
316            if subgrid.is_empty() {
317                continue;
318            }
319
320            let [fac] = grid
321                .scales()
322                .fac
323                .calc(&subgrid.node_values(), grid.kinematics())[..]
324            else {
325                return Err(Error::General("multiple scales detected".to_owned()));
326            };
327
328            if muf2 < 0.0 {
329                muf2 = fac;
330            } else if !subgrid::node_value_eq(muf2, fac) {
331                return Err(Error::General("multiple scales detected".to_owned()));
332            }
333        }
334
335        for channel in grid.channels() {
336            let entry = channel.entry();
337
338            if entry.len() != 1 || !subgrid::node_value_eq(entry[0].1, 1.0) {
339                return Err(Error::General(
340                    "complicated channel function detected".to_owned(),
341                ));
342            }
343        }
344
345        if (1..grid.channels().len())
346            .any(|i| grid.channels()[i..].contains(&grid.channels()[i - 1]))
347        {
348            return Err(Error::General(
349                "complicated channel function detected".to_owned(),
350            ));
351        }
352
353        Ok(Self { grid })
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    #[test]
362    fn fk_assumptions_try_from() {
363        assert_eq!(
364            FkAssumptions::from_str("Nf6Ind").unwrap(),
365            FkAssumptions::Nf6Ind
366        );
367        assert_eq!(
368            FkAssumptions::from_str("Nf6Sym").unwrap(),
369            FkAssumptions::Nf6Sym
370        );
371        assert_eq!(
372            FkAssumptions::from_str("Nf5Ind").unwrap(),
373            FkAssumptions::Nf5Ind
374        );
375        assert_eq!(
376            FkAssumptions::from_str("Nf5Sym").unwrap(),
377            FkAssumptions::Nf5Sym
378        );
379        assert_eq!(
380            FkAssumptions::from_str("Nf4Ind").unwrap(),
381            FkAssumptions::Nf4Ind
382        );
383        assert_eq!(
384            FkAssumptions::from_str("Nf4Sym").unwrap(),
385            FkAssumptions::Nf4Sym
386        );
387        assert_eq!(
388            FkAssumptions::from_str("Nf3Ind").unwrap(),
389            FkAssumptions::Nf3Ind
390        );
391        assert_eq!(
392            FkAssumptions::from_str("Nf3Sym").unwrap(),
393            FkAssumptions::Nf3Sym
394        );
395        assert_eq!(
396            FkAssumptions::from_str("XXXXXX").unwrap_err().to_string(),
397            "unknown variant for FkAssumptions: XXXXXX"
398        );
399    }
400
401    #[test]
402    fn fk_assumptions_display() {
403        assert_eq!(format!("{}", FkAssumptions::Nf6Ind), "Nf6Ind");
404        assert_eq!(format!("{}", FkAssumptions::Nf6Sym), "Nf6Sym");
405        assert_eq!(format!("{}", FkAssumptions::Nf5Ind), "Nf5Ind");
406        assert_eq!(format!("{}", FkAssumptions::Nf5Sym), "Nf5Sym");
407        assert_eq!(format!("{}", FkAssumptions::Nf4Ind), "Nf4Ind");
408        assert_eq!(format!("{}", FkAssumptions::Nf4Sym), "Nf4Sym");
409        assert_eq!(format!("{}", FkAssumptions::Nf3Ind), "Nf3Ind");
410        assert_eq!(format!("{}", FkAssumptions::Nf3Sym), "Nf3Sym");
411    }
412}