🌐 EN | 🇯🇵 日本語 Last sync: 2025-11-16

Chapter 3: Process Optimization

Process Optimization

This chapter covers Process Optimization. You will learn essential concepts and techniques.

3.1 Challenges in Food Process Optimization

Optimization of food manufacturing processes is critically important from the perspectives of quality improvement, cost reduction, and energy efficiency enhancement. However, there are many challenges, including raw material variability, complex nonlinear relationships, and the need for multi-objective optimization. AI technologies, especially Bayesian optimization and genetic algorithms, are powerful methods for addressing these challenges.

Major Challenges in Food Process Optimization

🎯 Optimization Method Selection Guide

Method Application Scenario Advantages Disadvantages
Bayesian Optimization High evaluation cost, optimization with few experiments High sample efficiency, considers uncertainty Somewhat high computational cost
Genetic Algorithm Multi-objective optimization, includes discrete variables Global optimal solution search, easy implementation Slow convergence
Response Surface Methodology (RSM) Combined with design of experiments Statistical basis, easy visualization Accuracy decreases with strong nonlinearity
Particle Swarm Optimization (PSO) Continuous variable optimization Simple implementation, few parameter adjustments Prone to local optima
# Requirements:
# - Python 3.9+
# - matplotlib>=3.7.0


            <div class="code-header">📊 Code Example 1: Heating Condition Optimization Using Bayesian Optimization</div>
            <pre><code class="language-python">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
import warnings
warnings.filterwarnings('ignore')

# Objective function for food heating process (simulates actual process)
def food_processing_objective(temperature, time):
    """
    Objective function: Maximize quality score (function of temperature and time)
    Optimal point near: temperature=90°C, time=20min
    """
    # Complex nonlinear function (simulating actual food process)
    quality = (
        100 * np.exp(-0.5 * ((temperature - 90)/10)**2) *
        np.exp(-0.5 * ((time - 20)/5)**2) +
        10 * np.sin(temperature / 10) * np.cos(time / 5) +
        np.random.normal(0, 2)  # Measurement noise
    )
    return quality

# Bayesian Optimization implementation
class BayesianOptimizer:
    def __init__(self, bounds, n_init=5):
        self.bounds = np.array(bounds)
        self.n_init = n_init
        self.X_sample = []
        self.y_sample = []

        # Gaussian Process Regression model
        kernel = C(1.0, (1e-3, 1e3)) * RBF([10, 10], (1e-2, 1e2))
        self.gp = GaussianProcessRegressor(kernel=kernel, n_restarts_optimizer=10,
                                           alpha=1e-6, normalize_y=True)

    def acquisition_function(self, X, xi=0.01):
        """
        Acquisition function: Expected Improvement (EI)
        """
        mu, sigma = self.gp.predict(X, return_std=True)
        mu_sample_opt = np.max(self.y_sample)

        with np.errstate(divide='warn'):
            imp = mu - mu_sample_opt - xi
            Z = imp / sigma
            ei = imp * norm.cdf(Z) + sigma * norm.pdf(Z)
            ei[sigma == 0.0] = 0.0

        return ei

    def propose_location(self):
        """
        Propose next evaluation point (maximize EI)
        """
        dim = self.bounds.shape[0]
        min_val = float('inf')
        min_x = None

        # Random sampling + optimization
        n_restarts = 25
        for _ in range(n_restarts):
            x0 = np.random.uniform(self.bounds[:, 0], self.bounds[:, 1], size=dim)
            res = self._minimize_acquisition(x0)
            if res < min_val:
                min_val = res
                min_x = x0

        return min_x

    def _minimize_acquisition(self, x0):
        """
        Minimize acquisition function (minimize negative EI)
        """
        from scipy.optimize import minimize

        def objective(x):
            return -self.acquisition_function(x.reshape(1, -1))

        bounds_list = [(self.bounds[i, 0], self.bounds[i, 1])
                       for i in range(self.bounds.shape[0])]

        res = minimize(objective, x0, bounds=bounds_list, method='L-BFGS-B')
        return res.fun

    def optimize(self, objective_func, n_iter=20):
        """
        Execute Bayesian optimization
        """
        # Initial random sampling
        for _ in range(self.n_init):
            x = np.random.uniform(self.bounds[:, 0], self.bounds[:, 1],
                                  size=self.bounds.shape[0])
            y = objective_func(x[0], x[1])
            self.X_sample.append(x)
            self.y_sample.append(y)

        # Bayesian optimization loop
        for i in range(n_iter):
            # Update GP model
            self.gp.fit(np.array(self.X_sample), np.array(self.y_sample))

            # Propose next evaluation point
            x_next = self.propose_location()
            y_next = objective_func(x_next[0], x_next[1])

            self.X_sample.append(x_next)
            self.y_sample.append(y_next)

            if (i + 1) % 5 == 0:
                print(f"Iteration {i+1}: Best quality = {np.max(self.y_sample):.2f} "
                      f"at T={self.X_sample[np.argmax(self.y_sample)][0]:.1f}°C, "
                      f"t={self.X_sample[np.argmax(self.y_sample)][1]:.1f}min")

        return np.array(self.X_sample), np.array(self.y_sample)

