Skip to content

Commit 0a9b887

Browse files
3-class strength GP + joint_hamming_matern production kernel
Migrates the strength GP from the prior 2-class (pooled mortar+Set-2, contaminated Set-3) dataset and `hamming` categorical source kernel to a corrected 3-class dataset and a new joint-distance source kernel `joint_hamming_matern` that combines per-feature ARD with a single learnable Hamming penalty inside one Matérn-3/2 radial basis. ## Production rationale (Full ablation deferred to a research-detail follow-up commit.) - The **previously-deployed model** treated Material Source as a continuous numeric coordinate — mathematically mis-specified for a 3-class categorical, but it preserved the joint kernel topology that factorised categorical-product kernels lose. The new `joint_hamming_matern` recovers the joint topology while keeping correct categorical handling. - **Strictly dominates the previous `hamming` production default** on every in-distribution metric: | metric | prior | new | |--- |---: |---: | | LOO RMSE | 510 | **495** psi | | bLOO RMSE | 738 | **717** psi | | bLOO PIT-KS | 0.039 | **0.030** | while passing all pre-registered LOCO acceptance criteria (every held-out class within 110% of Hamming's LOCO RMSE). - **Seed-deterministic across all metrics** — matches the prior production model's reproducibility and beats IndexKernel's seed sensitivity. ## Changes ### Data - `data/boxcrete_data.csv`: three-class merge with canonical mix-name table (`boxcrete/_mix_naming_table.csv`, `boxcrete/mix_naming.py`). Splits 12 corruption-collision mix names into 24 canonical names per actual recipe; corrects mortar/concrete misroutes; drops 3 clay-using mortars (`M75`/`M76`/`M77`) the GP feature set cannot represent. Strength values and `Strength (Std)` for the 638 common `(composition + temp + time)` tuples are byte-identical to the prior dataset; only labels and partition differ. - `test/fixtures/boxcrete_data_pre_v5.csv`: legacy fixture preserved under its established filename for regression / no-regression checks against the previous data. - `boxcrete/utils.py`: `Material Source` bounds bumped to `(0, 2)`; added per-class GWP coefficients for Class 2 (Set-3 Amrize cement / Class F fly ash concrete) — derived by `scripts/derive_class_2_gwp.py` from the new rows. ### Kernel framework (`boxcrete/kernels.py`) - Replaced the source-as-continuous-numeric kernel with a categorical-kernel switch `_categorical_source_branch`. Production variant: `joint_hamming_matern`. The codebase also registers research variants (`rbf_embedding_d{1,2,3}`, `joint_chain_matern`, `joint_hamming_matern_nu{05,25}`, `joint_embedding_matern_d{1,2,3}`, `additive_*`) as alternative ablation points; their detailed evaluation lives in the follow-up research commit. - New `JointHammingMaternKernel` (production source kernel): ``` K(z_i, z_j) = σ² · M_{3/2}(√( Σ_f Δx_f² / ℓ_f² + α · 𝟙[c_i ≠ c_j] )) ``` One learnable categorical penalty `α` plus per-feature ARD; the Hamming penalty applies **only** to the `Material Source` dim. Closes the +33 psi bLOO architectural gap to `legacy_continuous_ard` vs the simpler categorical-product topology, while keeping the LOCO advantages of proper categorical handling. - Both `JointHammingMaternKernel` and `JointEmbeddingMaternKernel` accept a `lengthscale_prior` kwarg and register it as a named prior on `raw_feat_lengthscale` (mirroring GPyTorch's standard `MaternKernel(lengthscale_prior=...)` contract). `_categorical_source_branch` passes `within_group_prior(...)` to the joint kernels so Cement/FlyAsh/Slag binder lengthscales and Fine/Coarse Aggregate lengthscales stay tied to within `max/min < 1.05` (the same tying behaviour the prior Hamming production fit had). - `DEFAULT_SOURCE_KERNEL = "joint_hamming_matern"` (was `"hamming"`). - `GATE_TAU = 0.10` (was `0.05`). The time gate `h(t) = 1 - exp(-t / GATE_TAU)` saturates ~3× more slowly, lengthening its monotonic envelope into the pre-1-day extrapolation region. Reduces the fraction of catalog compositions with non-monotonic strength curves from ~33% to ~22% with a slight LOO improvement. ### Priors (`boxcrete/priors.py`) - LogNormal ARD prior on non-grouped lengthscales prevents the lengthscale railing observed in the prior production fit (`max blind ℓ ≤ 100`). - Within-group prior ties Cement/FA/Slag and Fine/Coarse Aggregate lengthscales for binder and aggregate parsimony. ### JS port (`docs/gp.mjs` + `docs/gp_v2_fast.mjs`) - Added `jointHammingMatern` radial-basis function supporting `ν ∈ {0.5, 1.5, 2.5}` and the Hamming categorical penalty `α · 𝟙[c_i ≠ c_j]` inside the Matern distance argument. - Updated the `kernel(x1, x2, params)` dispatcher and the WASM-accelerated batched fast path to switch on `params.matern_specific.source_kernel_kind`. Both paths add the Hamming penalty before applying the radial basis, so single-point and batched predictors agree to FP-summation order (verified by `test_js_predictor_parity.mjs`). ### UI (`docs/ui.mjs` + `docs/model/compositions.json`) - `Material Source` panel now renders **N buttons (one per class)** via a loop over the `slider_bounds["Material Source"]` range, instead of hardcoded 2-button "Source A / Source B". Class labels (`Set 1` / `Set 2` / `Set 3`) come from a `MATERIAL_SOURCE_LABELS` map; adding a 4th class only requires extending the bounds + the labels map. - `compositions.json::slider_bounds["Material Source"]` updated from `max=1` (stale binary labelling) to `max=2` (3-class). - `Material Source` ingredient-insight panel contains a concise per-class summary (Set 1 mortar / Set 2 Heidelberg Class C / Set 3 Amrize Class F) with a reference to `docs/materials_background.md` for the full per-class plant / source / HRWR detail. ### Production artifacts Regenerated from the new kernel; all ground-truth-anchored to `data/boxcrete_data.csv`: - `docs/model/strength.json` — `schema_version=3`, `joint_hamming_matern`, `α=0.536`, `ν=1.5`, within-group prior installed on the joint kernel (verified by `test_lengthscale_identifiability` tying the binder / aggregate lengthscales within `max/min < 1.05`). - `docs/model/gwp.json` — 3 classes (previously had 0 and 1 only). - `docs/model/compositions.json` — correct `Material Source` labels (61 mortar / 30 Set-2 / 53 Set-3) looked up against the canonical raw data; refreshed `strength_predictions`, `gwp_predictions`, `pareto_mask`. - `docs/model/test_vectors.json` — 37 vectors. - `docs/model/strength_model.pt`. - `docs/model/mix_analyses.json` — migrated 138 LLM-authored mix narratives from the prior `Source A/B` labels to `Set 1` / `Set 3`. - Notebooks (`notebooks/{prediction_and_optimization_tutorial, strength_curve_prediction_demo, slump_prediction_demo}.ipynb`): re-executed with the new data; 0 references to the removed `MRWR (kg/m3)` column remain. ### Regeneration pipeline (`experiments/regenerate_*`) - `regenerate_strength_json.py` — extended introspection to handle the new `JointHammingMaternKernel` layout (`raw_feat_lengthscale`, `raw_alpha`, `nu`, `categorical_mode`, `feature_dims`, `source_dim`). - `regenerate_gwp_json.py` (new) — regenerates `gwp.json` from `DEFAULT_GWP_COEFFICIENTS` to include all 3 classes. - `regenerate_compositions_gwp_predictions.mjs` (new) — refreshes `compositions.gwp_predictions` after any `gwp.json` change. - `fix_compositions_material_source.py` (new) — recovery script for any future drift between catalog-stored class labels and the canonical raw data; looks up each catalog composition's class by 7-column composition fingerprint. - `regenerate_all_artifacts.sh` — pipeline orchestrator updated to include the new GWP step + the new `test_catalog_consistency` JS test. - `docs/generate_mix_analyses.py` fallback template — updated from binary labels to 3-class labels. ### Tests (production-essential) - `test/test_joint_hamming_matern_kernel.py` — 14 tests for the new kernel: PSD, Hamming penalty correctness, Matern smoothness, `α` gradient flow, gauge-fix invariance, etc. - `test/test_mix_naming.py` — canonical-naming pipeline (38 tests). - `test/test_composed_prior.py` — within-group prior composition (11 tests). - `test/test_kernel_layout.py` — updated for the new ProductKernel + joint-kernel layouts. The prior test assumed `ScaleKernel(MaternKernel)` only; broke when the categorical wrapper was introduced. Restored regression coverage for all registered source-kernel variants. - `test/test_lengthscale_identifiability.py` — updated to accept the joint-kernel layout (specific lengthscales have length 16 for joint kernels, 17 for the legacy continuous-source layout) and to walk `named_priors()` for kernels without a standard `.lengthscale_prior` attribute. ### Regression tests for newly-discovered bug-classes Each test below targets a class of bug that pre-existed silently in the prior test suite and is now caught at CI time: 1. `test/test_catalog_consistency.mjs` (new, 8 assertions): cross-validates `docs/model/compositions.json` against `data/boxcrete_data.csv`. Anchors to ground truth instead of merely checking internal `catalog ↔ predictions` consistency. Catches: - per-row `Material Source` mislabel, - missing classes in catalog, - stale `slider_bounds`, - out-of-range `Material Source` values. 2. `test/test_variance_orientation.py` (new, 1 test, ~60 s): asserts `σ²(observed_class) ≤ σ²(any other class)` for every training row. Direct coverage of the symptom that "switching class in the explorer makes uncertainty contract for a non-observed class" — fires on any future catalog-vs-data mislabelling **OR** on any kernel mis-specification that produces this counter-intuitive behaviour. 3. `test/test_data_freshness.mjs`: replaced the legacy binary `comp[7] >= 0.5 ? 1 : 0` `Material Source` collapse with the same `Math.round(comp[gwpParams.class_dim])` lookup the explorer uses. The previous binary collapse silently mapped class 2 to class 1, producing wrong GWP coefficients and a green test on wrong values. 4. `test/test_js_ui_smoke.mjs`: same fix to the same legacy binary collapse (also silently mapped class 2 → class 1). ## Test status - **Python**: ~300 PASS / 0 fail. Covers kernel + identifiability + mix naming + composed prior + layout + curve monotonicity + pretrained-loader fidelity + variance orientation + the rest of the inheritance suite from the previous PR. - **JS**: 10 / 10 PASS: - GP parity (296 assertions), - predictor parity (8), - physical constraints (37), - UI smoke (144 × 64), - catalog consistency (8), - data freshness (6), - curve monotonicity (144), - feature parity (49), - strength architecture, - units. - **Lint**: `black --check` clean; flake8 critical errors (`E9, F63, F7, F82`) = 0. ## Follow-up A separate research-detail commit (lands as a sibling) carries the non-essential experimental artefacts: - `experiments/THREE_CLASS_AND_PRIOR_BENCHMARK.md` — full ablation writeup with literature context, including the ablation series that motivated `GATE_TAU=0.10`, the time-only kernel choice, and the deployed-baseline-vs-new comparison. - `experiments/three_class_ablation.py` + `experiments/model_variant_study.py` — ablation infrastructure. - Tests for the registered research kernel variants (`test_joint_embedding_matern_kernel.py`, `test_rbf_embedding_kernel.py`). - Mobile-performance investigation doc + PR-prep meta-doc. - Legacy ablation infrastructure restored from the prior PR baseline.
1 parent 3d544a7 commit 0a9b887

