Skip to content

Commit 44d73e9

Browse files
committed
Uploaded BOxCrete Data
1 parent ed62256 commit 44d73e9

18 files changed

Lines changed: 6691 additions & 74 deletions

BOxCrete_models.py

Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
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+
"""
8+
Defines concrete strength and global warming potential (GWP) models.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from typing import List, Optional, Union
14+
15+
import torch
16+
from botorch import fit_gpytorch_mll
17+
from botorch.models import ModelList, ModelListGP, SingleTaskGP
18+
from botorch.models.model import Model
19+
from botorch.models.transforms.input import (
20+
AffineInputTransform,
21+
ChainedInputTransform,
22+
Log10,
23+
Normalize,
24+
)
25+
from botorch.models.transforms.outcome import Standardize
26+
27+
from botorch.posteriors import Posterior
28+
29+
30+
from gpytorch.constraints import Interval
31+
32+
33+
from gpytorch import ExactMarginalLogLikelihood
34+
from gpytorch.kernels import LinearKernel, MaternKernel, RBFKernel, ScaleKernel
35+
from gpytorch.likelihoods import GaussianLikelihood
36+
from gpytorch.models import ExactGP
37+
from torch import Tensor
38+
from BOxCrete_utils import get_day_zero_data, SustainableConcreteDataset
39+
40+
41+
class SustainableConcreteModel(object):
42+
def __init__(
43+
self,
44+
strength_days: List[int],
45+
strength_model: Model | None = None,
46+
gwp_model: Model | None = None,
47+
d: int | None = None,
48+
):
49+
"""A multi-output model that jointly predicts GWP and compressive strength at
50+
pre-defined days `strength_days`.
51+
52+
Args:
53+
strength_days (List[int]): A list days to predict stength for.
54+
strength_model (Optional[Model], optional): The strength model. Defaults to None.
55+
gwp_model (Optional[Model], optional): The GWP model. Defaults to None.
56+
d (Optional[int], optional): The dimensionality of the input to the strength model.
57+
Is inferred automatically if the fit functions are called. NOTE: The model
58+
assumes that the last element of the input corresponds to the time dimension.
59+
"""
60+
self.strength_days = strength_days
61+
self.strength_model = None
62+
self.gwp_model = None
63+
self.d = d
64+
65+
def fit_strength_model(
66+
self, data: SustainableConcreteDataset, use_fixed_noise: bool = False
67+
) -> SingleTaskGP:
68+
"""Fits the strength model to the given `data`. Upon completion, the model
69+
can be accessed with the `strength_model` attribute.
70+
71+
Args:
72+
data: A SustainableConcreteDataset containing the strength data.
73+
use_fixed_noise: Toggles the use of known observation variances.
74+
75+
Returns:
76+
The fitted strength model.
77+
"""
78+
X, Y, Yvar, X_bounds = data.strength_data
79+
self._set_d(X.shape[-1])
80+
self.strength_model = fit_strength_gp(
81+
X=X, Y=Y, Yvar=Yvar, X_bounds=X_bounds, use_fixed_noise=use_fixed_noise
82+
)
83+
return self.strength_model
84+
85+
def fit_gwp_model(
86+
self, data: SustainableConcreteDataset, use_fixed_noise: bool = False
87+
) -> SingleTaskGP:
88+
"""Fits the global warming potential (GWP) model to the given `data`.
89+
Upon completion, the model can be accessed with the `gwp_model` attribute.
90+
91+
Args:
92+
data: A SustainableConcreteDataset containing the GWP data.
93+
use_fixed_noise: Toggles the use of known observation variances.
94+
95+
Returns:
96+
The fitted GWP model.
97+
"""
98+
X, Y, Yvar, X_bounds = data.gwp_data
99+
self._set_d(X.shape[-1] + 1)
100+
self.gwp_model = fit_gwp_gp(
101+
X=X, Y=Y, Yvar=Yvar, X_bounds=X_bounds, use_fixed_noise=use_fixed_noise
102+
)
103+
return self.gwp_model
104+
105+
def _set_d(self, d: int) -> None:
106+
if self.d is None:
107+
self.d = d
108+
109+
def get_model_list(self) -> ModelListGP:
110+
"""Returns a ModelListGP modeling the GWP and compressive strength objectives as a function
111+
of composition only.
112+
Converts the strength and gwp models into a model list of independent models for gwp,
113+
and x-day strengths, by fixing the time input of the strength model at 1 and 28 days.
114+
"""
115+
if self.d is None:
116+
raise ValueError("Model not fit yet.")
117+
models = [
118+
self.gwp_model,
119+
*(
120+
FixedFeatureModel(
121+
base_model=self.strength_model,
122+
dim=self.d,
123+
indices=[self.d - 1],
124+
values=[day],
125+
)
126+
for day in self.strength_days
127+
),
128+
]
129+
model = ModelList(*models)
130+
return model # for use with multi-objective optimization
131+
132+
def plot_strength_curve(self, composition: Tensor, max_day: int = 28) -> None:
133+
time = torch.arange(max_day + 1)
134+
composition = composition.unsqueeze(0).expand(len(time))
135+
# IDEA: use FixedFeatureModel?
136+
137+
138+
# BatchedMultiOutputGPyTorchModel, ExactGP
139+
class FixedFeatureModel(Model):
140+
# advantage: only need to implement posterior for it to work with qNEHI
141+
# disadvantage: makes the strength outputs independent (IDEA: could add joint model)
142+
# TODO: check that these are appended before the InputTransforms are applied, not after.
143+
def __init__(
144+
self,
145+
base_model: Model,
146+
dim: int,
147+
indices: Union[List[int], Tensor],
148+
values: Union[List[float], Tensor],
149+
):
150+
"""A wrapper around a GP model that fixes some inputs to specific values.
151+
152+
Args:
153+
base_model: The base model to wrap.
154+
dim: The input dimensionality of the FixedFeatureModel. This is usually the
155+
input dimensionality of base_model minus the number of fixed features.
156+
indices: The indices of the inputs to fix.
157+
values: The values to fix the inputs to.
158+
159+
Raises:
160+
ValueError: If indices and values do not have the same length.
161+
"""
162+
super().__init__()
163+
self.base_model = base_model
164+
if len(indices) != len(values):
165+
raise ValueError("indices and values do not have the same length.")
166+
values = torch.as_tensor(values)
167+
self._dim = dim
168+
self._indices: Tensor = torch.as_tensor(indices)
169+
self._fixed = torch.tensor(
170+
[i in indices for i in torch.arange(dim, dtype=self._indices.dtype)]
171+
)
172+
self._values = values
173+
174+
def _add_fixed_features(self, X: Tensor) -> Tensor:
175+
"""
176+
Args:
177+
X: A `n x d`-dim Tensor.
178+
179+
Returns:
180+
A `n x (d + len(self._indices))`-dim Tensor.
181+
"""
182+
tkwargs = {"dtype": X.dtype, "device": X.device}
183+
Z = torch.zeros(*X.shape[:-1], X.shape[-1] + len(self._indices), **tkwargs)
184+
Z[..., self._fixed] = self._values
185+
Z[..., ~self._fixed] = X
186+
return Z
187+
188+
def forward(self, X: Tensor, *args, **kwargs) -> Tensor:
189+
"""The forward method of the FixedFeatureModel, based on the forward method of
190+
the base model with the fixed features added.
191+
192+
Args:
193+
X: The `batch_shape x d`-dim input Tensor.
194+
195+
Returns:
196+
The `batch_shape x m`-dim output Tensor.
197+
"""
198+
return self.base_model.forward(self._add_fixed_features(X), *args, **kwargs)
199+
200+
def posterior(self, X: Tensor, *args, **kwargs) -> Posterior:
201+
"""Computes the posterior of the FixedFeatureModel, based on the posterior of
202+
the base model with the fixed features added.
203+
204+
Args:
205+
X: The `batch_shape x d`-dim input Tensor.
206+
207+
Returns:
208+
The posterior of the FixedFeatureModel evaluated at `X`.
209+
"""
210+
return self.base_model.posterior(self._add_fixed_features(X), *args, **kwargs)
211+
212+
@property
213+
def num_outputs(self) -> int:
214+
return self.base_model.num_outputs # need to adjust if we batch fixed features
215+
216+
def subset_output(self, idcs: List[int]) -> FixedFeatureModel:
217+
raise FixedFeatureModel(
218+
base_model=self.base_model.subset_output(idcs),
219+
dim=self._dim,
220+
indices=self._indices,
221+
values=self._value,
222+
)
223+
224+
225+
def fit_gwp_gp(
226+
X: Tensor,
227+
Y: Tensor,
228+
Yvar: Tensor,
229+
X_bounds: Optional[Tensor] = None,
230+
use_fixed_noise: bool = False,
231+
) -> SingleTaskGP:
232+
"""Fits a Gaussian process model to the given global warming potential (GWP) data.
233+
234+
Args:
235+
X: `n x d`-dim Tensor of composition inputs without time.
236+
Y: `n x 1`-dim Tensor of GWP values.
237+
Yvar: `n x 1`-dim Tensor of GWP variances.
238+
239+
Returns:
240+
A SingleTaskGP model fit to the data.
241+
"""
242+
d_in = X.shape[-1]
243+
d_out = Y.shape[-1]
244+
if d_out != 1:
245+
raise ValueError("Output dimensions is not one in gwp fitting.")
246+
# GWP is a linear function of the inputs
247+
covar_module = LinearKernel()
248+
model_kwargs = {
249+
"train_X": X,
250+
"train_Y": Y,
251+
"covar_module": covar_module,
252+
"input_transform": Normalize(d_in, bounds=X_bounds),
253+
"outcome_transform": Standardize(d_out),
254+
}
255+
if use_fixed_noise:
256+
model_kwargs["train_Yvar"] = Yvar
257+
else:
258+
model_kwargs["likelihood"] = GaussianLikelihood(
259+
noise_constraint=Interval(1e-4, 1.0, initial_value=1e-2)
260+
)
261+
model = SingleTaskGP(**model_kwargs)
262+
mll = ExactMarginalLogLikelihood(model.likelihood, model)
263+
fit_gpytorch_mll(mll)
264+
return model
265+
266+
267+
def fit_strength_gp(
268+
X: Tensor,
269+
Y: Tensor,
270+
Yvar: Tensor,
271+
X_bounds: Tensor | None = None,
272+
use_fixed_noise: bool = False,
273+
) -> ExactGP:
274+
"""Fits a Gaussian process model to the given strength data.
275+
276+
IDEAS:
277+
- Features:
278+
- w / b ratio
279+
- maturity i.e. sum_i(max(0, temperature_i) * delta_time_i)
280+
- Kernels:
281+
- Try orthogonal additive kernel again
282+
- temperature modeling via additive kernel?
283+
284+
Args:
285+
X: Tensor of composition inputs including time (n x d).
286+
Y: Tensor of strength values (n x 1).
287+
Yvar: Tensor of strength variances (n x 1).
288+
289+
Returns:
290+
A SingleTaskGP model fit to the strength data.
291+
"""
292+
d_in = X.shape[-1]
293+
d_out = Y.shape[-1]
294+
if d_out != 1:
295+
raise ValueError("Output dimensions is not one in strength curve fitting.")
296+
297+
# add data to condition GP to be zero at day zero
298+
X_0, Y_0, Yvar_0 = get_day_zero_data(X=X, bounds=X_bounds, n=128)
299+
X = torch.cat((X, X_0), dim=0)
300+
Y = torch.cat((Y, Y_0), dim=0)
301+
Yvar = torch.cat((Yvar, Yvar_0), dim=0)
302+
303+
# joint kernel to model all interactions
304+
base_kernel = MaternKernel(
305+
nu=2.5,
306+
ard_num_dims=d_in,
307+
lengthscale_constraint=Interval(1e-2, 1e3, initial_value=1.0),
308+
lengthscale_prior=None,
309+
)
310+
scaled_base_kernel = ScaleKernel(
311+
base_kernel=base_kernel,
312+
outputscale_constraint=Interval(1e-2, 1e2, initial_value=1.0),
313+
outputscale_prior=None,
314+
)
315+
316+
# additive kernel to model behavior w.r.t. time
317+
# try matern?
318+
time_kernel = RBFKernel( # MaternKernel # smoother RBF seems to work better for additive components
319+
active_dims=torch.tensor([d_in - 1]), # last dimension is time
320+
ard_num_dims=1,
321+
lengthscale_constraint=Interval(1e-2, 1e3, initial_value=1.0),
322+
lengthscale_prior=None,
323+
)
324+
scaled_time_kernel = ScaleKernel(
325+
base_kernel=time_kernel,
326+
outputscale_constraint=Interval(1e-2, 1e2, initial_value=1.0),
327+
outputscale_prior=None,
328+
# batch_shape=batch_shape,
329+
)
330+
331+
# IDEA: + scaled_water_kernel and other additive components
332+
kernel = scaled_base_kernel + scaled_time_kernel
333+
model_kwargs = {
334+
"train_X": X,
335+
"train_Y": Y,
336+
"covar_module": kernel,
337+
"input_transform": get_strength_gp_input_transform(d=d_in, bounds=X_bounds),
338+
"outcome_transform": Standardize(d_out),
339+
}
340+
if use_fixed_noise:
341+
model_kwargs["train_Yvar"] = Yvar
342+
else:
343+
model_kwargs["likelihood"] = GaussianLikelihood(
344+
noise_constraint=Interval(1e-6, 1.0, initial_value=1e-1)
345+
)
346+
model = SingleTaskGP(**model_kwargs)
347+
mll = ExactMarginalLogLikelihood(model.likelihood, model)
348+
fit_gpytorch_mll(mll)
349+
return model
350+
351+
352+
def get_strength_gp_input_transform(
353+
d: int, bounds: Optional[Tensor]
354+
) -> ChainedInputTransform:
355+
"""Chains a log(time + 1) and Normalize transform on d dimensional input data,
356+
with the provided bounds.
357+
358+
Args:
359+
bounds: `2 x d` tensor of lower and upper bounds for each dimension.
360+
361+
Returns:
362+
A ChainedInputTransform that log-transforms the time dimension and subsequently
363+
normalizes all dimensions to the unit hyper-cube.
364+
"""
365+
time_index = [d - 1]
366+
tf1 = AffineInputTransform( # adds one to time dimension before taking log
367+
d,
368+
coefficient=torch.ones(1),
369+
offset=torch.ones(1),
370+
indices=time_index,
371+
reverse=True,
372+
)
373+
tf2 = Log10(
374+
indices=time_index
375+
) # taking log of time dimension for better extrapolation
376+
if bounds is not None:
377+
transformed_bounds = tf2(tf1(bounds))
378+
tf3 = Normalize(
379+
d, bounds=transformed_bounds
380+
) # normalizing after log(t + 1) transform
381+
else:
382+
tf3 = Normalize(d) # normalizing after log(t + 1) transform
383+
return ChainedInputTransform(tf1=tf1, tf2=tf2, tf3=tf3)

0 commit comments

Comments
 (0)