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

Functions

dict load_targets ()
 
dict discover_stages ()
 
list read_rows (Path path)
 
dict read_design (path)
 
 grid_components (dict row)
 
 classify (dict row, float dose_target=15.0)
 
 recover_V (dict row)
 
dict extent_pairs (list all_rows)
 
set verified_obs (dict row, dict ext)
 
dict score (dict row, dict targets, dict ext=None)
 
float worst_margin (dict row, dict targets, set only=None)
 
 _margin_or_none (dict row, dict targets, dict sc)
 
float log_distance (dict row, dict targets)
 
list covarying_groups (dict design)
 
tuple group_key (dict design_row, list group)
 
dict sensitivity (dict stage, list rows, dict targets, float dose_target=15.0)
 
dict affordability (dict stage, list rows, float dose_target)
 
 inventory (dict row, dict targets)
 
dict cost_model (list all_rows)
 
dict ingest ()
 
 fmt (v, sig=3)
 
str render_guide (dict led)
 
 main (argv=None)
 

Variables

 encoding
 
 errors
 
 HERE = Path(__file__).resolve().parent
 
str LEDGER = HERE / "calibration_ledger.json"
 
str GUIDE = HERE / "CALIBRATION_GUIDE.md"
 
str TARGETS = HERE / "targets_330C_15dpa.json"
 
dict BOOKKEEPING
 
list OBS
 
dict SHORT
 
float DEAD_THRESHOLD = 0.05
 
dict DEFAULT_POLICY
 
float PILE_TOL = 0.05
 
float TOPBIN_TOL = 0.02
 
float EXTENT_TOL = 0.10
 

Detailed Description

Ingest every calibration stage and re-derive what the campaign has learned.

WHY THIS EXISTS.  S1-S14 each answered one question, and the answer lived in a
commit message.  A machine that pulls the repo cannot read commit messages into
a plan, and a human returning after a crash re-derives the same verdicts by
hand (this happened on 2026-08-18).  This script turns the result files back
into two artefacts that DO travel:

    calibration_ledger.json   machine-readable state -- what was swept, what it
                              did, which levers are dead, where the best point
                              is, what each machine costs per row.
    CALIBRATION_GUIDE.md      the same thing for a human, regenerated so it can
                              never drift from the results it summarises.

`plan.py` reads the ledger and proposes the next stage.  Nothing here writes a
design or launches a run -- learning and planning are kept separate so that
re-ingesting results can never, by itself, start a job.

THE ONE RULE THIS FILE ENFORCES.  A row is a MEASUREMENT only if it reached the
target dose, is not grid-limited, and its grid converged.  Rows that piled at
the ceiling still carry information about DIRECTION, and are used for that, but
their size readouts are never scored or fitted.  S12 and the V2 block of S14
are the reason: both look like "big loops" and neither is a number.

Usage:
    python learn.py                 # re-ingest, rewrite ledger + guide
    python learn.py --quiet         # no stdout summary

Function Documentation

◆ _margin_or_none()

learn._margin_or_none ( dict  row,
dict  targets,
dict  sc 
)
protected
worst_margin over the VERIFIED AND IN-BAND observables, else None.

Definition at line 347 of file learn.py.

◆ affordability()

dict learn.affordability ( dict  stage,
list  rows,
float  dose_target 
)
Which lever VALUES prevent a row from ever reaching dose.

This is a COST verdict, not a physics one, and it is measured with a
controlled contrast: within one stage, a level counts as unaffordable only
if it produced zero full-dose rows while ANOTHER level of the same
co-varying group -- everything else held equal -- produced at least one.

S14 is the motivating case.  All six rows at E_b_i2 = 0.60 burned the full
20 h budget and reached 0.004-6.06 of 15 dpa; all six at 0.75 finished in
5.3-19.8 h.  Nothing about that is visible in the physics verdicts, because
a row that never reaches dose is excluded from scoring -- so without this
the planner cheerfully proposes another stage in the same dead direction.

Definition at line 459 of file learn.py.

◆ classify()

learn.classify ( dict  row,
float   dose_target = 15.0 
)
Is this row a measurement?  Returns (valid, flags).

THE VACANCY TOP-BIN DOES NOT DISQUALIFY (2026-08-20).  `topbin_v` is the
vacancy CONTENT fraction in the top bin, and on a log grid that fraction has
a floor set by the bin ratio, not by truncation: for a cavity tail
c(n) ~ n^-alpha it tends to 1 - r^-(2-alpha) whatever the ceiling is.  The
campaign's cavity tail has alpha ~ 0.47, so at the production r = 1.514 the
floor is ~0.47 and TOPBIN_TOL = 0.02 is unreachable at any affordable bin
count (it would need ~630 vacancy bins).

