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
1 change: 1 addition & 0 deletions projects/ExampleMonteCarlo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Manifest.toml
34 changes: 34 additions & 0 deletions projects/ExampleMonteCarlo/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name = "ExampleMonteCarlo"
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"
Lattice2D = "e8031020-b7ff-4f1d-bee4-f79aea4cf140"
ParamIO = "938a3ac2-d340-473c-bcf1-88af577e4ccf"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
SweepRunner = "be946ad2-3cb3-4b6e-8f7e-4a5ecc3c255b"

[sources]
# None of these four is in a public registry yet; ParamIO and DataVault resolve from General.
ClassicalMonteCarlo = {rev = "main", url = "https://github.com/sotashimozono/ClassicalMonteCarlo.jl"}
Lattice2D = {rev = "main", url = "https://github.com/sotashimozono/Lattice2D.jl"}
SweepRunner = {rev = "main", url = "https://github.com/QAtlasHub/SweepRunner.jl"}

[compat]
DataVault = "0.7"
ParamIO = "0.4"
julia = "1.10"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]
35 changes: 35 additions & 0 deletions projects/ExampleMonteCarlo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# ExampleMonteCarlo

Ising on a square lattice, swept over size and temperature.

```
configs/ the sweep at three sizes — smoke / debug / production
scripts/ compute.jl (the run), collect.jl (read it back as a table)
batch/ SLURM submission; the only place site-specific lines belong
report/ a SEPARATE environment that draws from the vault
out/ DataVault writes here; gitignored
```

```bash
julia --project=. -e 'using Pkg; Pkg.instantiate()'
julia --project=. scripts/compute.jl configs/smoke.toml # two points, seconds
julia --project=. scripts/collect.jl configs/smoke.toml
sbatch batch/run.sh configs/production.toml # the same command, at size
```

## What this project shows that the other does not

**A real work package.** The physics is `ClassicalMonteCarlo` over a `Lattice2D` lattice, both
reached through the `LatticeCore` interface. `run!(…; load=ExampleMonteCarlo)` ships this project's
module to the workers, and everything it reaches travels with it.

**Why the compute environment has no plotting backend.** It is not hygiene. Both physics packages
carried `Plots` as a hard dependency until it was moved behind an extension; with `load=`, that
would have put a plotting stack on every worker.

**Why every call is qualified.** `ClassicalMonteCarlo` exports `run!` and so does `SweepRunner`, and
they mean different things — one advances a Markov chain, the other dispatches the sweep. Every
module here is imported as `using X: X`, so neither can be called unqualified by accident.

The grids crowd `kbT ≈ 2.269`, the exact 2D Ising transition, because a sweep earns its cost when
the interesting thing happens between two of its points.
14 changes: 14 additions & 0 deletions projects/ExampleMonteCarlo/batch/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash
#SBATCH --job-name=ising-sweep
#SBATCH --nodes=1
#SBATCH --ntasks-per-node=16
#SBATCH --time=02:00:00
#SBATCH --output=out/log/%x-%j.out
#
# Site-specific lines (partition, account, module load) belong here and nowhere else.
# sbatch batch/run.sh configs/production.toml
set -euo pipefail
CONFIG="${1:-configs/smoke.toml}"
mkdir -p out/log
julia --project=. -e 'using Pkg; Pkg.instantiate()'
julia --project=. scripts/compute.jl "$CONFIG"
24 changes: 24 additions & 0 deletions projects/ExampleMonteCarlo/configs/debug.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Ising on a square lattice, swept over size and temperature. The exact transition of the 2D Ising
# model is at kbT/J = 2/ln(1+√2) ≈ 2.269, which is why the grids below crowd there: a sweep is worth
# running when the interesting thing happens between two of its points.
#
# A LIST value is a swept axis; a SCALAR is fixed. In DataKey.params the entries appear under their
# DOTTED names, e.g. "system.kbT".

[study]
project_name = "ising"
total_samples = 1
outdir = "out"

[datavault]
path_keys = ["system.L", "system.kbT"]

[[paramsets]]

[paramsets.system]
L = [8, 12]
kbT = [2.0, 2.27, 2.5]

