J. A. Formaggio, G. P. Zeller · Rev. Mod. Phys. 84, 1307 (2012)

From eV to EeV: Neutrino Cross Sections Across Energy Scales

Model inputs accelerator experiments atmospheric neutrinos 2012 cross sections Reproduced

Total charged-current νμ and ν̄μ cross sections per nucleon divided by energy, from 0.1 to 500 GeV, as carried by Newtrinos.jl (module xsec): the per-process GENIE cross sections of ten tunes (generator_sigma, GENIE 2.12 to AR23_20i, on carbon, i.e. an isoscalar target as in the compilation), NEUT 5.4.0 on water, and the nominal model of CorrelatedXsec (the mean of the tunes) with its prior 1σ band, the cross-section model shared by all experiments in global fits. They are compared with the world data and the NUANCE prediction (total, and QE / RES / DIS) of the review's Fig. 15. Curves and data points are extracted from the vector figures of the arXiv source (data/extract_fz.py). In the deep-inelastic region (30–300 GeV) the prior 1σ bands are [0.662, 0.701] (νμ) and [0.319, 0.345] (ν̄μ) × 10⁻³⁸ cm²/GeV, against 0.677 and 0.323 for the extracted world data; the ten tunes span 0.669–0.695 and 0.326–0.337. Below 1 GeV the tunes differ by up to 60 %; the band (νμ: 0.62–0.90 at 0.5 GeV, 0.88–1.05 at 1 GeV) covers most of them.

Figures

νμ CC cross section per nucleon / E: world data, NUANCE, the GENIE tunes and NEUT, and the CorrelatedXsec nominal with its prior band · Fig. 15 (top)

Paper · Fig. 15 (top)
Original: νμ CC cross section per nucleon / E: world data, NUANCE, the GENIE tunes and NEUT, and the CorrelatedXsec nominal with its prior band

Original figure: J. A. Formaggio, G. P. Zeller, Rev. Mod. Phys. 84, 1307 (2012), Fig. 15 (top), doi:10.1103/RevModPhys.84.1307. Not our work; shown for comparison, all rights with the authors and publisher.

Newtrinos.jl
Reproduction: νμ CC cross section per nucleon / E: world data, NUANCE, the GENIE tunes and NEUT, and the CorrelatedXsec nominal with its prior band

ν̄μ CC cross section per nucleon / E · Fig. 15 (bottom)

Paper · Fig. 15 (bottom)
Original: ν̄μ CC cross section per nucleon / E

Original figure: J. A. Formaggio, G. P. Zeller, Rev. Mod. Phys. 84, 1307 (2012), Fig. 15 (bottom), doi:10.1103/RevModPhys.84.1307. Not our work; shown for comparison, all rights with the authors and publisher.

Newtrinos.jl
Reproduction: ν̄μ CC cross section per nucleon / E

Per process (QE, RES, DIS; MEC): the nominal (mean of the tunes) and the spread of the tunes vs NUANCE · Fig. 15 (QE, RES, DIS curves)

Paper

Fig. 15 (QE, RES, DIS curves)

Newtrinos.jl
Reproduction: Per process (QE, RES, DIS; MEC): the nominal (mean of the tunes) and the spread of the tunes vs NUANCE

Known limitations

Reproduce

  1. git clone https://github.com/Newtrinos-org/Newtrinos.jl.git && cd Newtrinos.jl && git checkout 78e71c310b
  2. Download reproduce.jl and the data/ folder of this entry into an empty directory.
  3. julia -t 8 --project=/path/to/Newtrinos.jl reproduce.jl
# Charged-current νμ / ν̄μ cross sections of Newtrinos.jl (module `xsec`) compared with the world data and the
# NUANCE prediction compiled by Formaggio & Zeller, Rev. Mod. Phys. 84, 1307 (2012), Fig. 15. The curves and data
# points are extracted from the vector figures of the arXiv source (data/extract_fz.py).
#
# Newtrinos curves: the per-process GENIE splines carried by `xsec` (`generator_sigma`, all tunes) on carbon
# (isoscalar, as the compilation), NEUT 5.4.0 on water (the only target it is available for), and the reference model
# of `CorrelatedXsec` with its prior 1σ band (all its parameters sampled from their priors).
# Run from this directory with the Newtrinos.jl environment at the pinned commit:
#   julia --project=<path to Newtrinos.jl> reproduce.jl
using Newtrinos, CairoMakie, CSV, DataFrames, Printf, Statistics, Random, Distributions

