Skip to content

Commit 0793216

Browse files
committed
Rewrite example
1 parent a56d018 commit 0793216

File tree

2 files changed

+233
-232
lines changed

2 files changed

+233
-232
lines changed

examples/Constraints_Continuous/interpoint.py

Lines changed: 49 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,30 @@
22

33
# In this example, we demonstrate the use of **interpoint constraints** in a chemical
44
# 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:
1322
# * Ensure total resource consumption meets exact requirements
1423
# * Maintain chemical balances across multiple experiments
1524
# * Optimize the collective use of expensive materials
25+
# For more details on interpoint constraints, see the {ref}`user guide on constraints
26+
# <userguide/constraints>`.
1627

17-
# ## Imports
28+
# ## Imports and Settings
1829

1930
import os
2031

@@ -30,12 +41,6 @@
3041
from baybe.targets import NumericalTarget
3142
from baybe.utils.random import set_random_seed
3243

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-
3944
SMOKE_TEST = "SMOKE_TEST" in os.environ
4045
BATCH_SIZE = 3
4146
N_ITERATIONS = 4 if SMOKE_TEST else 15
@@ -48,16 +53,17 @@
4853

4954
# We'll optimize a synthetic chemical reaction with the following experimental parameters:
5055
# - **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
5257
# - **Catalyst Loading** (1-10 mol%): Catalyst amount as percentage of limiting reagent
5358
# - **Temperature** (60-120 °C): Reaction temperature
59+
# Note that these ranges are chosen arbitrary and do not represent a specific real-world reaction.
5460

5561
parameters = [
5662
NumericalContinuousParameter(
5763
name="Solvent_Volume", bounds=(10.0, 30.0), metadata={"unit": "mL"}
5864
),
5965
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"}
6167
),
6268
NumericalContinuousParameter(
6369
name="Catalyst_Loading", bounds=(1.0, 10.0), metadata={"unit": "mol%"}
@@ -111,7 +117,7 @@
111117
# ## Campaign Setup
112118

113119
# 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
115121
# `sequential_continuous=False` is required for interpoint constraints as they
116122
# operate on batches rather than individual experiments.
117123

@@ -126,7 +132,7 @@
126132

127133
# We create a synthetic model that represents a realistic chemical reaction with
128134
# 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)
130136
# - Catalyst shows diminishing returns and eventual inhibition
131137
# - Temperature has an optimum around 90°C (too high causes decomposition)
132138
# - Solvent volume has trade-offs (dissolution vs dilution effects)
@@ -180,41 +186,34 @@ def chemical_reaction_model(df: pd.DataFrame) -> pd.DataFrame:
180186

181187
results_log = []
182188

183-
for iteration in range(N_ITERATIONS):
189+
for it in range(N_ITERATIONS):
184190
recommendations = campaign.recommend(batch_size=BATCH_SIZE)
185191

186192
reaction_results = lookup(recommendations)
187193
measurements = pd.concat([recommendations, reaction_results], axis=1)
188-
189194
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)
196199

197200
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)"
199202
)
200203
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%)"
202205
)
203206

204207
results_log.append(
205208
{
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,
209212
"individual_solvent_mL": recommendations["Solvent_Volume"].tolist(),
210213
"individual_catalyst_mol%": recommendations["Catalyst_Loading"].tolist(),
211214
}
212215
)
213216

214-
print(
215-
f"Batch {iteration + 1}: Solvent={total_solvent:.1f}mL, Catalyst={total_catalyst:.1f}mol%"
216-
)
217-
218217
# ## Visualization
219218

220219
# 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:
224223

225224
results_df = pd.DataFrame(results_log)
226225

227-
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
226+
fig, axs = plt.subplots(1, 2, figsize=(10, 4))
228227

228+
plt.sca(axs[0])
229229
for exp_idx in range(BATCH_SIZE):
230230
individual_values = [
231231
batch[exp_idx] for batch in results_df["individual_solvent_mL"]
232232
]
233-
ax1.plot(
233+
plt.plot(
234234
results_df["iteration"],
235235
individual_values,
236236
"o-",
237237
alpha=0.6,
238238
label=f"Exp {exp_idx + 1}",
239239
)
240240

241-
ax1.plot(
241+
plt.plot(
242242
results_df["iteration"],
243243
results_df["total_solvent_mL"],
244244
"s-",
245245
color="blue",
246246
linewidth=2,
247247
label="Total",
248248
)
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()
252252

253+
plt.sca(axs[1])
253254
for exp_idx in range(BATCH_SIZE):
254255
individual_values = [
255256
batch[exp_idx] for batch in results_df["individual_catalyst_mol%"]
256257
]
257-
ax2.plot(
258+
plt.plot(
258259
results_df["iteration"],
259260
individual_values,
260261
"o-",
261262
alpha=0.6,
262263
label=f"Exp {exp_idx + 1}",
263264
)
264265

265-
ax2.plot(
266+
plt.plot(
266267
results_df["iteration"],
267268
results_df["total_catalyst_mol%"],
268269
"s-",
269270
color="orange",
270271
linewidth=2,
271272
label="Total",
272273
)
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()
276277

277278
plt.tight_layout()
278279
if not SMOKE_TEST:
279-
plt.savefig("interpoint_constraints.svg")
280+
plt.savefig("interpoint.svg")

0 commit comments

Comments
 (0)