RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
Functions | Variables
run_ensemble Namespace Reference

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"
 

Detailed Description

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.

Function Documentation

◆ _host_facts()

dict run_ensemble._host_facts ( )
protected
Everything the registry is allowed to match on.

Definition at line 1197 of file run_ensemble.py.

◆ _matches()

bool run_ensemble._matches ( dict  rule,
dict  f 
)
protected

Definition at line 1212 of file run_ensemble.py.

◆ _weighted_pattern()

run_ensemble._weighted_pattern (   of,
  weights 
)
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.

◆ apply_bin_config()

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.

◆ apply_theta()

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.

◆ assign_machine()

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.

◆ bin_layout()

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.

◆ detect_machine()

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.

◆ evaluate()

run_ensemble.evaluate (   args)

Definition at line 495 of file run_ensemble.py.

◆ git_sha()

str run_ensemble.git_sha ( )

Definition at line 214 of file run_ensemble.py.

◆ load_registry()

dict run_ensemble.load_registry ( Path   path = None)

Definition at line 1227 of file run_ensemble.py.

◆ main()

run_ensemble.main (   argv = None)

Definition at line 1303 of file run_ensemble.py.

◆ observe()

run_ensemble.observe (   res,
  sim,
  cfg,
  d_min_nm 
)
Observation operator + the admissibility block.

Definition at line 724 of file run_ensemble.py.

◆ per_size_populations()

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.

◆ read_design()

tuple[list[dict], dict] run_ensemble.read_design ( Path  path)

Definition at line 222 of file run_ensemble.py.

◆ register_host()

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.

◆ sha256_file()

str run_ensemble.sha256_file ( Path  p)

Definition at line 207 of file run_ensemble.py.

Variable Documentation

◆ _PATTERN_CACHE

dict run_ensemble._PATTERN_CACHE = {}
protected

Definition at line 1150 of file run_ensemble.py.

◆ B111_M

float run_ensemble.B111_M = 2.482e-10

Definition at line 97 of file run_ensemble.py.

◆ DFP_TOL

int run_ensemble.DFP_TOL = 1e-2

Definition at line 131 of file run_ensemble.py.

◆ DOSE_CHECKPOINTS

tuple run_ensemble.DOSE_CHECKPOINTS
Initial value:
1= (0.005, 0.01, 0.02, 0.05,
2 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
3 2.0, 4.0, 8.0, 11.0, 15.0, 15.72, 16.3, 20.0, 30.0, 32.0,
4 40.0, 47.0, 60.0, 80.0, 100.0)

Definition at line 185 of file run_ensemble.py.

◆ HERE

run_ensemble.HERE = Path(__file__).resolve().parent

Definition at line 88 of file run_ensemble.py.

◆ MACHINES_FILE

str run_ensemble.MACHINES_FILE = HERE / "machines.json"

Definition at line 1194 of file run_ensemble.py.

◆ OCC_TOL

float run_ensemble.OCC_TOL = 0.10

Definition at line 100 of file run_ensemble.py.

◆ PILE_TOL

float run_ensemble.PILE_TOL = 0.05

Definition at line 99 of file run_ensemble.py.

◆ PILE_TOP_FRAC

float run_ensemble.PILE_TOP_FRAC = 0.02

Definition at line 98 of file run_ensemble.py.

◆ REPO

run_ensemble.REPO = HERE.parent.parent

Definition at line 89 of file run_ensemble.py.

◆ STARVED_GATE

bool run_ensemble.STARVED_GATE = False

Definition at line 167 of file run_ensemble.py.

◆ STOP_FILE

str run_ensemble.STOP_FILE = HERE / "CAMPAIGN_STOP"

Definition at line 96 of file run_ensemble.py.

◆ TOPBIN_TOL

float run_ensemble.TOPBIN_TOL = 0.02

Definition at line 104 of file run_ensemble.py.

◆ TRUNCATION_GATES

bool run_ensemble.TRUNCATION_GATES = False

Definition at line 151 of file run_ensemble.py.