Derive a unique per-job seed for minicalosim shard generation

Concurrent job launches in create_root_files.py can start within the
same wall-clock second, and minicalosim's default seed falls back to
time(NULL) in that case — so two "independent" shards could silently
get identical RNG state and produce byte-identical physics. Requires
the companion MINICALOSIM_SEED env-var support in the minicalosim repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 10:08:04 +02:00
parent f387178dbf
commit 5dc086e35a
2 changed files with 63 additions and 3 deletions
+19 -2
View File
@@ -25,6 +25,7 @@ import shutil
import subprocess
import sys
import uuid
import zlib
from dataclasses import dataclass
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
@@ -104,6 +105,20 @@ def plan_jobs(
return jobs
def job_seed(kind: str, gen: str, job: SimJob) -> int:
"""Deterministic RNG seed for one sim job, unique per (kind, gen, detector, config, shard).
Jobs run concurrently (ThreadPoolExecutor below) and can start within the
same wall-clock second; minicalosim's default seed falls back to
time(NULL) in that case, so two concurrently-launched jobs can silently
get identical RNG state and produce byte-identical physics despite
landing in separate shard files. Deriving the seed from the full job
identity instead keeps it both unique and reproducible.
"""
key = f"{kind}|{gen}|{job.detector}|{job.config or ''}|{job.shard_index}"
return zlib.crc32(key.encode()) & 0x7FFFFFFF
def run_job(
job: SimJob,
executable: Path,
@@ -124,7 +139,8 @@ def run_job(
cmd.append(job.config)
cmd.append(str(events_per_file))
result = subprocess.run(cmd, cwd=workdir, capture_output=True, text=True)
env = dict(os.environ, MINICALOSIM_SEED=str(job_seed(kind, gen, job)))
result = subprocess.run(cmd, cwd=workdir, capture_output=True, text=True, env=env)
if result.returncode != 0:
return JobResult(
@@ -254,7 +270,8 @@ def run_make_root(
/ job.detector
/ f"shard-{job.shard_index:03d}.root"
)
print(f" {' '.join(cmd)} -> {dest}")
seed = job_seed(kind, gen, job)
print(f" MINICALOSIM_SEED={seed} {' '.join(cmd)} -> {dest}")
if not execute:
print("\nDry run only — pass --execute to apply.")
+44 -1
View File
@@ -9,6 +9,7 @@ from scripts import create_root_files
parse_detector_spec = create_root_files.parse_detector_spec
next_shard_index = create_root_files.next_shard_index
plan_jobs = create_root_files.plan_jobs
job_seed = create_root_files.job_seed
run_job = create_root_files.run_job
run_all = create_root_files.run_all
SimJob = create_root_files.SimJob
@@ -35,7 +36,13 @@ start = time.time()
time.sleep({sleep})
end = time.time()
payload = json.dumps(
{{"argv": sys.argv[1:], "cwd": os.getcwd(), "start": start, "end": end}}
{{
"argv": sys.argv[1:],
"cwd": os.getcwd(),
"start": start,
"end": end,
"seed": os.environ.get("MINICALOSIM_SEED"),
}}
)
for i in range({output_count}):
with open(f"out_{{i}}.root", "w") as f:
@@ -130,6 +137,42 @@ def test_plan_jobs_multiple_detectors_each_start_independently(tmp_path):
assert by_detector["sampling_fe_scint"] == [0, 1]
def test_job_seed_deterministic():
job = SimJob(detector="pbwo4", config=None, shard_index=3)
assert job_seed("steps", "gen1", job) == job_seed("steps", "gen1", job)
def test_job_seed_varies_by_shard_index():
a = SimJob(detector="pbwo4", config=None, shard_index=0)
b = SimJob(detector="pbwo4", config=None, shard_index=1)
assert job_seed("steps", "gen1", a) != job_seed("steps", "gen1", b)
def test_job_seed_varies_by_detector():
a = SimJob(detector="pbwo4", config=None, shard_index=0)
b = SimJob(detector="sampling_pb_scint", config="pb_scint", shard_index=0)
assert job_seed("steps", "gen1", a) != job_seed("steps", "gen1", b)
def test_job_seed_varies_by_gen():
job = SimJob(detector="pbwo4", config=None, shard_index=0)
assert job_seed("steps", "gen1", job) != job_seed("steps", "gen2", job)
def test_run_job_passes_deterministic_seed_env_var(tmp_path):
fake = _write_fake_executable(tmp_path / "fake_exe.py")
(tmp_path / "raw" / "steps" / "gen1").mkdir(parents=True)
tmp_root = tmp_path / ".sim-tmp"
tmp_root.mkdir()
job = SimJob(detector="pbwo4", config=None, shard_index=5)
result = run_job(job, fake, 10000, tmp_path, "steps", "gen1", tmp_root)
assert result.dest is not None
payload = json.loads(result.dest.read_text())
assert payload["seed"] == str(job_seed("steps", "gen1", job))
def test_run_job_moves_output_to_correct_shard_path(tmp_path):
fake = _write_fake_executable(tmp_path / "fake_exe.py")
gen_dir = tmp_path / "raw" / "steps" / "gen1"