Skip to content

Commit edb4161

Browse files
committed
add CLI with simulate subcommand, fix heat capacity, mean cluster size
1 parent d1ce9a9 commit edb4161

File tree

4 files changed

+243
-15
lines changed

4 files changed

+243
-15
lines changed

README.md

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,29 +32,35 @@ The following algorithms are currently supported:
3232

3333
## Quickstart
3434

35+
Run simulations directly from the terminal:
36+
37+
```bash
38+
# 2D ferromagnet with cluster updates and parallel tempering
39+
peapods simulate --shape 32 32 --temp-min 1.5 --temp-max 3.0 \
40+
--n-sweeps 5000 --cluster-interval 1 --pt-interval 1 --n-replicas 2
41+
42+
# 3D spin glass with Houdayer ICM
43+
peapods simulate --shape 8 8 8 --couplings bimodal \
44+
--temp-min 0.8 --temp-max 1.4 --n-temps 24 --n-sweeps 10000 \
45+
--pt-interval 1 --houdayer-interval 1 --n-replicas 4
46+
47+
# Save full results to .npz
48+
peapods simulate --shape 16 16 --temp-min 1.5 --temp-max 3.0 \
49+
--n-sweeps 5000 --pt-interval 1 --n-replicas 2 -o results.npz
50+
```
51+
52+
For full control over geometry and parameters, use the Python API directly:
53+
3554
```python
3655
import numpy as np
3756
from peapods import Ising
3857

39-
# 2D ferromagnet with cluster updates and parallel tempering
40-
model = Ising((32, 32), temperatures=np.linspace(1.5, 3.0, 32), n_replicas=2)
41-
model.sample(n_sweeps=5000, sweep_mode="metropolis",
42-
cluster_update_interval=1, pt_interval=1)
43-
print(model.binder_cumulant)
44-
4558
# Triangular lattice ferromagnet (T_c = 4/ln(3) ≈ 3.641)
4659
tri = Ising((32, 32), temperatures=np.linspace(3.0, 4.2, 32),
4760
n_replicas=2, neighbor_offsets=[[1, 0], [0, 1], [1, -1]])
4861
tri.sample(n_sweeps=5000, sweep_mode="metropolis",
4962
cluster_update_interval=1, pt_interval=1)
5063
print(tri.binder_cumulant)
51-
52-
# 3D spin glass with Houdayer ICM
53-
sg = Ising((8, 8, 8), couplings="bimodal",
54-
temperatures=np.linspace(0.8, 1.4, 24), n_replicas=4)
55-
sg.sample(n_sweeps=10000, sweep_mode="metropolis",
56-
pt_interval=1, houdayer_interval=1)
57-
print(sg.sg_binder)
5864
```
5965

6066
For a more complete example, check out [example.py](example.py).

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ license = {text = "MIT"}
1111
requires-python = ">=3.10"
1212
dependencies = ["numpy"]
1313

14+
[project.scripts]
15+
peapods = "peapods.cli:main"
16+
1417
[tool.maturin]
1518
python-source = "python"
1619
module-name = "peapods._core"