# Execute optimization
bounds = [[75, 105], [10, 30]]  # [temperature range, time range]
optimizer = BayesianOptimizer(bounds, n_init=5)
X_sample, y_sample = optimizer.optimize(food_processing_objective, n_iter=25)

# Optimal solution
best_idx = np.argmax(y_sample)
best_temp, best_time = X_sample[best_idx]
best_quality = y_sample[best_idx]

print(f"\n=== Optimization Results ===")
print(f"Optimal temperature: {best_temp:.2f}°C")
print(f"Optimal time: {best_time:.2f} min")
print(f"Maximum quality score: {best_quality:.2f}")
print(f"Total evaluations: {len(X_sample)}")

# Visualization
fig = plt.figure(figsize=(16, 10))
gs = fig.add_gridspec(2, 2, hspace=0.3, wspace=0.3)

# 1. True objective function (reference)
ax1 = fig.add_subplot(gs[0, 0])
T_grid = np.linspace(75, 105, 100)
t_grid = np.linspace(10, 30, 100)
T_mesh, t_mesh = np.meshgrid(T_grid, t_grid)
quality_grid = np.zeros_like(T_mesh)
for i in range(T_mesh.shape[0]):
    for j in range(T_mesh.shape[1]):
        quality_grid[i, j] = food_processing_objective(T_mesh[i, j], t_mesh[i, j])

contour = ax1.contourf(T_mesh, t_mesh, quality_grid, levels=20, cmap='viridis', alpha=0.8)
ax1.scatter(X_sample[:5, 0], X_sample[:5, 1], c='red', s=100,
            marker='o', edgecolors='white', linewidth=2, label='Initial samples', zorder=5)
ax1.scatter(X_sample[5:, 0], X_sample[5:, 1], c='white', s=80,
            marker='s', edgecolors='black', linewidth=1.5, label='BO proposals', zorder=5)
ax1.scatter(best_temp, best_time, c='gold', s=300, marker='*',
            edgecolors='red', linewidth=2, label='Optimal solution', zorder=10)
ax1.set_xlabel('Temperature (°C)', fontsize=12)
ax1.set_ylabel('Time (min)', fontsize=12)
ax1.set_title('Bayesian Optimization Search Process', fontsize=14, fontweight='bold')
ax1.legend(fontsize=10)
plt.colorbar(contour, ax=ax1, label='Quality Score')

# 2. Quality score progression
ax2 = fig.add_subplot(gs[0, 1])
iterations = np.arange(1, len(y_sample) + 1)
best_so_far = np.maximum.accumulate(y_sample)
ax2.plot(iterations, y_sample, 'o-', color='#11998e', alpha=0.6,
         linewidth=2, markersize=6, label='Quality at each evaluation')
ax2.plot(iterations, best_so_far, 'r-', linewidth=3, label='Best value progression')
ax2.axvline(x=5, color='gray', linestyle='--', alpha=0.5, label='Initial sampling end')
ax2.set_xlabel('Evaluation count', fontsize=12)
ax2.set_ylabel('Quality score', fontsize=12)
ax2.set_title('Optimization Convergence Process', fontsize=14, fontweight='bold')
ax2.legend(fontsize=10)
ax2.grid(True, alpha=0.3)

# 3. Gaussian process prediction (temperature direction slice, time=best_time)
ax3 = fig.add_subplot(gs[1, 0])
T_test = np.linspace(75, 105, 200).reshape(-1, 1)
t_test = np.full_like(T_test, best_time)
X_test = np.hstack([T_test, t_test])

