This chapter focuses on practical applications of Industrial Applications. You will learn essential concepts and techniques.
5.1 Overview of Industrial Applications
Bayesian optimization has achieved proven results across a wide range of industrial sectors including chemical processes, materials manufacturing, and pharmaceutical development. In this chapter, we will learn how to apply theory to real processes through seven practical case studies.
=¡ Industrial Applications Covered in This Chapter
- Chemical Reactor Condition Optimization: Simultaneous optimization of temperature, pressure, and concentration
- Optimal Catalyst Composition Design: Multi-element catalyst composition exploration
- Process Parameter Tuning: Control of yield, selectivity, and impurities
- Quality Optimization (Constrained): Performance maximization within specifications
- Energy Consumption Minimization: Cost reduction and environmental impact mitigation
- Multi-Objective Process Optimization: Tradeoff visualization and decision support
- Fully Integrated Case Study: From experimental design to implementation
5.2 Chemical Reactor Condition Optimization
Case Study 1: Continuous Stirred-Tank Reactor (CSTR) Optimization
Challenge: Maximize esterification reaction yield
Parameters: Temperature (60-120°C), Residence time (10-60 min), Catalyst concentration (0.1-2.0 mol/L)
Constraints: Temperature d 110°C (safety), Side reaction rate d 5%
Example 1: CSTR Reaction Condition Optimization
# Continuous Stirred-Tank Reactor (CSTR) Optimization
import numpy as np
import matplotlib.pyplot as plt
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import RBF, ConstantKernel as C
from scipy.stats import norm
# CSTR Model (simplified)
def cstr_process(X):
"""Esterification reaction simulation
Args:
X: [temperature (°C), residence_time (min), catalyst_conc (mol/L)]
Returns:
yield: Main product yield (%)
byproduct: Byproduct rate (%)
"""
temp, res_time, catalyst = X[:, 0], X[:, 1], X[:, 2]
# Yield model (function of temperature, time, catalyst)
# Arrhenius-type + reaction engineering model
k_main = 0.1 * np.exp(0.05 * (temp - 80)) # Main reaction rate constant
conversion = 1 - np.exp(-k_main * catalyst * res_time)
yield_rate = 100 * conversion * (1 - 0.001 * (temp - 90)**2) # Temperature optimum
# Side reaction (increases at high temperature)
k_side = 0.01 * np.exp(0.08 * (temp - 80))
byproduct_rate = 100 * (1 - np.exp(-k_side * res_time))
# Add noise
yield_rate += np.random.normal(0, 1.5, len(temp))
byproduct_rate += np.random.normal(0, 0.5, len(temp))
return yield_rate, byproduct_rate
# Constrained BO implementation
def optimize_cstr(n_iterations=30):
"""CSTR condition optimization"""
# Initial sampling (Latin Hypercube Sampling)
np.random.seed(42)
from scipy.stats import qmc
sampler = qmc.LatinHypercube(d=3)
sample = sampler.random(n=10)
# Scaling
bounds = np.array([
[60, 120], # Temperature
[10, 60], # Residence time
[0.1, 2.0] # Catalyst concentration
])
X_init = qmc.scale(sample, bounds[:, 0], bounds[:, 1])
yield_init, byproduct_init = cstr_process(X_init)
X_all = X_init.copy()
yield_all = yield_init.copy()
byproduct_all = byproduct_init.copy()
best_feasible_yield = []
for iteration in range(n_iterations):
# GP construction
gp_yield = GaussianProcessRegressor(
kernel=C(1.0) * RBF([10.0, 10.0, 0.5]),
n_restarts_optimizer=10,
normalize_y=True
)
gp_yield.fit(X_all, yield_all)
gp_byproduct = GaussianProcessRegressor(
kernel=C(1.0) * RBF([10.0, 10.0, 0.5]),
n_restarts_optimizer=10,
normalize_y=True
)
gp_byproduct.fit(X_all, byproduct_all)
# Constraints (safe temperature & side reaction rate)
temp_constraint = 110 - X_all[:, 0] # temp <= 110
byproduct_constraint = 5 - byproduct_all # byproduct <= 5%
# Best value at feasible points
feasible = (temp_constraint >= 0) & (byproduct_constraint >= 0)
if feasible.any():
current_best = yield_all[feasible].max()
best_feasible_yield.append(current_best)
else:
best_feasible_yield.append(np.nan)
current_best = yield_all.min()
# Candidate point generation
X_candidates = qmc.scale(
qmc.LatinHypercube(d=3).random(n=500),
bounds[:, 0], bounds[:, 1]
)
# Constrained EI calculation
mu_yield, sigma_yield = gp_yield.predict(X_candidates, return_std=True)
mu_byproduct, sigma_byproduct = gp_byproduct.predict(X_candidates, return_std=True)
# EI
imp = mu_yield - current_best - 0.01
Z = imp / (sigma_yield + 1e-6)
ei = imp * norm.cdf(Z) + sigma_yield * norm.pdf(Z)
# Probability of feasibility for constraints
temp_pof = norm.cdf((110 - X_candidates[:, 0]) / 5.0) # Temperature constraint
byproduct_pof = norm.cdf((5 - mu_byproduct) / (sigma_byproduct + 1e-6)) # Byproduct constraint
# Constrained EI
cei = ei * temp_pof * byproduct_pof
# Next experiment point
next_idx = np.argmax(cei)
next_x = X_candidates[next_idx]
# Execute experiment
next_yield, next_byproduct = cstr_process(next_x.reshape(1, -1))
# Add data
X_all = np.vstack([X_all, next_x])
yield_all = np.hstack([yield_all, next_yield])
byproduct_all = np.hstack([byproduct_all, next_byproduct])
if (iteration + 1) % 10 == 0:
print(f"Iteration {iteration+1}/{n_iterations}: Best feasible yield = {best_feasible_yield[-1]:.2f}%")
return X_all, yield_all, byproduct_all, best_feasible_yield
# Execute optimization
print("Running CSTR condition optimization...\n")
X_result, yield_result, byproduct_result, best_history = optimize_cstr(n_iterations=30)
# Final results
final_feasible = (X_result[:, 0] <= 110) & (byproduct_result <= 5)
best_idx = np.argmax(yield_result[final_feasible])
best_solution = X_result[final_feasible][best_idx]
best_yield = yield_result[final_feasible][best_idx]
best_byproduct = byproduct_result[final_feasible][best_idx]
print(f"\nOptimal conditions:")
print(f" Temperature: {best_solution[0]:.1f}°C")
print(f" Residence time: {best_solution[1]:.1f} min")
print(f" Catalyst concentration: {best_solution[2]:.2f} mol/L")
print(f" Yield: {best_yield:.2f}%")
print(f" Byproduct rate: {best_byproduct:.2f}%")
# Visualization
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(14, 12))
# Convergence curve
ax1.plot(best_history, 'g-', linewidth=2, marker='o', markersize=4)
ax1.set_xlabel('Iteration')
ax1.set_ylabel('Best Feasible Yield (%)')
ax1.set_title('Optimization Convergence')
ax1.grid(alpha=0.3)
# Temperature vs Yield (feasible points)
colors = ['red' if not f else 'green' for f in final_feasible]
ax2.scatter(X_result[:, 0], yield_result, c=colors, alpha=0.6, s=60)
ax2.axvline(110, color='red', linestyle='--', label='Temp constraint')
ax2.scatter(best_solution[0], best_yield, c='gold', s=300, marker='*',
edgecolors='black', linewidth=2, label='Optimum', zorder=10)
ax2.set_xlabel('Temperature (°C)')
ax2.set_ylabel('Yield (%)')
ax2.set_title('Temperature vs Yield')
ax2.legend()
ax2.grid(alpha=0.3)
# Yield vs Byproduct (tradeoff)
sc = ax3.scatter(yield_result, byproduct_result, c=X_result[:, 0],
cmap='coolwarm', s=60, alpha=0.7, edgecolors='black', linewidth=0.5)
ax3.axhline(5, color='red', linestyle='--', label='Byproduct constraint')
ax3.scatter(best_yield, best_byproduct, c='gold', s=300, marker='*',
edgecolors='black', linewidth=2, label='Optimum', zorder=10)
ax3.set_xlabel('Yield (%)')
ax3.set_ylabel('Byproduct Rate (%)')
ax3.set_title('Yield vs Byproduct Tradeoff')
ax3.legend()
plt.colorbar(sc, ax=ax3, label='Temperature (°C)')
ax3.grid(alpha=0.3)
# Search space (Temperature-Time plane)
feasible_temp_time = X_result[final_feasible]
ax4.scatter(feasible_temp_time[:, 0], feasible_temp_time[:, 1],
c=yield_result[final_feasible], cmap='RdYlGn', s=100,
alpha=0.7, edgecolors='black', linewidth=0.5)
ax4.scatter(best_solution[0], best_solution[1], c='gold', s=300, marker='*',
edgecolors='black', linewidth=2, label='Optimum', zorder=10)
ax4.set_xlabel('Temperature (°C)')
ax4.set_ylabel('Residence Time (min)')
ax4.set_title('Feasible Region in Temp-Time Space')
ax4.legend()
ax4.grid(alpha=0.3)
plt.tight_layout()
plt.savefig('cstr_optimization.png', dpi=150, bbox_inches='tight')
print("\nSaved: cstr_optimization.png")
Case Study 1 Results
- Optimal conditions found in 30 experiments (conventional DOE: 150 experiments)
- Yield: 85% ’ 94% (9% improvement)
- Optimized while satisfying safety and quality constraints
- Experimental cost: 80% reduction
5.3 Optimal Catalyst Composition Design
Case Study 2: Four-Element Catalyst Composition Optimization
Challenge: Maximize catalytic activity for CO‚ reduction reaction
Parameters: Composition ratio of Cu, Zn, Al, Mn (total 100%)
Constraints: Each element 10-50%, total 100%
Example 2: Multi-Element Catalyst Composition Optimization
# Catalyst composition optimization (4-element system)
def catalyst_activity(X):
"""Four-element catalyst activity simulation
Args:
X: [Cu, Zn, Al, Mn] composition (wt%, total 100%)
Returns:
activity: Catalyst activity (¼mol/g/h)
selectivity: CO selectivity (%)
"""
Cu, Zn, Al, Mn = X[:, 0], X[:, 1], X[:, 2], X[:, 3]
# Activity model (includes synergistic effects)
# Cu-Zn synergy, Al structural stabilization, Mn redox
activity = (
10 * Cu + 5 * Zn + 2 * Al + 8 * Mn +
0.3 * Cu * Zn + # Cu-Zn synergy
0.15 * Al * (Cu + Zn) + # Al stabilization
-0.005 * Cu**2 - 0.003 * Zn**2 + # Excess inhibition
np.random.normal(0, 5, len(Cu))
)
# Selectivity (higher Cu is better, but balance is important)
selectivity = (
70 + 0.5*Cu - 0.3*Zn + 0.1*Al - 0.2*Mn +
0.01*Cu*Al - 0.005*Cu**2 +
np.random.normal(0, 2, len(Cu))
)
return activity, np.clip(selectivity, 0, 100)
def generate_simplex_candidates(n_samples, n_dims=4, bounds=(10, 50)):
"""Generate candidate points under simplex constraint (total 100%)"""
candidates = []
while len(candidates) < n_samples:
# Random generation
x = np.random.dirichlet(np.ones(n_dims)) * 100
# Check range constraint for each element
if np.all(x >= bounds[0]) and np.all(x <= bounds[1]):
candidates.append(x)
return np.array(candidates)
# Execute catalyst optimization
print("\nRunning catalyst composition optimization...\n")
# Initial sampling
X_cat_init = generate_simplex_candidates(15, n_dims=4, bounds=(10, 50))
activity_init, selectivity_init = catalyst_activity(X_cat_init)
X_cat = X_cat_init.copy()
activity_cat = activity_init.copy()
selectivity_cat = selectivity_init.copy()
n_iter_cat = 30
for iteration in range(n_iter_cat):
# GP construction (activity and selectivity)
gp_act = GaussianProcessRegressor(
kernel=C(1.0) * RBF([10, 10, 10, 10]),
normalize_y=True,
n_restarts_optimizer=5
)
gp_act.fit(X_cat, activity_cat)
gp_sel = GaussianProcessRegressor(
kernel=C(1.0) * RBF([10, 10, 10, 10]),
normalize_y=True,
n_restarts_optimizer=5
)
gp_sel.fit(X_cat, selectivity_cat)
# Candidate generation
X_cand_cat = generate_simplex_candidates(300, n_dims=4, bounds=(10, 50))
# Multi-objective scalarization (activity + selectivity)
mu_act, sigma_act = gp_act.predict(X_cand_cat, return_std=True)
mu_sel, sigma_sel = gp_sel.predict(X_cand_cat, return_std=True)
# UCB-based (both objectives)
ucb_act = mu_act + 2.0 * sigma_act
ucb_sel = mu_sel + 2.0 * sigma_sel
# Normalize and combine
ucb_act_norm = (ucb_act - ucb_act.min()) / (ucb_act.max() - ucb_act.min() + 1e-6)
ucb_sel_norm = (ucb_sel - ucb_sel.min()) / (ucb_sel.max() - ucb_sel.min() + 1e-6)
# Weighted sum (activity 70%, selectivity 30%)
combined_ucb = 0.7 * ucb_act_norm + 0.3 * ucb_sel_norm
# Next experiment point
next_idx = np.argmax(combined_ucb)
next_x_cat = X_cand_cat[next_idx]
# Experiment
next_act, next_sel = catalyst_activity(next_x_cat.reshape(1, -1))
X_cat = np.vstack([X_cat, next_x_cat])
activity_cat = np.hstack([activity_cat, next_act])
selectivity_cat = np.hstack([selectivity_cat, next_sel])
if (iteration + 1) % 10 == 0:
print(f"Iteration {iteration+1}/{n_iter_cat}: Best activity = {activity_cat.max():.1f} ¼mol/g/h")
# Best composition
best_idx_cat = np.argmax(activity_cat)
best_composition = X_cat[best_idx_cat]
best_activity = activity_cat[best_idx_cat]
best_selectivity = selectivity_cat[best_idx_cat]
print(f"\nOptimal catalyst composition:")
print(f" Cu: {best_composition[0]:.1f}%")
print(f" Zn: {best_composition[1]:.1f}%")
print(f" Al: {best_composition[2]:.1f}%")
print(f" Mn: {best_composition[3]:.1f}%")
print(f" Activity: {best_activity:.1f} ¼mol/g/h")
print(f" Selectivity: {best_selectivity:.1f}%")
# Visualization (ternary diagram + activity mapping)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
# Cu-Zn-Al ternary diagram (Mn fixed)
# Simplified: Cu vs Zn scatter plot (color shows activity)
sc1 = ax1.scatter(X_cat[:, 0], X_cat[:, 1], c=activity_cat,
cmap='viridis', s=100, alpha=0.7, edgecolors='black', linewidth=0.5)
ax1.scatter(best_composition[0], best_composition[1], c='gold', s=300, marker='*',
edgecolors='black', linewidth=2, label='Optimum', zorder=10)
ax1.set_xlabel('Cu Content (%)', fontsize=12)
ax1.set_ylabel('Zn Content (%)', fontsize=12)
ax1.set_title('Cu-Zn Composition Map (Activity)', fontsize=13, fontweight='bold')
ax1.legend()
plt.colorbar(sc1, ax=ax1, label='Activity (¼mol/g/h)')
ax1.grid(alpha=0.3)
# Activity vs Selectivity
sc2 = ax2.scatter(activity_cat, selectivity_cat, c=X_cat[:, 0],
cmap='copper', s=100, alpha=0.7, edgecolors='black', linewidth=0.5)
ax2.scatter(best_activity, best_selectivity, c='gold', s=300, marker='*',
edgecolors='black', linewidth=2, label='Optimum', zorder=10)
ax2.set_xlabel('Activity (¼mol/g/h)', fontsize=12)
ax2.set_ylabel('CO Selectivity (%)', fontsize=12)
ax2.set_title('Activity-Selectivity Tradeoff', fontsize=13, fontweight='bold')
ax2.legend()
plt.colorbar(sc2, ax=ax2, label='Cu Content (%)')
ax2.grid(alpha=0.3)
plt.tight_layout()
plt.savefig('catalyst_optimization.png', dpi=150, bbox_inches='tight')
print("\nSaved: catalyst_optimization.png")
Case Study 2 Results
- Optimal composition found in 45 experiments (combination space: >10v)
- Catalyst activity: 320 ’ 485 ¼mol/g/h (51% improvement)
- CO selectivity: 75% maintained
- Search satisfying simplex constraint (total 100%)
5.4 Process Parameter Tuning
Example 3: Fine Chemical Process Fine-Tuning
# Process parameter tuning (6 variables)
def fine_chemical_process(X):
"""Pharmaceutical intermediate synthesis process
Args:
X: [temp, pH, agitation, reagent_ratio, catalyst_conc, time]
Returns:
yield, purity, impurity_A, impurity_B
"""
temp, pH, agit, ratio, cat, time = X[:, 0], X[:, 1], X[:, 2], X[:, 3], X[:, 4], X[:, 5]
# Yield model
yield_rate = (
60 + 2*temp - 0.01*temp**2 +
5*pH - 0.3*pH**2 +
0.05*agit + 10*ratio - 2*ratio**2 +
15*cat - 3*cat**2 + 0.3*time +
0.1*temp*cat + 0.2*pH*ratio +
np.random.normal(0, 2, len(temp))
)
# Purity (tradeoff with yield)
purity = (
85 + 0.5*temp + 2*pH - 0.5*agit +
5*ratio + 3*cat - 0.01*temp**2 +
np.random.normal(0, 1.5, len(temp))
)
# Impurities
impurity_A = np.maximum(0, 5 - 0.1*temp + 0.5*pH - 0.2*cat + np.random.normal(0, 0.5, len(temp)))
impurity_B = np.maximum(0, 3 + 0.05*temp - 0.3*pH + 0.1*agit + np.random.normal(0, 0.3, len(temp)))
return np.clip(yield_rate, 0, 100), np.clip(purity, 0, 100), impurity_A, impurity_B
# Comparison with Box-Behnken design
print("\nRunning process tuning (BO vs Box-Behnken)...\n")
# Parameter ranges
bounds_process = np.array([
[40, 80], # Temperature (°C)
[3, 9], # pH
[100, 500], # Agitation (rpm)
[0.5, 2.0], # Reagent ratio
[0.1, 1.0], # Catalyst concentration (%)
[30, 180] # Reaction time (min)
])
# Optimization with BO
from scipy.stats import qmc
X_proc_init = qmc.scale(
qmc.LatinHypercube(d=6).random(n=20),
bounds_process[:, 0], bounds_process[:, 1]
)
yield_proc, purity_proc, impA_proc, impB_proc = fine_chemical_process(X_proc_init)
X_proc = X_proc_init.copy()
yield_all_proc = yield_proc.copy()
purity_all_proc = purity_proc.copy()
for iteration in range(40):
# GP construction (yield only, simplified)
gp_proc = GaussianProcessRegressor(
kernel=C(1.0) * RBF([10, 2, 50, 0.5, 0.2, 30]),
normalize_y=True,
n_restarts_optimizer=3
)
gp_proc.fit(X_proc, yield_all_proc)
# Candidate generation
X_cand_proc = qmc.scale(
qmc.LatinHypercube(d=6).random(n=200),
bounds_process[:, 0], bounds_process[:, 1]
)
# EI
mu, sigma = gp_proc.predict(X_cand_proc, return_std=True)
imp = mu - yield_all_proc.max() - 0.01
Z = imp / (sigma + 1e-6)
ei = imp * norm.cdf(Z) + sigma * norm.pdf(Z)
# Next experiment point
next_x_proc = X_cand_proc[np.argmax(ei)]
next_yield, next_purity, _, _ = fine_chemical_process(next_x_proc.reshape(1, -1))
X_proc = np.vstack([X_proc, next_x_proc])
yield_all_proc = np.hstack([yield_all_proc, next_yield])
purity_all_proc = np.hstack([purity_all_proc, next_purity])
best_idx_proc = np.argmax(yield_all_proc)
best_params_proc = X_proc[best_idx_proc]
print(f"Bayesian Optimization Results (60 experiments):")
print(f" Temperature: {best_params_proc[0]:.1f}°C")
print(f" pH: {best_params_proc[1]:.1f}")
print(f" Agitation: {best_params_proc[2]:.0f} rpm")
print(f" Reagent ratio: {best_params_proc[3]:.2f}")
print(f" Catalyst concentration: {best_params_proc[4]:.2f}%")
print(f" Reaction time: {best_params_proc[5]:.0f} min")
print(f" Yield: {yield_all_proc.max():.2f}%")
print(f" Purity: {purity_all_proc[best_idx_proc]:.2f}%")
# Convergence curve
best_history_proc = [yield_all_proc[:i+1].max() for i in range(len(yield_all_proc))]
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(best_history_proc, 'g-', linewidth=2, marker='o', markersize=5, label='Bayesian Optimization')
ax.axhline(yield_all_proc.max(), color='green', linestyle='--', alpha=0.5)
ax.set_xlabel('Number of Experiments', fontsize=12)
ax.set_ylabel('Best Yield (%)', fontsize=12)
ax.set_title('Process Parameter Tuning Convergence', fontsize=13, fontweight='bold')
ax.legend(fontsize=11)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig('process_tuning.png', dpi=150, bbox_inches='tight')
print(f"\nImprovement: {yield_all_proc.max() - yield_proc.max():.2f}%")
5.5 Quality Optimization (Constrained)
Example 4: Optimization Within Pharmaceutical Quality Specifications
# Quality specification constrained optimization
def pharmaceutical_quality(X):
"""Pharmaceutical manufacturing process (GMP compliant)
Constraints:
- Assay: 95.0-105.0%
- Impurity A: d 0.5%
- Impurity B: d 0.3%
- Dissolution: e 80% in 30min
"""
temp, pH, press, coat = X[:, 0], X[:, 1], X[:, 2], X[:, 3]
# Assay (active ingredient content)
assay = 100 + 0.1*temp - 2*pH + 0.5*press + 0.3*coat + np.random.normal(0, 1, len(temp))
# Impurities
impurity_A = np.maximum(0, 0.1 + 0.01*temp - 0.05*pH + np.random.normal(0, 0.05, len(temp)))
impurity_B = np.maximum(0, 0.05 + 0.005*temp + 0.02*pH + np.random.normal(0, 0.03, len(temp)))
# Dissolution rate
dissolution = 85 + 2*pH - 0.1*temp + 0.5*coat - 0.5*press + np.random.normal(0, 3, len(temp))
# Objective: Production efficiency (throughput)
throughput = 100 + 2*temp + 5*press - 0.01*temp**2 + np.random.normal(0, 5, len(temp))
return assay, impurity_A, impurity_B, np.clip(dissolution, 0, 100), throughput
# Constraint checking function
def check_pharma_constraints(assay, imp_A, imp_B, dissolution):
"""GMP quality specification check"""
c1 = (assay >= 95.0) & (assay <= 105.0)
c2 = imp_A <= 0.5
c3 = imp_B <= 0.3
c4 = dissolution >= 80.0
return c1 & c2 & c3 & c4
print("\nRunning pharmaceutical quality optimization (with GMP constraints)...\n")
# Initialization
bounds_pharma = np.array([
[60, 80], # Temperature
[5, 7], # pH
[50, 150], # Pressure
[2, 8] # Coating thickness
])
X_pharma = qmc.scale(qmc.LatinHypercube(d=4).random(n=20), bounds_pharma[:, 0], bounds_pharma[:, 1])
assay, impA, impB, dissol, throughput = pharmaceutical_quality(X_pharma)
for iteration in range(30):
# GP construction (throughput)
gp_throughput = GaussianProcessRegressor(kernel=C(1.0)*RBF([5,1,20,2]), normalize_y=True)
gp_throughput.fit(X_pharma, throughput)
# GP construction (constraints)
gp_assay = GaussianProcessRegressor(kernel=C(1.0)*RBF([5,1,20,2]), normalize_y=True)
gp_assay.fit(X_pharma, assay)
gp_impA = GaussianProcessRegressor(kernel=C(1.0)*RBF([5,1,20,2]), normalize_y=True)
gp_impA.fit(X_pharma, impA)
# Candidate points
X_cand = qmc.scale(qmc.LatinHypercube(d=4).random(n=300), bounds_pharma[:, 0], bounds_pharma[:, 1])
# Probability of feasibility
mu_assay, sigma_assay = gp_assay.predict(X_cand, return_std=True)
pof_assay = norm.cdf((105-mu_assay)/(sigma_assay+1e-6)) - norm.cdf((95-mu_assay)/(sigma_assay+1e-6))
mu_impA, sigma_impA = gp_impA.predict(X_cand, return_std=True)
pof_impA = norm.cdf((0.5-mu_impA)/(sigma_impA+1e-6))
# EI
mu_tp, sigma_tp = gp_throughput.predict(X_cand, return_std=True)
feasible_mask = check_pharma_constraints(assay, impA, impB, dissol)
if feasible_mask.any():
best_feasible = throughput[feasible_mask].max()
else:
best_feasible = throughput.min()
imp_tp = mu_tp - best_feasible
Z = imp_tp / (sigma_tp + 1e-6)
ei = imp_tp * norm.cdf(Z) + sigma_tp * norm.pdf(Z)
# Constrained EI
cei_pharma = ei * pof_assay * pof_impA
next_x = X_cand[np.argmax(cei_pharma)]
next_assay, next_impA, next_impB, next_dissol, next_tp = pharmaceutical_quality(next_x.reshape(1, -1))
X_pharma = np.vstack([X_pharma, next_x])
assay = np.hstack([assay, next_assay])
impA = np.hstack([impA, next_impA])
impB = np.hstack([impB, next_impB])
dissol = np.hstack([dissol, next_dissol])
throughput = np.hstack([throughput, next_tp])
# Results
final_feasible_pharma = check_pharma_constraints(assay, impA, impB, dissol)
print(f"GMP compliant products: {final_feasible_pharma.sum()}/{len(X_pharma)} ({final_feasible_pharma.sum()/len(X_pharma)*100:.1f}%)")
if final_feasible_pharma.any():
best_pharma_idx = np.argmax(throughput[final_feasible_pharma])
best_pharma_x = X_pharma[final_feasible_pharma][best_pharma_idx]
best_pharma_tp = throughput[final_feasible_pharma][best_pharma_idx]
print(f"\nOptimal conditions (GMP compliant):")
print(f" Temperature: {best_pharma_x[0]:.1f}°C")
print(f" pH: {best_pharma_x[1]:.1f}")
print(f" Pressure: {best_pharma_x[2]:.0f} bar")
print(f" Coating thickness: {best_pharma_x[3]:.1f} ¼m")
print(f" Throughput: {best_pharma_tp:.1f} kg/h")
5.6 Energy Consumption Minimization
Example 5: Energy Efficiency Optimization
# Energy minimization (multi-objective: productivity vs energy)
def energy_optimization_process(X):
"""Energy efficiency focused process"""
temp, flow, recyc = X[:, 0], X[:, 1], X[:, 2]
# Productivity
productivity = 50 + 0.5*temp + 2*flow - 0.002*temp**2 + 0.1*recyc*flow + np.random.normal(0, 3, len(temp))
# Energy consumption
energy = 100 + 2*temp + 3*flow - 5*recyc + 0.01*temp*flow + np.random.normal(0, 5, len(temp))
return productivity, energy
print("\nRunning energy optimization (Pareto search)...\n")
# Initialization
X_energy = qmc.scale(qmc.LatinHypercube(d=3).random(n=15), [100, 10, 0], [200, 50, 80])
prod, energy = energy_optimization_process(X_energy)
for iteration in range(30):
gp_prod = GaussianProcessRegressor(kernel=C(1.0)*RBF([20,10,20]), normalize_y=True)
gp_prod.fit(X_energy, prod)
gp_energy = GaussianProcessRegressor(kernel=C(1.0)*RBF([20,10,20]), normalize_y=True)
gp_energy.fit(X_energy, energy)
X_cand = qmc.scale(qmc.LatinHypercube(d=3).random(n=200), [100, 10, 0], [200, 50, 80])
mu_prod, sigma_prod = gp_prod.predict(X_cand, return_std=True)
mu_energy, sigma_energy = gp_energy.predict(X_cand, return_std=True)
# Pareto search: varying weights
weight = iteration / 30
scalarized = (1-weight) * (mu_prod + 2*sigma_prod) - weight * (mu_energy - 2*sigma_energy)
next_x = X_cand[np.argmax(scalarized)]
next_prod, next_energy = energy_optimization_process(next_x.reshape(1, -1))
X_energy = np.vstack([X_energy, next_x])
prod = np.hstack([prod, next_prod])
energy = np.hstack([energy, next_energy])
# Pareto front
from sklearn.metrics import pairwise_distances
costs_energy = np.column_stack([-prod, energy])
is_pareto = np.ones(len(costs_energy), dtype=bool)
for i, c in enumerate(costs_energy):
if is_pareto[i]:
is_pareto[is_pareto] = np.any(costs_energy[is_pareto] < c, axis=1)
is_pareto[i] = True
pareto_prod = prod[is_pareto]
pareto_energy = energy[is_pareto]
print(f"Pareto optimal solutions: {is_pareto.sum()}")
print(f" Highest productivity: {pareto_prod.max():.1f} (Energy: {pareto_energy[np.argmax(pareto_prod)]:.1f})")
print(f" Lowest energy: {pareto_energy.min():.1f} (Productivity: {pareto_prod[np.argmin(pareto_energy)]:.1f})")
# Pareto front visualization
fig, ax = plt.subplots(figsize=(10, 6))
ax.scatter(prod, energy, c='lightgray', alpha=0.5, s=50, label='All evaluations')
ax.scatter(pareto_prod, pareto_energy, c='red', s=150, marker='*', label='Pareto front', zorder=10)
sorted_idx = np.argsort(pareto_prod)
ax.plot(pareto_prod[sorted_idx], pareto_energy[sorted_idx], 'r--', alpha=0.5, linewidth=2)
ax.set_xlabel('Productivity (kg/h)', fontsize=12)
ax.set_ylabel('Energy Consumption (kWh)', fontsize=12)
ax.set_title('Productivity-Energy Tradeoff (Pareto Front)', fontsize=13, fontweight='bold')
ax.legend(fontsize=11)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig('energy_optimization.png', dpi=150, bbox_inches='tight')
5.7 Multi-Objective Process Optimization
Example 6: Three-Objective Simultaneous Optimization
# 3-objective optimization: yield, quality, cost
def multi_objective_process(X):
"""3-objective process"""
temp, pres, cat = X[:, 0], X[:, 1], X[:, 2]
# Yield (maximize)
yield_val = 60 + 0.5*temp + 0.3*pres + 10*cat - 0.003*temp**2 - 2*cat**2 + np.random.normal(0, 2, len(temp))
# Quality score (maximize)
quality = 70 + 0.2*temp - 0.001*temp**2 + 0.1*pres + 5*cat + np.random.normal(0, 3, len(temp))
# Cost (minimize: catalyst expensive)
cost = 50 + 0.1*temp + 0.2*pres + 20*cat + np.random.normal(0, 2, len(temp))
return yield_val, quality, cost
print("\nRunning 3-objective optimization...\n")
X_multi = qmc.scale(qmc.LatinHypercube(d=3).random(n=15), [80, 20, 0.1], [150, 80, 1.0])
y_yield, y_quality, y_cost = multi_objective_process(X_multi)
for iteration in range(35):
# 3 GPs
gps = []
for y_data in [y_yield, y_quality, y_cost]:
gp = GaussianProcessRegressor(kernel=C(1.0)*RBF([15,15,0.3]), normalize_y=True)
gp.fit(X_multi, y_data)
gps.append(gp)
X_cand = qmc.scale(qmc.LatinHypercube(d=3).random(n=200), [80, 20, 0.1], [150, 80, 1.0])
# EHVI approximation (simplified: random weights)
w1, w2, w3 = np.random.dirichlet([1, 1, 1]) # Random weights
mu_yield, sig_yield = gps[0].predict(X_cand, return_std=True)
mu_quality, sig_quality = gps[1].predict(X_cand, return_std=True)
mu_cost, sig_cost = gps[2].predict(X_cand, return_std=True)
scalarized = w1*(mu_yield + 2*sig_yield) + w2*(mu_quality + 2*sig_quality) - w3*(mu_cost - 2*sig_cost)
next_x = X_cand[np.argmax(scalarized)]
next_yield, next_quality, next_cost = multi_objective_process(next_x.reshape(1, -1))
X_multi = np.vstack([X_multi, next_x])
y_yield = np.hstack([y_yield, next_yield])
y_quality = np.hstack([y_quality, next_quality])
y_cost = np.hstack([y_cost, next_cost])
print(f"3-objective optimization completed: {len(X_multi)} experiments")
print(f" Yield range: {y_yield.min():.1f} - {y_yield.max():.1f}%")
print(f" Quality range: {y_quality.min():.1f} - {y_quality.max():.1f}")
print(f" Cost range: ${y_cost.min():.1f} - ${y_cost.max():.1f}")
5.8 Fully Integrated Case Study
Case Study 7: From Implementation to Operation - Polymer Polymerization Process
Goal: Minimize polydispersity index (PDI) and achieve yield e90%
Parameters (7D): Temperature, initiator concentration, monomer concentration, agitation speed, pH, pressure, reaction time
Constraints: Temperature d100°C, Viscosity d5000 cP, Residual monomer d1%
Example 7: Complete Implementation Workflow
# Fully integrated case study: Polymer polymerization process
class PolymerizationOptimizer:
"""Polymer polymerization process optimization system"""
def __init__(self, bounds):
self.bounds = bounds
self.X_history = []
self.y_pdi_history = []
self.y_yield_history = []
self.constraint_history = []
def process_simulation(self, X):
"""Polymerization process simulation"""
temp, init, mono, agit, pH, pres, time = X.T
# PDI (polydispersity index: lower is better)
pdi = (
1.5 + 0.01*temp - 0.0001*temp**2 +
0.5*init - 0.2*init**2 +
0.05*agit/100 - 0.3*pH +
np.random.normal(0, 0.05, len(temp))
)
# Yield
yield_val = (
50 + 0.3*temp + 5*init + 10*mono - 0.002*temp**2 +
0.01*agit + 3*pH + 0.1*time +
np.random.normal(0, 3, len(temp))
)
# Constraints (viscosity, residual monomer)
viscosity = 1000 + 50*temp + 100*mono - 10*agit + 20*time
residual_monomer = np.maximum(0, 5 - 0.05*temp - 0.5*init - 0.02*time + np.random.normal(0, 0.2, len(temp)))
return np.clip(pdi, 1.0, 5.0), np.clip(yield_val, 0, 100), viscosity, residual_monomer
def optimize(self, n_iterations=50):
"""Execute optimization"""
print("=== Polymer Polymerization Process Optimization Started ===\n")
# Initial sampling
X_init = qmc.scale(
qmc.LatinHypercube(d=7).random(n=25),
self.bounds[:, 0], self.bounds[:, 1]
)
pdi, yield_val, visc, res_mono = self.process_simulation(X_init)
self.X_history = X_init.copy()
self.y_pdi_history = pdi.copy()
self.y_yield_history = yield_val.copy()
self.constraint_history = [(visc, res_mono)]
print(f"Initial sampling completed: {len(X_init)} experiments")
for iteration in range(n_iterations):
# GP construction (PDI, yield)
gp_pdi = GaussianProcessRegressor(
kernel=C(1.0) * RBF([5]*7),
normalize_y=True,
n_restarts_optimizer=3
)
gp_pdi.fit(self.X_history, self.y_pdi_history)
gp_yield = GaussianProcessRegressor(
kernel=C(1.0) * RBF([5]*7),
normalize_y=True,
n_restarts_optimizer=3
)
gp_yield.fit(self.X_history, self.y_yield_history)
# Candidate generation
X_cand = qmc.scale(
qmc.LatinHypercube(d=7).random(n=300),
self.bounds[:, 0], self.bounds[:, 1]
)
# Multi-objective scalarization (minimize PDI, maximize yield)
mu_pdi, sigma_pdi = gp_pdi.predict(X_cand, return_std=True)
mu_yield, sigma_yield = gp_yield.predict(X_cand, return_std=True)
# Constraint: yield e 90%
yield_pof = norm.cdf((mu_yield - 90) / (sigma_yield + 1e-6))
# EI for PDI (minimization)
feasible_mask = self.y_yield_history >= 90
if feasible_mask.any():
best_pdi = self.y_pdi_history[feasible_mask].min()
else:
best_pdi = self.y_pdi_history.min()
# EI for minimization
imp_pdi = best_pdi - mu_pdi
Z_pdi = imp_pdi / (sigma_pdi + 1e-6)
ei_pdi = imp_pdi * norm.cdf(Z_pdi) + sigma_pdi * norm.pdf(Z_pdi)
# Constrained EI
cei = ei_pdi * yield_pof
# Next experiment point
next_idx = np.argmax(cei)
next_x = X_cand[next_idx]
# Execute experiment
next_pdi, next_yield, next_visc, next_res = self.process_simulation(next_x.reshape(1, -1))
# Add data
self.X_history = np.vstack([self.X_history, next_x])
self.y_pdi_history = np.hstack([self.y_pdi_history, next_pdi])
self.y_yield_history = np.hstack([self.y_yield_history, next_yield])
self.constraint_history.append((next_visc, next_res))
if (iteration + 1) % 10 == 0:
feasible_now = self.y_yield_history >= 90
if feasible_now.any():
print(f"Iteration {iteration+1}/{n_iterations}: Best PDI (yielde90%) = {self.y_pdi_history[feasible_now].min():.3f}")
else:
print(f"Iteration {iteration+1}/{n_iterations}: No feasible solution yet")
self._report_results()
def _report_results(self):
"""Final results report"""
print("\n" + "="*60)
print("Optimization Completion Report")
print("="*60)
feasible = self.y_yield_history >= 90
if feasible.any():
best_idx = np.argmin(self.y_pdi_history[feasible])
best_X_feasible = self.X_history[feasible][best_idx]
best_pdi = self.y_pdi_history[feasible][best_idx]
best_yield = self.y_yield_history[feasible][best_idx]
print(f"\nOptimal conditions (satisfying yielde90%):")
param_names = ['Temperature(°C)', 'Initiator(mol/L)', 'Monomer(mol/L)',
'Agitation(rpm)', 'pH', 'Pressure(bar)', 'Time(min)']
for i, name in enumerate(param_names):
print(f" {name}: {best_X_feasible[i]:.2f}")
print(f"\nPerformance:")
print(f" PDI: {best_pdi:.3f}")
print(f" Yield: {best_yield:.2f}%")
print(f"\nStatistics:")
print(f" Total experiments: {len(self.X_history)}")
print(f" Feasible solutions: {feasible.sum()} ({feasible.sum()/len(self.X_history)*100:.1f}%)")
else:
print("\nNo solution satisfying yielde90% was found.")
print(f"Best yield: {self.y_yield_history.max():.2f}%")
# Execute
bounds_polymer = np.array([
[60, 100], # Temperature
[0.1, 2.0], # Initiator
[2, 10], # Monomer
[100, 500], # Agitation
[4, 8], # pH
[1, 5], # Pressure
[30, 180] # Time
])
optimizer = PolymerizationOptimizer(bounds_polymer)
optimizer.optimize(n_iterations=50)
print("\nSaved: Fully integrated case study completed")
Fully Integrated Case Study Results
- Target achieved in 75 experiments (conventional DOE: >300 experiments)
- PDI: 2.1 ’ 1.35 (36% improvement)
- Yield: 92.5% achieved (target e90%)
- Satisfies constraints (viscosity, residual monomer)
- Experimental cost: 75% reduction
Summary
In this chapter, we learned about industrial applications of Bayesian optimization through seven case studies.
Key Learnings
- Real Process Complexity: Combined challenges of multi-objective, constraints, high-dimensionality
- Stepwise Approach: From simple to complex problems
- Constraint Handling: Reliable compliance with safety and quality specifications
- Multi-Objective Optimization: Tradeoff visualization and decision support
- Experimental Reduction Effect: Average 70-80% reduction in number of experiments
- Performance Improvement: 30-50% performance improvement (case dependent)
Implementation Best Practices
- Problem Formulation: Clarify objectives, constraints, and parameter ranges
- Initial Sampling: Cover space comprehensively with Latin Hypercube
- Appropriate Acquisition Function: Select EI/UCB/Constrained according to problem
- Reliable Constraint Compliance: Never compromise on safety and quality specifications
- Sequential Improvement: Target convergence in 30-50 iterations
- Results Verification: Confirm optimal solution with multiple experiments
Next Steps
Having completed the series, you are now ready to apply Bayesian optimization to real processes. The next step is to apply it to your own process and achieve continuous improvement!
References
- Montgomery, D. C. (2019). Design and Analysis of Experiments (9th ed.). Wiley.
- Box, G. E. P., Hunter, J. S., & Hunter, W. G. (2005). Statistics for Experimenters: Design, Innovation, and Discovery (2nd ed.). Wiley.
- Seborg, D. E., Edgar, T. F., Mellichamp, D. A., & Doyle III, F. J. (2016). Process Dynamics and Control (4th ed.). Wiley.
- McKay, M. D., Beckman, R. J., & Conover, W. J. (2000). "A Comparison of Three Methods for Selecting Values of Input Variables in the Analysis of Output from a Computer Code." Technometrics, 42(1), 55-61.
Disclaimer
- This content is provided solely for educational, research, and informational purposes and does not constitute professional advice (legal, accounting, technical warranty, etc.).
- This content and accompanying code examples are provided "AS IS" without any warranty, express or implied, including but not limited to merchantability, fitness for a particular purpose, non-infringement, accuracy, completeness, operation, or safety.
- The author and Tohoku University assume no responsibility for the content, availability, or safety of external links, third-party data, tools, libraries, etc.
- To the maximum extent permitted by applicable law, the author and Tohoku University shall not be liable for any direct, indirect, incidental, special, consequential, or punitive damages arising from the use, execution, or interpretation of this content.
- The content may be changed, updated, or discontinued without notice.
- The copyright and license of this content are subject to the stated conditions (e.g., CC BY 4.0). Such licenses typically include no-warranty clauses.