NOvA · Phys. Rev. Lett. 136, 011802 (2026)

Precision measurement of neutrino oscillation parameters with 10 years of data from the NOvA experiment

Experimental results NOvA 2026 δCP θ23 Δm²32 mass ordering Reproduced

Three-flavour fit to the NOvA far-detector νμ disappearance (4 quartiles × FHC/RHC) and νe appearance samples of the 2024 data release (26.6×10²⁰ ν + 12.5×10²⁰ ν̄ POT), with the 1D Daya Bay constraint on θ₁₃, conditional on each mass ordering.

Figures

sin²θ₂₃ – Δm²₃₂, normal ordering · Suppl. Fig. S8

Paper · Suppl. Fig. S8
Original: sin²θ₂₃ – Δm²₃₂, normal ordering

Original figure: NOvA, Phys. Rev. Lett. 136, 011802 (2026), Suppl. Fig. S8, doi:10.1103/x53y-2b86. Not our work; shown for comparison, all rights with the authors and publisher.

Newtrinos.jl
Reproduction: sin²θ₂₃ – Δm²₃₂, normal ordering

sin²θ₂₃ – Δm²₃₂, inverted ordering · Suppl. Fig. S8

Paper · Suppl. Fig. S8
Original: sin²θ₂₃ – Δm²₃₂, inverted ordering

Original figure: NOvA, Phys. Rev. Lett. 136, 011802 (2026), Suppl. Fig. S8, doi:10.1103/x53y-2b86. Not our work; shown for comparison, all rights with the authors and publisher.

Newtrinos.jl
Reproduction: sin²θ₂₃ – Δm²₃₂, inverted ordering

δCP – sin²θ₂₃, normal ordering · Suppl. Fig. S7

Paper · Suppl. Fig. S7
Original: δCP – sin²θ₂₃, normal ordering

Original figure: NOvA, Phys. Rev. Lett. 136, 011802 (2026), Suppl. Fig. S7, doi:10.1103/x53y-2b86. Not our work; shown for comparison, all rights with the authors and publisher.

Newtrinos.jl
Reproduction: δCP – sin²θ₂₃, normal ordering

δCP – sin²θ₂₃, inverted ordering · Suppl. Fig. S7

Paper · Suppl. Fig. S7
Original: δCP – sin²θ₂₃, inverted ordering

Original figure: NOvA, Phys. Rev. Lett. 136, 011802 (2026), Suppl. Fig. S7, doi:10.1103/x53y-2b86. Not our work; shown for comparison, all rights with the authors and publisher.

Newtrinos.jl
Reproduction: δCP – sin²θ₂₃, inverted ordering

Known limitations

Reproduce

  1. git clone https://github.com/Newtrinos-org/Newtrinos.jl.git && cd Newtrinos.jl && git checkout c6612c255a
  2. Download reproduce.jl into an empty directory.
  3. julia -t 8 --project=/path/to/Newtrinos.jl reproduce.jl
# Reproduction of NOvA, Phys. Rev. Lett. 136, 011802 (2026) with Newtrinos.jl (module `nova`).
# Run from this directory with the Newtrinos.jl environment at the pinned commit:
#   julia -t 8 --project=<path to Newtrinos.jl> reproduce.jl
using Newtrinos, Distributions, DataStructures, Accessors, FileIO, CairoMakie, HDF5, CSV, DataFrames

cd(@__DIR__); mkpath("ours"); mkpath("results"); mkpath("cache")

# CSV export of a profile scan: one row per grid point, ASCII column names, oscillation parameters in physical
# units (sin²θ, Δm²₃₂ = Δm²₃₁ − Δm²₂₁ in eV², δCP in rad), dchi2 = -2Δlog L w.r.t. `ref` (default: the scan's best fit)
function save_csv(path, r; ref = maximum(r.values.log_posterior))
    ascii(k) = replace(string(k), "θ" => "theta", "Δm²" => "dm2_", "δ" => "d", "₁" => "1", "₂" => "2", "₃" => "3", "CP" => "cp")
    cols = OrderedDict{String,Any}()
    for (k, v) in pairs(r.values)
        x = vec(v)
        if startswith(string(k), "θ")
            cols["sin2_" * ascii(k)] = sin.(x) .^ 2
        elseif k == :Δm²₃₁
            cols["dm2_32"] = x .- vec(r.values.Δm²₂₁)
        end
        cols[ascii(k)] = x
    end
    cols["dchi2"] = 2 .* (ref .- vec(r.values.log_posterior))
    haskey(cols, "llh") && (cols["log_likelihood"] = pop!(cols, "llh"))
    first_cols = filter(in(keys(cols)), ["sin2_theta23", "dm2_32", "dcp", "dchi2", "log_likelihood", "log_posterior"])
    CSV.write(path, DataFrame(cols)[:, vcat(first_cols, setdiff(collect(keys(cols)), first_cols))])
end

# draw each confidence level with its own line style (1σ solid, 2σ dashed, 3σ dotted)
function contours!(ax, res, levels; color, label)
    for (lv, ls) in zip(levels, (:solid, :dash, :dot))
        plot!(ax, res, levels = [lv], color = color, linestyle = ls, label = lv == first(levels) ? label : nothing)
    end
end

experiments = (nova = Newtrinos.nova.configure(),)
likelihood = Newtrinos.generate_likelihood(experiments)
p = Newtrinos.get_params(experiments)
priors = Newtrinos.get_priors(experiments)

