|
| 1 | +/** |
| 2 | + * Engineered-feature registry for the strength GP. |
| 3 | + * |
| 4 | + * Each feature is computed from the RAW 10-dim composition vector |
| 5 | + * [Cement, Fly Ash, Slag, Water, HRWR, Fine, Coarse, Source, Temp, Time]. |
| 6 | + * The same offsets (+1.0, +1e-3, +1e-4) used by the Python builders |
| 7 | + * apply here to match the GP's training-time numerical conditioning. |
| 8 | + * |
| 9 | + * Two callers consume this: |
| 10 | + * - gp.mjs's `transformInput` (general path, uses `appendFeatures`) |
| 11 | + * - gp_v2_fast.mjs's batched curve predictor (which inlines for speed, |
| 12 | + * so it imports `FEATURE_FNS` and walks the names array directly). |
| 13 | + * |
| 14 | + * Adding a new feature: add it to `FEATURE_FNS`. The strength.json's |
| 15 | + * `engineered_feature_names` field is the source of truth for which |
| 16 | + * features are active in the deployed model. |
| 17 | + */ |
| 18 | + |
| 19 | +// Helper accessors for the raw composition vector. |
| 20 | +const I_CEMENT = 0, I_FLYASH = 1, I_SLAG = 2, I_WATER = 3; |
| 21 | +const I_HRWR = 4, I_FINE = 5, I_COARSE = 6; |
| 22 | +// I_SOURCE = 7 (not a feature) |
| 23 | +const I_TEMP = 8, I_TIME = 9; |
| 24 | + |
| 25 | +/** |
| 26 | + * Map: feature_name → (xRaw: number[10]) → number. |
| 27 | + * |
| 28 | + * Source-of-truth: the union of the V2 deployed builders in |
| 29 | + * `boxcrete/features.py::FEATURE_BUILDERS` (the 7 names in |
| 30 | + * `engineered_feature_names`) and a subset of the research-only catalog |
| 31 | + * `experiments/_research_features.py::RESEARCH_FEATURE_BUILDERS` (kept |
| 32 | + * here so a research strength.json shipped with a non-default |
| 33 | + * `engineered_feature_names` list still resolves on the JS side without |
| 34 | + * the explorer needing a redeploy). The deployed model only consumes |
| 35 | + * the names it advertises in `engineered_feature_names`; the rest are |
| 36 | + * dead at runtime. Where Python uses |
| 37 | + * `x[..., _IDX["water"]:_IDX["water"]+1]`, we read `xRaw[I_WATER]`. |
| 38 | + */ |
| 39 | +export const FEATURE_FNS = { |
| 40 | + wb_ratio: (x) => x[I_WATER] / (x[I_CEMENT] + x[I_FLYASH] + x[I_SLAG] + 1.0), |
| 41 | + scm_frac: (x) => (x[I_FLYASH] + x[I_SLAG]) / (x[I_CEMENT] + x[I_FLYASH] + x[I_SLAG] + 1.0), |
| 42 | + hrwr_binder: (x) => x[I_HRWR] / (x[I_CEMENT] + x[I_FLYASH] + x[I_SLAG] + 1.0), |
| 43 | + log_hrwr_binder: (x) => Math.log(x[I_HRWR] / (x[I_CEMENT] + x[I_FLYASH] + x[I_SLAG] + 1.0) + 1e-4), |
| 44 | + wc_ratio: (x) => x[I_WATER] / (x[I_CEMENT] + 1.0), |
| 45 | + log_wc_ratio: (x) => Math.log(x[I_WATER] / (x[I_CEMENT] + 1.0) + 1e-3), |
| 46 | + coarse_fine: (x) => x[I_COARSE] / (x[I_FINE] + 1.0), |
| 47 | + log_coarse_fine: (x) => Math.log(x[I_COARSE] / (x[I_FINE] + 1.0) + 1e-3), |
| 48 | + agg_paste: (x) => (x[I_FINE] + x[I_COARSE]) / |
| 49 | + (x[I_CEMENT] + x[I_FLYASH] + x[I_SLAG] + x[I_WATER] + 1.0), |
| 50 | + log_agg_paste: (x) => Math.log( |
| 51 | + (x[I_FINE] + x[I_COARSE]) / |
| 52 | + (x[I_CEMENT] + x[I_FLYASH] + x[I_SLAG] + x[I_WATER] + 1.0) + 1e-3, |
| 53 | + ), |
| 54 | + maturity_robust: (x) => Math.max(0, x[I_TEMP] + 10.0) * x[I_TIME], |
| 55 | + log_maturity_robust: (x) => Math.log(Math.max(0, x[I_TEMP] + 10.0) * x[I_TIME] + 1.0), |
| 56 | +}; |
| 57 | + |
| 58 | +/** |
| 59 | + * Append features to a raw input. Returns a NEW array of length |
| 60 | + * `xRaw.length + names.length`. |
| 61 | + */ |
| 62 | +export function appendFeatures(xRaw, names) { |
| 63 | + const out = new Array(xRaw.length + names.length); |
| 64 | + for (let i = 0; i < xRaw.length; i++) out[i] = xRaw[i]; |
| 65 | + for (let k = 0; k < names.length; k++) { |
| 66 | + const fn = FEATURE_FNS[names[k]]; |
| 67 | + if (!fn) { |
| 68 | + throw new Error(`Unknown engineered feature: '${names[k]}'. Add it to docs/feature_registry.mjs.`); |
| 69 | + } |
| 70 | + out[xRaw.length + k] = fn(xRaw); |
| 71 | + } |
| 72 | + return out; |
| 73 | +} |
0 commit comments