[paramsets.numerics]
nsteps = 2000
burn = 1000
24 changes: 24 additions & 0 deletions projects/ExampleMonteCarlo/configs/production.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Ising on a square lattice, swept over size and temperature. The exact transition of the 2D Ising
# model is at kbT/J = 2/ln(1+√2) ≈ 2.269, which is why the grids below crowd there: a sweep is worth
# running when the interesting thing happens between two of its points.
#
# A LIST value is a swept axis; a SCALAR is fixed. In DataKey.params the entries appear under their
# DOTTED names, e.g. "system.kbT".

[study]
project_name = "ising"
total_samples = 4
outdir = "out"

[datavault]
path_keys = ["system.L", "system.kbT"]

[[paramsets]]

[paramsets.system]
L = [8, 16, 32]
kbT = [1.6, 1.8, 2.0, 2.15, 2.27, 2.4, 2.6, 2.8, 3.2]

[paramsets.numerics]
nsteps = 20000
burn = 5000
24 changes: 24 additions & 0 deletions projects/ExampleMonteCarlo/configs/smoke.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Ising on a square lattice, swept over size and temperature. The exact transition of the 2D Ising
# model is at kbT/J = 2/ln(1+√2) ≈ 2.269, which is why the grids below crowd there: a sweep is worth
# running when the interesting thing happens between two of its points.
#
# A LIST value is a swept axis; a SCALAR is fixed. In DataKey.params the entries appear under their
# DOTTED names, e.g. "system.kbT".

[study]
project_name = "ising"
total_samples = 1
outdir = "out"

[datavault]
path_keys = ["system.L", "system.kbT"]

[[paramsets]]

[paramsets.system]
L = [8]
kbT = [2.0, 2.5]

[paramsets.numerics]
nsteps = 200
burn = 100
4 changes: 4 additions & 0 deletions projects/ExampleMonteCarlo/note/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# note/