Measured 2026-08-20 on the reference point and on the 4/6 anchor, refining
v_bin 20 -> 47 (r 1.514 -> 1.202):
  * topbin_v fell 0.468 -> 0.1715 and 0.472 -> 0.1718 -- the SAME value to
    three digits at mean_n_v = 1566 and 7115, a 4.5x span in mean void size.
    A truncation measure cannot be blind to that; a geometric one is.
  * the four loop observables moved by <= 0.1 % (grid-exact), and the 4/6
    anchor still scored 4/6 with every in-band observable still in band.
So the vacancy top bin is recorded as a soft TOPBINV flag instead.

The SIA-axis top bin KEEPS disqualifying: those bins run at r = 1.204, where
the geometric floor is small, and the argument above does not transfer.
`pile` also keeps disqualifying -- it is measured on the reconstructed
distribution and is the real truncation test (1095 of the 1226 grid-flagged
rows fail it, so this change rescues 105 rows, not the whole campaign).

This restores agreement with run_ensemble.py, which withdrew truncation as a
reject criterion outright on 2026-08-05 (TRUNCATION_GATES = False); learn.py
had reintroduced it, so the ledger and CALIBRATION_GUIDE.md were reporting a
best-of-campaign that excluded the campaign's best rows -- and plan.py plans
from that ledger.

Definition at line 175 of file learn.py.

◆ cost_model()

dict learn.cost_model ( list  all_rows)
Wall time per row, per machine -- from COMPLETED rows only.

A row cut at the budget records wall_s == the budget, which measures the
timeout and not the cost.  Counting those pins max_row_h to the budget, and
since plan.py refuses a design whose worst row exceeds the budget, one
timed-out stage would make the planner refuse every subsequent design on
that machine.  S14 did exactly this: six rows at 20.0 h that never reached
dose.  Timeouts are counted separately, because a machine that times out
often is still worth knowing about.

Definition at line 550 of file learn.py.

◆ covarying_groups()

list learn.covarying_groups ( dict  design)
Partition swept columns into blocks that always move together.

V0/V1/V2 changes f_cl_v, E_b_v2 and s_v as one knob; the S12 sink block
moves five Z-factors at once.  Treating those as independent columns would
find no single-column pairs at all and report every lever as untested.

Definition at line 371 of file learn.py.

◆ discover_stages()

