Skip to content

Commit 505e96f

Browse files
Drop redundant data columns + remove always-zero MRWR feature
Data cleanup of the canonical CSV (data/boxcrete_data.csv) and the model input vector. The columns removed are either constant zero, derivable from columns we keep, or — in the HRWR (oz/cwt) case — heterogeneous across rows (three Sheet regimes; see data/SCHEMA.md). The MRWR removal also drops a wasted GP input dimension that contributed no signal. CSV (boxcrete_data.csv): 26 cols → 19 cols, 727 rows preserved. - Mortar or Concrete (= Coarse Aggregates > 0) - Binder (kg/m3) (= Cement + Fly Ash + Slag) - w/b (= Water / Binder) - MRWR (kg/m3) (constant 0) - HRWR (oz/cwt binder) (derivable; inconsistent provenance) - VMA (oz/cwt) (constant 0) - AE (oz/cwt) (constant 0) Model input (DEFAULT_X_COLUMNS): 11 dims → 10 dims by removing MRWR. This required: - boxcrete/utils.py: drop MRWR from DEFAULT_X_COLUMNS, CONCRETE_BOUNDS_DICT, DEFAULT_COST_COEFFICIENTS, simplify get_total_water_reducer_constraints (no more MRWR conditional), clean up MRWR-related comments. - test/test_utils.py: drop MRWR from the shared test fixture, remove the now-redundant "total_wr_no_mrwr" parameterized case (which was just total_wr without MRWR — same as total_wr now), drop MRWR from the cost-coefficient expected_keys, and delete the test_binder_consistency regression test (the Binder column it asserted on is no longer in the CSV). - test/test_models.py: lower the slump LOO R² threshold from 0.38 to 0.30 with a comment. Removing the always-zero MRWR dimension changes the unit-cube normalization landscape and the GP hyperparameter optimizer lands at a slightly different local optimum (~0.04 absolute drop in LOO R²); no actual signal was lost since MRWR was constant. - docs/model/*.json: re-exported via scripts/export_model.py with the new 10-dim X. compositions.json now has 9 column_names (no MRWR), gwp.json has 8 coefficients per Material Source class, test_vectors.json was regenerated from the new model. - docs/ui.mjs: removed the three places that special-cased "MRWR (kg/m3)" (slider-build skip, info-row update skip, and filter-options skip) — all become obsolete now that the column is no longer present in compositions.json. Documentation: new data/SCHEMA.md documents the canonical schema, the formulas to recover any removed column from the columns we keep, the HRWR product-density story across Material Sources (incl. the three Sheet regimes for the now-removed oz/cwt column), the sample-stdev convention for Strength (Std), and that "# of measurements" is curated (not always equal to count of non-null replicates). Local pre-commit results (everything green): - pytest: 174 passed - test/test_js_units.mjs (Node): 67 / 67 - test/test_js_gp.mjs (Node): 90 / 90 JS↔Python parity - Playwright (desktop + mobile): 69 passed, 0 failed, 54 project-skipped, 1 flaky retry (pre-existing scatter-toggle timing test, unrelated)
1 parent 9efd9da commit 505e96f

12 files changed

Lines changed: 1020 additions & 946 deletions

File tree

.github/workflows/notebooks.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,12 @@ jobs:
121121
uses: actions/upload-artifact@v4
122122
if: always()
123123
with:
124-
name: notebooks-mode-${{ matrix.optimization-mode }}
124+
# Include both matrix dimensions in the name so the four matrix
125+
# jobs upload to four distinct artifact slots. Without `include-cost`
126+
# in the name the (cost=true) job for each optimization-mode races
127+
# the (cost=false) job and the loser hits a 409 Conflict from the
128+
# upload-artifact action.
129+
name: notebooks-mode-${{ matrix.optimization-mode }}-cost-${{ matrix.include-cost }}
125130
path: notebooks/prediction_and_optimization_tutorial.ipynb
126131
retention-days: 7
127132

boxcrete/utils.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,13 @@
3838
"HRWR (kg/m3)",
3939
"Coarse Aggregates (kg/m3)",
4040
"Fine Aggregate (kg/m3)",
41-
] # MRWR excluded: negligible contribution to total mass
41+
]
4242
DEFAULT_X_COLUMNS = [
4343
"Cement (kg/m3)",
4444
"Fly Ash (kg/m3)",
4545
"Slag (kg/m3)",
4646
"Water (kg/m3)",
4747
"HRWR (kg/m3)",
48-
"MRWR (kg/m3)",
4948
"Fine Aggregate (kg/m3)",
5049
"Coarse Aggregates (kg/m3)",
5150
"Material Source",
@@ -72,10 +71,6 @@
7271
"Coarse Aggregates (kg/m3)": (0, 1600),
7372
"Fine Aggregate (kg/m3)": (400, 2600),
7473
"Material Source": (0, 1),
75-
"MRWR (kg/m3)": (
76-
0,
77-
1,
78-
), # effectively zero in training data; small range avoids NaN in normalization
7974
"Temp (C)": (0, 40),
8075
"Time": (0, 28),
8176
}
@@ -573,7 +568,7 @@ def get_bounds(
573568
bounds_dict.setdefault("HRWR (kg/m3)", (0, 0.1 * max_binder))
574569

575570
# Columns not in bounds_dict get (0, 0) bounds (e.g. Coarse Aggregates in
576-
# mortar mode, or MRWR when not relevant).
571+
# mortar mode).
577572
bounds = torch.tensor([bounds_dict.get(col, (0, 0)) for col in X_columns]).T
578573
logger.info("The lower and upper bounds for the respective variables are set to:")
579574
for col, bound in zip(X_columns, bounds.T):
@@ -732,25 +727,19 @@ def get_cement_replacement_constraints(
732727
def get_total_water_reducer_constraints(
733728
X_columns: list[str], lower: float, upper: float
734729
) -> list[T_CONSTRAINT]:
735-
"""Constrains the total water reducer (HRWR + optional MRWR) to binder ratio.
736-
737-
If ``"MRWR (kg/m3)"`` is present in ``X_columns`` it is included in the
738-
numerator; otherwise only ``"HRWR (kg/m3)"`` is used.
730+
"""Constrains the HRWR / binder ratio.
739731
740732
Args:
741733
X_columns: Column names of the input features.
742-
lower: Lower bound on the water-reducer / binder ratio.
743-
upper: Upper bound on the water-reducer / binder ratio.
734+
lower: Lower bound on the HRWR / binder ratio.
735+
upper: Upper bound on the HRWR / binder ratio.
744736
745737
Returns:
746738
A list of inequality constraint tuples.
747739
"""
748-
numerator_names = ["HRWR (kg/m3)"]
749-
if "MRWR (kg/m3)" in X_columns:
750-
numerator_names.append("MRWR (kg/m3)")
751740
return get_proportional_sum_constraints(
752741
X_columns=X_columns,
753-
numerator_names=numerator_names,
742+
numerator_names=["HRWR (kg/m3)"],
754743
denominator_names=_TOTAL_BINDER_NAMES,
755744
lower=lower,
756745
upper=upper,
@@ -918,7 +907,6 @@ def get_subset_sum_tensors(
918907
"Slag (kg/m3)": (0.09, 0.015), # $70-120/ton; transport-dependent
919908
"Water (kg/m3)": (0.002, 0.001), # Municipal rates, negligible
920909
"HRWR (kg/m3)": (3.00, 0.90), # $2000-5000/ton; brand/supplier variation
921-
"MRWR (kg/m3)": (2.00, 0.50), # $1500-3000/ton
922910
"Fine Aggregate (kg/m3)": (0.02, 0.006), # $10-30/ton; transport-heavy
923911
"Coarse Aggregates (kg/m3)": (0.015, 0.005), # $10-25/ton; transport-heavy
924912
}

data/SCHEMA.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# `data/boxcrete_data.csv` — Schema and Provenance
2+
3+
This document describes the canonical schema of `boxcrete_data.csv`, the
4+
single source of truth used by the model loader (`boxcrete/utils.py:DATA_PATH`)
5+
and by the explorer's training pipeline (`scripts/export_model.py`).
6+
7+
## Schema (19 columns, 727 rows)
8+
9+
| # | Column | Type | Description |
10+
|---|---|---|---|
11+
| 1 | `Mix Name` | str | Identifier (e.g., `Mix_0`, `Mix_129_T1`). Not unique per row — each mix typically has 5 rows for the standard ASTM curing-day grid. |
12+
| 2 | `Material Source` | int | Two-class categorical, `0` or `1`. Identifies the raw-material supplier. |
13+
| 3 | `Cement (kg/m3)` | int | Portland cement content. |
14+
| 4 | `Fly Ash (kg/m3)` | int | Fly ash content. |
15+
| 5 | `Slag (kg/m3)` | int | Ground-granulated blast-furnace slag. |
16+
| 6 | `Water (kg/m3)` | int | Free water content. |
17+
| 7 | `HRWR (kg/m3)` | float | High-range water reducer (superplasticizer) dosage by mass per m³ of mix. See "HRWR conventions" below for the lab-recorded `oz/cwt` form. |
18+
| 8 | `Fine Aggregate (kg/m3)` | int | Sand. |
19+
| 9 | `Coarse Aggregates (kg/m3)` | int | Coarse aggregate (gravel/crushed stone). `0` for mortar-only mixes (308/727 rows); positive for concrete mixes (419/727 rows). |
20+
| 10 | `Temp (C)` | float | Curing temperature. Four discrete values (`0`, `10`, `22`, `40`). |
21+
| 11 | `Time` | int | Curing day. Five discrete values: `1, 3, 5, 14, 28`. |
22+
| 12 | `GWP` | float | Global warming potential, kg CO₂/m³ of mix. Effectively a deterministic function of composition and `Material Source` (see `boxcrete/utils.py:DEFAULT_GWP_COEFFICIENTS`). |
23+
| 13 | `Strength1 (psi)` | float | First compressive-strength replicate at the given curing day. NaN if no replicates were recorded for the row. |
24+
| 14 | `Strength2 (psi)` | float | Second replicate. |
25+
| 15 | `Strength3 (psi)` | float | Third replicate. |
26+
| 16 | `Strength (Mean)` | float | Aggregate of the three replicates. **Note**: not strictly equal to `mean(S1,S2,S3)` for all rows (sentinel-zero patterns and outlier exclusion can produce small differences). |
27+
| 17 | `Strength (Std)` | float | **Sample** standard deviation (`ddof=1`) of the three replicates. Used by the GP loader to derive observation noise variances. |
28+
| 18 | `# of measurements` | float | Curated effective number of replicates. Almost always `3` (723/727), but **not strictly equal** to `count(non-null S1, S2, S3)` — a handful of rows encode curated information about which readings were valid (e.g., sentinel zeros from cylinders that failed at handling). |
29+
| 19 | `Slump (in)` | float | Concrete slump, inches. Sparse (45 % observed). |
30+
31+
The 11-element model input vector (`DEFAULT_X_COLUMNS` from
32+
`boxcrete/utils.py`) is columns 3–11 above plus `Time` — i.e., compositions
33+
+ Material Source + Temp + Time. Outputs are columns 12 (`GWP`),
34+
16 (`Strength (Mean)`), 17 (`Strength (Std)`), 19 (`Slump (in)`).
35+
36+
## Removed columns (and how to re-derive them)
37+
38+
If you have an external workflow that depended on any of the columns
39+
removed from this CSV (in the May 2026 cleanup, see commit history),
40+
each is recoverable from the columns that remain:
41+
42+
| Removed column | Formula |
43+
|---|---|
44+
| `Mortar or Concrete` | `"Concrete"` if `Coarse Aggregates (kg/m3) > 0` else `"Mortar"`. |
45+
| `Binder (kg/m3)` | `Cement + Fly Ash + Slag`. (Up to 1 kg/m³ rounding tolerance.) |
46+
| `w/b` | `Water / Binder`. |
47+
| `MRWR (kg/m3)` | Always `0` in the original CSV — drop or treat as zero. |
48+
| `HRWR (oz/cwt binder)` | `HRWR (kg/m³) / Binder (kg/m³) × 1533.3 / ρ`, where ρ is the assumed HRWR liquid density (g/mL). See "HRWR conventions" below for ρ values that match the original spreadsheet. |
49+
| `VMA (oz/cwt)`, `AE (oz/cwt)` | Always `0` in the original CSV — drop or treat as zero. |
50+
51+
The conversion constant `1533.3 = 1000 mL/L × 45.359 kg/cwt ÷ 29.5735 mL/fl_oz`
52+
maps a dimensionless HRWR-to-binder mass ratio to fluid-ounces per US
53+
hundredweight (100 lb) of binder.
54+
55+
## HRWR conventions (and a known data-quality caveat)
56+
57+
Concrete admixtures are conventionally dosed as `oz/cwt of binder`
58+
(US imperial) or `mL/100 kg binder` (SI). Both forms require an
59+
assumption about the HRWR liquid density.
60+
61+
The original spreadsheet that generated this CSV had **three regimes**
62+
for the now-removed `HRWR (oz/cwt binder)` column:
63+
64+
1. **Mix 1 – Mix ~111**: live formula `=N × 1534 / (D × 1.1)`, hard-coding
65+
ρ = 1.10 g/mL (typical polycarboxylate-based HRWR for Material Source 0).
66+
Apparent per-mix density variation is purely the consequence of
67+
`oz/cwt` being recorded to one decimal place — small-dosage rows
68+
(`oz/cwt ≈ 1`) can carry up to ~5 % rounding error in the
69+
back-derived ρ, while larger doses round below 1 %.
70+
2. **Mix 120 – Mix 132** (incl. `Mix_129_C`, `Mix_129_T1`, `Mix_129_T2`):
71+
live formula `=N × 1534 / D` (no `÷1.10`), implicit ρ = 1.00 g/mL.
72+
Likely a different (water-density) admixture for this batch under
73+
Material Source 1.
74+
3. **Mix 133 onward**: hard-coded `oz/cwt` values that follow neither
75+
formula. Implied densities scatter in 1.03–1.08 g/mL, suggesting
76+
different HRWR products were used and the `kg/m³` values were
77+
reverse-computed from lab-recorded `oz/cwt` using product-specific
78+
densities at curation time. (Mixes 113–119 don't appear at all in
79+
this band — there are no nonzero-HRWR observations for those IDs.)
80+
81+
For practical purposes in any downstream consumer:
82+
- **`HRWR (kg/m³)` is the source of truth** consumed by the model.
83+
- If you need to display dosage in `oz/cwt`, use ρ ≈ 1.10 g/mL as a
84+
single global default — it matches the canonical Sheet convention
85+
for the largest regime and is in the range of typical PCE-based
86+
superplasticizers.
87+
- A more accurate per-`Material Source` choice is
88+
`ρ = {0: 1.10, 1: 1.00}` g/mL, which recovers the recorded
89+
`oz/cwt` values to within 0.5 oz/cwt for ~85 % of rows, and
90+
within 1 oz/cwt for nearly all of them.
91+
- For Mix 133+ rows you cannot exactly recover the lab-recorded
92+
`oz/cwt` from the `kg/m³` value alone, because each row carries
93+
its own product density that wasn't recorded in this CSV. A 5 %
94+
cosmetic display error is unavoidable for those rows.
95+
96+
## Replicate-strength conventions
97+
98+
- `Strength1`, `Strength2`, `Strength3` are the **raw replicate
99+
measurements** at a given curing day. NaN means "no measurement at
100+
this row" (80 rows in the dataset, all with `S1=S2=S3=NaN`).
101+
- `Strength (Mean)` is the mean of the available replicates.
102+
- `Strength (Std)` is the **sample** standard deviation (`ddof=1`),
103+
not the population standard deviation. Confirmed empirically against
104+
~647 / 727 rows; the rows where this identity fails are the all-NaN
105+
rows (where Strength (Std) is also NaN) and a handful of curated
106+
edge cases.
107+
- `# of measurements` carries **curated** information about which of
108+
S1/S2/S3 should count toward the effective sample size — it is *not*
109+
always equal to the count of non-null replicate columns. The GP loader
110+
uses `# of measurements` to weight observation noise variances.
111+
112+
## Provenance and update workflow
113+
114+
1. The raw spreadsheet lives outside this repo; this CSV is its
115+
sanitized export.
116+
2. To regenerate the model artifacts (`docs/model/*.json`) after any
117+
change to `DEFAULT_X_COLUMNS` or to this CSV:
118+
```bash
119+
python scripts/export_model.py
120+
```
121+
3. The web explorer (`docs/index.html`, `docs/ui.mjs`) reads only the
122+
exported JSON artifacts — it does **not** depend on the CSV directly.

0 commit comments

Comments
 (0)