mu, sigma = optimizer.gp.predict(X_test, return_std=True)

ax3.plot(T_test, mu, 'b-', linewidth=2, label='GP prediction mean')
ax3.fill_between(T_test.ravel(), mu - 1.96*sigma, mu + 1.96*sigma,
                 alpha=0.3, color='blue', label='95% confidence interval')
ax3.scatter(X_sample[:, 0], y_sample, c='red', s=80, marker='o',
            edgecolors='white', linewidth=1.5, label='Observations', zorder=5)
ax3.axvline(x=best_temp, color='green', linestyle='--', linewidth=2, label='Optimal temperature')
ax3.set_xlabel('Temperature (°C)', fontsize=12)
ax3.set_ylabel('Quality score', fontsize=12)
ax3.set_title(f'Gaussian Process Prediction (time={best_time:.1f}min fixed)', fontsize=14, fontweight='bold')
ax3.legend(fontsize=10)
ax3.grid(True, alpha=0.3)

# 4. Acquisition function (Expected Improvement)
ax4 = fig.add_subplot(gs[1, 1])
ei = optimizer.acquisition_function(X_test)
ax4.plot(T_test, ei, 'g-', linewidth=2)
ax4.fill_between(T_test.ravel(), 0, ei, alpha=0.3, color='green')
ax4.set_xlabel('Temperature (°C)', fontsize=12)
ax4.set_ylabel('Expected Improvement', fontsize=12)
ax4.set_title('Acquisition Function (Selection Criterion for Next Evaluation)', fontsize=14, fontweight='bold')
ax4.grid(True, alpha=0.3)

plt.savefig('bayesian_optimization_food.png', dpi=300, bbox_inches='tight')
plt.show()

3.2 Multi-Objective Optimization Using Genetic Algorithms

In food processes, it is necessary to simultaneously optimize multiple objectives such as quality, cost, and energy efficiency. Genetic algorithms (GA), especially NSGA-II (Non-dominated Sorting Genetic Algorithm II), can efficiently search for Pareto optimal solutions.

# Requirements:
# - Python 3.9+
# - matplotlib>=3.7.0

"""
Purpose: Demonstrate data visualization techniques
Target: Advanced
Execution time: 1-5 minutes
Dependencies: None
"""

            <div class="code-header">📊 Code Example 2: Multi-Objective Optimization (Quality vs Cost)</div>
            <pre><code class="language-python">import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import differential_evolution

# Multi-objective optimization problem definition
class FoodProcessMultiObjective:
    def __init__(self):
        # Parameter ranges
        self.bounds = [
            (75, 105),   # Temperature (°C)
            (10, 30),    # Time (min)
            (3.0, 5.0),  # pH
            (50, 150),   # Flow rate (L/min)
        ]

    def quality_objective(self, params):
        """
        Objective function 1: Maximize quality score (negative value for minimization problem)
        """
        temp, time, ph, flow = params

        # Quality function (nonlinear)
        quality = (
            100 * np.exp(-0.5 * ((temp - 90)/10)**2) *
            np.exp(-0.5 * ((time - 20)/5)**2) *
            np.exp(-0.5 * ((ph - 4.0)/0.5)**2) *
            (1 + 0.1 * np.sin(flow / 20))
        )

        return -quality  # Convert to minimization problem

    def cost_objective(self, params):
        """
        Objective function 2: Minimize cost
        """
        temp, time, ph, flow = params

        # Energy cost (depends on temperature and time)
        energy_cost = 0.1 * (temp - 70) * time

        # pH adjuster cost
        ph_cost = 5 * abs(ph - 4.0)

        # Pump operation cost
        pump_cost = 0.05 * flow * time

        total_cost = energy_cost + ph_cost + pump_cost

        return total_cost

    def constraints_check(self, params):
        """
        Constraint condition check (HACCP standards, etc.)
        """
        temp, time, ph, flow = params

        # F-value (sterilization effect) calculation: F0 = time * 10^((temp-121)/10)
        F0 = time * (10 ** ((temp - 121) / 10))

        # Minimum F-value requirement (example: F0 >= 3)
        if F0 < 3:
            return False

        # pH range constraint
        if not (3.0 <= ph <= 5.0):
            return False

        return True

# Simple Pareto optimization (grid search + Pareto determination)
problem = FoodProcessMultiObjective()

# Pareto front approximation by random sampling
np.random.seed(42)
n_samples = 1000