Working notes for this project — what was tried, what the numbers looked like,
why a parameter range was chosen. Not documentation for others; a record for you.
6 changes: 6 additions & 0 deletions projects/ExampleMonteCarlo/out/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# DataVault writes here. Results are not source; keep the directory, drop the data.
*
!.gitignore
!log/
log/*
!log/.gitkeep
Empty file.
20 changes: 20 additions & 0 deletions projects/ExampleMonteCarlo/report/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 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"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"

[sources]
ExampleMonteCarlo = {path = ".."}

[compat]
DataVault = "0.7"
ParamIO = "0.4"
Plots = "1"
julia = "1.10"
17 changes: 17 additions & 0 deletions projects/ExampleMonteCarlo/report/report.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Draw the sweep. Run with `--project=report`, never the compute env.
#
# julia --project=report report/report.jl configs/production.toml

using DataVault: DataVault
using ExampleMonteCarlo: ExampleMonteCarlo
using ParamIO: ParamIO

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

vault = DataVault.Vault(CONFIG; run="phase1", outdir=OUTDIR)
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.
28 changes: 28 additions & 0 deletions projects/ExampleMonteCarlo/scripts/collect.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# The reader side. No plotting dependency, so it runs where the compute did.
#
# julia --project=. scripts/collect.jl configs/smoke.toml

using DataVault: DataVault
using ExampleMonteCarlo: ExampleMonteCarlo
using ParamIO: ParamIO
using Printf

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

vault = DataVault.Vault(CONFIG; run="phase1", outdir=OUTDIR)
rows = ExampleMonteCarlo.summarise(vault)

@printf("\n %-5s %-7s %-11s %-11s %s\n", "L", "kbT", "E/N", "|M|", "Binder")
println(" ────────────────────────────────────────────────────")
for r in rows
@printf(
" %-5d %-7.3f %-11.5f %-11.5f %.4f\n",
r.L,
r.kbT,
r.energy,
r.magnetization,
r.binder
)
end
@printf("\n %d points\n\n", length(rows))
36 changes: 36 additions & 0 deletions projects/ExampleMonteCarlo/scripts/compute.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#==============================================================================
compute.jl — the three-layer driver, over an Ising sweep.

ParamIO : config TOML -> Vector{DataKey} (what to compute)
DataVault : (study, run) -> file storage (where it goes)
SweepRunner: run!(work_fn) -> parallel runtime (do it, lock-safe, resumable)

julia --project=. scripts/compute.jl configs/smoke.toml

Every module is imported as `using X: X` and every call is qualified. Here that is load-bearing
rather than tidy: `ClassicalMonteCarlo` exports `run!` and so does `SweepRunner`, and the two mean
different things — one advances a Markov chain, the other dispatches the sweep.
==============================================================================#

using DataVault: DataVault
using ExampleMonteCarlo: ExampleMonteCarlo
using ParamIO: ParamIO
using SweepRunner: SweepRunner

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

spec = ParamIO.load(CONFIG)
keys = ParamIO.expand(spec)
vault = DataVault.Vault(CONFIG; run="phase1", outdir=OUTDIR)

SweepRunner.init_workers!(; mode=:auto)
opts = SweepRunner.RunOpts(; stop_flag=get(ENV, "PM_STOP_FLAG", nothing))

result = SweepRunner.run!(
ExampleMonteCarlo.work_fn, vault, keys; opts=opts, load=ExampleMonteCarlo
)
@info "phase1 complete" result

ledger = DataVault.build_ledger(vault)
@info "ledger written" ledger
106 changes: 106 additions & 0 deletions projects/ExampleMonteCarlo/src/ExampleMonteCarlo.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
module ExampleMonteCarlo

# This project's own code: one equilibrated chain, and the reduction over finished points.
#
# `work_fn` lives here rather than in `scripts/` because `run!(…; load=…)` ships THIS module to the
# workers. Everything it reaches travels with it, which is why the compute environment is kept free
# of plotting.
#
# Note the qualified calls. `ClassicalMonteCarlo` exports `run!` and so does `SweepRunner`; the sweep
# calls one and a chain calls the other, so neither is imported unqualified anywhere in this project.

using ClassicalMonteCarlo: ClassicalMonteCarlo
using DataVault: DataVault
using Lattice2D: Lattice2D
using Random: MersenneTwister

export work_fn, summarise

"""
work_fn(key) -> Dict{String,Any}

One `(lattice, L, kbT)` point of the Ising sweep: burn in with no observers, then accumulate a
`ThermodynamicObserver` over `nsteps` sweeps and return the thermodynamics.

Returns the payload; it never saves. The runtime writes what comes back, plus the `.done` marker.
"""
function work_fn(key)
L = Int(key.params["system.L"])
kbT = Float64(key.params["system.kbT"])
nsteps = Int(key.params["numerics.nsteps"])
burn = Int(key.params["numerics.burn"])

# `build_lattice` takes the topology TYPE, not an instance:
# `build_lattice(::Type{<:AbstractTopology{2}}, Int, Int)`.
lat = Lattice2D.build_lattice(Lattice2D.Square, L, L)
model = ClassicalMonteCarlo.IsingModel(; J=1.0, h=0.0)
alg = ClassicalMonteCarlo.LocalUpdate(;
rule=ClassicalMonteCarlo.Metropolis(),
selection=ClassicalMonteCarlo.RandomSiteSelection(),
)

N = Lattice2D.num_sites(lat)
rng = MersenneTwister(hash((L, kbT, key.sample)))
grids = rand(rng, [-1, 1], N)

# Burn-in carries no observer: measuring the approach to equilibrium would bias the average.
ClassicalMonteCarlo.run!(
rng,
grids,
lat,
model,
alg,
ClassicalMonteCarlo.AbstractObserver[];
kbT=kbT,
nsteps=burn,
)
obs = ClassicalMonteCarlo.ThermodynamicObserver(; interval=10)
ClassicalMonteCarlo.run!(
rng,
grids,
lat,
model,
alg,
ClassicalMonteCarlo.AbstractObserver[obs];
kbT=kbT,
nsteps=nsteps,
)
res = ClassicalMonteCarlo.get_thermodynamics(obs, kbT, N, model)

return Dict{String,Any}(
"L" => L,
"kbT" => kbT,
"N" => N,
"energy" => res["Energy"],
"magnetization" => res["Magnetization"],
"specific_heat" => res["SpecificHeat"],
"susceptibility" => res["Susceptibility"],
"binder" => res["BinderParam"],
)
end

"""
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.
"""
function summarise(vault)
rows = NamedTuple[]
for key in DataVault.keys(vault; status=:done)
d = DataVault.load(vault, key)
push!(
rows,
(;
L=d["L"],
kbT=d["kbT"],
energy=d["energy"],
magnetization=d["magnetization"],
binder=d["binder"],
),
)
end
return sort(rows; by=r -> (r.L, r.kbT))
end

end # module ExampleMonteCarlo
Loading
Loading