python/peapods/cli.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import argparse
2+
import json
3+
import sys
4+
5+
import numpy as np
6+
7+
from peapods import Ising
8+
9+
10+
def build_parser():
11+
parser = argparse.ArgumentParser(
12+
prog="peapods",
13+
description="Ising Monte Carlo simulations from the command line.",
14+
)
15+
sub = parser.add_subparsers(dest="command")
16+
17+
sim = sub.add_parser("simulate", help="Run an Ising simulation")
18+
19+
# Model setup
20+
sim.add_argument(
21+
"--shape",
22+
type=int,
23+
nargs="+",
24+
required=True,
25+
help="Lattice dimensions, e.g. --shape 32 32",
26+
)
27+
sim.add_argument(
28+
"--couplings",
29+
default="ferro",
30+
choices=["ferro", "bimodal", "gaussian"],
31+
help="Coupling distribution (default: ferro)",
32+
)
33+
sim.add_argument(
34+
"--geometry",
35+
choices=["triangular", "tri", "fcc", "bcc"],
36+
help="Named lattice geometry",
37+
)
38+
sim.add_argument(
39+
"--neighbor-offsets",
40+
type=str,
41+
default=None,
42+
help="JSON list of offset vectors, e.g. '[[1,0],[0,1]]'",
43+
)
44+
sim.add_argument("--n-replicas", type=int, default=1)
45+
sim.add_argument("--n-disorder", type=int, default=1)
46+
47+
# Temperature grid
48+
sim.add_argument("--temp-min", type=float, required=True)
49+
sim.add_argument("--temp-max", type=float, required=True)
50+
sim.add_argument("--n-temps", type=int, default=32)
51+
sim.add_argument(
52+
"--temp-scale",
53+
default="linear",
54+
choices=["linear", "log"],
55+
help="Temperature spacing (default: linear)",
56+
)
57+
58+
# Sampling
59+
sim.add_argument("--n-sweeps", type=int, required=True)
60+
sim.add_argument(
61+
"--sweep-mode", default="metropolis", choices=["metropolis", "gibbs"]
62+
)
63+
sim.add_argument(
64+
"--cluster-interval",
65+
type=int,
66+
default=None,
67+
help="Cluster update every N sweeps",
68+
)
69+
sim.add_argument("--cluster-mode", default="sw", choices=["sw", "wolff"])
70+
sim.add_argument(
71+
"--pt-interval",
72+
type=int,
73+
default=None,
74+
help="Parallel tempering every N sweeps",
75+
)
76+
sim.add_argument(
77+
"--houdayer-interval",
78+
type=int,
79+
default=None,
80+
help="Houdayer moves every N sweeps (requires n_replicas >= 2)",
81+
)
82+
sim.add_argument(
83+
"--houdayer-mode", default="houdayer", choices=["houdayer", "jorg", "cmr"]
84+
)
85+
sim.add_argument("--overlap-cluster-mode", default="wolff", choices=["wolff", "sw"])
86+
sim.add_argument("--warmup-ratio", type=float, default=0.25)
87+
sim.add_argument(
88+
"--collect-csd",
89+
action="store_true",
90+
help="Collect FK cluster size distribution",
91+
)
92+
93+
# Output
94+
sim.add_argument(
95+
"-o", "--output", type=str, default=None, help="Save full results to .npz file"
96+
)
97+
98+
return parser
99+
100+
101+
def run_simulate(args):
102+
if args.temp_scale == "linear":
103+
temperatures = np.linspace(args.temp_min, args.temp_max, args.n_temps)
104+
else:
105+
temperatures = np.geomspace(args.temp_min, args.temp_max, args.n_temps)
106+
107+
neighbor_offsets = None
108+
if args.neighbor_offsets is not None:
109+
neighbor_offsets = json.loads(args.neighbor_offsets)
110+
111+
model = Ising(
112+
tuple(args.shape),
113+
couplings=args.couplings,
114+
temperatures=temperatures,
115+
n_replicas=args.n_replicas,
116+
n_disorder=args.n_disorder,
117+
neighbor_offsets=neighbor_offsets,
118+
geometry=args.geometry,
119+
)
120+
121+
result = model.sample(
122+
args.n_sweeps,
123+
sweep_mode=args.sweep_mode,
124+
cluster_update_interval=args.cluster_interval,
125+
cluster_mode=args.cluster_mode,
126+
pt_interval=args.pt_interval,
127+
houdayer_interval=args.houdayer_interval,
128+
houdayer_mode=args.houdayer_mode,
129+
overlap_cluster_mode=args.overlap_cluster_mode,
130+
warmup_ratio=args.warmup_ratio,
131+
collect_csd=args.collect_csd,
132+
)
133+
134+
has_overlap = hasattr(model, "sg_binder")
135+
has_csd = hasattr(model, "mean_cluster_size")
136+
print_table(model, has_overlap, has_csd)
137+
138+
if args.output:
139+
save_dict = {
140+
"temperatures": model.temperatures,
141+
"binder_cumulant": model.binder_cumulant,
142+
"heat_capacity": model.heat_capacity,
143+
}
144+
for key in (
145+
"mags",
146+
"mags2",
147+
"mags4",
148+
"energies",
149+
"energies2",
150+
"overlap",
151+
"overlap2",
152+
"overlap4",
153+
):
154+
if key in result:
155+
save_dict[key] = result[key]
156+
if has_overlap:
157+
save_dict["sg_binder"] = model.sg_binder
158+
if has_csd:
159+
save_dict["mean_cluster_size"] = model.mean_cluster_size
160+
if hasattr(model, "fk_csd"):
161+
save_dict["fk_csd"] = model.fk_csd
162+
np.savez(args.output, **save_dict)
163+
print(f"\nResults saved to {args.output}")
164+
165+
166+
def print_table(model, has_overlap, has_csd):
167+
temps = model.temperatures
168+
energy = model.energies_avg
169+
binder = model.binder_cumulant
170+
hcap = model.heat_capacity
171+
172+
cols = [f"{'T':>8}", f"{'E':>10}", f"{'Binder':>10}", f"{'C_v':>10}"]
173+
if has_overlap:
174+
cols.append(f"{'Overlap Binder':>15}")
175+
if has_csd:
176+
cols.append(f"{'Cluster Size':>14}")
177+
178+
header = " ".join(cols)
179+
print(header)
180+
print("-" * len(header))
181+
182+
for i in range(len(temps)):
183+
row = [
184+
f"{temps[i]:8.4f}",
185+
f"{energy[i]:10.6f}",
186+
f"{binder[i]:10.6f}",
187+
f"{hcap[i]:10.4f}",
188+
]
189+
if has_overlap:
190+
row.append(f"{model.sg_binder[i]:15.6f}")
191+
if has_csd:
192+
row.append(f"{model.mean_cluster_size[i]:14.2f}")
193+
print(" ".join(row))
194+
195+
196+
def main():
197+
parser = build_parser()
198+
args = parser.parse_args()
199+
200+
if args.command is None:
201+
parser.print_help()
202+
sys.exit(1)
203+
204+
if args.command == "simulate":
205+
run_simulate(args)
206+
207+
208+
if __name__ == "__main__":
209+
main()

