2
2
3
3
# In this example, we demonstrate the use of **interpoint constraints** in a chemical
4
4
# optimization scenario. We optimize reaction conditions for a batch of chemical
5
- # experiments where exactly 60 mL of solvent must be used across the entire batch.
6
-
7
- # This scenario illustrates a common challenge in laboratory settings where:
8
- # * **Solvent requirement**: Exactly 60 mL must be used across the entire batch
9
- # * **Reagent ratios** must maintain specific stoichiometric relationships
10
- # * **Catalyst loading** needs to be balanced across experiments for cost efficiency
11
-
12
- # Interpoint constraints are particularly valuable here because they allow us to:
5
+ # experiments where exactly 60 mL of solvent must be used across the entire batch,
6
+ # while not using more than 30 mol% of catalyst across the batch.
7
+
8
+ # This scenario illustrates a common challenge in laboratory settings.
9
+ # First, it demonstrates how to enforce a **solvent requirement**:
10
+ # Exactly 60 mL of the solvent must be used across the entire batch
11
+ # since the solvent is supplied in a fixed volume container and cannot be used later.
12
+ # Second, it shows how to also include a **Catalyst loading** constraint for balancing
13
+ # the catalyst loading across experiments for cost efficiency.
14
+
15
+ # This example demonstrates how to use interpoint constraints and intrapoint constraints.
16
+ # An intrapoint constraint, often simply referred to as a constraint, applies to each individual
17
+ # experiment, ensuring that certain conditions are met within that single point.
18
+ # In contrast, an interpoint constraint applies across a batch of experiments,
19
+ # enforcing conditions that relate to the collective set of points rather than
20
+ # individual ones. These constraints are particularly useful when resources or conditions must be
21
+ # managed at a batch level and they allow us to:
13
22
# * Ensure total resource consumption meets exact requirements
14
23
# * Maintain chemical balances across multiple experiments
15
24
# * Optimize the collective use of expensive materials
25
+ # For more details on interpoint constraints, see the {ref}`user guide on constraints
26
+ # <userguide/constraints>`.
16
27
17
- # ## Imports
28
+ # ## Imports and Settings
18
29
19
30
import os
20
31
30
41
from baybe .targets import NumericalTarget
31
42
from baybe .utils .random import set_random_seed
32
43
33
- # ## Settings
34
-
35
- # We configure the optimization with a small batch size and limited iterations to keep
36
- # the example concise while demonstrating the key concepts. The tolerance parameter
37
- # is used for constraint validation to account for numerical precision.
38
-
39
44
SMOKE_TEST = "SMOKE_TEST" in os .environ
40
45
BATCH_SIZE = 3
41
46
N_ITERATIONS = 4 if SMOKE_TEST else 15
48
53
49
54
# We'll optimize a synthetic chemical reaction with the following experimental parameters:
50
55
# - **Solvent Volume** (10-30 mL per experiment): The amount of solvent used
51
- # - **Reactant A Concentration** (0.1-2.0 M ): Primary reactant concentration
56
+ # - **Reactant A Concentration** (0.1-2.0 g/L ): Primary reactant concentration
52
57
# - **Catalyst Loading** (1-10 mol%): Catalyst amount as percentage of limiting reagent
53
58
# - **Temperature** (60-120 °C): Reaction temperature
59
+ # Note that these ranges are chosen arbitrary and do not represent a specific real-world reaction.
54
60
55
61
parameters = [
56
62
NumericalContinuousParameter (
57
63
name = "Solvent_Volume" , bounds = (10.0 , 30.0 ), metadata = {"unit" : "mL" }
58
64
),
59
65
NumericalContinuousParameter (
60
- name = "Reactant_A_Conc" , bounds = (0.1 , 2.0 ), metadata = {"unit" : "M " }
66
+ name = "Reactant_A_Conc" , bounds = (0.1 , 2.0 ), metadata = {"unit" : "g/L " }
61
67
),
62
68
NumericalContinuousParameter (
63
69
name = "Catalyst_Loading" , bounds = (1.0 , 10.0 ), metadata = {"unit" : "mol%" }
111
117
# ## Campaign Setup
112
118
113
119
# We construct the search space by combining parameters with constraints, then create
114
- # a campaign targeting maximum reaction yield. The BotorchRecommender with
120
+ # a campaign targeting maximum reaction yield. The {class}`~baybe.recommenders. BotorchRecommender` with
115
121
# `sequential_continuous=False` is required for interpoint constraints as they
116
122
# operate on batches rather than individual experiments.
117
123
126
132
127
133
# We create a synthetic model that represents a realistic chemical reaction with
128
134
# trade-offs and optimal operating ranges rather than simply maximizing all parameters:
129
- # - Concentration has an optimum around 1.0 M (Gaussian peak)
135
+ # - Concentration has an optimum around 1.0 g/L (Gaussian peak)
130
136
# - Catalyst shows diminishing returns and eventual inhibition
131
137
# - Temperature has an optimum around 90°C (too high causes decomposition)
132
138
# - Solvent volume has trade-offs (dissolution vs dilution effects)
@@ -180,41 +186,34 @@ def chemical_reaction_model(df: pd.DataFrame) -> pd.DataFrame:
180
186
181
187
results_log = []
182
188
183
- for iteration in range (N_ITERATIONS ):
189
+ for it in range (N_ITERATIONS ):
184
190
recommendations = campaign .recommend (batch_size = BATCH_SIZE )
185
191
186
192
reaction_results = lookup (recommendations )
187
193
measurements = pd .concat ([recommendations , reaction_results ], axis = 1 )
188
-
189
194
campaign .add_measurements (measurements )
190
-
191
- total_solvent = recommendations ["Solvent_Volume" ].sum ()
192
- total_catalyst = recommendations ["Catalyst_Loading" ].sum ()
193
-
194
- solvent_ok = abs (total_solvent - 60.0 ) < TOLERANCE
195
- catalyst_ok = total_catalyst <= (30.0 + TOLERANCE )
195
+ total_sol = recommendations ["Solvent_Volume" ].sum ()
196
+ total_cat = recommendations ["Catalyst_Loading" ].sum ()
197
+ solvent_ok = abs (total_sol - 60.0 ) < TOLERANCE
198
+ catalyst_ok = total_cat <= (30.0 + TOLERANCE )
196
199
197
200
assert solvent_ok , (
198
- f"Solvent constraint violated: { total_solvent :.1f} mL (expected 60.0 mL)"
201
+ f"Solvent constraint violated: { total_sol :.1f} mL (expected 60.0 mL)"
199
202
)
200
203
assert catalyst_ok , (
201
- f"Catalyst constraint violated: { total_catalyst :.1f} mol% (max 30.0 mol%)"
204
+ f"Catalyst constraint violated: { total_cat :.1f} mol% (max 30.0 mol%)"
202
205
)
203
206
204
207
results_log .append (
205
208
{
206
- "iteration" : iteration + 1 ,
207
- "total_solvent_mL" : total_solvent ,
208
- "total_catalyst_mol%" : total_catalyst ,
209
+ "iteration" : it + 1 ,
210
+ "total_solvent_mL" : total_sol ,
211
+ "total_catalyst_mol%" : total_cat ,
209
212
"individual_solvent_mL" : recommendations ["Solvent_Volume" ].tolist (),
210
213
"individual_catalyst_mol%" : recommendations ["Catalyst_Loading" ].tolist (),
211
214
}
212
215
)
213
216
214
- print (
215
- f"Batch { iteration + 1 } : Solvent={ total_solvent :.1f} mL, Catalyst={ total_catalyst :.1f} mol%"
216
- )
217
-
218
217
# ## Visualization
219
218
220
219
# We create plots showing both individual experiment values and their totals to
@@ -224,56 +223,58 @@ def chemical_reaction_model(df: pd.DataFrame) -> pd.DataFrame:
224
223
225
224
results_df = pd .DataFrame (results_log )
226
225
227
- fig , ( ax1 , ax2 ) = plt .subplots (1 , 2 , figsize = (10 , 4 ))
226
+ fig , axs = plt .subplots (1 , 2 , figsize = (10 , 4 ))
228
227
228
+ plt .sca (axs [0 ])
229
229
for exp_idx in range (BATCH_SIZE ):
230
230
individual_values = [
231
231
batch [exp_idx ] for batch in results_df ["individual_solvent_mL" ]
232
232
]
233
- ax1 .plot (
233
+ plt .plot (
234
234
results_df ["iteration" ],
235
235
individual_values ,
236
236
"o-" ,
237
237
alpha = 0.6 ,
238
238
label = f"Exp { exp_idx + 1 } " ,
239
239
)
240
240
241
- ax1 .plot (
241
+ plt .plot (
242
242
results_df ["iteration" ],
243
243
results_df ["total_solvent_mL" ],
244
244
"s-" ,
245
245
color = "blue" ,
246
246
linewidth = 2 ,
247
247
label = "Total" ,
248
248
)
249
- ax1 .axhline (y = 60 , color = "red" , linestyle = "--" , label = "Required" )
250
- ax1 . set_title ("Solvent: Individual + Total" )
251
- ax1 .legend ()
249
+ plt .axhline (y = 60 , color = "red" , linestyle = "--" , label = "Required" )
250
+ plt . title ("Solvent: Individual + Total" )
251
+ plt .legend ()
252
252
253
+ plt .sca (axs [1 ])
253
254
for exp_idx in range (BATCH_SIZE ):
254
255
individual_values = [
255
256
batch [exp_idx ] for batch in results_df ["individual_catalyst_mol%" ]
256
257
]
257
- ax2 .plot (
258
+ plt .plot (
258
259
results_df ["iteration" ],
259
260
individual_values ,
260
261
"o-" ,
261
262
alpha = 0.6 ,
262
263
label = f"Exp { exp_idx + 1 } " ,
263
264
)
264
265
265
- ax2 .plot (
266
+ plt .plot (
266
267
results_df ["iteration" ],
267
268
results_df ["total_catalyst_mol%" ],
268
269
"s-" ,
269
270
color = "orange" ,
270
271
linewidth = 2 ,
271
272
label = "Total" ,
272
273
)
273
- ax2 .axhline (y = 30 , color = "red" , linestyle = "--" , label = "Limit" )
274
- ax2 . set_title ("Catalyst: Individual + Total" )
275
- ax2 .legend ()
274
+ plt .axhline (y = 30 , color = "red" , linestyle = "--" , label = "Limit" )
275
+ plt . title ("Catalyst: Individual + Total" )
276
+ plt .legend ()
276
277
277
278
plt .tight_layout ()
278
279
if not SMOKE_TEST :
279
- plt .savefig ("interpoint_constraints .svg" )
280
+ plt .savefig ("interpoint .svg" )
0 commit comments