|
RadCluster
Graph-based cluster dynamics for irradiated materials
|
Functions | |
| str | sha256_file (Path p) |
| str | git_sha () |
| tuple[list[dict], dict] | read_design (Path path) |
| apply_theta (sim, dict spec, dict row, dict cond) | |
| apply_bin_config (sim, cfg) | |
| bin_layout (sim, cfg) | |
| evaluate (args) | |
| per_size_populations (res, sim, cfg, idx=-1) | |
| observe (res, sim, cfg, d_min_nm) | |
| _weighted_pattern (of, weights) | |
| dict | _host_facts () |
| bool | _matches (dict rule, dict f) |
| dict | load_registry (Path path=None) |
| detect_machine (dict reg, dict facts=None) | |
| None | register_host (int index, Path path=None) |
| assign_machine (row_id, of, weights=None) | |
| main (argv=None) | |
Variables | |
| HERE = Path(__file__).resolve().parent | |
| REPO = HERE.parent.parent | |
| str | STOP_FILE = HERE / "CAMPAIGN_STOP" |
| float | B111_M = 2.482e-10 |
| float | PILE_TOP_FRAC = 0.02 |
| float | PILE_TOL = 0.05 |
| float | OCC_TOL = 0.10 |
| float | TOPBIN_TOL = 0.02 |
| int | DFP_TOL = 1e-2 |
| bool | TRUNCATION_GATES = False |
| bool | STARVED_GATE = False |
| tuple | DOSE_CHECKPOINTS |
| dict | _PATTERN_CACHE = {} |
| str | MACHINES_FILE = HERE / "machines.json" |
run_ensemble - the campaign worker. One instance per machine.
machine k of M -> runs every design row with row_id % M == k
Deterministic, restartable, needs no shared filesystem and no coordination: a
dead machine leaves an identifiable GAP rather than corrupting the design.
Results are appended one JSON object per line, so a crash costs one row rather
than the file, and merging is plain concatenation.
python run_ensemble.py --design design/T2_design_v1.csv \
--machine 0 --of 4 --workers 10
WHY THE ADMISSIBILITY BLOCK EXISTS
----------------------------------
delta_FP is BLIND to grid truncation. On 2026-08-01 it held at 1e-8 while
99.96 % of the <100> content was stacked against the top of the grid, because
the `if (n < I)` growth guard in rate_kernels.cpp halts growth WITHOUT losing
atoms. Conservation is a health check, not a grid-adequacy check.
Across ~1250 rows with theta varying over its full prior box, some cells WILL
saturate the grid. Without pile/occupancy recorded per row those cells enter
the Sobol estimator as ordinary data and their sensitivity is an artefact of
the grid, not of the physics. So every row carries:
occ_111, occ_100 mean size / I (project rule: > ~0.1 suspect)
pile_111, pile_100 content fraction in the top 2 % of the grid
pile_v, occ_v the SAME test on the VACANCY axis (added 2026-08-02)
d_over_ceiling_100 d_100 / d(n=I)
dose_reached, starved, delta_FP, delta_He, solver_rc, wall_s
The vacancy axis went unchecked until 2026-08-02 and is not a minor addition:
at V=120 the check_machine probe reported mean_n_v = 18.7 against a converged
153.3 and N_voids 10.6x low. Note the asymmetry -- delta_FP is blind to
SIA-axis truncation but IS sensitive to the vacancy axis, because the swelling
identity S = S_I + Delta J^d has the vacancy inventory on the left. So on the
vacancy side delta_FP is a real guard, and the admissibility bar was tightened
from 1e-3 to 1e-6 to make it bite.
and `admissible`, which merge_and_sobol uses to decide what may be estimated
from. A row that is inadmissible is NOT a failed row -- it ran fine, it just
cannot answer the question asked of it.
|
protected |
Everything the registry is allowed to match on.
Definition at line 1197 of file run_ensemble.py.
|
protected |
Definition at line 1212 of file run_ensemble.py.
|
protected |
Deterministic machine-assignment pattern of length sum(weights). Sainte-Lague / highest-averages: repeatedly hand the next slot to the machine with the largest w_k / (2 a_k + 1), where a_k is how many slots it already holds. This spreads each machine's slots as evenly as the weights allow, rather than giving it one contiguous block. Interleaving is the point, not an aesthetic: the design rows are Saltelli A / B / AB_j matrices, so a contiguous block belongs disproportionately to a few base samples. If one machine dies, scattered gaps cost pairwise deletion a few rows spread over many base samples; a contiguous gap would wipe out whole base samples and take their partners with them. Pure function of (of, weights) -- every machine computes the same pattern from the same CLI arguments, so no coordination is needed.
Definition at line 1153 of file run_ensemble.py.
| run_ensemble.apply_bin_config | ( | sim, | |
| cfg | |||
| ) |
Write the bin-moment layout onto the workbook dict. MUST be called after the constructor and before _calculate_derived() + rebuild_rates(). Bin configuration is NOT a constructor argument -- passing i_discrete=/I_bin=/shape_function= to RadClusterSimulation() raises TypeError since 2026-08-02, and before that it was silently dropped, which produced nine bit-identical "refinement" runs (plan S10(i)). No-op in discrete mode, so it is safe to call unconditionally.
Definition at line 419 of file run_ensemble.py.
| run_ensemble.apply_theta | ( | sim, | |
| dict | spec, | ||
| dict | row, | ||
| dict | cond | ||
| ) |
Write one design row into the simulation's InputData. Returns the dict of values actually written, for the provenance record. Constructor-level parameters (i_mobile, v_mobile) are handled by the caller BEFORE construction -- they cannot be set here.
Definition at line 250 of file run_ensemble.py.
| run_ensemble.assign_machine | ( | row_id, | |
| of, | |||
weights = None |
|||
| ) |
Which machine owns this row_id. weights=None reproduces the original even split (row_id % of) EXACTLY, so an existing campaign resumed without --weights keeps its assignment and every already-computed row stays where it was.
Definition at line 1290 of file run_ensemble.py.
| run_ensemble.bin_layout | ( | sim, | |
| cfg | |||
| ) |
Read the REALISED bin layout back off sim.rate_equations and verify it.
Two reasons this is not bookkeeping:
1. The layout must be read from `rate_equations` (the BinMomentRateEquations
that owns i_discrete/I_bin/r/shape_function), NOT `reaction_rates` --
querying the wrong object returns nan rather than raising (plan S10(i)).
2. The realised bin count is NOT the requested one. `r` is derived as
(I/i_discrete)**(1/I_bin) and build_bins() then walks floor(edge*r) with
a minimum increment of 1, so the walk overshoots or undershoots the
target by a bin or two. Recording the request and silently running a
different layout is exactly the class of error S10(i) is about.
Raises if the request did not take effect at all -- a silent fallback to the
coarse default (i_discrete=10, I_bin=6) is the failure mode that must not
reach a production row.
Definition at line 440 of file run_ensemble.py.
| run_ensemble.detect_machine | ( | dict | reg, |
| dict | facts = None |
||
| ) |
Which registry entry is this host? Returns the entry, or raises. FAILS LOUDLY ON NO MATCH, and equally loudly on an AMBIGUOUS match. Both are the same underlying hazard: an index chosen wrongly means two machines compute the same rows and some other rows are computed by nobody, and that surfaces at merge time as "rows MISSING" -- which reads like a machine that never reported rather than the misconfiguration it is. A campaign is cheaper to start late than to re-run.
Definition at line 1234 of file run_ensemble.py.
| run_ensemble.evaluate | ( | args | ) |
Definition at line 495 of file run_ensemble.py.
| str run_ensemble.git_sha | ( | ) |
Definition at line 214 of file run_ensemble.py.
| dict run_ensemble.load_registry | ( | Path | path = None | ) |
Definition at line 1227 of file run_ensemble.py.
| run_ensemble.main | ( | argv = None | ) |
Definition at line 1303 of file run_ensemble.py.
| run_ensemble.observe | ( | res, | |
| sim, | |||
| cfg, | |||
| d_min_nm | |||
| ) |
Observation operator + the admissibility block.
Definition at line 724 of file run_ensemble.py.
| run_ensemble.per_size_populations | ( | res, | |
| sim, | |||
| cfg, | |||
idx = -1 |
|||
| ) |
Return (c111, c_v, binned, topbin) at time index `idx`, per size.
`idx` defaults to -1 (the final time), which is every legacy call site. It
is a parameter because the CHARACTER has to be readable at a dose-ladder
checkpoint, not only at whatever dose a row happened to stop at: f_100 moves
strongly with dose (<100> is sessile and one-way, so it accumulates), and
two rows of the SAME theta that timed out at different doses cannot be
compared on it. Measured 2026-08-13 on T9 batch 1: theta 4's two ends
stopped at 15.000 and 0.106 dpa, theta 0's at 2.813 and 0.020 -- a 140x dose
ratio masquerading as a temperature effect.
One extraction that works in both modes, so every downstream gate is
computed the same way and is comparable across the discrete/bin_moment
bridge (plan S11(e)-2).
discrete : `y` is (I + V + extras, nt) -- plain slices.
bin_moment : `y` is (N_eq, nt) with the layout
[0 : i_d] discrete SIA
[i_d : i_d + P*I_bin] SIA bin moments, P per bin
[i_VAC : i_VAC + v_d] discrete vacancies
[i_VAC+v_d : ... + P*V_bin] vacancy bin moments
mirroring RadClusterSimulation._size_distributions.
`topbin` maps axis -> content fraction in the top bin, computed from the RAW
mu1 moments (not the reconstruction). Empty dict in discrete mode.
Returns c111 / c_v as None when an axis genuinely cannot be extracted; the
caller treats None as FAILING the gate, never as passing.
Definition at line 619 of file run_ensemble.py.
| tuple[list[dict], dict] run_ensemble.read_design | ( | Path | path | ) |
Definition at line 222 of file run_ensemble.py.
| None run_ensemble.register_host | ( | int | index, |
| Path | path = None |
||
| ) |
Pin THIS host to a registry index, by writing a fingerprint it will match. Deliberately narrow: node + system + cpu_count. A loose rule is how two hosts end up sharing an index.
Definition at line 1268 of file run_ensemble.py.
| str run_ensemble.sha256_file | ( | Path | p | ) |
Definition at line 207 of file run_ensemble.py.
|
protected |
Definition at line 1150 of file run_ensemble.py.
| float run_ensemble.B111_M = 2.482e-10 |
Definition at line 97 of file run_ensemble.py.
| int run_ensemble.DFP_TOL = 1e-2 |
Definition at line 131 of file run_ensemble.py.
| tuple run_ensemble.DOSE_CHECKPOINTS |
Definition at line 185 of file run_ensemble.py.
| run_ensemble.HERE = Path(__file__).resolve().parent |
Definition at line 88 of file run_ensemble.py.
| str run_ensemble.MACHINES_FILE = HERE / "machines.json" |
Definition at line 1194 of file run_ensemble.py.
| float run_ensemble.OCC_TOL = 0.10 |
Definition at line 100 of file run_ensemble.py.
| float run_ensemble.PILE_TOL = 0.05 |
Definition at line 99 of file run_ensemble.py.
| float run_ensemble.PILE_TOP_FRAC = 0.02 |
Definition at line 98 of file run_ensemble.py.
| run_ensemble.REPO = HERE.parent.parent |
Definition at line 89 of file run_ensemble.py.
| bool run_ensemble.STARVED_GATE = False |
Definition at line 167 of file run_ensemble.py.
| str run_ensemble.STOP_FILE = HERE / "CAMPAIGN_STOP" |
Definition at line 96 of file run_ensemble.py.
| float run_ensemble.TOPBIN_TOL = 0.02 |
Definition at line 104 of file run_ensemble.py.
| bool run_ensemble.TRUNCATION_GATES = False |
Definition at line 151 of file run_ensemble.py.