python/peapods/spin_models.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def __init__(
7676
neighbor_offsets = GEOMETRIES[geometry]
7777

7878
self.lattice_shape = tuple(lattice_shape)
79+
self.n_spins = int(np.prod(lattice_shape))
7980
self.n_dims = len(lattice_shape)
8081
self.n_neighbors = len(neighbor_offsets) if neighbor_offsets else self.n_dims
8182
self.temperatures = temperatures.copy().astype(np.float32)
@@ -178,8 +179,10 @@ def sample(
178179

179180
self.binder_cumulant = 1 - self.mags4 / (3 * self.mags2**2)
180181
self.heat_capacity = (
181-
self.energies2_avg - self.energies_avg**2
182-
) / self.temperatures**2
182+
self.n_spins
183+
* (self.energies2_avg - self.energies_avg**2)
184+
/ self.temperatures**2
185+
)
183186

184187
if "overlap2" in result:
185188
self.overlap = result["overlap"]
@@ -189,6 +192,13 @@ def sample(
189192

190193
if "fk_csd" in result:
191194
self.fk_csd = result["fk_csd"]
195+
mcs = np.empty(self.n_temps)
196+
for t, h in enumerate(self.fk_csd):
197+
s = np.arange(len(h))
198+
sh = s * h
199+
n_sites = sh.sum()
200+
mcs[t] = (s * sh).sum() / n_sites if n_sites > 0 else 0.0
201+
self.mean_cluster_size = mcs
192202

193203
return result
194204

0 commit comments

Comments
 (0)