45 files changed

Lines changed: 7253 additions & 2007 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

boxcrete/_mix_naming_table.csv

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
legacy_int_name,canonical_name,material_class,source_evidence
2+
Mix_1,M1,0,row-FP: M1
3+
Mix_2,M2,0,row-FP: M2
4+
Mix_3,M3,0,row-FP: M3
5+
Mix_4,M4,0,row-FP: M4
6+
Mix_5,M5,0,row-FP: M5
7+
Mix_6,M6,0,row-FP: M6
8+
Mix_7,M7,0,row-FP: M7
9+
Mix_8,M8,0,row-FP: M8
10+
Mix_9,M9,0,row-FP: M9
11+
Mix_10,M10,0,row-FP: M10
12+
Mix_11,M11,0,row-FP: M11
13+
Mix_12,M12,0,row-FP: M12
14+
Mix_13,M13,0,row-FP: M13
15+
Mix_14,M14,0,row-FP: M14
16+
Mix_15,M15,0,row-FP: M15
17+
Mix_16,M16,0,row-FP: M16
18+
Mix_17,M17,0,row-FP: M17
19+
Mix_18,M18,0,row-FP: M18
20+
Mix_19,M19,0,row-FP: M19
21+
Mix_20,M20,0,row-FP: M20
22+
Mix_21,M21,0,row-FP: M21
23+
Mix_22,M22,0,row-FP: M22
24+
Mix_23,M23,0,row-FP: M23
25+
Mix_24,M24,0,row-FP: M24
26+
Mix_25,M25,0,row-FP: M25
27+
Mix_26,M26,0,row-FP: M26
28+
Mix_27,M27,0,row-FP: M27
29+
Mix_28,M28,0,row-FP: M28
30+
Mix_29,M29,0,row-FP: M29
31+
Mix_30,M30,0,row-FP: M30
32+
Mix_31,M31,0,row-FP: M31
33+
Mix_32,M32,0,row-FP: M32
34+
Mix_33,M33,0,row-FP: M33
35+
Mix_34,M34,0,row-FP: M34
36+
Mix_35,M35,0,row-FP: M35
37+
Mix_36,M36,0,row-FP: M36
38+
Mix_37,M37,0,row-FP: M37
39+
Mix_38,M38,0,row-FP: M38
40+
Mix_39,M39,0,row-FP: M39
41+
Mix_40,M40,0,row-FP: M40
42+
Mix_41,M41,0,row-FP: M41
43+
Mix_42,M42,0,row-FP: M42
44+
Mix_43,M43,0,row-FP: M43
45+
Mix_44,M44,0,row-FP: M44
46+
Mix_45,M45,0,row-FP: M45
47+
Mix_46,M46,0,row-FP: M46
48+
Mix_47,M47,0,row-FP: M47
49+
Mix_48,M48,0,row-FP: M48
50+
Mix_49,M49,0,row-FP: M49
51+
Mix_50,M50,0,row-FP: M50
52+
Mix_51,M51,0,row-FP: M51
53+
Mix_52,M52,0,row-FP: M52
54+
Mix_53,M53,0,row-FP: M53
55+
Mix_54,M54,0,row-FP: M54
56+
Mix_55,M55,0,row-FP: M55
57+
Mix_56,M56,0,row-FP: M56
58+
Mix_57,M57,0,row-FP: M57
59+
Mix_78,M58,0,row-FP: M78
60+
Mix_79,M59,0,row-FP: M79
61+
Mix_80,M60,0,"ambig ['M80', 'M81'] -> M80"
62+
Mix_81,M61,0,"ambig ['M80', 'M81'] -> M81 (others used)"
63+
Mix_82,M62,0,row-FP: M82
64+
Mix_83,C1,1,row-FP: M83
65+
Mix_84,C2,1,row-FP: M84
66+
Mix_85,C3,1,row-FP: M85
67+
Mix_86,C4,1,row-FP: M86
68+
Mix_87,C5,1,row-FP: M87
69+
Mix_88,C6,1,row-FP: M88
70+
Mix_89,C7,1,row-FP: M89
71+
Mix_90,C8,1,row-FP: M90
72+
Mix_91,C9,1,row-FP: M91
73+
Mix_92,C10,1,row-FP: M92
74+
Mix_93,C11,1,row-FP: M93
75+
Mix_94,C12,1,row-FP: M94
76+
Mix_95,C13,1,row-FP: M95
77+
Mix_96,C14,1,row-FP: M96
78+
Mix_97,C15,1,row-FP: M97
79+
Mix_98,C16,1,row-FP: M98
80+
Mix_99,C17,1,row-FP: M99
81+
Mix_100,C18,1,row-FP: M100
82+
Mix_101,C19,1,row-FP: M101
83+
Mix_102,C20,1,row-FP: M102
84+
Mix_103,C21,1,row-FP: M103
85+
Mix_104,C22,1,row-FP: M104
86+
Mix_105,C23,1,row-FP: M105
87+
Mix_106,C24,1,row-FP: M106
88+
Mix_107,C25,1,row-FP: M107
89+
Mix_108,C26,1,row-FP: M108
90+
Mix_109,C27,1,row-FP: M109
91+
Mix_110,C28,1,row-FP: M110
92+
Mix_111,C29,1,row-FP: M111
93+
Mix_112,C30,1,row-FP: M112
94+
Mix_120,C31,2,row-FP: M120
95+
Mix_121,C32,2,row-FP: M121
96+
Mix_122,C33,2,row-FP: M122
97+
Mix_123,C34,2,row-FP: M123
98+
Mix_124,C35,2,row-FP: M124
99+
Mix_125,C36,2,row-FP: M125
100+
Mix_126_split0,C37,2,row-FP: M126
101+
Mix_127_split0,C38,2,row-FP: M127
102+
Mix_128_split0,C39,2,row-FP: M128
103+
Mix_129_split0,C40,2,row-FP: M129
104+
Mix_129_T1,C41,2,"ambig ['M129 T1', 'M129 T2'] -> M129 T1"
105+
Mix_129_T2,C42,2,"ambig ['M129 T1', 'M129 T2'] -> M129 T2 (others used)"
106+
Mix_129_C,C43,2,row-FP: M129C
107+
Mix_130_split0,C44,2,row-FP: M130
108+
Mix_131_split0,C45,2,row-FP: M131
109+
Mix_132_split0,C46,2,row-FP: M132
110+
Mix_133_split0,C47,2,row-FP: M133
111+
Mix_134_split0,C48,2,row-FP: M134
112+
Mix_135_split0,C49,2,row-FP: M135
113+
Mix_136_split0,C50,2,row-FP: M136
114+
Mix_137_split0,C51,2,row-FP: M137
115+
Mix_126_split1,C52,2,row-FP: Mix 126
116+
Mix_127_split1,C53,2,row-FP: Mix 127
117+
Mix_128_split1,C54,2,row-FP: Mix 128
118+
Mix_129_split1,C55,2,row-FP: Mix 129
119+
Mix_130_split1,C56,2,row-FP: Mix 130
120+
Mix_131_split1,C57,2,row-FP: Mix 131
121+
Mix_132_split1,C58,2,row-FP: Mix 132
122+
Mix_133_split1,C59,2,row-FP: Mix 133
123+
Mix_134_split1,C60,2,row-FP: Mix 134
124+
Mix_135_split1,C61,2,row-FP: Mix 135
125+
Mix_136_split1,C62,2,row-FP: Mix 136
126+
Mix_137_split1,C63,2,row-FP: Mix 137
127+
Mix_138,C64,2,row-FP: Mix 138
128+
Mix_139,C65,2,row-FP: Mix 139
129+
Mix_140,C66,2,row-FP: Mix 140
130+
Mix_141,C67,2,row-FP: Mix 141
131+
Mix_142,C68,2,row-FP: Mix 142
132+
Mix_143,C69,2,row-FP: Mix 143
133+
Mix_144,C70,2,row-FP: Mix 144
134+
Mix_145,C71,2,row-FP: Mix 145
135+
Mix_146,C72,2,row-FP: Mix 146
136+
Mix_147,C73,2,row-FP: Mix 147
137+
Mix_148,C74,2,row-FP: Mix 148
138+
Mix_149,C75,2,row-FP: Mix 149
139+
Mix_150,C76,2,row-FP: Mix 150
140+
Mix_151,C77,2,row-FP: Mix 151
141+
Mix_152,C78,2,row-FP: Mix 152
142+
Mix_153,C79,2,row-FP: Mix 153
143+
Mix_154,C80,2,row-FP: Mix 154
144+
Mix_155,C81,2,row-FP: Mix 155
145+
Mix_156,C82,2,row-FP: Mix 156
146+
Mix_157,C83,2,row-FP: Mix 157
147+
Mix_158,C84,2,row-FP: Mix 158

