From 7ea1fcc0c7c7a89ea150c27e52e7d522544a0009 Mon Sep 17 00:00:00 2001 From: sotashimozono Date: Wed, 9 Sep 2026 07:28:49 +0000 Subject: [PATCH 1/2] fix: report/ is built on the extension it needs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `report/` was the one seam nothing in this repository exercises, and the two projects had drifted into two different broken states. ExampleSweep/report Pinax, no ParamIO — but report.jl says `using ParamIO` ExampleMonteCarlo/report ParamIO, no Pinax — the render env had no renderer An indirect dependency cannot be `using`-ed by name, so the first of those is a hard error: DataVault brings ParamIO in, but the script naming it needs the line in `[deps]`. The one project that had Pinax could not start its own report. Neither report.jl called Pinax at all. Both ended at `@info` and a comment saying to draw here, so `Pinax.report(vault, recipe)` — the entry point the whole `report/` split exists for — appeared nowhere in the template. That method lives in `PinaxDataVaultExt`, whose triggers are DataVault and ParamIO; the core method is an error stub. A reader had no worked example, and a missing dependency showed up as a message about DataVault rather than as anything about report/. The reduction contract was the deeper mismatch. `summarise(vault)` re-walks the vault, but `Pinax.report` has ALREADY discovered the `:done` keys and loaded each payload before it calls the recipe. Calling the vault method from a recipe reads every file twice and leaves no way into the driver. `summarise` now takes the `(DataKey, Dict)` pairs and a thin `summarise(::DataVault.Vault)` reads them for `scripts/collect.jl`, which is unchanged. One reduction, two entry points, so the table printed on the cluster and the figure drawn afterwards still cannot differ. Also: `julia = "1.12"` in both report envs, because Pinax declares that floor. The compute envs stay at 1.10 deliberately — a dependency only the render side has must not raise the cluster side's floor. Verified by running it, both projects, from a clean vault: ExampleSweep instantiate -> compute.jl -> collect.jl -> report.jl PinaxDataVaultExt precompiled and loaded, n=4, gallery + agent.json written ExampleMonteCarlo same, n=2, Binder and magnetisation pages The sample-averaging branch was checked on a separate collision-free fixture (total_samples=2): 8 raw rows reduce to 4 plotted points, which is the shape a broken grouping would get wrong. The arithmetic itself is not covered — work_fn is deterministic, so the samples behind one key are identical. One caveat found while verifying, NOT fixed here: `ExampleSweep`'s debug.toml and production.toml collide on disk. `ParamIO.diagnose` reports 2 and 10 colliding paths respectively, because the default `format_path` renders floats with `%.2f` and the swept `dt` values differ below that. CI cannot see it — the `project` job runs only smoke.toml, the one config with a single `dt`. Co-Authored-By: Claude Opus 5 (1M context) --- .../ExampleMonteCarlo/report/Project.toml | 12 ++- projects/ExampleMonteCarlo/report/report.jl | 88 ++++++++++++++++-- .../src/ExampleMonteCarlo.jl | 21 +++-- projects/ExampleSweep/report/Project.toml | 14 ++- projects/ExampleSweep/report/report.jl | 89 +++++++++++++++++-- projects/ExampleSweep/src/ExampleSweep.jl | 25 ++++-- 6 files changed, 223 insertions(+), 26 deletions(-) diff --git a/projects/ExampleMonteCarlo/report/Project.toml b/projects/ExampleMonteCarlo/report/Project.toml index 31e8167..455b6c4 100644 --- a/projects/ExampleMonteCarlo/report/Project.toml +++ b/projects/ExampleMonteCarlo/report/Project.toml @@ -2,19 +2,29 @@ # 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. # +# Pinax + DataVault + ParamIO together are what make `Pinax.report` exist: it is defined in +# `PinaxDataVaultExt`, whose triggers are DataVault and ParamIO, and the core Pinax method is an +# error stub. All three are therefore declared, not left to arrive as somebody's indirect dependency. +# # 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, like the compute env's own sources: Pinax is awaiting General registration. +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" +# Pinax's floor, not a preference: it declares `julia = "1.12"`. The compute env one level up stays +# at 1.10 — a dependency only the render side has must not raise the cluster side's floor. +julia = "1.12" diff --git a/projects/ExampleMonteCarlo/report/report.jl b/projects/ExampleMonteCarlo/report/report.jl index 9308794..0310a35 100644 --- a/projects/ExampleMonteCarlo/report/report.jl +++ b/projects/ExampleMonteCarlo/report/report.jl @@ -1,16 +1,90 @@ -# Draw the sweep. Run with `--project=report`, never the compute env. -# -# julia --project=report report/report.jl configs/production.toml +#============================================================================== + report.jl — the vault → figures driver. Run with `--project=report`, never the compute env. + + julia --project=report report/report.jl configs/production.toml + + `Pinax.report(vault, recipe)` is the seam, and it lives in `PinaxDataVaultExt` — an extension that + loads only when Pinax, DataVault and ParamIO are all present. That is why report/Project.toml + declares ParamIO even though DataVault would pull it in regardless; without the extension the core + `Pinax.report` is an error stub, so a missing dependency surfaces as a message about DataVault + rather than as a MethodError. + + The driver is project-independent — discover the `:done` keys, load each payload, hand the + `(DataKey, Dict)` pairs to `recipe`, render the human gallery and agent.json with the vault wired + in. Only `recipe` is this project's. +==============================================================================# using DataVault: DataVault using ExampleMonteCarlo: ExampleMonteCarlo using ParamIO: ParamIO +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 from `(DataKey, Dict)` pairs. `summarise` is `../src/ExampleMonteCarlo.jl`'s — +the same reduction `scripts/collect.jl` prints as a table. +""" +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)) + # `total_samples > 1` puts several independent chains behind one (L, kbT); average them. + 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 get the label, not just the first: an unlabelled series reaches `agent.json` as a + # nameless row, and the machine-readable face is half of what Pinax is for. + 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 diff --git a/projects/ExampleMonteCarlo/src/ExampleMonteCarlo.jl b/projects/ExampleMonteCarlo/src/ExampleMonteCarlo.jl index 5c778bc..ceba4bb 100644 --- a/projects/ExampleMonteCarlo/src/ExampleMonteCarlo.jl +++ b/projects/ExampleMonteCarlo/src/ExampleMonteCarlo.jl @@ -80,15 +80,20 @@ 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. The `pairs` method is the reduction; the `vault` method only reads the +vault and hands it over. + +Two entry points because the two callers arrive holding different things. `scripts/collect.jl` has a +vault and nothing else. `report/report.jl` is handed `(DataKey, Dict)` pairs by `Pinax.report`, which +already walked the vault — a `vault` method there would read the same files twice. One reduction +either way, so the table printed on the cluster and the figure drawn afterwards cannot disagree. """ -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, (; @@ -103,4 +108,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 diff --git a/projects/ExampleSweep/report/Project.toml b/projects/ExampleSweep/report/Project.toml index 8fbe7b3..4cb26e7 100644 --- a/projects/ExampleSweep/report/Project.toml +++ b/projects/ExampleSweep/report/Project.toml @@ -5,11 +5,18 @@ # vault and draws it. The reduction is NOT duplicated here — it lives in # `../src/ExampleSweep.jl` and both sides call it. # +# ParamIO is declared even though DataVault already depends on it. `Pinax.report` +# EXISTS only when Pinax, DataVault and ParamIO are all loaded — they are the trigger +# pair of `PinaxDataVaultExt`, which is where that method is defined; the core Pinax +# method is an error stub. DataVault would pull ParamIO in, but an indirect dependency +# cannot be `using`-ed by name, so report.jl naming it requires this line. +# # 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" @@ -20,5 +27,10 @@ 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" +# Pinax's floor, not a preference: it declares `julia = "1.12"`. The compute env one +# level up still says 1.10, and that is the point — the cluster side is not dragged +# up by a dependency only the render side has. +julia = "1.12" diff --git a/projects/ExampleSweep/report/report.jl b/projects/ExampleSweep/report/report.jl index c10da56..731270f 100644 --- a/projects/ExampleSweep/report/report.jl +++ b/projects/ExampleSweep/report/report.jl @@ -1,16 +1,91 @@ -# Draw from the finished vault. Run with `--project=report`, never the compute env — -# that is the whole reason the two environments are separate. -# -# julia --project=report report/report.jl configs/smoke.toml +#============================================================================== + report.jl — the vault → figures driver. Run with `--project=report`, never the + compute env; that separation is the whole reason `report/` has its own manifest. + + julia --project=report report/report.jl configs/production.toml + + `Pinax.report(vault, recipe)` is the seam. It lives in `PinaxDataVaultExt`, an + extension that loads when Pinax, DataVault and ParamIO are all present — which + is why report/Project.toml declares ParamIO even though DataVault would drag it + in anyway. Without the extension the core `Pinax.report` is an error stub, so a + missing dependency shows up as a message about DataVault, not as a MethodError. + + The DRIVER is project-independent: discover the vault's `:done` keys, load each + payload, hand the `(DataKey, Dict)` pairs to `recipe`, render the gallery (for a + human) and agent.json (for an LLM) with the vault wired in, so the figure cache + tracks each key's `.done` fingerprint and provenance is recorded. Only `recipe` + below is yours. +==============================================================================# using DataVault: DataVault using ExampleSweep: ExampleSweep using ParamIO: ParamIO +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 from `(DataKey, Dict)` pairs. This is the project-specific half; +everything around it is the same in every project. + +`summarise` is `../src/ExampleSweep.jl`'s — the same function `scripts/collect.jl` +prints, so the figure and the cluster's table cannot drift apart. +""" +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)) + # `total_samples > 1` puts several samples behind one (a, dt); average within the key + # rather than drawing each one, so the curve has one point per swept value. + 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 diff --git a/projects/ExampleSweep/src/ExampleSweep.jl b/projects/ExampleSweep/src/ExampleSweep.jl index 7a28781..bb8ff2d 100644 --- a/projects/ExampleSweep/src/ExampleSweep.jl +++ b/projects/ExampleSweep/src/ExampleSweep.jl @@ -9,18 +9,33 @@ 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. The `pairs` method is the reduction; the `vault` method +only reads the vault and hands it over. + +Two entry points because the two callers arrive holding different things. +`scripts/collect.jl` has a vault and nothing else. `report/report.jl` has already been +handed `(DataKey, Dict)` pairs by `Pinax.report`, which walked the vault itself — so a +`vault` method would make it read the same files a second time. Splitting here keeps +ONE reduction: the table printed on the cluster and the figure drawn afterwards 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 From 9252e225bf4c9abab4f7e332927dca2093eade31 Mon Sep 17 00:00:00 2001 From: sotashimozono Date: Wed, 9 Sep 2026 07:44:16 +0000 Subject: [PATCH 2/2] chore: cut the prose out of the example projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Project.toml files were carrying paragraphs that argue for the layout — why the compute env is lean, why report/ is separate, why ParamIO is declared. That argument belongs in README.md, which already makes it; a dependency table is not where a reader goes looking for it. Removed from all four, including the blocks that predate the report/ fix. What stays is the registration note on each `[sources]` line, since that one says when to DELETE the line it sits on. Both report envs are now byte-identical apart from the project's own two lines, which they should have been all along. The report.jl headers were walkthroughs. They are abstracts now — what the file produces, who consumes each output, and the one thing that is project-specific — with the rest moved to the commit that introduced it. The one non-obvious dependency keeps a half-line where it can actually be acted on: on the `using ParamIO` line, not in a Project.toml paragraph. Comment and docstring text only. Verified by comparing the parsed AST before and after with LineNumberNodes dropped and docstring bodies blanked: identical in all four Julia files. Then re-run, because a comment edit is still an unverified edit — both reports render (n=4, n=2), and the repository is format-clean under JuliaFormatter 2.14. Co-Authored-By: Claude Opus 5 (1M context) --- projects/ExampleMonteCarlo/Project.toml | 5 --- .../ExampleMonteCarlo/report/Project.toml | 14 +------- projects/ExampleMonteCarlo/report/report.jl | 31 ++++++---------- .../src/ExampleMonteCarlo.jl | 10 ++---- projects/ExampleSweep/Project.toml | 4 --- projects/ExampleSweep/report/Project.toml | 20 +---------- projects/ExampleSweep/report/report.jl | 35 +++++-------------- projects/ExampleSweep/src/ExampleSweep.jl | 12 ++----- 8 files changed, 27 insertions(+), 104 deletions(-) diff --git a/projects/ExampleMonteCarlo/Project.toml b/projects/ExampleMonteCarlo/Project.toml index d7630d0..12c1621 100644 --- a/projects/ExampleMonteCarlo/Project.toml +++ b/projects/ExampleMonteCarlo/Project.toml @@ -3,11 +3,6 @@ uuid = "00000000-0000-4000-8000-000000000003" version = "0.1.0" authors = ["Your Name "] -# 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" diff --git a/projects/ExampleMonteCarlo/report/Project.toml b/projects/ExampleMonteCarlo/report/Project.toml index 455b6c4..c10b767 100644 --- a/projects/ExampleMonteCarlo/report/Project.toml +++ b/projects/ExampleMonteCarlo/report/Project.toml @@ -1,13 +1,3 @@ -# 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. -# -# Pinax + DataVault + ParamIO together are what make `Pinax.report` exist: it is defined in -# `PinaxDataVaultExt`, whose triggers are DataVault and ParamIO, and the core Pinax method is an -# error stub. All three are therefore declared, not left to arrive as somebody's indirect dependency. -# -# No `name`/`uuid`: an environment, not a package. - [deps] DataVault = "23f5f8f6-b4da-40ee-8c72-c53b6c5de94f" ExampleMonteCarlo = "00000000-0000-4000-8000-000000000003" @@ -17,7 +7,7 @@ Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" [sources] ExampleMonteCarlo = {path = ".."} -# TEMPORARY, like the compute env's own sources: 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] @@ -25,6 +15,4 @@ DataVault = "0.7" ParamIO = "0.4" Pinax = "0.1" Plots = "1" -# Pinax's floor, not a preference: it declares `julia = "1.12"`. The compute env one level up stays -# at 1.10 — a dependency only the render side has must not raise the cluster side's floor. julia = "1.12" diff --git a/projects/ExampleMonteCarlo/report/report.jl b/projects/ExampleMonteCarlo/report/report.jl index 0310a35..0277cd6 100644 --- a/projects/ExampleMonteCarlo/report/report.jl +++ b/projects/ExampleMonteCarlo/report/report.jl @@ -1,22 +1,12 @@ -#============================================================================== - report.jl — the vault → figures driver. Run with `--project=report`, never the compute env. - - julia --project=report report/report.jl configs/production.toml - - `Pinax.report(vault, recipe)` is the seam, and it lives in `PinaxDataVaultExt` — an extension that - loads only when Pinax, DataVault and ParamIO are all present. That is why report/Project.toml - declares ParamIO even though DataVault would pull it in regardless; without the extension the core - `Pinax.report` is an error stub, so a missing dependency surfaces as a message about DataVault - rather than as a MethodError. - - The driver is project-independent — discover the `:done` keys, load each payload, hand the - `(DataKey, Dict)` pairs to `recipe`, render the human gallery and agent.json with the vault wired - in. Only `recipe` is this project's. -==============================================================================# +# 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 @@ -31,8 +21,8 @@ vault = DataVault.Vault(CONFIG; run="phase1") """ recipe(pairs) -Build the document from `(DataKey, Dict)` pairs. `summarise` is `../src/ExampleMonteCarlo.jl`'s — -the same reduction `scripts/collect.jl` prints as a table. +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) @@ -42,13 +32,12 @@ function recipe(pairs) 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)) - # `total_samples > 1` puts several independent chains behind one (L, kbT); average them. + # 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 get the label, not just the first: an unlabelled series reaches `agent.json` as a - # nameless row, and the machine-readable face is half of what Pinax is for. + # 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") diff --git a/projects/ExampleMonteCarlo/src/ExampleMonteCarlo.jl b/projects/ExampleMonteCarlo/src/ExampleMonteCarlo.jl index ceba4bb..0b938ae 100644 --- a/projects/ExampleMonteCarlo/src/ExampleMonteCarlo.jl +++ b/projects/ExampleMonteCarlo/src/ExampleMonteCarlo.jl @@ -83,13 +83,9 @@ end summarise(pairs) -> Vector{NamedTuple} summarise(vault) -> Vector{NamedTuple} -Every finished point, sorted. The `pairs` method is the reduction; the `vault` method only reads the -vault and hands it over. - -Two entry points because the two callers arrive holding different things. `scripts/collect.jl` has a -vault and nothing else. `report/report.jl` is handed `(DataKey, Dict)` pairs by `Pinax.report`, which -already walked the vault — a `vault` method there would read the same files twice. One reduction -either way, 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(pairs::AbstractVector) rows = NamedTuple[] diff --git a/projects/ExampleSweep/Project.toml b/projects/ExampleSweep/Project.toml index 6464d34..c1a210e 100644 --- a/projects/ExampleSweep/Project.toml +++ b/projects/ExampleSweep/Project.toml @@ -3,10 +3,6 @@ uuid = "00000000-0000-4000-8000-000000000001" version = "0.1.0" authors = ["Your Name "] -# 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" diff --git a/projects/ExampleSweep/report/Project.toml b/projects/ExampleSweep/report/Project.toml index 4cb26e7..0e20d7a 100644 --- a/projects/ExampleSweep/report/Project.toml +++ b/projects/ExampleSweep/report/Project.toml @@ -1,18 +1,3 @@ -# 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. -# -# ParamIO is declared even though DataVault already depends on it. `Pinax.report` -# EXISTS only when Pinax, DataVault and ParamIO are all loaded — they are the trigger -# pair of `PinaxDataVaultExt`, which is where that method is defined; the core Pinax -# method is an error stub. DataVault would pull ParamIO in, but an indirect dependency -# cannot be `using`-ed by name, so report.jl naming it requires this line. -# -# No `name`/`uuid`: this is an environment, not a package. - [deps] DataVault = "23f5f8f6-b4da-40ee-8c72-c53b6c5de94f" ExampleSweep = "00000000-0000-4000-8000-000000000001" @@ -22,7 +7,7 @@ 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] @@ -30,7 +15,4 @@ DataVault = "0.7" ParamIO = "0.4" Pinax = "0.1" Plots = "1" -# Pinax's floor, not a preference: it declares `julia = "1.12"`. The compute env one -# level up still says 1.10, and that is the point — the cluster side is not dragged -# up by a dependency only the render side has. julia = "1.12" diff --git a/projects/ExampleSweep/report/report.jl b/projects/ExampleSweep/report/report.jl index 731270f..2704752 100644 --- a/projects/ExampleSweep/report/report.jl +++ b/projects/ExampleSweep/report/report.jl @@ -1,25 +1,12 @@ -#============================================================================== - report.jl — the vault → figures driver. Run with `--project=report`, never the - compute env; that separation is the whole reason `report/` has its own manifest. - - julia --project=report report/report.jl configs/production.toml - - `Pinax.report(vault, recipe)` is the seam. It lives in `PinaxDataVaultExt`, an - extension that loads when Pinax, DataVault and ParamIO are all present — which - is why report/Project.toml declares ParamIO even though DataVault would drag it - in anyway. Without the extension the core `Pinax.report` is an error stub, so a - missing dependency shows up as a message about DataVault, not as a MethodError. - - The DRIVER is project-independent: discover the vault's `:done` keys, load each - payload, hand the `(DataKey, Dict)` pairs to `recipe`, render the gallery (for a - human) and agent.json (for an LLM) with the vault wired in, so the figure cache - tracks each key's `.done` fingerprint and provenance is recorded. Only `recipe` - below is yours. -==============================================================================# +# 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 ExampleSweep: ExampleSweep -using ParamIO: ParamIO +using ParamIO: ParamIO # also a PinaxDataVaultExt trigger; without it `Pinax.report` is a stub using Pinax using Plots @@ -33,11 +20,8 @@ vault = DataVault.Vault(CONFIG; run="phase1") """ recipe(pairs) -Build the document from `(DataKey, Dict)` pairs. This is the project-specific half; -everything around it is the same in every project. - -`summarise` is `../src/ExampleSweep.jl`'s — the same function `scripts/collect.jl` -prints, so the figure and the cluster's table cannot drift apart. +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) @@ -52,8 +36,7 @@ function recipe(pairs) 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)) - # `total_samples > 1` puts several samples behind one (a, dt); average within the key - # rather than drawing each one, so the curve has one point per swept value. + # 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 ] diff --git a/projects/ExampleSweep/src/ExampleSweep.jl b/projects/ExampleSweep/src/ExampleSweep.jl index bb8ff2d..bd08a8c 100644 --- a/projects/ExampleSweep/src/ExampleSweep.jl +++ b/projects/ExampleSweep/src/ExampleSweep.jl @@ -12,15 +12,9 @@ export summarise summarise(pairs) -> Vector{NamedTuple} summarise(vault) -> Vector{NamedTuple} -Reduce every finished point. The `pairs` method is the reduction; the `vault` method -only reads the vault and hands it over. - -Two entry points because the two callers arrive holding different things. -`scripts/collect.jl` has a vault and nothing else. `report/report.jl` has already been -handed `(DataKey, Dict)` pairs by `Pinax.report`, which walked the vault itself — so a -`vault` method would make it read the same files a second time. Splitting here keeps -ONE reduction: the table printed on the cluster and the figure drawn afterwards cannot -report different numbers. +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. """