Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions projects/ExampleMonteCarlo/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@ uuid = "00000000-0000-4000-8000-000000000003"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]

# COMPUTE env. No plotting backend and no Pinax — those are in `report/`, which reads the same vault
# off disk. That separation is the reason this project can exist at all: the physics packages below
# were each carrying Plots as a hard dependency until it was moved behind an extension, and a
# `load=` of the work module ships whatever it pulls to every worker.

[deps]
ClassicalMonteCarlo = "faaa8aa3-7fc9-41cf-84af-9d0c24f78989"
DataVault = "23f5f8f6-b4da-40ee-8c72-c53b6c5de94f"
Expand Down
12 changes: 5 additions & 7 deletions projects/ExampleMonteCarlo/report/Project.toml
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
# report/ — the render environment. SEPARATE from the compute env one level up: that one runs on a
# cluster and carries no plotting backend, while this reads the same on-disk vault and draws it.
# The reduction is not duplicated here — it is `../src/ExampleMonteCarlo.jl`, and both sides call it.
#
# No `name`/`uuid`: an environment, not a package.

[deps]
DataVault = "23f5f8f6-b4da-40ee-8c72-c53b6c5de94f"
ExampleMonteCarlo = "00000000-0000-4000-8000-000000000003"
ParamIO = "938a3ac2-d340-473c-bcf1-88af577e4ccf"
Pinax = "e782a80a-1ac9-479e-ba2b-0f3433ef5b4f"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"

[sources]
ExampleMonteCarlo = {path = ".."}
# TEMPORARY: Pinax is awaiting its first General registration. Delete this line once it lands.
Pinax = {rev = "main", url = "https://github.com/QAtlasHub/Pinax.jl.git"}

[compat]
DataVault = "0.7"
ParamIO = "0.4"
Pinax = "0.1"
Plots = "1"
julia = "1.10"
julia = "1.12"
75 changes: 69 additions & 6 deletions projects/ExampleMonteCarlo/report/report.jl
Original file line number Diff line number Diff line change
@@ -1,16 +1,79 @@
# Draw the sweep. Run with `--project=report`, never the compute env.
# report.jl — the finished vault, rendered twice: an HTML gallery for a human and an
# `agent.json` for an LLM. `Pinax.report` discovers the `:done` keys, loads each payload and
# hands the `(DataKey, Dict)` pairs to `recipe`; only `recipe` is this project's.
#
# julia --project=report report/report.jl configs/production.toml

using DataVault: DataVault
using ExampleMonteCarlo: ExampleMonteCarlo
using ParamIO: ParamIO
using ParamIO: ParamIO # also a PinaxDataVaultExt trigger; without it `Pinax.report` is a stub
using Pinax
using Plots

ENV["GKSwstype"] = get(ENV, "GKSwstype", "100") # headless GR: renders with no display attached
gr()

const CONFIG = get(ARGS, 1, joinpath(@__DIR__, "..", "configs", "smoke.toml"))
const TC = 2 / log(1 + sqrt(2)) # the exact 2D Ising transition, kbT/J ≈ 2.269

vault = DataVault.Vault(CONFIG; run="phase1")
rows = ExampleMonteCarlo.summarise(vault) # the same reduction scripts/collect.jl uses

@info "reporting over" n = length(rows)
# The Binder cumulant's crossing in L is where the transition shows up — draw it here, with Plots,
# which is a dependency of THIS environment and of nothing that runs on a compute node.
"""
recipe(pairs)

Build the document. `summarise` is `../src/ExampleMonteCarlo.jl`'s, so this and
`scripts/collect.jl` cannot report different numbers.
"""
function recipe(pairs)
rows = ExampleMonteCarlo.summarise(pairs)

binder = plot(; xlabel="kbT / J", ylabel="Binder cumulant U₄", legend=:bottomleft)
mag = plot(; xlabel="kbT / J", ylabel="|magnetization| per spin", legend=:topright)
for L in sort(unique(r.L for r in rows))
sel = filter(r -> r.L == L, rows)
Ts = sort(unique(r.kbT for r in sel))
# one point per swept `kbT`, so `total_samples > 1` averages the chains rather than overplots
avg(f) = [(v=[f(r) for r in sel if r.kbT == T]; sum(v) / length(v)) for T in Ts]
plot!(binder, Ts, avg(r -> r.binder); marker=:circle, label="L = $L")
plot!(mag, Ts, avg(r -> r.magnetization); marker=:circle, label="L = $L")
end
# both labelled: an unlabelled series reaches `agent.json` as a nameless row
vline!(binder, [TC]; ls=:dash, c=:red, label="exact Tc")
vline!(mag, [TC]; ls=:dash, c=:red, label="exact Tc")

