Skip to content

Commit 39fffbf

Browse files
[DO NOT LAND] Design exploration: 3-class kernel study, ablations, provenance
Research provenance for the three-class strength GP. This commit is NOT intended to merge -- it preserves the variants, ablations and diagnostics that produced the production model so the design choices can be audited. Kernel and prior variants evaluated (restored into boxcrete/ so the ablation scripts and their tests run as written): - RBFEmbeddingKernel: learned per-class embedding source kernel. - JointHammingMaternKernel / JointEmbeddingMaternKernel: joint feature-plus-categorical metrics, chain and hamming modes. - IndexKernel ranks 1-3, onehot_ard, legacy_continuous_ard, additive hybrid source kernels, and the fixed-task-covar pooling kernel. - TaskPoolingPrior and the cross-component time-lengthscale tying prior. - PerClassGatedGaussianLikelihood for the per-class noise ablation. These are reachable via the `source_kernel` parameter of build_strength_kernel_for_aug_dim, which the production commit removes because hamming is the only shipped topology. Benchmark writeups and result CSVs: STRENGTH_GP_BENCHMARK, THREE_CLASS_AND_PRIOR_BENCHMARK, the RBF-embedding / joint-distance / hybrid-and-twin-drop / no-blind / per-class-noise ablations, the noise audit, the Pareto and headroom analysis, and the v5-vs-pre-v5 regression investigation. Data provenance: scripts/merge_three_class_data.py (the 2-class to 3-class merge, including the Mix_126..Mix_137 collision split and the M60..M74 strength-less drop), its invariant tests, and the frozen test/fixtures/boxcrete_data_pre_v5.csv the comparisons read from. experiments/mix_narratives/ documents how docs/model/mix_analyses.json was authored: build_mix_facts.py joins the explorer catalog back to the dataset to recover canonical mix names (exact match, 149/149), and author_mix_analyses.py composes each entry from verified figures plus hand-authored interpretation. Running both reproduces the shipped artifact byte-for-byte. The entries omit measured strength points, embodied carbon and Pareto status, all of which the explorer already renders in its own panels. Its check_claims pass asserts 27 superlative claims against the data and caught 7 false statements during authoring. experiments/notes/ carries the gate-tau and 3-class explorer parity investigation. Known CI state, accepted for a do-not-land commit: - 326 tests pass, but coverage is 85.3% against the repo's 100% gate: the exploratory variants are exercised by the ablation scripts, not by unit tests. - flake8 reports 100 style violations under experiments/, scripts/ and test/, carried over from the exploration branch. boxcrete/ and experiments/mix_narratives/ are clean and black-formatted.
1 parent 19ea2b7 commit 39fffbf

79 files changed

Lines changed: 33729 additions & 50 deletions

File tree

Some content is hidden

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