dict learn.discover_stages ( )
Pair every results/*.jsonl with its design/*.csv and labels, if present.

Definition at line 105 of file learn.py.

◆ extent_pairs()

dict learn.extent_pairs ( list  all_rows)
theta_hash -> {observable: max relative drift across grid EXTENTS}.

Only thetas actually run at two or more distinct V contribute.  Everything
else is UNVERIFIED -- not passed, not failed, simply not yet tested.

Definition at line 258 of file learn.py.

◆ fmt()

learn.fmt (   v,
  sig = 3 
)

Definition at line 881 of file learn.py.

◆ grid_components()

learn.grid_components ( dict  row)
Split `grid_limited` into its parts.  Returns (pile, sia_top, vac_top,
unmeasured, decomposable).

run_ensemble records the components; only the OR of them survives into
`grid_limited`, which is too coarse to act on (see classify).

Definition at line 157 of file learn.py.

◆ group_key()

tuple learn.group_key ( dict  design_row,
list  group 
)

Definition at line 392 of file learn.py.

◆ ingest()

dict learn.ingest ( )

Definition at line 587 of file learn.py.

◆ inventory()

learn.inventory ( dict  row,
dict  targets 
)
Compare SIA content locked in loops against the measurement.

Uses the ROW OWN n<->d relation (n / d^2 from its own mean_n and mean d),
so no lattice constant or Burgers vector is assumed here and the comparison
cannot drift from whatever the solver actually used.

Definition at line 518 of file learn.py.

◆ load_targets()

dict learn.load_targets ( )

Definition at line 101 of file learn.py.

◆ log_distance()

float learn.log_distance ( dict  row,
dict  targets 
)
RMS of log10(model/target) over the six.  Scale-free, so a 300x miss on
voids is not drowned out by a 1.2x miss on a diameter.

Definition at line 357 of file learn.py.

◆ main()

learn.main (   argv = None)

Definition at line 1193 of file learn.py.

◆ read_design()

dict learn.read_design (   path)

Definition at line 145 of file learn.py.

◆ read_rows()

list learn.read_rows ( Path  path)

Definition at line 132 of file learn.py.

◆ recover_V()

learn.recover_V ( dict  row)
Vacancy ceiling V.  Not stored directly; occ_v = mean_n_v / V is.

Definition at line 250 of file learn.py.

◆ render_guide()

str learn.render_guide ( dict  led)

Definition at line 891 of file learn.py.

◆ score()

dict learn.score ( dict  row,
dict  targets,
dict   ext = None 
)
Band membership, reported THREE ways.

`n_in_range`          - every observable, the historical figure.
`n_verified`          - how many observables survived an extent change.
`n_in_range_verified` - in band AND extent-verified.  THIS is the score
                        that ranks rows; the other two are diagnostics.

Definition at line 293 of file learn.py.

◆ sensitivity()

dict learn.sensitivity ( dict  stage,
list  rows,
dict  targets,
float   dose_target = 15.0 
)
Response of each observable to each co-varying lever group.

Only pairs that differ in EXACTLY ONE group are used, so the response is
attributable.  Magnitude is measured on valid rows only; a pair where one
side went invalid is recorded as a direction with `drove_invalid`.

Definition at line 396 of file learn.py.

◆ verified_obs()

set learn.verified_obs ( dict  row,
dict  ext 
)
The observables this row is entitled to be scored on.

Definition at line 285 of file learn.py.

◆ worst_margin()

float learn.worst_margin ( dict  row,
dict  targets,
set   only = None 
)
Decades from the nearest band edge, for the observable closest to one.

Band membership is BINARY and so rewards edge-sitting: V15 row 7109 counts
the same 3/4 as row 7100 while sitting 1 % inside the N_111 ceiling
(1.49e22 against 1.5e22) where 7100 is 18 % clear on its tightest
observable.  One of those is a calibration; the other is a coin flip against
solver noise and the next grid change.  `log_distance` cannot separate them
either -- it is a global RMS to the target CENTRES, so it says nothing about
proximity to an EDGE.

Returned in decades so it is scale-free across densities and diameters.
Negative when an observable is outside its band (how far out it is).
None when nothing is scoreable.

Definition at line 318 of file learn.py.

Variable Documentation

◆ BOOKKEEPING

dict learn.BOOKKEEPING
Initial value:
1= {"row_id", "condition", "cond_row_id", "matrix", "base_idx",
2 "param_j", "theta_id"}

Definition at line 48 of file learn.py.

◆ DEAD_THRESHOLD

float learn.DEAD_THRESHOLD = 0.05

Definition at line 59 of file learn.py.

◆ DEFAULT_POLICY

dict learn.DEFAULT_POLICY

Definition at line 65 of file learn.py.

◆ encoding

learn.encoding

Definition at line 40 of file learn.py.

◆ errors

learn.errors

Definition at line 40 of file learn.py.

◆ EXTENT_TOL

float learn.EXTENT_TOL = 0.10

Definition at line 247 of file learn.py.

◆ GUIDE

str learn.GUIDE = HERE / "CALIBRATION_GUIDE.md"

Definition at line 44 of file learn.py.

◆ HERE

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

Definition at line 42 of file learn.py.

◆ LEDGER

str learn.LEDGER = HERE / "calibration_ledger.json"

Definition at line 43 of file learn.py.

◆ OBS

list learn.OBS
Initial value:
1= ["N_loops_100", "d_100_nm", "N_loops_111", "d_111_nm",
2 "N_voids", "d_cavity_nm"]

Definition at line 51 of file learn.py.

◆ PILE_TOL

float learn.PILE_TOL = 0.05

Definition at line 153 of file learn.py.

◆ SHORT

dict learn.SHORT
Initial value:
1= {"N_loops_100": "N_100", "d_100_nm": "d_100", "N_loops_111": "N_111",
2 "d_111_nm": "d_111", "N_voids": "N_void", "d_cavity_nm": "d_void"}

Definition at line 53 of file learn.py.

◆ TARGETS

str learn.TARGETS = HERE / "targets_330C_15dpa.json"

Definition at line 45 of file learn.py.

◆ TOPBIN_TOL

float learn.TOPBIN_TOL = 0.02

Definition at line 154 of file learn.py.