Skip to main content

pineappl/
evolution.rs

1//! Supporting classes and functions for [`Grid::evolve`].
2
3use super::boc::{Channel, Kinematics, Order};
4use super::convolutions::ConvType;
5use super::error::{Error, Result};
6use super::grid::Grid;
7use super::packed_array::PackedArray;
8use super::pids::PidBasis;
9use super::subgrid::{self, ImportSubgridV1, Subgrid, SubgridEnum};
10use float_cmp::approx_eq;
11use itertools::izip;
12use itertools::Itertools;
13use ndarray::linalg;
14use ndarray::{
15    s, Array1, Array2, Array3, ArrayD, ArrayView1, ArrayView4, ArrayViewD, ArrayViewMutD, Axis,
16    Ix1, Ix2,
17};
18use rayon::iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator};
19use std::iter;
20
21/// This structure captures the information needed to create an evolution kernel operator (EKO) for
22/// a specific [`Grid`].
23pub struct EvolveInfo {
24    /// Squared factorization scales of the `Grid`.
25    pub fac1: Vec<f64>,
26    /// Squared fragmentation scales of the `Grid`.
27    pub frg1: Vec<f64>,
28    /// Particle identifiers of the `Grid`.
29    pub pids1: Vec<i32>,
30    /// `x`-grid coordinates of the `Grid`.
31    pub x1: Vec<f64>,
32    /// Renormalization scales of the `Grid`.
33    pub ren1: Vec<f64>,
34}
35
36/// Information about the evolution kernel operator slice (EKO) passed to [`Grid::evolve`] as
37/// `operator`, which is used to convert a [`Grid`] into an [`FkTable`](super::fk_table::FkTable).
38/// The dimensions of the EKO must correspond to the values given in [`fac1`](Self::fac1),
39/// [`pids0`](Self::pids0), [`x0`](Self::x0), [`pids1`](Self::pids1) and [`x1`](Self::x1), exactly
40/// in this order. Members with a `1` are defined at the squared factorization scale given as
41/// `fac1` (often called process scale) and are found in the [`Grid`] that [`Grid::evolve`] is
42/// called with. Members with a `0` are defined at the squared factorization scale
43/// [`fac0`](Self::fac0) (often called fitting scale or starting scale) and are found in the
44/// `FkTable` resulting from [`Grid::evolve`].
45///
46/// The EKO slice may convert a `Grid` from a basis given by the particle identifiers `pids1` to a
47/// possibly different basis given by `pids0`. This basis must also be identified using
48/// [`pid_basis`](Self::pid_basis), which tells
49/// [`FkTable::convolve`](super::fk_table::FkTable::convolve) how to perform a convolution.
50///
51/// [`Grid::evolve`]: super::grid::Grid::evolve
52#[derive(Clone)]
53pub struct OperatorSliceInfo {
54    /// Squared factorization/fragmentation scale of the `FkTable`.
55    pub fac0: f64,
56    /// Particle identifiers of the `FkTable`.
57    pub pids0: Vec<i32>,
58    /// `x`-grid coordinates of the `FkTable`
59    pub x0: Vec<f64>,
60    /// Squared factorization/fragmentation scale of the slice of `Grid` that should be evolved.
61    pub fac1: f64,
62    /// Particle identifiers of the `Grid`. If the `Grid` contains more particle identifiers than
63    /// given here, the contributions of them are silently ignored.
64    pub pids1: Vec<i32>,
65    /// `x`-grid coordinates of the `Grid`.
66    pub x1: Vec<f64>,
67
68    /// Particle ID basis for `FkTable`.
69    pub pid_basis: PidBasis,
70    /// Type of convolution which this operator evolves. This also determines whether
71    /// [`Self::fac0`] and [`Self::fac1`] is a factorization ([`ConvType::UnpolPDF`] or
72    /// [`ConvType::PolPDF`]) or a fragmentation ([`ConvType::UnpolFF`] or [`ConvType::PolFF`])
73    /// scale.
74    pub conv_type: ConvType,
75}
76
77/// A mapping of squared renormalization scales in `ren1` to strong couplings in `alphas`. The
78/// ordering of both members defines the mapping.
79pub struct AlphasTable {
80    /// Renormalization scales of the `Grid`.
81    pub ren1: Vec<f64>,
82    /// Strong couplings corresponding to the order given in [`ren1`](Self::ren1).
83    pub alphas: Vec<f64>,
84}
85
86impl AlphasTable {
87    /// Create an `AlphasTable` for `grid`, varying the renormalization scale by `xir` for the
88    /// strong couplings given by `alphas`. The only argument of `alphas` must be the squared
89    /// renormalization scale.
90    pub fn from_grid(grid: &Grid, xir: f64, alphas: &dyn Fn(f64) -> f64) -> Self {
91        let mut ren1: Vec<_> = grid
92            .subgrids()
93            .iter()
94            .flat_map(|subgrid| {
95                grid.scales()
96                    .ren
97                    .calc(&subgrid.node_values(), grid.kinematics())
98                    .into_owned()
99                    .into_iter()
100                    .map(|ren| xir * xir * ren)
101            })
102            .collect();
103        ren1.sort_by(f64::total_cmp);
104        ren1.dedup_by(subgrid::node_value_eq_ref_mut);
105        let ren1 = ren1;
106        let alphas: Vec<_> = ren1.iter().map(|&mur2| alphas(mur2)).collect();
107
108        Self { ren1, alphas }
109    }
110}
111
112fn gluon_has_pid_zero(grid: &Grid) -> bool {
113    // if there are any PID zero particles ...
114    grid.channels()
115        .iter()
116        .any(|entry| entry.entry().iter().any(|(pids, _)| pids.contains(&0)))
117        // and if the particle IDs are encoded using PDG MC IDs
118        && *grid.pid_basis() == PidBasis::Pdg
119}
120
121type Pid01IndexTuples = Vec<(usize, usize)>;
122type Pid01Tuples = Vec<(i32, i32)>;
123
124fn pid_slices(
125    operator: &ArrayView4<f64>,
126    info: &OperatorSliceInfo,
127    gluon_has_pid_zero: bool,
128    pid1_nonzero: &dyn Fn(i32) -> bool,
129) -> Result<(Pid01IndexTuples, Pid01Tuples)> {
130    // list of all non-zero PID indices
131    let pid_indices: Vec<_> = (0..operator.dim().2)
132        .cartesian_product(0..operator.dim().0)
133        .filter(|&(pid0_idx, pid1_idx)| {
134            // 1) at least one element of the operator must be non-zero, and 2) the pid must be
135            // contained in some channel
136            operator
137                .slice(s![pid1_idx, .., pid0_idx, ..])
138                .iter()
139                .any(|&value| value != 0.0)
140                && pid1_nonzero(if gluon_has_pid_zero && info.pids1[pid1_idx] == 21 {
141                    0
142                } else {
143                    info.pids1[pid1_idx]
144                })
145        })
146        .collect();
147
148    if pid_indices.is_empty() {
149        return Err(Error::General(
150            "no non-zero operator found; result would be an empty FkTable".to_owned(),
151        ));
152    }
153
154    // list of all non-zero (pid0, pid1) combinations
155    let pids = pid_indices
156        .iter()
157        .map(|&(pid0_idx, pid1_idx)| {
158            (
159                info.pids0[pid0_idx],
160                if gluon_has_pid_zero && info.pids1[pid1_idx] == 21 {
161                    0
162                } else {
163                    info.pids1[pid1_idx]
164                },
165            )
166        })
167        .collect();
168
169    Ok((pid_indices, pids))
170}
171
172fn operator_slices(
173    operator: &ArrayView4<f64>,
174    info: &OperatorSliceInfo,
175    pid_indices: &[(usize, usize)],
176    x1: &[f64],
177) -> Result<Vec<Array2<f64>>> {
178    // permutation between the grid x values and the operator x1 values
179    let x1_indices: Vec<_> = x1
180        .iter()
181        .map(|&x1p| {
182            info.x1
183                .iter()
184                .position(|&x1| subgrid::node_value_eq(x1p, x1))
185                .ok_or_else(|| Error::General(format!("no operator for x = {x1p} found")))
186        })
187        // TODO: use `try_collect` once stabilized
188        .collect::<Result<_>>()?;
189
190    // create the corresponding operators accessible in the form [muf2, x0, x1]
191    let operators: Vec<_> = pid_indices
192        .iter()
193        .map(|&(pid0_idx, pid1_idx)| {
194            operator
195                .slice(s![pid1_idx, .., pid0_idx, ..])
196                .select(Axis(0), &x1_indices)
197                .reversed_axes()
198                .as_standard_layout()
199                .into_owned()
200        })
201        .collect();
202
203    Ok(operators)
204}
205
206type X1aX1bOpDTuple = (Vec<Vec<f64>>, Option<ArrayD<f64>>);
207
208fn ndarray_from_subgrid_orders_slice(
209    grid: &Grid,
210    fac1: f64,
211    kinematics: &[Kinematics],
212    subgrids: &ArrayView1<SubgridEnum>,
213    orders: &[Order],
214    order_mask: &[bool],
215    (xir, xif, xia): (f64, f64, f64),
216    alphas_table: &AlphasTable,
217) -> Result<X1aX1bOpDTuple> {
218    // TODO: remove these assumptions from the following code
219    assert_eq!(grid.kinematics()[0], Kinematics::Scale(0));
220    let x_start = grid
221        .kinematics()
222        .iter()
223        .position(|kin| matches!(kin, Kinematics::X(_)))
224        // UNWRAP: there is always at least one X
225        .unwrap();
226
227    // create a Vec of all x values for each dimension
228    let mut x1n: Vec<_> = kinematics
229        .iter()
230        .enumerate()
231        .filter_map(|(idx, kin)| matches!(kin, Kinematics::X(_)).then_some(idx))
232        .map(|kin_idx| {
233            subgrids
234                .iter()
235                .enumerate()
236                .filter(|&(ord_idx, subgrid)| {
237                    order_mask.get(ord_idx).copied().unwrap_or(true)
238                        // TODO: empty subgrids don't have node values
239                        && !subgrid.is_empty()
240                })
241                .flat_map(|(_, subgrid)| subgrid.node_values()[kin_idx].clone())
242                .collect::<Vec<_>>()
243        })
244        .collect();
245
246    for x1 in &mut x1n {
247        x1.sort_by(f64::total_cmp);
248        x1.dedup_by(subgrid::node_value_eq_ref_mut);
249    }
250
251    let dim: Vec<_> = x1n.iter().map(Vec::len).collect();
252    let mut array = ArrayD::<f64>::zeros(dim);
253    let mut zero = true;
254    let mut x1_idx = vec![0; grid.convolutions().len()];
255
256    // for the same bin and channel, sum subgrids of different orders, using the right couplings
257    for (subgrid, order) in subgrids
258        .iter()
259        .zip(orders.iter())
260        .zip(order_mask.iter().chain(iter::repeat(&true)))
261        .filter_map(|((subgrid, order), &enabled)| {
262            (enabled && !subgrid.is_empty()).then_some((subgrid, order))
263        })
264    {
265        let mut logs = 1.0;
266
267        if order.logxir > 0 {
268            if approx_eq!(f64, xir, 1.0, ulps = 4) {
269                continue;
270            }
271
272            logs *= (xir * xir).ln();
273        }
274
275        if order.logxif > 0 {
276            if approx_eq!(f64, xif, 1.0, ulps = 4) {
277                continue;
278            }
279
280            logs *= (xif * xif).ln();
281        }
282
283        if order.logxia > 0 {
284            if approx_eq!(f64, xia, 1.0, ulps = 4) {
285                continue;
286            }
287
288            logs *= (xia * xia).ln();
289        }
290
291        let node_values = subgrid.node_values();
292        let scale_dims: Vec<_> = grid
293            .kinematics()
294            .iter()
295            .zip(&node_values)
296            .filter_map(|(kin, node_values)| {
297                matches!(kin, Kinematics::Scale(_)).then_some(node_values.len())
298            })
299            .collect();
300
301        let x1_indices: Vec<Vec<_>> = kinematics
302            .iter()
303            .enumerate()
304            .filter_map(|(idx, kin)| matches!(kin, Kinematics::X(_)).then_some(idx))
305            .zip(&x1n)
306            .map(|(kin_idx, x1)| {
307                node_values[kin_idx]
308                    .iter()
309                    .map(|&xs| {
310                        x1.iter()
311                            .position(|&x| subgrid::node_value_eq(x, xs))
312                            // UNWRAP: `x1n` contains all x-values, so we must find each `x`
313                            .unwrap()
314                    })
315                    .collect()
316            })
317            .collect();
318
319        let rens = grid.scales().ren.calc(&node_values, grid.kinematics());
320        let facs = grid.scales().fac.calc(&node_values, grid.kinematics());
321
322        for (indices, value) in subgrid.indexed_iter() {
323            // TODO: implement evolution for non-zero fragmentation scales
324            let ren = rens[grid.scales().ren.idx(&indices, &scale_dims)];
325            let fac = facs[grid.scales().fac.idx(&indices, &scale_dims)];
326
327            if !subgrid::node_value_eq(xif * xif * fac, fac1) {
328                continue;
329            }
330
331            let mur2 = xir * xir * ren;
332
333            let als = if order.alphas == 0 {
334                1.0
335            } else if let Some(alphas) = alphas_table
336                .ren1
337                .iter()
338                .zip(alphas_table.alphas.iter())
339                .find_map(|(&ren1, &alphas)| subgrid::node_value_eq(ren1, mur2).then_some(alphas))
340            {
341                alphas.powi(order.alphas.into())
342            } else {
343                return Err(Error::General(format!("no alphas for mur2 = {mur2} found")));
344            };
345
346            zero = false;
347
348            for (i, &index) in indices.iter().skip(x_start).enumerate() {
349                x1_idx[i] = x1_indices[i][index];
350            }
351
352            array[x1_idx.as_slice()] += als * logs * value;
353        }
354    }
355
356    Ok((x1n, (!zero).then_some(array)))
357}
358
359pub(crate) fn evolve_slice(
360    grid: &Grid,
361    operators: &[ArrayView4<f64>],
362    infos: &[OperatorSliceInfo],
363    scale_values: &[f64],
364    order_mask: &[bool],
365    xi: (f64, f64, f64),
366    alphas_table: &AlphasTable,
367) -> Result<(Array3<SubgridEnum>, Vec<Channel>)> {
368    let gluon_has_pid_zero = gluon_has_pid_zero(grid);
369
370    // TODO: implement matching of different scales for different EKOs
371    let mut fac1_scales: Vec<_> = infos.iter().map(|info| info.fac1).collect();
372    fac1_scales.sort_by(f64::total_cmp);
373    assert!(fac1_scales
374        .windows(2)
375        .all(|scales| subgrid::node_value_eq(scales[0], scales[1])));
376    let fac1 = fac1_scales[0];
377
378    assert_eq!(operators.len(), infos.len());
379    assert_eq!(operators.len(), grid.convolutions().len());
380
381    let (pid_indices, pids01): (Vec<_>, Vec<_>) = izip!(0..infos.len(), operators, infos)
382        .map(|(d, operator, info)| {
383            pid_slices(operator, info, gluon_has_pid_zero, &|pid1| {
384                grid.channels()
385                    .iter()
386                    .flat_map(Channel::entry)
387                    .any(|(pids, _)| pids[d] == pid1)
388            })
389        })
390        .collect::<Result<Vec<_>>>()?
391        .into_iter()
392        .unzip();
393
394    let mut channels0: Vec<_> = pids01
395        .iter()
396        .map(|pids| pids.iter().map(|&(pid0, _)| pid0))
397        .multi_cartesian_product()
398        .collect();
399    channels0.sort_unstable();
400    channels0.dedup();
401    let channels0 = channels0;
402
403    let mut sub_fk_tables = Vec::with_capacity(grid.bwfl().len() * channels0.len());
404    let mut last_x1 = vec![Vec::new(); infos.len()];
405    let mut eko_slices = vec![Vec::new(); infos.len()];
406    let dim: Vec<_> = infos.iter().map(|info| info.x0.len()).collect();
407
408    for subgrids_oc in grid.subgrids().axis_iter(Axis(1)) {
409        let mut tables = vec![ArrayD::zeros(dim.clone()); channels0.len()];
410
411        for (subgrids_o, channel1) in subgrids_oc.axis_iter(Axis(1)).zip(grid.channels()) {
412            let (x1, array) = ndarray_from_subgrid_orders_slice(
413                grid,
414                fac1,
415                grid.kinematics(),
416                &subgrids_o,
417                grid.orders(),
418                order_mask,
419                xi,
420                alphas_table,
421            )?;
422
423            // skip over zero arrays to speed up evolution and avoid problems with NaNs
424            let Some(array) = array else {
425                continue;
426            };
427
428            for (last_x1, x1, pid_indices, slices, operator, info) in izip!(
429                &mut last_x1,
430                x1,
431                &pid_indices,
432                &mut eko_slices,
433                operators,
434                infos
435            ) {
436                if (last_x1.len() != x1.len())
437                    || last_x1
438                        .iter()
439                        .zip(x1.iter())
440                        .any(|(&lhs, &rhs)| !subgrid::node_value_eq(lhs, rhs))
441                {
442                    *slices = operator_slices(operator, info, pid_indices, &x1)?;
443                    *last_x1 = x1;
444                }
445            }
446
447            // for each channel in the FK-table ...
448            tables
449                .par_iter_mut()
450                .zip(&channels0)
451                .for_each(|(fk_table, pids0)| {
452                    // and each sub-channel in the grid ...
453                    for (pids1, factor) in channel1.entry() {
454                        // find the tuple of EKOs that evolve this combination
455                        let ops: Option<Box<[_]>> = izip!(pids0, pids1, &pids01, &eko_slices)
456                            .map(|(&pid0, &pid1, pids, slices)| {
457                                // for each convolution ...
458                                pids.iter().zip(slices).find_map(|(pid01, op)| {
459                                    // find the EKO that matches both the FK-table and the grid PID
460                                    (pid01 == &(pid0, pid1)).then_some(op)
461                                })
462                            })
463                            // if an EKO isn't found, it's zero and therefore the whole FK-table
464                            // channel contribution will be zero
465                            .collect();
466
467                        // if there's one zero EKO, the entire tuple is `None`
468                        if let Some(ops) = ops {
469                            general_tensor_mul(*factor, array.view(), &ops, fk_table.view_mut());
470                        }
471                    }
472                });
473        }
474
475        let mut node_values = vec![scale_values.to_vec()];
476        for info in infos {
477            node_values.push(info.x0.clone());
478        }
479
480        sub_fk_tables.extend(tables.into_iter().map(|table| {
481            ImportSubgridV1::new(
482                // TODO: insert one more axis if initial frg scale unequals fac
483                PackedArray::from(table.insert_axis(Axis(0)).view()),
484                node_values.clone(),
485            )
486            .into()
487        }));
488    }
489
490    Ok((
491        Array1::from_iter(sub_fk_tables)
492            .into_shape((1, grid.bwfl().len(), channels0.len()))
493            .unwrap(),
494        channels0
495            .into_iter()
496            .map(|c| Channel::new(vec![(c, 1.0)]))
497            .collect(),
498    ))
499}
500
501fn general_tensor_mul(
502    factor: f64,
503    array: ArrayViewD<f64>,
504    ops: &[&Array2<f64>],
505    mut fk_table: ArrayViewMutD<f64>,
506) {
507    let convolutions = array.shape().len();
508
509    match convolutions {
510        // the case `0` is not possible
511        1 => {
512            let array = array
513                .into_dimensionality::<Ix1>()
514                // UNWRAP: cannot fail due to previous `match`
515                .unwrap();
516            let mut fk_table = fk_table
517                .into_dimensionality::<Ix1>()
518                // UNWRAP: cannot fail due to previous `match`
519                .unwrap();
520
521            // fk_table += factor * ops[0] * array
522            linalg::general_mat_vec_mul(factor, ops[0], &array, 1.0, &mut fk_table);
523        }
524        2 => {
525            let array = array
526                .into_dimensionality::<Ix2>()
527                // UNWRAP: cannot fail due to previous `match`
528                .unwrap();
529            let mut fk_table = fk_table
530                .into_dimensionality::<Ix2>()
531                // UNWRAP: cannot fail due to previous `match`
532                .unwrap();
533
534            let mut tmp = Array2::zeros((array.shape()[0], ops[1].shape()[0]));
535            // tmp = array * ops[1]^T
536            linalg::general_mat_mul(1.0, &array, &ops[1].t(), 0.0, &mut tmp);
537            // fk_table += factor * ops[0] * tmp
538            linalg::general_mat_mul(factor, ops[0], &tmp, 1.0, &mut fk_table);
539        }
540        _ => {
541            let (ops_0, ops_dm1) = ops.split_first().unwrap();
542
543            for (mut fk_table_i, ops_0_i) in fk_table
544                .axis_iter_mut(Axis(0))
545                .zip(ops_0.axis_iter(Axis(0)))
546            {
547                for (array_j, ops_0_ij) in array.axis_iter(Axis(0)).zip(ops_0_i.iter()) {
548                    general_tensor_mul(factor * ops_0_ij, array_j, ops_dm1, fk_table_i.view_mut());
549                }
550            }
551        }
552    }
553}