Skip to content

Commit 335b02f

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 335b02f

12 files changed

Lines changed: 999 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: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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 field-conventional `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 10-element model input vector (`DEFAULT_X_COLUMNS` from
32+
`boxcrete/utils.py`) consists of `Material Source`, the seven composition
33+
columns (Cement, Fly Ash, Slag, Water, HRWR, Fine/Coarse Aggregate),
34+
`Temp (C)`, and `Time`. Outputs are `GWP`, `Strength (Mean)`,
35+
`Strength (Std)`, and `Slump (in)`.
36+
37+
## HRWR conventions
38+
39+
Concrete admixtures are conventionally dosed as `oz/cwt of binder`
40+
(US imperial) or `mL/100 kg binder` (SI). Both forms require an
41+
assumption about the HRWR liquid density. The model uses the simpler
42+
`HRWR (kg/m³)` form, but if you want to display dosage in field-
43+
conventional units:
44+
45+
```
46+
oz/cwt of binder = HRWR (kg/m³) / Binder (kg/m³) × 1533.3 / ρ
47+
```
48+
49+
where `Binder = Cement + Fly Ash + Slag` and ρ is the assumed HRWR
50+
liquid density (g/mL). The conversion constant
51+
`1533.3 = 1000 mL/L × 45.359 kg/cwt ÷ 29.5735 mL/fl_oz` maps a
52+
dimensionless mass ratio to fluid-ounces per US hundredweight (100 lb)
53+
of binder.
54+
55+
The HRWR product used to generate this dataset varies across the
56+
experimental campaigns and `Material Source` classes:
57+
58+
- **`Material Source = 0`** rows are consistent with ρ ≈ 1.10 g/mL
59+
(typical polycarboxylate-based HRWR).
60+
- **`Material Source = 1`** rows split between ρ ≈ 1.00 g/mL (the early
61+
Mix_120s series) and ρ ≈ 1.03–1.08 g/mL (Mix_133+, where each
62+
recorded dosage was tied to a specific product whose density isn't
63+
itself in the CSV).
64+
65+
Practical recommendations for any downstream consumer:
66+
67+
- For a single global default, use **ρ = 1.10 g/mL** — it matches the
68+
largest sub-population and is squarely in the range of typical PCE-
69+
based superplasticizers (~5 % cosmetic error elsewhere).
70+
- For a more accurate per-source choice, use
71+
`ρ = {0: 1.10, 1: 1.00}` g/mL.
72+
73+
## Replicate-strength conventions
74+
75+
- `Strength1`, `Strength2`, `Strength3` are the **raw replicate
76+
measurements** at a given curing day. NaN means "no measurement at
77+
this row" (80 rows in the dataset, all with `S1=S2=S3=NaN`).
78+
- `Strength (Mean)` is the mean of the available replicates.
79+
- `Strength (Std)` is the **sample** standard deviation (`ddof=1`),
80+
not the population standard deviation. Confirmed empirically against
81+
~647 / 727 rows; the rows where this identity fails are the all-NaN
82+
rows (where `Strength (Std)` is also NaN) and a handful of curated
83+
edge cases.
84+
- `# of measurements` carries **curated** information about which of
85+
S1/S2/S3 should count toward the effective sample size — it is *not*
86+
always equal to the count of non-null replicate columns. The GP loader
87+
uses `# of measurements` to weight observation noise variances.
88+
89+
## Provenance and update workflow
90+
91+
1. The raw spreadsheet lives outside this repo; this CSV is its
92+
sanitized export.
93+
2. To regenerate the model artifacts (`docs/model/*.json`) after any
94+
change to `DEFAULT_X_COLUMNS` or to this CSV:
95+
```bash
96+
python scripts/export_model.py
97+
```
98+
3. The web explorer (`docs/index.html`, `docs/ui.mjs`) reads only the
99+
exported JSON artifacts — it does **not** depend on the CSV directly.

0 commit comments

Comments
 (0)