@page :ising "ExampleMonteCarlo — 2D Ising on a square lattice" summary = "A Metropolis sweep over lattice size and temperature, read back out of the vault." begin
@section :binder "Binder cumulant" begin
@desc md"""
$U_4$ is dimensionless at the critical point, so the curves for different $L$ cross
there and the crossing locates $T_c$ without an extrapolation. The dashed line is the
exact $k_BT/J = 2/\ln(1+\sqrt{2}) \approx 2.269$ — it is a check on the sweep, not an
output of it. A single $L$ draws one curve and no crossing; that needs `production.toml`.
"""
@figure binder caption = "One curve per lattice size; they should meet at the dashed line."
end
@section :mag "Order parameter" begin
@desc md"The magnetisation falls through the transition, and the fall sharpens with $L$ — the finite-size rounding is the thing the sweep is measuring."
@figure mag caption = "|m| per spin against temperature."
end
@section :table "The points themselves" begin
@desc md"The same rows `scripts/collect.jl` prints, so what an LLM reads out of `agent.json` is what the cluster printed."
@table (
L=[r.L for r in rows],
kbT=[r.kbT for r in rows],
energy=[r.energy for r in rows],
magnetization=[r.magnetization for r in rows],
binder=[r.binder for r in rows],
) caption = "Every finished point."
end
end
end

res = Pinax.report(
vault,
recipe;
title="ExampleMonteCarlo",
out=joinpath(vault.outdir, "report"),
study="phase1",
)

@info "report written" n = res.n gallery = res.gallery agent = res.agent
17 changes: 12 additions & 5 deletions projects/ExampleMonteCarlo/src/ExampleMonteCarlo.jl
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,16 @@ function work_fn(key)
end