# Generate random samples
samples = []
qualities = []
costs = []

for _ in range(n_samples):
    params = [
        np.random.uniform(low, high)
        for low, high in problem.bounds
    ]

    if problem.constraints_check(params):
        quality = -problem.quality_objective(params)  # Restore (maximize)
        cost = problem.cost_objective(params)

        samples.append(params)
        qualities.append(quality)
        costs.append(cost)

samples = np.array(samples)
qualities = np.array(qualities)
costs = np.array(costs)

# Determine Pareto optimal solutions
def is_pareto_efficient(costs_qualities):
    """
    Determine Pareto optimal solutions (for two objectives)
    cost: minimize, quality: maximize
    """
    is_efficient = np.ones(costs_qualities.shape[0], dtype=bool)
    for i, c in enumerate(costs_qualities):
        if is_efficient[i]:
            # Check if other solutions dominate this solution
            # Dominance: cost <= c[0] and quality >= c[1] with at least one strictly better
            dominated = np.logical_and(
                costs_qualities[:, 0] <= c[0],  # Cost is same or less
                costs_qualities[:, 1] >= c[1]   # Quality is same or higher
            )
            # Strictly better (not equal)
            strictly_better = np.logical_or(
                costs_qualities[:, 0] < c[0],
                costs_qualities[:, 1] > c[1]
            )
            dominated = np.logical_and(dominated, strictly_better)

            if np.any(dominated):
                is_efficient[i] = False

    return is_efficient

# Extract Pareto optimal solutions
costs_qualities = np.column_stack([costs, qualities])
pareto_mask = is_pareto_efficient(costs_qualities)
pareto_samples = samples[pareto_mask]
pareto_costs = costs[pareto_mask]
pareto_qualities = qualities[pareto_mask]

# Sort Pareto front by cost
sorted_indices = np.argsort(pareto_costs)
pareto_costs_sorted = pareto_costs[sorted_indices]
pareto_qualities_sorted = pareto_qualities[sorted_indices]
pareto_samples_sorted = pareto_samples[sorted_indices]

