Skip to content

Commit e94fd75

Browse files
Unify mortar/concrete optimization, consolidate constraints, 100% test coverage
- Rename BOxCrete_data.csv → boxcrete_data.csv (lowercase, remove clay columns) - Remove compressive_strength.csv (subsumed by boxcrete data) - Rename strength_model_tutorial.ipynb → prediction_and_optimization_tutorial.ipynb - Update all constants to boxcrete column names (with units) - Consolidate get_mortar_bounds + get_bounds → single get_bounds with MORTAR_BOUNDS_DICT / CONCRETE_BOUNDS_DICT presets - Consolidate get_mortar_constraints + get_concrete_constraints → single get_constraints with equality_sums parameter and MORTAR_CONSTRAINTS / CONCRETE_CONSTRAINTS presets - Inline thin constraint wrapper functions into get_constraints - Generalize get_model_list(fixed_features=None) for mortar/concrete optimization — backward compatible, wraps GWP model when non-Time features are fixed - Add reduce_to_optimization_space for dimension reduction of bounds and constraints when using FixedFeatureModel - Add boxcrete/plotting.py with plot_strength_curve utility - Parameterize notebooks with optimization_mode env var (mortar/concrete) - Fix demo notebook hardcoded tensors → data-driven column lookup - CI executes notebooks in both mortar and concrete modes - Add docstrings to all public methods and classes - 79 tests, 100% coverage
1 parent e32deea commit e94fd75

16 files changed

Lines changed: 2886 additions & 2218 deletions

.github/workflows/notebooks.yml

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,23 @@ jobs:
5858
run: |
5959
python -m ipykernel install --user --name python3
6060
61-
- name: Execute notebooks
61+
- name: Execute notebooks (mortar and concrete modes)
6262
run: |
63-
python - << 'PYEOF'
64-
import subprocess, sys
63+
for mode in mortar concrete; do
64+
echo "========================================"
65+
echo "Running notebooks with BOXCRETE_OPTIMIZATION_MODE=$mode"
66+
echo "========================================"
67+
BOXCRETE_OPTIMIZATION_MODE=$mode python - << 'PYEOF'
68+
import subprocess, sys, os
6569
from pathlib import Path
6670
71+
mode = os.environ.get("BOXCRETE_OPTIMIZATION_MODE", "concrete")
6772
notebooks = sorted(Path("notebooks").glob("*.ipynb"))
6873
failed = []
6974
7075
for nb in notebooks:
7176
print(f"{'=' * 40}")
72-
print(f"Executing: {nb}")
77+
print(f"Executing ({mode}): {nb}")
7378
print(f"{'=' * 40}")
7479
7580
result = subprocess.run(
@@ -87,19 +92,20 @@ jobs:
8792
8893
if result.returncode != 0:
8994
failed.append(str(nb))
90-
print(f"❌ Failed: {nb}")
95+
print(f"❌ Failed ({mode}): {nb}")
9196
else:
92-
print(f"✅ Successfully executed: {nb}")
97+
print(f"✅ Successfully executed ({mode}): {nb}")
9398
print()
9499
95100
if failed:
96-
print(f"\n{len(failed)} notebook(s) failed:")
101+
print(f"\n{len(failed)} notebook(s) failed in {mode} mode:")
97102
for f in failed:
98103
print(f" - {f}")
99104
sys.exit(1)
100105
else:
101-
print(f"\nAll {len(notebooks)} notebook(s) executed successfully.")
106+
print(f"\nAll {len(notebooks)} notebook(s) executed successfully in {mode} mode.")
102107
PYEOF
108+
done
103109
104110
- name: Upload executed notebooks as artifacts
105111
uses: actions/upload-artifact@v4

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,17 @@ Thumbs.db
5656
*.tar.gz
5757
*.zip
5858

59+
# Legacy root-level modules (superseded by boxcrete/ package)
60+
/models.py
61+
/utils.py
62+
/input_transform.py
63+
/doodles.py
64+
/posterior_mean_pareto.py
65+
66+
# Notebook-generated output files
67+
ConcreteFormulae.csv
68+
ConcreteFormula_*.png
69+
GWPvsStrength.png
70+
5971
# Planning files
6072
*.plan.md

README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# BOxCrete: A Bayesian Optimization open-source AI Model for Concrete Mix Design & Optimization
22

3-
Concrete, the second most widely used material in the world, accounts for **6–8% of global anthropogenic CO₂ emissions**, largely due to Portland cement production (~0.8 tons CO₂ per ton of cement). Partial replacement with Supplementary Cementitious Materials (SCMs) such as fly ash, slag, and natural pozzolan reduces embodied carbon and often improves durability, but high SCM usage makes compressive strength a highly nonlinear function of multiple interacting mix parameters, rendering traditional design empirical and trial-and-error driven. To systematically navigate this complex composition space, data-driven frameworks are needed.
4-
Here, we introduce BOxCrete, an open-source Bayesian optimization framework for probabilistic strength curve prediction and sustainable mix design.
3+
Concrete, the second most widely used material in the world, accounts for **6–8% of global anthropogenic CO₂ emissions**, largely due to Portland cement production (~0.8 tons CO₂ per ton of cement). Partial replacement with Supplementary Cementitious Materials (SCMs) such as fly ash, slag, and natural pozzolan reduces embodied carbon and often improves durability, but high SCM usage makes compressive strength a highly nonlinear function of multiple interacting mix parameters, rendering traditional design empirical and trial-and-error driven. To systematically navigate this complex composition space, data-driven frameworks are needed.
4+
Here, we introduce BOxCrete, an open-source Bayesian optimization framework for probabilistic strength curve prediction and sustainable mix design.
55
We invite researchers and practitioners of both machine learning and civil engineering
66
to collaborate on discovering more sustainable concrete formulations that are applicable
77
to a wide array of construction projects, at scale.
@@ -13,9 +13,11 @@ This repository contains probabilistic models and data for the
1313
1) Compressive strength of concrete and mortar mixes
1414
2) The associated global warming potential (GWP)
1515