cd(@__DIR__); mkpath("ours"); mkpath("results")
const X = Newtrinos.xsec

E = 10 .^ range(-1, log10(500), length = 400)
tab = X.load_table()
at(v) = X.at_energies(tab.E, v, E)
TUNES = Symbol.(sort(tab.tunes))
σ_tune(t, anti; target = :C) = at(sum(X.generator_sigma(t, target, :numu, anti, :CC)))
σ_proc(t, anti, p; target = :C) = at(getfield(X.generator_sigma(t, target, :numu, anti, :CC), p))

# CorrelatedXsec: reference model and prior band on carbon
xs = X.configure(X.CorrelatedXsec())
ref = xs.cfg.reference
sig = Dict(anti => xs.sigma(E; flavour = :numu, anti, current = :CC, target = :C) for anti in (false, true))
rng = MersenneTwister(1)
draw() = map(d -> d isa Distribution ? rand(rng, d) : d, xs.priors)
samples = [draw() for _ in 1:500]
band = Dict(anti => begin
    S = reduce(hcat, [sig[anti](p) for p in samples])
    (lo = [quantile(r, 0.16) for r in eachrow(S)], hi = [quantile(r, 0.84) for r in eachrow(S)])
end for anti in (false, true))

fz(name) = (CSV.read("data/fz_$(name)_curves.csv", DataFrame), CSV.read("data/fz_$(name)_data.csv", DataFrame))
tunecol(i) = Makie.wong_colors()[mod1(i, 7)]
for (k, (anti, name, ymax, lab)) in enumerate(((false, "nu", 1.5, "ν"), (true, "nubar", 0.42, "ν̄")))
    cur, dat = fz(name)
    fig = Figure(size = (760, 520))
    ax = Axis(fig[1, 1], xscale = log10, xlabel = "E_ν (GeV)", ylabel = "$lab CC cross section / E (10⁻³⁸ cm²/GeV)",
              limits = (0.08, 500, 0, ymax), xticks = ([0.1, 1, 10, 100], ["0.1", "1", "10", "100"]))
    band!(ax, E, band[anti].lo, band[anti].hi, color = (:dodgerblue, 0.25), label = "CorrelatedXsec prior 1σ (C)")
    errorbars!(ax, dat.E_GeV, dat.sigma_over_E, dat.err, color = (:gray40, 0.7), whiskerwidth = 0)
    scatter!(ax, dat.E_GeV, dat.sigma_over_E, color = :gray40, markersize = 5, label = "world data (Formaggio & Zeller)")
    s = cur[cur.curve .== "TOTAL", :]
    lines!(ax, s.E_GeV, s[!, 3], color = :gray20, linewidth = 4, alpha = 0.35, label = "NUANCE total, isoscalar")
    for (i, t) in enumerate(TUNES)
        lines!(ax, E, σ_tune(t, anti), color = (tunecol(i), 0.8), linewidth = 1, label = "GENIE $(t), C")
    end
    lines!(ax, E, sig[anti](xs.params), color = :dodgerblue, linewidth = 2.5, label = "nominal ($(ref)), C")
    lines!(ax, E, σ_tune(:NEUT5_4_0, anti; target = :H2O), color = :black, linewidth = 2, linestyle = :dash, label = "NEUT 5.4.0, H₂O")
    Legend(fig[1, 2], ax, framevisible = false, labelsize = 9)
    save("ours/fig15_$(name).png", fig)
end

# per process: reference model and tune spread vs NUANCE (QE, RES, DIS)
fig = Figure(size = (1000, 420))
for (k, (anti, name, lab)) in enumerate(((false, "nu", "ν"), (true, "nubar", "ν̄")))
    cur, _ = fz(name)
    ax = Axis(fig[1, k], xscale = log10, xlabel = "E_ν (GeV)", ylabel = "$lab CC σ / E (10⁻³⁸ cm²/GeV)", title = "$lab: per process",
              limits = (0.08, 500, 0, anti ? 0.42 : 1.5), xticks = ([0.1, 1, 10, 100], ["0.1", "1", "10", "100"]))
    for (c, p, col) in (("QE", (:QE,), :orangered), ("RES", (:RES,), :green4), ("DIS", (:DIS,), :purple))
        s = cur[cur.curve .== c, :]
        lines!(ax, s.E_GeV, s[!, 3], color = (col, 0.35), linewidth = 5, label = "NUANCE $c")
        curves = [sum(σ_proc(t, anti, q) for q in p) for t in TUNES]
        band!(ax, E, minimum(reduce(hcat, curves), dims = 2)[:], maximum(reduce(hcat, curves), dims = 2)[:], color = (col, 0.15))
        lines!(ax, E, sum(σ_proc(ref, anti, q) for q in p), color = col, linewidth = 1.8, label = "$(ref) $c (band: tunes)")
    end
    lines!(ax, E, σ_proc(ref, anti, :MEC), color = :black, linestyle = :dot, label = "$(ref) MEC")
    k == 2 && Legend(fig[1, 3], ax, framevisible = false, labelsize = 9)