# Select three representative solutions
n_pareto = len(pareto_costs_sorted)
representative_indices = [0, n_pareto // 2, n_pareto - 1]
representative_solutions = []

for idx in representative_indices:
    sol = {
        'params': pareto_samples_sorted[idx],
        'quality': pareto_qualities_sorted[idx],
        'cost': pareto_costs_sorted[idx],
        'label': ['Cost-focused', 'Balanced', 'Quality-focused'][representative_indices.index(idx)]
    }
    representative_solutions.append(sol)

# Visualization
fig = plt.figure(figsize=(16, 10))
gs = fig.add_gridspec(2, 2, hspace=0.3, wspace=0.3)

# 1. Pareto front
ax1 = fig.add_subplot(gs[0, :])
ax1.scatter(costs, qualities, c='lightgray', alpha=0.4, s=30, label='All samples')
ax1.plot(pareto_costs_sorted, pareto_qualities_sorted, 'r-', linewidth=2.5,
         label='Pareto front', zorder=10)
ax1.scatter(pareto_costs_sorted, pareto_qualities_sorted, c='red', s=100,
            marker='o', edgecolors='white', linewidth=2, label='Pareto optimal solutions', zorder=11)

# Highlight representative solutions
colors_rep = ['#1f77b4', '#ff7f0e', '#2ca02c']
for i, sol in enumerate(representative_solutions):
    ax1.scatter(sol['cost'], sol['quality'], c=colors_rep[i], s=300,
                marker='*', edgecolors='black', linewidth=2,
                label=sol['label'], zorder=12)
    ax1.annotate(sol['label'],
                xy=(sol['cost'], sol['quality']),
                xytext=(15, 15), textcoords='offset points',
                fontsize=11, fontweight='bold',
                bbox=dict(boxstyle='round,pad=0.5', fc=colors_rep[i], alpha=0.7),
                arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0'))

ax1.set_xlabel('Cost (JPY/batch)', fontsize=13)
ax1.set_ylabel('Quality score', fontsize=13)
ax1.set_title('Multi-Objective Optimization: Pareto Front', fontsize=15, fontweight='bold')
ax1.legend(fontsize=11, loc='upper right')
ax1.grid(True, alpha=0.3)

# 2-4. Parameter comparison of representative solutions
param_names = ['Temperature (°C)', 'Time (min)', 'pH', 'Flow rate (L/min)']
axes = [fig.add_subplot(gs[1, 0]), fig.add_subplot(gs[1, 1])]

# Temperature and time
ax2 = axes[0]
x_pos = np.arange(len(representative_solutions))
width = 0.35

temp_values = [sol['params'][0] for sol in representative_solutions]
time_values = [sol['params'][1] for sol in representative_solutions]

ax2_twin = ax2.twinx()
bars1 = ax2.bar(x_pos - width/2, temp_values, width, label='Temperature', color='#ff6b6b', alpha=0.8)
bars2 = ax2_twin.bar(x_pos + width/2, time_values, width, label='Time', color='#4ecdc4', alpha=0.8)

ax2.set_xlabel('Solution type', fontsize=12)
ax2.set_ylabel('Temperature (°C)', fontsize=12, color='#ff6b6b')
ax2_twin.set_ylabel('Time (min)', fontsize=12, color='#4ecdc4')
ax2.set_title('Representative Solution Parameter Comparison (Temperature/Time)', fontsize=13, fontweight='bold')
ax2.set_xticks(x_pos)
ax2.set_xticklabels([sol['label'] for sol in representative_solutions])
ax2.tick_params(axis='y', labelcolor='#ff6b6b')
ax2_twin.tick_params(axis='y', labelcolor='#4ecdc4')
ax2.grid(True, alpha=0.3, axis='y')

# pH and flow rate
ax3 = axes[1]
ph_values = [sol['params'][2] for sol in representative_solutions]
flow_values = [sol['params'][3] for sol in representative_solutions]

ax3_twin = ax3.twinx()
bars3 = ax3.bar(x_pos - width/2, ph_values, width, label='pH', color='#95e1d3', alpha=0.8)
bars4 = ax3_twin.bar(x_pos + width/2, flow_values, width, label='Flow rate', color='#f38181', alpha=0.8)

ax3.set_xlabel('Solution type', fontsize=12)
ax3.set_ylabel('pH', fontsize=12, color='#95e1d3')
ax3_twin.set_ylabel('Flow rate (L/min)', fontsize=12, color='#f38181')
ax3.set_title('Representative Solution Parameter Comparison (pH/Flow rate)', fontsize=13, fontweight='bold')
ax3.set_xticks(x_pos)
ax3.set_xticklabels([sol['label'] for sol in representative_solutions])
ax3.tick_params(axis='y', labelcolor='#95e1d3')
ax3_twin.tick_params(axis='y', labelcolor='#f38181')
ax3.grid(True, alpha=0.3, axis='y')

plt.savefig('multi_objective_optimization.png', dpi=300, bbox_inches='tight')
plt.show()

# Report output
print("=== Multi-Objective Optimization Results ===")
print(f"Total samples: {n_samples}")
print(f"Samples satisfying constraints: {len(samples)}")
print(f"Pareto optimal solutions: {len(pareto_samples)}")

print("\n=== Three Representative Solutions ===")
for i, sol in enumerate(representative_solutions):
    print(f"\n{i+1}. {sol['label']}")
    print(f"   Temperature: {sol['params'][0]:.2f}°C")
    print(f"   Time: {sol['params'][1]:.2f} min")
    print(f"   pH: {sol['params'][2]:.2f}")
    print(f"   Flow rate: {sol['params'][3]:.2f} L/min")
    print(f"   Quality score: {sol['quality']:.2f}")
    print(f"   Cost: {sol['cost']:.2f} JPY/batch")

    # F-value calculation
    F0 = sol['params'][1] * (10 ** ((sol['params'][0] - 121) / 10))
    print(f"   Sterilization effect (F0 value): {F0:.2f}")

3.3 Optimization Using Response Surface Methodology (RSM)

Response Surface Methodology (RSM) is an optimization method that combines design of experiments with statistical modeling. With relatively few experiments, it models the relationship between process parameters and response variables with quadratic polynomials and searches for optimal conditions.

# Requirements:
# - Python 3.9+
# - matplotlib>=3.7.0
# - pandas>=2.0.0, <2.2.0


            <div class="code-header">📊 Code Example 3: Response Surface Methodology (Box-Behnken Design)</div>
            <pre><code class="language-python">import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from mpl_toolkits.mplot3d import Axes3D

# Box-Behnken experimental design (3 factors)
def box_behnken_design(factors):
    """
    Generate Box-Behnken experimental design (for 3 factors)
    """
    # Coded levels: -1, 0, +1
    design = np.array([
        [-1, -1,  0], [1, -1,  0], [-1,  1,  0], [1,  1,  0],
        [-1,  0, -1], [1,  0, -1], [-1,  0,  1], [1,  0,  1],
        [ 0, -1, -1], [0,  1, -1], [ 0, -1,  1], [0,  1,  1],
        [ 0,  0,  0], [0,  0,  0], [ 0,  0,  0]  # Center point (3 replicates)
    ])

    # Convert to actual values
    actual_design = []
    for row in design:
        actual_row = []
        for i, level in enumerate(row):
            low, center, high = factors[i]
            if level == -1:
                actual_row.append(low)
            elif level == 0:
                actual_row.append(center)
            else:  # level == 1
                actual_row.append(high)
        actual_design.append(actual_row)

    return np.array(actual_design)

# Factor level settings (low, medium, high)
factors = [
    (80, 90, 100),   # Temperature (°C)
    (15, 20, 25),    # Time (min)
    (3.5, 4.0, 4.5), # pH
]
factor_names = ['Temperature', 'Time', 'pH']

# Generate experimental design
X_design = box_behnken_design(factors)

# Simulate response variable (quality score)
def response_function(temp, time, ph):
    """
    True response function (quadratic polynomial + interaction + noise)
    """
    response = (
        50 +
        2 * (temp - 90) + 0.5 * (time - 20) + 10 * (ph - 4.0) +
        -0.05 * (temp - 90)**2 - 0.1 * (time - 20)**2 - 15 * (ph - 4.0)**2 +
        0.02 * (temp - 90) * (time - 20) +
        0.5 * (temp - 90) * (ph - 4.0) +
        np.random.normal(0, 0.5)  # Experimental error
    )
    return response

y_response = np.array([response_function(x[0], x[1], x[2]) for x in X_design])

# Create dataframe
df_experiment = pd.DataFrame(X_design, columns=factor_names)
df_experiment['Quality Score'] = y_response

print("=== Box-Behnken Experimental Design ===")
print(df_experiment)

# Build quadratic response surface model
poly = PolynomialFeatures(degree=2, include_bias=True)
X_poly = poly.fit_transform(X_design)
model = LinearRegression()
model.fit(X_poly, y_response)

# Model performance
y_pred = model.predict(X_poly)
r2_score = model.score(X_poly, y_response)
print(f"\n=== Response Surface Model ===")
print(f"R² score: {r2_score:.4f}")

# Display coefficients
feature_names = poly.get_feature_names_out(factor_names)
coefficients = pd.DataFrame({
    'Term': feature_names,
    'Coefficient': model.coef_
})
print("\n=== Model Coefficients ===")
print(coefficients)

# Optimization: Search for maximum value by grid search
temp_range = np.linspace(80, 100, 50)
time_range = np.linspace(15, 25, 50)
ph_fixed = 4.0  # Fix pH

T_mesh, t_mesh = np.meshgrid(temp_range, time_range)
response_mesh = np.zeros_like(T_mesh)

for i in range(T_mesh.shape[0]):
    for j in range(T_mesh.shape[1]):
        X_test = np.array([[T_mesh[i, j], t_mesh[i, j], ph_fixed]])
        X_test_poly = poly.transform(X_test)
        response_mesh[i, j] = model.predict(X_test_poly)[0]

# Optimal point
max_idx = np.unravel_index(np.argmax(response_mesh), response_mesh.shape)
optimal_temp = T_mesh[max_idx]
optimal_time = t_mesh[max_idx]
optimal_response = response_mesh[max_idx]

print(f"\n=== Optimal Conditions (pH={ph_fixed} fixed) ===")
print(f"Optimal temperature: {optimal_temp:.2f}°C")
print(f"Optimal time: {optimal_time:.2f} min")
print(f"Predicted quality score: {optimal_response:.2f}")

# Visualization
fig = plt.figure(figsize=(16, 12))

# 1. 3D response surface
ax1 = fig.add_subplot(2, 2, 1, projection='3d')
surf = ax1.plot_surface(T_mesh, t_mesh, response_mesh, cmap='viridis', alpha=0.8)
ax1.scatter(X_design[:, 0], X_design[:, 1], y_response, c='red', s=100,
            marker='o', edgecolors='white', linewidth=2, label='Experimental points')
ax1.scatter(optimal_temp, optimal_time, optimal_response, c='gold', s=300,
            marker='*', edgecolors='red', linewidth=2, label='Optimal point')
ax1.set_xlabel('Temperature (°C)', fontsize=11)
ax1.set_ylabel('Time (min)', fontsize=11)
ax1.set_zlabel('Quality Score', fontsize=11)
ax1.set_title(f'Response Surface (pH={ph_fixed} fixed)', fontsize=13, fontweight='bold')
plt.colorbar(surf, ax=ax1, shrink=0.5, label='Quality Score')

# 2. Contour plot
ax2 = fig.add_subplot(2, 2, 2)
contour = ax2.contourf(T_mesh, t_mesh, response_mesh, levels=15, cmap='viridis', alpha=0.9)
contour_lines = ax2.contour(T_mesh, t_mesh, response_mesh, levels=10, colors='white',
                             linewidths=0.5, alpha=0.7)
ax2.clabel(contour_lines, inline=True, fontsize=8, fmt='%.1f')
ax2.scatter(X_design[:, 0], X_design[:, 1], c='red', s=100,
            marker='o', edgecolors='white', linewidth=2, label='Experimental points')
ax2.scatter(optimal_temp, optimal_time, c='gold', s=300,
            marker='*', edgecolors='red', linewidth=2, label='Optimal point')
ax2.set_xlabel('Temperature (°C)', fontsize=12)
ax2.set_ylabel('Time (min)', fontsize=12)
ax2.set_title(f'Contour Plot (pH={ph_fixed} fixed)', fontsize=13, fontweight='bold')
ax2.legend(fontsize=10)
plt.colorbar(contour, ax=ax2, label='Quality Score')

# 3. Predicted vs observed values
ax3 = fig.add_subplot(2, 2, 3)
ax3.scatter(y_response, y_pred, c='#11998e', s=100, alpha=0.7, edgecolors='white', linewidth=1.5)
ax3.plot([y_response.min(), y_response.max()], [y_response.min(), y_response.max()],
         'r--', linewidth=2, label='Ideal line')
ax3.set_xlabel('Observed value', fontsize=12)
ax3.set_ylabel('Predicted value', fontsize=12)
ax3.set_title(f'Prediction Accuracy (R²={r2_score:.4f})', fontsize=13, fontweight='bold')
ax3.legend(fontsize=10)
ax3.grid(True, alpha=0.3)

# 4. Main effect plot (temperature effect)
ax4 = fig.add_subplot(2, 2, 4)
temp_test = np.linspace(80, 100, 100)
time_fixed = 20
ph_test_fixed = 4.0

response_temp_effect = []
for t in temp_test:
    X_test = np.array([[t, time_fixed, ph_test_fixed]])
    X_test_poly = poly.transform(X_test)
    response_temp_effect.append(model.predict(X_test_poly)[0])

ax4.plot(temp_test, response_temp_effect, linewidth=2.5, color='#11998e', label='Predicted response')
ax4.scatter(X_design[:, 0], y_response, c='red', s=80, alpha=0.6,
            edgecolors='white', linewidth=1.5, label='Experimental points')
ax4.axvline(x=optimal_temp, color='green', linestyle='--', linewidth=2, label='Optimal temperature')
ax4.set_xlabel('Temperature (°C)', fontsize=12)
ax4.set_ylabel('Quality score', fontsize=12)
ax4.set_title(f'Main Effect Plot (time={time_fixed}min, pH={ph_test_fixed} fixed)',
              fontsize=13, fontweight='bold')
ax4.legend(fontsize=10)
ax4.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('response_surface_method.png', dpi=300, bbox_inches='tight')
plt.show()

⚠️ Precautions for Optimization Implementation

Summary

In this chapter, we learned optimization methods for food processes:

In the next chapter, we will learn practical methods for predictive maintenance and troubleshooting.

References

  1. Montgomery, D. C. (2019). Design and Analysis of Experiments (9th ed.). Wiley.
  2. Box, G. E. P., Hunter, J. S., & Hunter, W. G. (2005). Statistics for Experimenters: Design, Innovation, and Discovery (2nd ed.). Wiley.
  3. Seborg, D. E., Edgar, T. F., Mellichamp, D. A., & Doyle III, F. J. (2016). Process Dynamics and Control (4th ed.). Wiley.
  4. 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