16-
as a function of their composition, consisting of
17-
cement, slag, water, to name a few basic ingredients.
18-
See `boxcrete/models.py` for implementation details.
16+
as a function of their composition, consisting of cement, fly ash, slag, fine and coarse aggregate, admixtures, and water, to name a few basic ingredients. See `boxcrete/models.py` for implementation details.
17+
18+
### Included Datasets
19+
20+
- **BOxCrete data** (`data/boxcrete_data.csv`): Combined mortar and concrete mix compositions with strength measurements at multiple curing ages, GWP values, and multiple material sources. This is the single unified dataset used for all model training.
1921

2022
## Installation
2123

@@ -45,7 +47,7 @@ pip install -e ".[notebooks]"
4547

4648
```python
4749
from boxcrete.models import SustainableConcreteModel
48-
from boxcrete.utils import load_concrete_strength, get_mortar_bounds
50+
from boxcrete.utils import load_concrete_strength, get_bounds
4951
```
5052

5153
The models can be used for a variety of tasks, including but not limited to
@@ -58,7 +60,7 @@ The models can be used for a variety of tasks, including but not limited to
5860

5961
## Compressive Strength Model
6062

61-
The `SustainableConcreteModel` in ['BOxCrete_models.py'](BOxCrete_models.py) includes a strength_model that predicts the evolution of compressive strength as a function of mixture composition. A tutorial is provided in [notebooks/BOxCrete Concrete Strength Prediction for GitHub.ipynb](<notebooks/BOxCrete Concrete Strength Prediction for GitHub.ipynb>), which demonstrates how the model can be used to predict the full strength development curve for any user-specified mix. The model is based on Gaussian Process (GP) regression and incorporates custom modeling steps to ensure physically consistent strength evolution and calibrated uncertainty. Example strength curve predictions generated using the notebook are shown in the figures below.
63+
The `SustainableConcreteModel` in [`boxcrete/models.py`](boxcrete/models.py) includes a strength_model that predicts the evolution of compressive strength as a function of mixture composition. A demo is provided in [`notebooks/strength_curve_prediction_demo.ipynb`](notebooks/strength_curve_prediction_demo.ipynb), which demonstrates how the model can be used to predict the full strength development curve for any user-specified mix. A comprehensive tutorial covering prediction, calibration, Pareto frontiers, and gradient-based experimental design is available in [`notebooks/prediction_and_optimization_tutorial.ipynb`](notebooks/prediction_and_optimization_tutorial.ipynb). The model is based on Gaussian Process (GP) regression and incorporates custom modeling steps to ensure physically consistent strength evolution and calibrated uncertainty. Example strength curve predictions generated using the notebook are shown in the figures below.
6264