# as in the paper: solar parameters fixed, 1D Daya Bay constraint sin²2θ₁₃ = 0.0851 ± 0.0024, flat δCP
ref = Newtrinos.nova.ref_params()
p = merge(p, (θ₁₂ = ref.θ₁₂, Δm²₂₁ = ref.Δm²₂₁, θ₁₃ = ref.θ₁₃))
priors = merge(priors, (θ₁₂ = p.θ₁₂, Δm²₂₁ = p.Δm²₂₁, θ₁₃ = Normal(ref.θ₁₃, 0.0024 / (4 * sqrt(0.0851 * (1 - 0.0851)))),
                        δCP = Uniform(0, 2π), θ₂₃ = Uniform(asin(sqrt(0.40)), asin(sqrt(0.64)))))

orderings = (NO = (Δm²₃₂ = (2.25e-3, 2.65e-3), start = (Δm²₃₂ = 2.441e-3, δCP = 0.87π)),
             IO = (Δm²₃₂ = (-2.65e-3, -2.25e-3), start = (Δm²₃₂ = -2.481e-3, δCP = 1.53π)))

# conditional fits per mass ordering (profile likelihood); δCP has several local optima (and optima just across the
# 0/2π boundary of its prior), so each point is fitted from the default start and four δCP starts
const STARTS = [(;), ((δCP = k * π / 4,) for k in (1, 3, 5, 7))...]
results = map(keys(orderings), values(orderings)) do mo, o
    pr = @set priors.Δm²₃₁ = Uniform((o.Δm²₃₂ .+ p.Δm²₂₁)...)
    p0 = merge(p, (Δm²₃₁ = o.start.Δm²₃₂ + p.Δm²₂₁, δCP = o.start.δCP))
    th23dm = Newtrinos.profile(likelihood, pr, OrderedDict(:θ₂₃ => 21, :Δm²₃₁ => 21), p0, cache_dir = "cache/$(mo)_th23dm31", starts = STARTS)
    dcpth23 = Newtrinos.profile(likelihood, pr, OrderedDict(:δCP => 25, :θ₂₃ => 21), p0, cache_dir = "cache/$(mo)_dcpth23", starts = STARTS)
    FileIO.save("results/nova_$(mo).jld2", Dict("th23dm31" => th23dm, "dcpth23" => dcpth23))
    save_csv("results/nova_$(mo)_ssth23_dm32.csv", th23dm); save_csv("results/nova_$(mo)_dcp_ssth23.csv", dcpth23)
    mo => (; th23dm, dcpth23)
end |> NamedTuple

# official Bayesian credible regions shipped with the data release (1, 2, 3σ)
levels = (("06827", :solid), ("09545", :dash), ("09973", :dot))
function official!(ax, pair, mo)
    h5open(Newtrinos.nova.datafile) do f
        g = f["contours/RCDB1D_cond"]
        for (lvl, ls) in levels, k in keys(g)
            occursin("$(pair)_$(mo)_cred_int_$(lvl)_", k) || continue
            lines!(ax, read(g[k]["x"]), read(g[k]["y"]), color = :red, linestyle = ls,
                   label = lvl == "06827" && endswith(k, "_0") ? "NOvA (Bayesian)" : nothing)
        end
    end
end

for mo in keys(orderings)
    r = results[mo].th23dm
    fig = Figure(size = (700, 500))
    ax = Axis(fig[1, 1], title = "NOvA 2024, $(mo): 1σ, 2σ, 3σ", xlabel = "sin²θ₂₃", ylabel = "Δm²₃₂ (10⁻³ eV²)")
    official!(ax, "ssth23dm32", mo)
    contours!(ax, Newtrinos.NewtrinosResult(axes = (ss23 = sin.(r.axes.θ₂₃) .^ 2, dm2 = (r.axes.Δm²₃₁ .- p.Δm²₂₁) .* 1e3), values = r.values),
              [0.6827, 0.9545, 0.9973]; color = :blue, label = "Newtrinos (profile)")
    axislegend(ax, position = :rt)
    save("ours/ssth23_dm32_$(mo).png", fig)

    r = results[mo].dcpth23
    fig = Figure(size = (700, 500))
    ax = Axis(fig[1, 1], title = "NOvA 2024, $(mo): 1σ, 2σ, 3σ", xlabel = "δCP / π", ylabel = "sin²θ₂₃")
    official!(ax, "dcpssth23", mo)
    contours!(ax, Newtrinos.NewtrinosResult(axes = (dcp = r.axes.δCP ./ π, ss23 = sin.(r.axes.θ₂₃) .^ 2), values = r.values), [0.6827, 0.9545, 0.9973]; color = :blue, label = "Newtrinos (profile)")
    xlims!(ax, 0, 2); ylims!(ax, 0.35, 0.67)
    axislegend(ax, position = :lb)
    save("ours/dcp_ssth23_$(mo).png", fig)
end

Provenance

Newtrinos.jl commitc6612c255a (nova)
Run date2026-10-21T15:53:06Z
Wall time4.5 min (128 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/dcp_ssth23_IO.png109.5 kBe031bee5ab33…
ours/dcp_ssth23_NO.png153.7 kBb1f713fea02d…
ours/ssth23_dm32_IO.png126.3 kB0770dbdb5856…
ours/ssth23_dm32_NO.png127.9 kB19b680c940d5…
results/nova_IO.jld2166.2 kB723183ad4600…
results/nova_IO_dcp_ssth23.csv212.6 kB25480810096f…
results/nova_IO_ssth23_dm32.csv168.5 kBe973b1810d67…
results/nova_NO.jld2166.2 kBb18886c679e3…
results/nova_NO_dcp_ssth23.csv211.6 kBb6a2d6802bc4…
results/nova_NO_ssth23_dm32.csv170.2 kB6e26cceca574…