boxcrete/features.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,17 @@
3838
from boxcrete.utils import DEFAULT_X_COLUMNS
3939

4040
# Time gate constant: h(t) = 1 - exp(-t / GATE_TAU).
41-
# tau=0.05 (post-input-transform time units) was found to be optimal in
42-
# the τ-sweep; see §4.5 of the benchmark.
43-
GATE_TAU = 0.05
41+
# tau=0.10 (post-input-transform time units) is the v5 production
42+
# default. The v5 + joint_hamming_matern kernel exhibits non-monotonic
43+
# strength curves in the t < 1 day extrapolation region for ~30% of
44+
# catalog compositions when tau=0.05; the gate's monotonic ramp
45+
# saturates by t_norm=0.3 (raw t ~ 1 d) and stops dampening kernel
46+
# oscillations earlier than that. tau=0.10 lengthens the gate envelope
47+
# into the oscillation region and reduces the affected fraction to
48+
# ~22% (a relative ~32% reduction) at zero cost in LOO/bLOO RMSE
49+
# (~0.6 psi IMPROVEMENT vs tau=0.05). See
50+
# experiments/ABLATION_GATE_TAU.md for the full sweep.
51+
GATE_TAU = 0.10
4452

4553

4654
# Default feature set for the V2 strength GP. See STRENGTH_GP_BENCHMARK.md

0 commit comments

Comments
 (0)