6365
<p align="center">
6466
<img src="fig/Picture1.png">
@@ -80,7 +82,7 @@ Further, when trained on the mortar and concrete mix strength data contained in
8082

8183
## Experimental Design
8284

83-
The probabilistic compressive strength model can also be used to design new concrete mixtures that achieve optimal trade-offs between mechanical performance and environmental impact. In particular, the framework enables multi-objective optimization of early-age (1-day) and later-age (28-day) compressive strength alongside Global Warming Potential (GWP). By systematically exploring the composition space, BOxCrete can generate candidate mixes that balance structural performance requirements with carbon reduction targets.
85+
The probabilistic compressive strength model can also be used to design new concrete mixtures that achieve optimal trade-offs between mechanical performance and environmental impact. In particular, the framework enables multi-objective optimization of early-age (1-day) and later-age (28-day) compressive strength alongside Global Warming Potential (GWP). By systematically exploring the composition space, BOxCrete can generate candidate mixes that balance structural performance requirements with carbon reduction targets.
8486

8587
As illustrated in the figure below, the model identifies a Pareto front capturing the trade-off between 1-day strength, 28-day strength, and GWP across candidate mixtures.
8688

boxcrete/__init__.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,47 @@
1111
fit_strength_gp,
1212
get_strength_gp_input_transform,
1313
)
14+
from boxcrete.plotting import plot_strength_curve
1415
from boxcrete.utils import (
15-
DEFAULT_DATA_PATH,
16+
CONCRETE_BOUNDS_DICT,
17+
CONCRETE_CONSTRAINTS,
18+
DATA_PATH,
19+
DEFAULT_BOUNDS_DICT,
1620
DEFAULT_X_COLUMNS,
1721
DEFAULT_Y_COLUMNS,
1822
DEFAULT_YSTD_COLUMNS,
23+
MORTAR_BOUNDS_DICT,
24+
MORTAR_CONSTRAINTS,
1925
SustainableConcreteDataset,
26+
get_bounds,
27+
get_constraints,
2028
get_day_zero_data,
21-
get_mortar_bounds,
22-
get_mortar_constraints,
2329
get_reference_point,
2430
load_concrete_strength,
31+
reduce_to_optimization_space,
2532
)
2633

2734
__all__ = [
28-
"FixedFeatureModel",
29-
"SustainableConcreteModel",
30-
"fit_gwp_gp",
31-
"fit_strength_gp",
32-
"get_strength_gp_input_transform",
33-
"DEFAULT_DATA_PATH",
35+
"CONCRETE_BOUNDS_DICT",
36+
"CONCRETE_CONSTRAINTS",
37+
"DATA_PATH",
38+
"DEFAULT_BOUNDS_DICT",
3439
"DEFAULT_X_COLUMNS",
3540
"DEFAULT_Y_COLUMNS",
3641
"DEFAULT_YSTD_COLUMNS",
42+
"FixedFeatureModel",
43+
"MORTAR_BOUNDS_DICT",
44+
"MORTAR_CONSTRAINTS",
45+
"SustainableConcreteModel",
3746
"SustainableConcreteDataset",
47+
"fit_gwp_gp",
48+
"fit_strength_gp",
49+
"get_bounds",
50+
"get_constraints",
3851
"get_day_zero_data",
39-
"get_mortar_bounds",
40-
"get_mortar_constraints",
4152
"get_reference_point",
53+
"get_strength_gp_input_transform",
4254
"load_concrete_strength",
55+
"plot_strength_curve",
56+
"reduce_to_optimization_space",
4357
]

boxcrete/models.py

Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@
3333

3434