boxcrete/data_cleaning.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) Meta Platforms, Inc. and affiliates.
3+
#
4+
# This source code is licensed under the MIT license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""Break-level screening transforms for the concrete strength dataset.
8+
9+
The raw dataset (``data/boxcrete_data.csv``) stores every measured
10+
cylinder break in ``Strength{1,2,3} (psi)``. These transforms screen
11+
those raw breaks at load time and recompute the derived columns the
12+
strength GP consumes — ``Strength (Mean)``, ``Strength (Std)`` (sample
13+
SD, ddof=1) and ``# of measurements`` — WITHOUT mutating the raw break
14+
columns. Keeping the raw values in the CSV and screening in code makes
15+
each screening rule reproducible, parameterisable, and ablatable.
16+
17+
Two screens, matching the collaborator's protocol:
18+
19+
1. **COV screen** (``apply_cov_screen``): for each test with three
20+
breaks, if the coefficient of variation (COV = sample SD / mean)
21+
exceeds ``threshold`` percent, drop the single break furthest from
22+
the mean and recompute on the remaining two. If still over
23+
threshold, drop the whole (mix, age) row.
24+
25+
2. **Monotonicity screen** (``apply_monotonicity_screen``): within a
26+
mix, ordered by curing age, remove any later-age result whose
27+
(post-COV) mean strength falls below an earlier age — concrete
28+
strength should not decrease with age.
29+
"""
30+
31+
from __future__ import annotations
32+
33+
import numpy as np
34+
import pandas as pd
35+
36+
DEFAULT_BREAK_COLS = ["Strength1 (psi)", "Strength2 (psi)", "Strength3 (psi)"]
37+
DEFAULT_MEAN_COL = "Strength (Mean)"
38+
DEFAULT_STD_COL = "Strength (Std)"
39+
DEFAULT_N_COL = "# of measurements"
40+
DEFAULT_MIX_COL = "Mix Name"
41+
DEFAULT_TIME_COL = "Time"
42+
43+
44+
def cov_percent(breaks) -> float:
45+
"""Coefficient of variation (%) of a set of breaks: 100 * sample SD / mean.
46+
47+
NaNs are ignored. Uses the sample standard deviation (``ddof=1``) to
48+
match the ``Strength (Std)`` convention in the dataset. Returns
49+
``inf`` for a non-positive mean (degenerate sentinel rows) so the
50+
caller treats them as failing any finite threshold.
51+
"""
52+
vals = np.asarray([b for b in breaks if pd.notna(b)], dtype=float)
53+
mean = vals.mean()
54+
if mean <= 0:
55+
return float("inf")
56+
return 100.0 * vals.std(ddof=1) / mean
57+
58+
59+
def _screen_breaks(values: list[float], threshold: float) -> list[float] | None:
60+
"""Apply the COV protocol to the non-null breaks of one test.
61+
62+
Returns the list of breaks to keep, or ``None`` if the row should be
63+
dropped entirely. Rows with fewer than three breaks are returned
64+
unchanged (nothing to trim).
65+
"""
66+
if len(values) < 3:
67+
return values
68+
if cov_percent(values) <= threshold:
69+
return values
70+
arr = np.asarray(values, dtype=float)
71+
furthest = int(np.argmax(np.abs(arr - arr.mean())))
72+
pair = np.delete(arr, furthest).tolist()
73+
if cov_percent(pair) <= threshold:
74+
return pair
75+
return None
76+
77+
78+
def apply_cov_screen(
79+
df: pd.DataFrame,
80+
threshold: float = 10.0,
81+
break_cols: list[str] = DEFAULT_BREAK_COLS,
82+
mean_col: str = DEFAULT_MEAN_COL,
83+
std_col: str = DEFAULT_STD_COL,
84+
n_col: str = DEFAULT_N_COL,
85+
) -> pd.DataFrame:
86+
"""Screen each row by within-test COV and recompute derived columns.
87+
88+
The raw ``break_cols`` are preserved untouched; only ``mean_col``,
89+
``std_col`` and ``n_col`` are recomputed from the retained breaks.
90+
Rows whose scatter cannot be brought under ``threshold`` are dropped.
91+
"""
92+
kept_rows = []
93+
for idx, row in df.iterrows():
94+
values = [float(row[c]) for c in break_cols if pd.notna(row[c])]
95+
kept = _screen_breaks(values, threshold)
96+
if kept is None:
97+
continue
98+
new_row = row.copy()
99+
arr = np.asarray(kept, dtype=float)
100+
new_row[mean_col] = arr.mean()
101+
new_row[std_col] = arr.std(ddof=1) if len(arr) > 1 else 0.0
102+
new_row[n_col] = len(arr)
103+
kept_rows.append(new_row)
104+
if not kept_rows:
105+
return df.iloc[0:0].copy()
106+
return pd.DataFrame(kept_rows).reset_index(drop=True)
107+
108+
109+
def apply_monotonicity_screen(
110+
df: pd.DataFrame,
111+
mix_col: str = DEFAULT_MIX_COL,
112+
time_col: str = DEFAULT_TIME_COL,
113+
mean_col: str = DEFAULT_MEAN_COL,
114+
) -> pd.DataFrame:
115+
"""Remove later-age results whose mean falls below an earlier age.
116+
117+
Within each mix, rows are ordered by ``time_col`` and a running
118+
maximum of ``mean_col`` is tracked; any row below the running max of
119+
strictly earlier ages is dropped (concrete strength should not
120+
regress with curing age).
121+
"""
122+
keep_mask = pd.Series(True, index=df.index)
123+
for _, group in df.groupby(mix_col, sort=False):
124+
running_max = -np.inf
125+
for idx in group.sort_values(time_col).index:
126+
mean = df.loc[idx, mean_col]
127+
if mean < running_max:
128+
keep_mask[idx] = False
129+
else:
130+
running_max = mean
131+
return df[keep_mask].reset_index(drop=True)

0 commit comments

Comments
 (0)