Grid creation
Creating grids
PineAPFEL can create PineAPPL interpolation grids pre-filled with analytically computed coefficient functions from APFEL++. This is useful for structure function computations where the hard-scattering kernels are known analytically and only the non-perturbative input (PDFs or Fragmentation Functions) needs to be provided at convolution time.
The grid creation workflow requires three YAML configuration files:
- A grid card that defines the process, observable, binning, and perturbative orders
- A theory card that specifies the QCD parameters (coupling, thresholds, perturbative order)
- An operator card that defines the x-space interpolation grid and tabulation parameters
Supported processes and observables
The build_grid() function currently supports the following combinations:
| Process | Observable | Current | APFEL++ initializer (ZM) | Mass schemes |
|---|---|---|---|---|
| DIS | \(F_2\) | NC | InitializeF2NCObjectsZM |
ZM, FFN, FONLL |
| DIS | \(F_L\) | NC | InitializeFLNCObjectsZM |
ZM, FFN, FONLL |
| DIS | \(F_3\) | NC | InitializeF3NCObjectsZM |
ZM only |
| DIS | \(F_2\) | CC\(+\) | InitializeF2CCPlusObjectsZM |
ZM only |
| DIS | \(F_L\) | CC\(+\) | InitializeFLCCPlusObjectsZM |
ZM only |
| DIS | \(F_3\) | CC\(+\) | InitializeF3CCPlusObjectsZM |
ZM only |
| DIS | \(F_2\) | CC\(-\) | InitializeF2CCMinusObjectsZM |
ZM only |
| DIS | \(F_L\) | CC\(-\) | InitializeFLCCMinusObjectsZM |
ZM only |
| DIS | \(F_3\) | CC\(-\) | InitializeF3CCMinusObjectsZM |
ZM only |
| SIA | \(F_2\) | NC | InitializeF2NCObjectsZMT |
ZM only |
| SIA | \(F_L\) | NC | InitializeFLNCObjectsZMT |
ZM only |
| SIA | \(F_3\) | NC | InitializeF3NCObjectsZMT |
ZM only |
| SIDIS | \(F_T\) | NC | InitializeSidisObjects |
ZM only |
| SIDIS | \(F_L\) | NC | InitializeSidisObjects |
ZM only |
| DIS (polarized) | \(g_1\) | NC | Initializeg1NCObjectsZM |
ZM only |
| DIS (polarized) | \(g_L\) | NC | InitializegLNCObjectsZM |
ZM only |
| DIS (polarized) | \(g_4\) | NC | Initializeg4NCObjectsZM |
ZM only |
| SIDIS (polarized) | \(G_1\) | NC | InitializeSidisObjects |
ZM only |
Polarized grids are selected by setting Polarized: true in the grid card. The
Observable field retains its unpolarized name (F2, FL, F3) and is interpreted
as the corresponding polarized observable (\(g_1\), \(g_L\), \(g_4\), or \(G_1\) for
SIDIS) when Polarized: true.
The mass scheme is selected with the MassScheme field in the grid card (see
Mass scheme below). Combinations that only support ZM will emit a
warning and fall back to ZM when a non-ZM scheme is requested.
What is not yet supported
- SIA + CC is not supported (APFEL++ only provides CC initializers for DIS).
- SIDIS + CC is not supported.
- SIDIS + F3 is not supported (APFEL++ does not provide SIDIS F3 coefficient functions).
- Polarized + CC is not supported.
- Polarized SIA is not supported (APFEL++ does not provide time-like polarized SF initializers).
- Polarized SIDIS + FL is not supported (only \(G_1\) is available).
- QCD+QED coefficient functions are not implemented in grid filling (QED corrections are only available in the evolution step).
Convolution types
Each process type requires a specific convolution type, which determines the type of non-perturbative input the grid will be convoluted with:
| Process | ConvolutionTypes |
Description |
|---|---|---|
| DIS | [(UN)POL_PDF] |
(Un)polarised parton distribution functions |
| SIA | [(UN)POL_FF] |
(Un)polarised fragmentation functions |
| SIDIS | [(UN)POL_PDF, (UN)POL_FF] |
PDF for the initial state, FF for the final state |
Mass scheme
The heavy-quark mass scheme is set via the MassScheme field in the grid card:
| Value | Description |
|---|---|
ZM |
Zero-mass Variable Flavor Number (default). Quarks are massless above their thresholds. Applies to all processes and observables. |
FFN |
Fixed-Flavor Number. Uses InitializeF2/FLNCObjectsMassive from APFEL++. Massive charm and bottom coefficient functions depending on \(\xi = Q^2/m^2\) are included. DIS NC \(F_2\)/\(F_L\) only. |
FONLL |
FONLL scheme. Implemented as \(F_\mathrm{FONLL} = F_\mathrm{ZM} + F_\mathrm{FFN}\), following APFEL++ conventions. Available for DIS NC \(F_2\)/\(F_L\) only. |
MassiveZero |
Massless limit of FFN (InitializeF2/FLNCObjectsMassiveZero). In APFEL++ the total channel is set to zero for this initializer, so the resulting grid carries zero coefficient functions. Included for completeness. |
Warning
When a non-ZM scheme is requested for an unsupported combination (CC current, \(F_3\), polarized, SIA, SIDIS), PineAPFEL prints a warning and falls back to ZM automatically.
The heavy-quark masses used by FFN and FONLL are read from the HeavyQuarkMasses field
in the theory card (6 entries, all flavors including top). The ξ-grid tabulation is
controlled by the MassNxi, MassXiMin, MassXiMax, MassIntDeg, MassLambda, and
MassIMod theory-card fields. See Configuration cards for
the full list.
Perturbative orders
DIS and SIA (unpolarized)
The coefficient functions are available at the following perturbative orders:
alpha_s power |
Label | \(F_2\)/\(F_L\) content | \(F_3\) content |
|---|---|---|---|
| 0 | LO | \(\delta(1-x)\) for quarks, 0 for gluon | \(\delta(1-x)\) for quarks, 0 for gluon |
| 1 | NLO | \(C_{2,\mathrm{NS}}^{(1)}\), \(C_{2,g}^{(1)}\) | \(C_{3,\mathrm{NS}}^{(1)}\), no gluon |
| 2 | NNLO | \(C_{2,\mathrm{NS}}^{(2)}\), \(C_{2,\mathrm{PS}}^{(2)}\), \(C_{2,g}^{(2)}\) | \(C_{3,\mathrm{NS}}^{(2)}\), no gluon |
DIS (polarized)
For polarized DIS (ConvolutionTypes: [POL_PDF]), the structure functions \(g_1\), \(g_L\), and \(g_4\)
are selected instead. The available coefficient functions mirror the unpolarized case:
alpha_s power |
Label | \(g_1\) / \(g_L\) content | \(g_4\) content |
|---|---|---|---|
| 0 | LO | \(\delta(1-x)\) for quarks, 0 for gluon | \(\delta(1-x)\) for quarks, 0 for gluon |
| 1 | NLO | \(G_{1,\mathrm{NS}}^{(1)}\), \(G_{1,g}^{(1)}\) | \(G_{4,\mathrm{NS}}^{(1)}\), no gluon |
| 2 | NNLO | \(G_{1,\mathrm{NS}}^{(2)}\), \(G_{1,\mathrm{PS}}^{(2)}\), \(G_{1,g}^{(2)}\) | \(G_{4,\mathrm{NS}}^{(2)}\), no gluon |
SIDIS (unpolarized)
SIDIS coefficient functions depend on two momentum-fraction variables (\(x\) and \(z\)) and are
provided as DoubleOperator instances by APFEL++ via InitializeSidisObjects(). This is the
exact NNLO implementation (arXiv:2401.16281), covering all nine partonic channel types.
The available channels per perturbative order are:
alpha_s power |
Label | \(qq\) | \(gq\) | \(qg\) | \(gg\) | \(ps\) | \(q\bar{q}\) | \(qpq\) |
|---|---|---|---|---|---|---|---|---|
| 0 | LO | \(C_{T,qq}^{(0)}\) | — | — | — | — | — | — |
| 1 | NLO | \(C_{T,qq}^{(1)}\) | \(C_{T,gq}^{(1)}\) | \(C_{T,qg}^{(1)}\) | — | — | — | — |
| 2 | NNLO | \(C_{T,\mathrm{NS}}^{(2)}\) (\(n_f\)-dep.) | \(C_{T,gq}^{(2)}\) | \(C_{T,qg}^{(2)}\) | \(C_{T,gg}^{(2)}\) | \(C_{T,\mathrm{PS}}^{(2)}\) | \(C_{T,q\bar{q}}^{(2)}\) | \(C_{T,qpq_{1,2,3}}^{(2)}\) |
The same structure applies to \(F_L\) (replacing \(C_T\) with \(C_L\)); \(F_L\) has no LO contribution.
The nine NNLO channel labels refer to the convolution pair (PDF flavour, FF flavour):
| Channel | PDF side | FF side | Notes |
|---|---|---|---|
| \(qq\) (NS) | quark \(q\) | quark \(q\) | \(n_f\)-dependent non-singlet |
| \(gq\) | quark \(q\) | gluon | |
| \(qg\) | gluon | quark \(q\) | |
| \(gg\) | gluon | gluon | |
| \(ps\) | quark \(q\) | quark \(q\) | pure-singlet, weight \(\sum_i e_i^2\) |
| \(q\bar{q}\) | quark \(q\) | antiquark \(\bar{q}\) | |
| \(qpq_1\) | quark \(j\) | quark \(k \neq j\) | sum over target FF |
| \(qpq_2\) | quark \(k \neq i\) | quark \(i\) | sum over source PDF |
| \(qpq_3\) | quark \(a\) | quark \(b \neq a\) | charge product \(e_a e_b\) weight |
SIDIS (polarized)
For polarized SIDIS (first entry of ConvolutionTypes is POL_PDF), only \(G_1\) is
available (set Observable: F2). The exact NNLO implementation (arXiv:2404.08597) covers
the same nine channel types. Note that at NNLO the \(gq\) and \(qg\) channels become
\(n_f\)-dependent for the polarized case:
alpha_s power |
Label | \(qq\) | \(gq\) | \(qg\) | \(gg\) | \(ps\) | \(q\bar{q}\) | \(qpq\) |
|---|---|---|---|---|---|---|---|---|
| 0 | LO | \(G_{1,qq}^{(0)}\) | — | — | — | — | — | — |
| 1 | NLO | \(G_{1,qq}^{(1)}\) | \(G_{1,gq}^{(1)}\) | \(G_{1,qg}^{(1)}\) | — | — | — | — |
| 2 | NNLO | \(G_{1,\mathrm{NS}}^{(2)}\) (\(n_f\)-dep.) | \(G_{1,gq}^{(2)}\) (\(n_f\)-dep.) | \(G_{1,qg}^{(2)}\) (\(n_f\)-dep.) | \(G_{1,gg}^{(2)}\) | \(G_{1,\mathrm{PS}}^{(2)}\) | \(G_{1,q\bar{q}}^{(2)}\) | \(G_{1,qpq_{1,2,3}}^{(2)}\) |
The orders are specified in the grid card via the Orders field. Each order entry is a
5-element array [alpha_s, alpha, log_xir, log_xif, log_xia]. For central-scale QCD
coefficient functions set all but alpha_s to 0. To include renormalization-scale
logarithms (DIS/SIA only), set log_xir to the desired power; see
Renormalization scale variation below.
Each entry stores the coefficient function at that specific power of \(\alpha_s\) (and \(\ln\xi_R^2\)), not the cumulative sum. For a complete NNLO prediction, all three orders (LO, NLO, NNLO) must be listed so that the grid contains separate subgrids for each perturbative contribution.
Warning
Orders beyond NNLO (alpha_s > 2) are silently skipped during grid filling, even
though APFEL++ provides N3LO coefficient functions for some observables. This is a
current limitation that may be lifted in a future release.
Channel decomposition
Channels are automatically derived by build_grid() from the process, observable, and
the number of active flavours. You do not need to specify them in the grid card. The number
of active flavours \(n_{f}^{\mathrm{max}}\) is determined from the maximum \(Q^2\) across
all bins using the quark thresholds from the theory card.
The derive_channels() function generates channels in the physical (PDG) basis.
DIS and SIA channels
For \(F_2\) and \(F_L\) (C-even, Neutral Current):
- One quark channel per active flavour \(q = 1, \ldots, n_{f}^{\mathrm{max}}\):
pids: [[q], [-q]],factors: [1.0, 1.0](i.e. \(q + \bar{q}\)) - One gluon channel:
pids: [[21]],factors: [1.0]
For \(F_3\) (C-odd, Neutral Current):
- One quark channel per active flavour \(q = 1, \ldots, n_{f}^{\mathrm{max}}\):
pids: [[q], [-q]],factors: [1.0, -1.0](i.e. \(q - \bar{q}\)) - No gluon channel (\(C_\mathrm{G} = 0\) at all perturbative orders)
For Charged-Current (CC) processes, the channel structure depends on both the observable and the CC sign variant. The C-parity of the observable determines the quark combination:
- C-even (factors \([1, 1]\), i.e. \(q + \bar{q}\)): \(F_2\)/\(F_L\) CC\(+\), \(F_3\) CC\(-\)
- C-odd (factors \([1, -1]\), i.e. \(q - \bar{q}\)): \(F_2\)/\(F_L\) CC\(-\), \(F_3\) CC\(+\)
A gluon channel is present only for \(F_2/F_L\) with NC or CC\(+\). For CC, the per-quark weights \(w_q\) are the sum of CKM² elements where quark \(q\) participates (filtered by active partner flavours), replacing the electroweak charges used in NC.
For example, with 5 active flavours and observable F2 NC, the auto-derived channels are:
| Channel | PIDs | Factors |
|---|---|---|
| \(d + \bar{d}\) | [[1], [-1]] |
[1.0, 1.0] |
| \(u + \bar{u}\) | [[2], [-2]] |
[1.0, 1.0] |
| \(s + \bar{s}\) | [[3], [-3]] |
[1.0, 1.0] |
| \(c + \bar{c}\) | [[4], [-4]] |
[1.0, 1.0] |
| \(b + \bar{b}\) | [[5], [-5]] |
[1.0, 1.0] |
| \(g\) | [[21]] |
[1.0] |
The per-channel coefficient functions are constructed from the APFEL++ operators \(C_\mathrm{NS}\), \(C_\mathrm{S}\), and \(C_\mathrm{G}\) using the general formula:
| Channel | Coefficient function |
|---|---|
| Quark \(q\) | \(\mathcal{C}_q = w_q \, C_\mathrm{NS} + \frac{\Sigma_w}{6}\,(C_\mathrm{S} - C_\mathrm{NS})\) |
| Gluon | \(\mathcal{C}_g = \Sigma_w \, C_\mathrm{G}\) |
where \(w_q\) is the per-quark weight (electroweak charge \(e_q^2\) for NC, or CKM
weight for CC), \(\Sigma_w = \sum_{i=1}^{n_f^\mathrm{light}} w_i\) sums over light quarks
only, and the factor of 6 matches the internal normalisation convention used in APFEL++'s
DISNCBasis/DISCCBasis. APFEL++ sets \(C_\mathrm{S} = C_\mathrm{NS}\) and/or
\(C_\mathrm{G} = 0\) where the physics requires it, so the same formula works for all
observables and currents.
For the FFN and FONLL schemes, all \(n_f^\mathrm{max}\) quark channels contribute to the pure-singlet (PS) term \(\frac{\Sigma_w}{6}(C_\mathrm{S}-C_\mathrm{NS})\): light quarks \((q \leq n_f^\mathrm{light})\) also receive the non-singlet (NS) weight \(w_q\), while heavy quarks \((q > n_f^\mathrm{light})\) have \(w_q = 0\) for the NS term. This ensures that the summed PS contribution is proportional to the full SIGMA distribution \(\sum_{i=1}^{n_f^\mathrm{max}} 2f_i\) as required by APFEL++. Additionally, heavy-quark massive gluon coefficients \(C_\mathrm{G}^{(\mathrm{h})}\) are accumulated into the gluon channel weighted by \(e_{q_h}^2\).
SIDIS channels
SIDIS grids carry two convolutions (PDF ⊗ FF), so each channel entry specifies a pair of PIDs rather than a single one. The complete set of channel types depends on the perturbative order requested.
LO and NLO (3 types × \(n_f\) quarks):
| Channel type | PIDs | Factors | Description |
|---|---|---|---|
| \(qq\) | [[q, q], [-q, -q]] |
[1.0, 1.0] |
Quark PDF ⊗ quark FF (and anti-quark) |
| \(gq\) | [[q, 21], [-q, 21]] |
[1.0, 1.0] |
Quark PDF ⊗ gluon FF |
| \(qg\) | [[21, q], [21, -q]] |
[1.0, 1.0] |
Gluon PDF ⊗ quark FF |
Additional NNLO-only channels (shared across all quarks):
| Channel type | PIDs | Factors | Description |
|---|---|---|---|
| \(gg\) | [[21, 21]] |
[1.0] |
Gluon PDF ⊗ gluon FF; weight \(\sum_i e_i^2\) |
| \(ps\) (per quark \(q\)) | [[q, q], [-q, -q]] |
[1.0, 1.0] |
Pure-singlet; weight \(\sum_i e_i^2\) |
| \(q\bar{q}\) (per quark \(q\)) | [[q, -q], [-q, q]] |
[1.0, 1.0] |
Quark PDF ⊗ antiquark FF |
| \(qpq_1\) (per source \(j\)) | [[j,k],[-j,k],[j,-k],[-j,-k]] |
[1.0,...] |
Sum over \(k \neq j\); weight \(e_j^2\) |
| \(qpq_2\) (per target \(i\)) | [[k,i],[k,-i],[-k,i],[-k,-i]] |
[1.0,...] |
Sum over \(k \neq i\); weight \(e_i^2\) |
| \(qpq_3\) (per pair \(a \neq b\)) | [[a,b],[-a,b],[a,-b],[-a,-b]] |
[1.,-1.,-1.,1.] |
Weight \(e_a \cdot e_b\) (may be negative) |
Channels are always auto-derived from \(n_f^{\mathrm{max}}\) regardless of the orders requested — the fill loop simply returns zero for channel–order combinations where no coefficient function exists (e.g. \(gg\) at LO or NLO).
The channel ordering in the grid is: qq, gq, qg per quark (1…\(n_f\)), then gg,
ps per quark, qbq per quark, qpq1 per quark, qpq2 per quark, and finally
qpq3 for each ordered pair \((a, b)\) with \(a \neq b\).
With 3 active flavours this gives \(3 \times 3 + 1 + 3 + 3 + 3 + 3 + 6 = 22\) channels in total.
The electroweak weight (\(e_q^2\), \(\sum_i e_i^2\), or \(e_a e_b\)) is applied per-channel directly to the subgrid values during filling; the channel PIDs themselves are charge-neutral in the grid card.
Note
The Channels field in the grid card is still accepted for backward compatibility,
but it is always overridden by the auto-derived channels in build_grid().
It is recommended to omit Channels from the grid card entirely.
Operator card: SIDIS-only settings (sidis_* keys)
Two optional fields in the operator card control SIDIS-specific behaviour. They are ignored for DIS and SIA.
| Field | Default | Purpose |
|---|---|---|
sidis_mode |
legacy |
legacy = Gauss–Legendre \(z\) quadrature inside each \(z\) bin; bsf_exact = APFEL++-style interpolation weights (IntInterpolant) over the \(z\) range. |
sidis_int_eps |
1e-3 |
Relative tolerance for InitializeSidisObjects (SIDIS NNLO coefficient-function setup). Increase to 1e-1 for faster but coarser initialisation in tests. |
For APFEL++-aligned (BSF-style) SIDIS grids, set sidis_mode: bsf_exact explicitly
in the operator YAML:
See also Configuration cards — operator card.
The grid card
The grid card defines the structure of the PineAPPL grid. Below is a complete reference with all fields.
# The scattering process.
# Supported values: DIS, SIA, SIDIS
Process: DIS
# The structure function observable.
# Supported values: F2, FL, F3
# Optional, defaults to F2.
Observable: F2
# The electroweak current type.
# Supported values: NC, CC
# Optional, defaults to NC.
Current: NC
# CC sign variant (only used when Current: CC).
# Supported values: Plus, Minus
# Plus = (F(nu) + F(nubar)) / 2
# Minus = (F(nu) - F(nubar)) / 2
# Optional, defaults to Plus.
# CCSign: Plus
# Heavy-quark mass treatment scheme.
# Supported values: ZM, FFN, FONLL, MassiveZero
# ZM — zero-mass VFN scheme (default for all processes)
# FFN — fixed-flavor-number scheme with massive CFs (DIS NC F2/FL only)
# FONLL — FONLL = F_ZM + F_FFN (DIS NC F2/FL only)
# MassiveZero — massless limit of FFN; outputs zero CFs (APFEL++ convention)
# For unsupported combinations the scheme falls back to ZM with a warning.
# Optional, defaults to ZM.
# MassScheme: ZM
# PID basis for the channel definitions.
# Supported values: PDG, EVOL
PidBasis: PDG
# Hadron PIDs involved in the process (one per convolution).
# DIS: [2212] (proton)
# SIA: [211] (pion)
# SIDIS: [2212, 211] (proton + pion)
HadronPids: [2212]
# Convolution types (one per convolution).
# UNPOL_PDF — unpolarised parton distributions
# POL_PDF — longitudinally polarised parton distributions
# UNPOL_FF — unpolarised fragmentation functions
# POL_FF — polarised fragmentation functions
#
# Polarization is inferred from the first entry (the PDF slot):
# POL_PDF / POL_FF → polarized coefficient functions are used
# DIS: F2 -> g1, FL -> gL, F3 -> g4 (NC only)
# SIDIS: F2 -> G1 (NC only)
# UNPOL_PDF / UNPOL_FF → unpolarized coefficient functions
# Polarized CC and polarized SIA are not supported.
# For SIDIS the PDF and FF types can differ independently
# (e.g. [POL_PDF, UNPOL_FF] for helicity-weighted cross sections).
ConvolutionTypes: [UNPOL_PDF]
# Perturbative orders as 5-element arrays:
# [alpha_s, alpha, log(xi_R), log(xi_F), log(xi_A)]
# For QCD-only coefficient functions, set all but alpha_s to 0.
Orders:
- [0, 0, 0, 0, 0] # O(alpha_s^0) = LO
- [1, 0, 0, 0, 0] # O(alpha_s^1) = NLO
- [2, 0, 0, 0, 0] # O(alpha_s^2) = NNLO
# Partonic channels (optional).
# Channels are automatically derived by build_grid() from the observable
# and the number of active flavours. You do not need to specify this field.
# If present, it will be overridden during grid building.
#
# Channels:
# - pids: [[2], [-2]] # u + ubar
# factors: [1.0, 1.0]
# - pids: [[1], [-1]] # d + dbar
# factors: [1.0, 1.0]
# - pids: [[21]] # gluon
# factors: [1.0]
# Kinematic points. Each entry is a coordinate tuple:
# DIS/SIA: [Q^2, x] — fully pointwise
# SIDIS: [Q^2, x, z_lo, z_hi] — Q^2 and x are pointwise; z is an integration range
Points:
- [10.0, 0.001]
- [100.0, 0.01]
# Bin normalisation factors (one per bin). Optional — defaults to 1.0 per bin.
# Normalizations: [1.0, 1.0]
# Legacy alternative to Points (still accepted for backward compatibility).
# Each bin is defined by lower and upper edges in each dimension:
# DIS/SIA: [Q^2, x] (2 dimensions)
# SIDIS: [Q^2, x, z] (3 dimensions)
# The Q^2 node is taken as the geometric centre sqrt(Q^2_lo * Q^2_hi).
# Bins:
# - lower: [10.0, 0.001]
# upper: [100.0, 0.01]
DIS example
A DIS \(F_2\) grid up to NNLO at two \((Q^2, x)\) points.
Process: DIS
Observable: F2
Current: NC
PidBasis: PDG
HadronPids: [2212]
ConvolutionTypes: [UNPOL_PDF]
Orders:
- [0, 0, 0, 0, 0]
- [1, 0, 0, 0, 0]
- [2, 0, 0, 0, 0]
Points:
- [10.0, 0.001]
- [1000.0, 0.1]
SIA example
An SIA \(F_2\) grid for pion production up to NNLO. Note that the second kinematic
dimension is the hadron momentum fraction \(z\) instead of Bjorken \(x\),
and the convolution type is UNPOL_FF. Channels are auto-derived as for DIS:
Process: SIA
Observable: F2
Current: NC
PidBasis: PDG
HadronPids: [211]
ConvolutionTypes: [UNPOL_FF]
Orders:
- [0, 0, 0, 0, 0]
- [1, 0, 0, 0, 0]
- [2, 0, 0, 0, 0]
Points:
- [10.0, 0.2]
- [1000.0, 0.6]
SIDIS example
A SIDIS \(F_T\) grid for proton→pion semi-inclusive production up to NLO. Each point specifies \((Q^2, x)\) exactly and a \(z\) integration range \([z_\mathrm{lo}, z_\mathrm{hi}]\). Two convolution types are required (PDF and FF):
Process: SIDIS
Observable: F2
Current: NC
PidBasis: PDG
HadronPids: [2212, 211]
ConvolutionTypes: [UNPOL_PDF, UNPOL_FF]
Orders:
- [0, 0, 0, 0, 0] # LO
- [1, 0, 0, 0, 0] # NLO
# [Q^2, x, z_lo, z_hi] — Q^2 and x are pointwise; z is an integration range.
Points:
- [10.0, 0.001, 0.2, 0.4]
- [1000.0, 0.1, 0.4, 0.6]
SIDIS NNLO example
Adding NNLO extends the channel set from 3 per quark to 9 types (see SIDIS channels). Simply add the NNLO order entry — channels are auto-derived and all 9 NNLO channel types are filled automatically:
Process: SIDIS
Observable: F2
Current: NC
PidBasis: PDG
HadronPids: [2212, 211]
ConvolutionTypes: [UNPOL_PDF, UNPOL_FF]
Orders:
- [0, 0, 0, 0, 0] # LO
- [1, 0, 0, 0, 0] # NLO
- [2, 0, 0, 0, 0] # NNLO (exact, arXiv:2401.16281)
Points:
- [10.0, 0.001, 0.2, 0.4]
- [1000.0, 0.1, 0.4, 0.6]
Note
The NNLO SIDIS coefficient functions are computed exactly from the full 2D
DoubleExpression classes in APFEL++ via InitializeSidisObjects(). This replaces
the former approximated NNLO (non-singlet \(qq\) only, based on threshold
resummation). The exact implementation covers all nine partonic channels for both
the unpolarised \(F_T\)/\(F_L\) (arXiv:2401.16281) and the polarised \(G_1\)
(arXiv:2404.08597).
Polarized DIS example
A polarized DIS \(g_1\) grid up to NLO. Setting ConvolutionTypes: [POL_PDF] signals
polarized coefficient functions; Observable: F2 then selects \(g_1\):
Process: DIS
Observable: F2
Current: NC
PidBasis: PDG
HadronPids: [2212]
ConvolutionTypes: [POL_PDF]
Orders:
- [0, 0, 0, 0, 0]
- [1, 0, 0, 0, 0]
Points:
- [10.0, 0.001]
- [1000.0, 0.1]
Polarized SIDIS example
A polarized SIDIS \(G_1\) grid up to NLO. Only Observable: F2 is valid here.
Note that the PDF is polarized while the FF remains unpolarized — this distinction
is expressed directly through ConvolutionTypes without any separate flag:
Process: SIDIS
Observable: F2
Current: NC
PidBasis: PDG
HadronPids: [2212, 211]
ConvolutionTypes: [POL_PDF, UNPOL_FF]
Orders:
- [0, 0, 0, 0, 0]
- [1, 0, 0, 0, 0]
Points:
- [10.0, 0.001, 0.2, 0.4]
- [1000.0, 0.1, 0.4, 0.6]
FFN DIS example
A DIS \(F_2\) grid in the fixed-flavor-number scheme. Requires HeavyQuarkMasses in the
theory card (6 entries). The massive coefficient functions are tabulated internally on a
\(\xi = Q^2/m^2\) grid; the tabulation parameters can be tuned with MassNxi etc. in
the theory card.
Process: DIS
Observable: F2
Current: NC
MassScheme: FFN
PidBasis: PDG
HadronPids: [2212]
ConvolutionTypes: [UNPOL_PDF]
Orders:
- [0, 0, 0, 0, 0]
- [1, 0, 0, 0, 0]
- [2, 0, 0, 0, 0]
Points:
- [10.0, 0.001]
- [1000.0, 0.1]
FONLL DIS example
A DIS \(F_2\) FONLL grid. PineAPFEL computes \(F_\mathrm{FONLL} = F_\mathrm{ZM} + F_\mathrm{FFN}\) following APFEL++ conventions. The same theory card as for FFN is required.
Process: DIS
Observable: F2
Current: NC
MassScheme: FONLL
PidBasis: PDG
HadronPids: [2212]
ConvolutionTypes: [UNPOL_PDF]
Orders:
- [0, 0, 0, 0, 0]
- [1, 0, 0, 0, 0]
- [2, 0, 0, 0, 0]
Points:
- [10.0, 0.001]
- [1000.0, 0.1]
CC DIS example
A DIS \(F_2\) charged-current (CC) grid with the Plus variant \((F(\nu) + F(\bar\nu))/2\).
The CCSign field selects between Plus and Minus. The CKM matrix elements are specified
in the theory card (see below):
Process: DIS
Observable: F2
Current: CC
CCSign: Plus
PidBasis: PDG
HadronPids: [2212]
ConvolutionTypes: [UNPOL_PDF]
Orders:
- [0, 0, 0, 0, 0]
- [1, 0, 0, 0, 0]
- [2, 0, 0, 0, 0]
Points:
- [10.0, 0.001]
- [1000.0, 0.1]
The theory card should include a CKM field with 9 squared CKM matrix elements
\(|V_{ij}|^2\) in row-major order: \([V_{ud}^2, V_{us}^2, V_{ub}^2, V_{cd}^2,
V_{cs}^2, V_{cb}^2, V_{td}^2, V_{ts}^2, V_{tb}^2]\). If absent, standard PDG
values are used.
Renormalization scale variation
PineAPFEL supports renormalization-scale variation for DIS and SIA grids. When one or
more orders with log_xir > 0 are requested, build_grid() automatically computes the
corresponding coefficient-function contributions and stores them as separate PineAPPL
subgrids. At convolution time, PineAPPL evaluates
where \(\xi_R = \mu_R / Q\) and \(W_{n,m}\) are the stored subgrid weights for order
[n, 0, m, 0, 0]. Setting \(\xi_R = 1\) recovers the central-scale result; varying it
around 1 gives the renormalization-scale uncertainty band.
Available renorm-scale orders
The following log_xir orders are derived automatically from the central-scale
coefficient functions \(C_0\), \(C_1\), \(C_2\) via the renormalization group:
| Order entry | Label | Stored weight | Derivation |
|---|---|---|---|
[1, 0, 1, 0, 0] |
NLO × \(\ln\xi_R^2\) | \(\beta_0(n_f)\,\tfrac{1}{4\pi}\,C_0\) | \(\partial_{\ln\mu_R^2} F\big\lvert_{\mathrm{NLO}}\) |
[2, 0, 1, 0, 0] |
NNLO × \(\ln\xi_R^2\) | \(\beta_0(n_f)\,\tfrac{1}{4\pi}\,C_1\) | \(\partial_{\ln\mu_R^2} F\big\lvert_{\mathrm{NNLO}}\) |
[2, 0, 2, 0, 0] |
NNLO × \(\ln^2\xi_R^2\) | \(\tfrac{\beta_0(n_f)^2}{2}\,\tfrac{1}{(4\pi)^2}\,C_0\) | \(\tfrac{1}{2}\partial^2_{\ln\mu_R^2} F\big\lvert_{\mathrm{NNLO}}\) |
Here \(\beta_0(n_f) = 11 - \tfrac{2}{3}n_f\) (the one-loop QCD \(\beta\)-function coefficient) and the factors of \(\tfrac{1}{4\pi}\) absorb the difference between APFEL++'s \((\alpha_s/4\pi)^n\) convention and PineAPPL's \(\alpha_s^n\) convention, \(n_f\) is the number of active flavours at the Q² node.
The central-scale orders [0,0,0,0,0], [1,0,0,0,0], [2,0,0,0,0] must also be
included in Orders for the corresponding \(C_0\), \(C_1\), \(C_2\) subgrids to exist
(scale-log orders are derived from them). If a base order is absent, the derived log
order will be empty.
Example: DIS NLO + NNLO with renorm-scale variation
Process: DIS
Observable: F2
Current: NC
PidBasis: PDG
HadronPids: [2212]
ConvolutionTypes: [UNPOL_PDF]
Orders:
- [0, 0, 0, 0, 0] # LO (C_0)
- [1, 0, 0, 0, 0] # NLO (C_1)
- [2, 0, 0, 0, 0] # NNLO (C_2)
- [1, 0, 1, 0, 0] # NLO × ln ξ_R²
- [2, 0, 1, 0, 0] # NNLO × ln ξ_R²
- [2, 0, 2, 0, 0] # NNLO × ln² ξ_R²
Points:
- [10.0, 0.001]
The six-subgrid grid can then be convoluted at any \(\xi_R\) by passing a non-unity
scale factor to pineappl_grid_convolve_with_one (or the Python Grid.convolve
wrapper).
DIS/SIA only — SIDIS and factorization logs not yet implemented
Renormalization-scale logs are currently filled only for DIS and SIA processes.
SIDIS grids ignore log_xir > 0 entries (no error is raised; the subgrid is simply
left empty). Factorization-scale logs (log_xif > 0) require convolving with
DGLAP splitting functions and are not yet implemented for any process.
Using the CLI
The pineapfel-build executable creates a filled PineAPPL grid from three YAML cards:
If -o is not specified, the output filename is derived from the grid card by replacing
.yaml with .pineappl.lz4.
# Build a DIS F2 grid
pineapfel-build runcards/grid_dis.yaml runcards/theory.yaml runcards/operator.yaml
# Build an SIA grid with a custom output name
pineapfel-build runcards/grid_sia.yaml runcards/theory.yaml runcards/operator.yaml \
-o sia_f2.pineappl.lz4
Using the library
The same functionality is available programmatically through the build_grid() function:
#include <pineapfel.h>
#include <pineappl_capi.h>
#include <iostream>
int main() {
// 1. Load all three cards
auto grid_def = pineapfel::load_grid_def("runcards/grid_dis.yaml");
auto theory = pineapfel::load_theory_card("runcards/theory.yaml");
auto op_card = pineapfel::load_operator_card("runcards/operator.yaml");
// 2. Build and fill the grid
auto* grid = pineapfel::build_grid(grid_def, theory, op_card);
// 3. Write the grid
pineappl_grid_write(grid, "dis_f2.pineappl.lz4");
// 4. Cleanup
pineappl_grid_delete(grid);
return 0;
}
The returned grid can also be passed directly to pineapfel::evolve() to produce an
FK table in the same program:
auto* grid = pineapfel::build_grid(grid_def, theory, op_card);
auto* fktable = pineapfel::evolve(grid, theory, op_card);
pineappl_grid_write(fktable, "dis_f2.fk.pineappl.lz4");
pineappl_grid_delete(fktable);
pineappl_grid_delete(grid);
Grid structure internals
Understanding the internal layout of the generated grid is useful for debugging and for writing custom grid-filling code.
Node selection
The grid nodes are defined automatically:
-
\(x/z\) nodes: Taken from the APFEL++ joint interpolation grid, which is built from the
xgriddefinition in the operator card. This ensures consistency between the coefficient function grid and any subsequent evolution step. -
\(Q^2\) nodes (DIS and SIA): Each bin contributes exactly one \(Q^2\) node, taken from the
Pointsentry directly. All coefficient functions are evaluated at that single \(Q^2\) value, and the subgrid has shape[1, n_x]. -
\(Q^2\) nodes (SIDIS): Same pointwise strategy — each bin contributes exactly one \(Q^2\) node equal to the \(Q^2\) coordinate from the
Pointsentry (geometric centre \(\sqrt{Q^2_\mathrm{lo} \cdot Q^2_\mathrm{hi}}\) when the oldBinsformat is used). The subgrid has shape[1, n_x, n_z].
Subgrid layout
DIS and SIA
Each subgrid (one per combination of bin, perturbative order, and channel) is a
two-dimensional array of shape [1, n_x], stored in row-major order. The
node_values vector is:
The coefficient function operator is evaluated at the bin's \(Q^2\) and \(x\) point to produce a distribution on the APFEL++ joint grid, which fills the single row.
SIDIS
SIDIS subgrids are three-dimensional arrays of shape [1, n_x, n_z], stored in
row-major order. The same APFEL++ joint grid is used for both the \(x\) (PDF) and
\(z\) (FF) dimensions. The node_values vector is:
Each coefficient function is stored as a DoubleOperator — a full 2D kernel on the
\((x, z)\) grid. The 2D operator is evaluated at the bin's \((Q^2, x)\) point and over
the bin's \(z\) range \([z_\mathrm{lo}, z_\mathrm{hi}]\) using
eval_double_op_column(). The subgrid entry at [ix, iz] accumulates the weighted
kernel:
where fill_weight is the per-channel electroweak factor (\(e_q^2\), \(\sum_i e_i^2\),
or \(e_a e_b\) depending on channel type) and \(W\) is the 2D interpolation kernel
extracted from the DoubleOperator. The \(z\) direction accumulates contributions
from APFEL++-style interpolation weights over joint-grid nodes (sidis_mode: bsf_exact)
or from internal Gauss–Legendre quadrature on the \(z\) range (sidis_mode: legacy).
The same DoubleOperator column machinery is used for all perturbative orders (LO, NLO, NNLO).
Programmatic grid definition
In addition to loading from YAML, you can construct a GridDef programmatically.
Note that channels can be left empty — build_grid() will auto-derive them:
pineapfel::GridDef def;
def.process = pineapfel::ProcessType::DIS;
def.observable = pineapfel::Observable::F2;
def.current = pineapfel::Current::NC;
def.pid_basis = PINEAPPL_PID_BASIS_PDG;
def.hadron_pids = {2212};
def.convolution_types = {PINEAPPL_CONV_TYPE_UNPOL_PDF};
def.orders = {{0, 0, 0, 0, 0}, {1, 0, 0, 0, 0}, {2, 0, 0, 0, 0}}; // LO + NLO + NNLO
// channels are auto-derived by build_grid() — no need to set them
// Pointwise bins: lower == upper == {Q^2, x}
def.bins = {
{{10.0, 0.001}, {10.0, 0.001}},
{{1000.0, 0.1}, {1000.0, 0.1}},
};
def.normalizations = {1.0, 1.0};
auto* grid = pineapfel::build_grid(def, theory, op_card);
You can also call derive_channels() directly if you need the channels before
calling build_grid():
// Derive channels for DIS F2 NC with 5 active flavours
auto channels = pineapfel::derive_channels(
pineapfel::ProcessType::DIS,
pineapfel::Observable::F2,
pineapfel::Current::NC,
pineapfel::CCSign::Plus,
5);
// Returns 6 channels: d+dbar, u+ubar, s+sbar, c+cbar, b+bbar, gluon
// Derive channels for SIDIS F2 with 5 active flavours (includes all NNLO types)
auto sidis_channels = pineapfel::derive_channels(
pineapfel::ProcessType::SIDIS,
pineapfel::Observable::F2,
pineapfel::Current::NC,
pineapfel::CCSign::Plus,
5);
// Returns 50 channels:
// 15 LO/NLO : (qq, gq, qg) x 5 quarks
// 1 NNLO gg
// 5 NNLO ps (one per quark)
// 5 NNLO qbq (one per quark)
// 5 NNLO qpq1 (one per source quark)
// 5 NNLO qpq2 (one per target quark)
// 20 NNLO qpq3 (one per ordered pair a≠b, 5x4=20)
// Channels that have no coefficient function at a given order contribute zero.