"""
summarise(pairs) -> Vector{NamedTuple}
summarise(vault) -> Vector{NamedTuple}

Every finished point, sorted. `scripts/collect.jl` and `report/report.jl` both call this, so the
table printed on the cluster and the figure drawn afterwards cannot disagree.
Every finished point, sorted. `pairs` is the reduction; the `vault` method only reads them off
disk first — `Pinax.report` already holds the pairs, `scripts/collect.jl` does not. One reduction
either way, so the two cannot report different numbers.
"""
function summarise(vault)
function summarise(pairs::AbstractVector)
rows = NamedTuple[]
for key in DataVault.keys(vault; status=:done)
d = DataVault.load(vault, key)
for (_, d) in pairs
push!(
rows,
(;
Expand All @@ -103,4 +104,10 @@ function summarise(vault)
return sort(rows; by=r -> (r.L, r.kbT))
end

function summarise(vault::DataVault.Vault)
return summarise([
(k, DataVault.load(vault, k)) for k in DataVault.keys(vault; status=:done)
])
end

end # module ExampleMonteCarlo
4 changes: 0 additions & 4 deletions projects/ExampleSweep/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ uuid = "00000000-0000-4000-8000-000000000001"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]

# COMPUTE env. Kept lean on purpose: no plotting, no Pinax. Those live in
# `report/`, which reads the same DataVault off disk rather than sharing this
# environment — so a figure dependency can never pull itself onto a compute node.

[deps]
DataVault = "23f5f8f6-b4da-40ee-8c72-c53b6c5de94f"
MyModule = "00000000-0000-4000-8000-000000000000"
Expand Down
16 changes: 5 additions & 11 deletions projects/ExampleSweep/report/Project.toml
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
# report/ — the render environment.
#
# SEPARATE from the compute env one level up, deliberately: `scripts/compute.jl`
# runs with lean deps and may run on a cluster, while this reads the same on-disk
# vault and draws it. The reduction is NOT duplicated here — it lives in
# `../src/ExampleSweep.jl` and both sides call it.
#
# No `name`/`uuid`: this is an environment, not a package.

[deps]
DataVault = "23f5f8f6-b4da-40ee-8c72-c53b6c5de94f"
ExampleSweep = "00000000-0000-4000-8000-000000000001"
ParamIO = "938a3ac2-d340-473c-bcf1-88af577e4ccf"
Pinax = "e782a80a-1ac9-479e-ba2b-0f3433ef5b4f"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"

[sources]
ExampleSweep = {path = ".."}
# TEMPORARY, like SweepRunner one level up: Pinax is awaiting General registration.
# TEMPORARY: Pinax is awaiting its first General registration. Delete this line once it lands.
Pinax = {rev = "main", url = "https://github.com/QAtlasHub/Pinax.jl.git"}

[compat]
DataVault = "0.7"
ParamIO = "0.4"
Pinax = "0.1"
Plots = "1"
julia = "1.10"
julia = "1.12"
72 changes: 65 additions & 7 deletions projects/ExampleSweep/report/report.jl
Original file line number Diff line number Diff line change
@@ -1,16 +1,74 @@
# Draw from the finished vault. Run with `--project=report`, never the compute env —
# that is the whole reason the two environments are separate.
# report.jl — the finished vault, rendered twice: an HTML gallery for a human and an
# `agent.json` for an LLM. `Pinax.report` discovers the `:done` keys, loads each payload and
# hands the `(DataKey, Dict)` pairs to `recipe`; only `recipe` is this project's.
#
# julia --project=report report/report.jl configs/smoke.toml
# julia --project=report report/report.jl configs/production.toml

using DataVault: DataVault
using ExampleSweep: ExampleSweep
using ParamIO: ParamIO
using ParamIO: ParamIO # also a PinaxDataVaultExt trigger; without it `Pinax.report` is a stub
using Pinax
using Plots

ENV["GKSwstype"] = get(ENV, "GKSwstype", "100") # headless GR: renders with no display attached
gr()

const CONFIG = get(ARGS, 1, joinpath(@__DIR__, "..", "configs", "smoke.toml"))

vault = DataVault.Vault(CONFIG; run="phase1")
rows = ExampleSweep.summarise(vault) # the same reduction scripts/collect.jl uses

@info "reporting over" n = length(rows)
# Draw here — Pinax and a plotting backend are dependencies of THIS environment.
"""
recipe(pairs)

Build the document. `summarise` is `../src/ExampleSweep.jl`'s, so this and
`scripts/collect.jl` cannot report different numbers.
"""
function recipe(pairs)
rows = ExampleSweep.summarise(pairs)

fig = plot(;
xscale=:log10,
yscale=:log10,
xlabel="step size dt",
ylabel="relative error at t = 1",
legend=:bottomright,
)
for a in sort(unique(r.a for r in rows))
sel = filter(r -> r.a == a, rows)
dts = sort(unique(r.dt for r in sel))
# one point per swept `dt`, so `total_samples > 1` averages rather than overplots
errs = [
(v=[r.rel_error for r in sel if r.dt == d]; sum(v) / length(v)) for d in dts
]
plot!(fig, dts, errs; marker=:circle, label="a = $a")
end

@page :convergence "ExampleSweep — explicit Euler on x' = -a x" summary = "Relative error of the placeholder kernel against its exact solution, over the swept step size." begin
@section :rate "Error vs step size" begin
@desc md"""
Explicit Euler is first order, so on these log axes each curve should approach slope 1:
halving `dt` halves the error. A curve that flattens has hit round-off rather than
discretisation, and one that steepens is a bug — this figure is what tells them apart.
"""
@figure fig caption = "One curve per decay rate `a`; each point is one swept `dt`."
end
@section :table "The points themselves" begin
@desc md"The same rows `scripts/collect.jl` prints, so the number an LLM reads out of `agent.json` is the number on the cluster's stdout."
@table (
a=[r.a for r in rows],
dt=[r.dt for r in rows],
rel_error=[r.rel_error for r in rows],
) caption = "Every finished point."
end
end
end

res = Pinax.report(
vault,
recipe;
title="ExampleSweep",
out=joinpath(vault.outdir, "report"),
study="phase1",
)

@info "report written" n = res.n gallery = res.gallery agent = res.agent
19 changes: 14 additions & 5 deletions projects/ExampleSweep/src/ExampleSweep.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,27 @@ using DataVault: DataVault
export summarise

"""
summarise(pairs) -> Vector{NamedTuple}
summarise(vault) -> Vector{NamedTuple}

Read every finished point back and reduce it. Replace the body; keep the shape,
so `scripts/collect.jl` and `report/report.jl` stay in agreement.
Reduce every finished point. `pairs` is the reduction; the `vault` method only reads
them off disk first — `Pinax.report` already holds the pairs, `scripts/collect.jl` does
not. One reduction either way, so the two cannot report different numbers.

Replace the body; keep the shape.
"""
function summarise(vault)
function summarise(pairs::AbstractVector)
rows = NamedTuple[]
for key in DataVault.keys(vault; status=:done)
d = DataVault.load(vault, key)
for (_, d) in pairs
push!(rows, (; a=d["a"], dt=d["dt"], rel_error=d["rel_error"]))
end
return sort(rows; by=r -> (r.a, r.dt))
end

function summarise(vault::DataVault.Vault)
return summarise([
(k, DataVault.load(vault, k)) for k in DataVault.keys(vault; status=:done)
])
end

end # module ExampleSweep
Loading