3535
class SustainableConcreteModel:
36+
"""Multi-output model that jointly predicts GWP and compressive strength.
37+
38+
The model consists of a GWP model (independent of curing time) and a strength
39+
model (dependent on composition *and* time). At optimisation time the strength
40+
model is sliced at each of the ``strength_days`` via ``FixedFeatureModel`` to
41+
produce a ``ModelList`` that maps composition only to ``[GWP, 1-day strength,
42+
28-day strength, ...]``.
43+
"""
44+
3645
def __init__(
3746
self,
3847
strength_days: list[int],
@@ -100,37 +109,82 @@ def _set_d(self, d: int) -> None:
100109
if self.d is None:
101110
self.d = d
102111

103-
def get_model_list(self) -> ModelList:
104-
"""Returns a ModelList modeling the GWP and compressive strength objectives as a function
105-
of composition only.
106-
Converts the strength and gwp models into a model list of independent models for gwp,
107-
and x-day strengths, by fixing the time input of the strength model at 1 and 28 days.
112+
def get_model_list(
113+
self, fixed_features: dict[int, float] | None = None
114+
) -> ModelList:
115+
"""Returns a ``ModelList`` modelling GWP and compressive strength as a
116+
function of composition only.
117+
118+
Converts the strength and GWP models into a model list of independent
119+
models for GWP and *x*-day strengths by fixing the time input of the
120+
strength model at each ``strength_day``.
121+
122+
Args:
123+
fixed_features: Optional mapping from input column **index** to a
124+
fixed value. When provided these features are fixed *in
125+
addition to* the Time dimension for the strength models, and
126+
the non-Time entries are also applied to the GWP model via
127+
``FixedFeatureModel``. Useful for fixing e.g.
128+
``Coarse Aggregates = 0`` in mortar mode.
129+
130+
Returns:
131+
A ``ModelList`` with ``1 + len(strength_days)`` sub-models.
132+
133+
Raises:
134+
ValueError: If the model has not been fitted yet.
108135
"""
109136
if self.d is None:
110137
raise ValueError("Model not fit yet.")
111-
models = [
112-
self.gwp_model,
113-
*(
138+
139+
time_idx = self.d - 1 # last column is Time
140+
141+
if fixed_features is None:
142+
gwp_model = self.gwp_model
143+
else:
144+
non_time = {k: v for k, v in fixed_features.items() if k != time_idx}
145+
if non_time:
146+
gwp_indices = sorted(non_time.keys())
147+
gwp_values = [non_time[i] for i in gwp_indices]
148+
gwp_model = FixedFeatureModel(
149+
base_model=self.gwp_model,
150+
dim=self.d - 1, # GWP input has no Time
151+
indices=gwp_indices,
152+
values=gwp_values,
153+
)
154+
else:
155+
gwp_model = self.gwp_model
156+
157+
strength_models = []
158+
for day in self.strength_days:
159+
indices = [time_idx]
160+
values: list[float] = [float(day)]
161+
if fixed_features is not None:
162+
for idx, val in sorted(fixed_features.items()):
163+
if idx != time_idx:
164+
indices.append(idx)
165+
values.append(val)
166+
strength_models.append(
114167
FixedFeatureModel(
115168
base_model=self.strength_model,
116169
dim=self.d,
117-
indices=[self.d - 1],
118-
values=[day],
170+
indices=indices,
171+
values=values,
119172
)
120-
for day in self.strength_days
121-
),
122-
]
123-
model = ModelList(*models)
124-
return model # for use with multi-objective optimization
173+
)
174+
175+
return ModelList(gwp_model, *strength_models)
125176

126177
# TODO: add plot_strength_curve utility for visualizing predicted strength
127178
# curves as a function of time for a given composition.
128179

129180

130181
class FixedFeatureModel(Model):
131-
# advantage: only need to implement posterior for it to work with qNEHI
132-
# disadvantage: makes the strength outputs independent (IDEA: could add joint model)
133-
# TODO: check that these are appended before the InputTransforms are applied, not after.
182+
"""Wraps a GP model to fix a subset of inputs to constant values.
183+
184+
At evaluation time the fixed features are spliced back into the input
185+
tensor before delegating to the ``base_model``.
186+
"""
187+
134188
def __init__(
135189
self,
136190
base_model: Model,
@@ -202,9 +256,19 @@ def posterior(self, X: Tensor, *args, **kwargs) -> Posterior:
202256

203257
@property
204258
def num_outputs(self) -> int:
205-
return self.base_model.num_outputs # need to adjust if we batch fixed features
259+
"""The number of outputs of the base model."""
260+
return self.base_model.num_outputs
206261

207262
def subset_output(self, idcs: list[int]) -> FixedFeatureModel:
263+
"""Returns a new ``FixedFeatureModel`` whose base model is subset to
264+
the given output indices.
265+
266+
Args:
267+
idcs: Output indices to keep.
268+
269+
Returns:
270+
A ``FixedFeatureModel`` wrapping the subset base model.
271+
"""
208272
return FixedFeatureModel(
209273
base_model=self.base_model.subset_output(idcs),
210274
dim=self._dim,

0 commit comments

Comments
 (0)