end
save("ours/fig15_processes.png", fig)

# numbers: σ/E at fixed energies, and the 30–300 GeV average vs the inclusive data in that range
interp(x, y, t) = (i = clamp(searchsortedlast(x, t), 1, length(x) - 1); y[i] + (y[i+1] - y[i]) * (t - x[i]) / (x[i+1] - x[i]))
rows = []
for (anti, name) in ((false, "nu"), (true, "nubar"))
    cur, dat = fz(name)
    tot = cur[cur.curve .== "TOTAL", :]
    # inclusive measurements only: the figure also shows QE-only data, well below the total
    incl = dat.sigma_over_E .> 0.5 .* [interp(tot.E_GeV, tot[!, 3], e) for e in dat.E_GeV]
    hi = (dat.E_GeV .> 30) .& (dat.E_GeV .< 300) .& (dat.err .> 0) .& incl
    w = 1 ./ dat.err[hi] .^ 2
    data_hi = sum(w .* dat.sigma_over_E[hi]) / sum(w)
    sel = (E .> 30) .& (E .< 300)
    model(f) = (e -> interp(E, f, e))
    curves = merge(Dict(string(t) => σ_tune(t, anti) for t in TUNES), Dict("NEUT5_4_0_H2O" => σ_tune(:NEUT5_4_0, anti; target = :H2O)))
    for e in (0.5, 1.0, 3.0, 10.0, 100.0, -1.0)
        r = Dict{Symbol, Any}(:neutrino => name, :E_GeV => e,
                              :data_or_nuance => round(e < 0 ? data_hi : interp(tot.E_GeV, tot[!, 3], e), digits = 3),
                              :band_lo => round(e < 0 ? mean(band[anti].lo[sel]) : interp(E, band[anti].lo, e), digits = 3),
                              :band_hi => round(e < 0 ? mean(band[anti].hi[sel]) : interp(E, band[anti].hi, e), digits = 3))
        for (c, f) in curves
            r[Symbol(c)] = round(e < 0 ? mean(f[sel]) : interp(E, f, e), digits = 3)
        end
        push!(rows, NamedTuple(r))
    end
end
out = DataFrame(rows)
select!(out, :neutrino, :E_GeV, :data_or_nuance, :band_lo, :band_hi, Not([:neutrino, :E_GeV, :data_or_nuance, :band_lo, :band_hi]))
CSV.write("results/cc_sigma_over_E.csv", out)
println("(E_GeV = -1: 30-300 GeV average; data_or_nuance: inverse-variance mean of the extracted inclusive data there, else NUANCE)")
show(stdout, out, allrows = true, allcols = true); println()

Provenance

Newtrinos.jl commit78e71c310b (xsec (generator_sigma, CorrelatedXsec))
Run date2026-10-12T12:13:09Z
Wall time1.6 min (4 threads)
Julia1.12.5
MachineAMD EPYC 7702P 64-Core Processor, 128 hardware threads, Linux
Environmentnewtrinos_Manifest.toml · run.toml

Downloads

CSV files hold one row per scan point: the scanned and profiled parameters (sin2_theta23, dm2_32 in eV², dcp in rad, nuisance parameters), dchi2 = −2Δ log L and the log-likelihood. JLD2 files contain the full NewtrinosResult objects.

FileSizeSHA-256
ours/fig15_nu.png236.3 kB662414ba79b7…
ours/fig15_nubar.png248.0 kB6a3b33221d77…
ours/fig15_processes.png239.0 kB0ce6a266c006…
results/cc_sigma_over_E.csv1.3 kBdba3092e71a4…