Chapter 4: Hands-on Tutorial

Complete Materials Optimization Workflow

📖 Reading Time: 25-30 minutes 📊 Difficulty: Intermediate 💻 Code Examples: 8 📝 Exercises: 2

Video Lecture

This video covers the same content as the text below. Choose your preferred learning format.

Learning Objectives

4.1 Problem Setup: Optimizing a Ternary Alloy

In this tutorial, we'll optimize a simulated ternary alloy system. Our goal is to find the composition (x1, x2, x3) that maximizes a target property.

About This Tutorial

Since we don't have access to a real robot system, we'll simulate the experiments using a known mathematical function. In real applications, the robot would perform actual experiments and return measured values.

The Objective Function

We'll use the Branin function as our simulated material property:

$$f(x_1, x_2) = \left(x_2 - \frac{5.1}{4\pi^2}x_1^2 + \frac{5}{\pi}x_1 - 6\right)^2 + 10\left(1 - \frac{1}{8\pi}\right)\cos(x_1) + 10$$

Code Example 1: Define the Objective Function
import numpy as np

def branin(x1, x2):
    """
    Branin function - simulates a material property.
    We negate it because NIMO maximizes by default.
    """
    a = 1
    b = 5.1 / (4 * np.pi**2)
    c = 5 / np.pi
    r = 6
    s = 10
    t = 1 / (8 * np.pi)

    result = a * (x2 - b*x1**2 + c*x1 - r)**2 + s*(1-t)*np.cos(x1) + s
    return -result  # Negate for maximization

# Test the function
print(f"f(0, 0) = {branin(0, 0):.4f}")
print(f"f(5, 5) = {branin(5, 5):.4f}")

4.2 Creating the Candidate File

First, we need to create a CSV file containing all possible candidates to explore.

Code Example 2: Generate Candidate Grid
import pandas as pd
import numpy as np

# Create a grid of candidates
# x1: 0 to 15 (16 points)
# x2: 0 to 15 (16 points)
x1_values = np.linspace(0, 15, 16)
x2_values = np.linspace(0, 15, 16)

# Create all combinations
candidates = []
for x1 in x1_values:
    for x2 in x2_values:
        candidates.append({
            'x1': x1,
            'x2': x2,
            'objective': np.nan  # Not yet tested
        })

# Convert to DataFrame and save
df = pd.DataFrame(candidates)
df.to_csv('candidates.csv', index=False)

print(f"Created {len(df)} candidates")
print(df.head(10))

Output:

Created 256 candidates
    x1   x2  objective
0  0.0  0.0        NaN
1  0.0  1.0        NaN
2  0.0  2.0        NaN
3  0.0  3.0        NaN
4  0.0  4.0        NaN
5  0.0  5.0        NaN
6  0.0  6.0        NaN
7  0.0  7.0        NaN
8  0.0  8.0        NaN
9  0.0  9.0        NaN

4.3 Running the Optimization Loop

Now let's run a complete optimization using NIMO:

Code Example 3: Complete Optimization Loop
import nimo
import pandas as pd
import numpy as np

def branin(x1, x2):
    """Simulated material property (negated for maximization)"""
    b = 5.1 / (4 * np.pi**2)
    c = 5 / np.pi
    result = (x2 - b*x1**2 + c*x1 - 6)**2 + 10*(1-1/(8*np.pi))*np.cos(x1) + 10
    return -result

def simulate_experiments(proposals_file, output_file):
    """Simulate robot experiments by calculating objective values"""
    df = pd.read_csv(proposals_file)
    df['objective'] = df.apply(lambda row: branin(row['x1'], row['x2']), axis=1)
    df.to_csv(output_file, index=False)
    return df

def update_candidates(candidates_file, results_file):
    """Update candidates with new experimental results"""
    candidates = pd.read_csv(candidates_file)
    results = pd.read_csv(results_file)

    for _, result in results.iterrows():
        mask = (candidates['x1'] == result['x1']) & (candidates['x2'] == result['x2'])
        candidates.loc[mask, 'objective'] = result['objective']

    candidates.to_csv(candidates_file, index=False)
    return candidates

# Configuration
NUM_CYCLES = 10
PROPOSALS_PER_CYCLE = 3

# Track optimization history
history = []

# Main optimization loop
for cycle in range(NUM_CYCLES):
    print(f"\n{'='*50}")
    print(f"Cycle {cycle + 1}/{NUM_CYCLES}")
    print('='*50)

    # Step 1: Select candidates
    if cycle == 0:
        method = "RE"  # Random for first cycle
        print("Using Random Exploration for initial data collection")
    else:
        method = "PHYSBO"  # Bayesian Optimization for subsequent cycles
        print("Using Bayesian Optimization")

    nimo.selection(
        method=method,
        input_file="candidates.csv",
        output_file="proposals.csv",
        num_objectives=1,
        num_proposals=PROPOSALS_PER_CYCLE,
        re_seed=42 + cycle if method == "RE" else None,
        physbo_seed=42 if method == "PHYSBO" else None
    )

    # Step 2: Show selected candidates
    proposals = pd.read_csv("proposals.csv")
    print(f"\nSelected candidates:")
    print(proposals[['x1', 'x2']])

    # Step 3: Simulate experiments (in real use, robot does this)
    results = simulate_experiments("proposals.csv", "results.csv")
    print(f"\nExperiment results:")
    print(results)

    # Step 4: Update candidates file
    candidates = update_candidates("candidates.csv", "results.csv")

    # Track best value found
    tested = candidates[candidates['objective'].notna()]
    best_value = tested['objective'].max()
    best_idx = tested['objective'].idxmax()
    best_x1 = tested.loc[best_idx, 'x1']
    best_x2 = tested.loc[best_idx, 'x2']

    history.append({
        'cycle': cycle + 1,
        'best_value': best_value,
        'best_x1': best_x1,
        'best_x2': best_x2,
        'num_tested': len(tested)
    })

    print(f"\nBest so far: f({best_x1:.2f}, {best_x2:.2f}) = {best_value:.4f}")

# Print final summary
print("\n" + "="*50)
print("OPTIMIZATION COMPLETE")
print("="*50)
print(f"Total experiments: {history[-1]['num_tested']}")
print(f"Best value found: {history[-1]['best_value']:.4f}")
print(f"Best composition: x1={history[-1]['best_x1']:.2f}, x2={history[-1]['best_x2']:.2f}")

4.4 Visualizing Results

Let's visualize the optimization progress:

Code Example 4: Plot Optimization History
import matplotlib.pyplot as plt

# Convert history to DataFrame
history_df = pd.DataFrame(history)

# Plot best value over cycles
plt.figure(figsize=(10, 6))
plt.plot(history_df['cycle'], history_df['best_value'], 'b-o', linewidth=2, markersize=8)
plt.xlabel('Cycle', fontsize=12)
plt.ylabel('Best Objective Value', fontsize=12)
plt.title('Optimization Progress', fontsize=14)
plt.grid(True, alpha=0.3)
plt.savefig('optimization_history.png', dpi=150, bbox_inches='tight')
plt.show()

print("Saved: optimization_history.png")
Code Example 5: Visualize Sampled Points
import matplotlib.pyplot as plt
import numpy as np

# Load final candidates
candidates = pd.read_csv('candidates.csv')
tested = candidates[candidates['objective'].notna()]
untested = candidates[candidates['objective'].isna()]

# Create contour plot of true function
x1_grid = np.linspace(0, 15, 100)
x2_grid = np.linspace(0, 15, 100)
X1, X2 = np.meshgrid(x1_grid, x2_grid)
Z = np.vectorize(branin)(X1, X2)

plt.figure(figsize=(12, 5))

# Left: Contour with sampled points
plt.subplot(1, 2, 1)
plt.contourf(X1, X2, Z, levels=20, cmap='viridis')
plt.colorbar(label='Objective Value')
plt.scatter(tested['x1'], tested['x2'], c='red', s=100, edgecolors='white', label='Tested')
plt.xlabel('x1')
plt.ylabel('x2')
plt.title('Tested Points on Objective Landscape')
plt.legend()

# Right: Tested values distribution
plt.subplot(1, 2, 2)
plt.scatter(tested['x1'], tested['x2'], c=tested['objective'], s=100, cmap='viridis', edgecolors='black')
plt.colorbar(label='Measured Value')
plt.xlabel('x1')
plt.ylabel('x2')
plt.title('Measured Values at Tested Points')

plt.tight_layout()
plt.savefig('sampling_visualization.png', dpi=150, bbox_inches='tight')
plt.show()

4.5 Comparing Algorithms

Let's compare the performance of different algorithms:

Code Example 6: Algorithm Comparison
import nimo
import pandas as pd
import numpy as np

def run_optimization(method, num_cycles=10, proposals_per_cycle=3, seed=42):
    """Run optimization with specified method and return history"""
    # Reset candidates
    x1_values = np.linspace(0, 15, 16)
    x2_values = np.linspace(0, 15, 16)
    candidates = []
    for x1 in x1_values:
        for x2 in x2_values:
            candidates.append({'x1': x1, 'x2': x2, 'objective': np.nan})
    pd.DataFrame(candidates).to_csv('candidates.csv', index=False)

    history = []
    for cycle in range(num_cycles):
        # First cycle always uses RE
        current_method = "RE" if cycle == 0 else method

        nimo.selection(
            method=current_method,
            input_file="candidates.csv",
            output_file="proposals.csv",
            num_objectives=1,
            num_proposals=proposals_per_cycle,
            re_seed=seed + cycle,
            physbo_seed=seed
        )

        # Simulate and update
        simulate_experiments("proposals.csv", "results.csv")
        candidates_df = update_candidates("candidates.csv", "results.csv")

        tested = candidates_df[candidates_df['objective'].notna()]
        best_value = tested['objective'].max()
        history.append({'cycle': cycle + 1, 'best_value': best_value})

    return pd.DataFrame(history)

# Compare different methods
methods = ['PHYSBO', 'BLOX', 'RE']
results = {}

for method in methods:
    print(f"Running {method}...")
    results[method] = run_optimization(method)

# Plot comparison
plt.figure(figsize=(10, 6))
for method, history in results.items():
    plt.plot(history['cycle'], history['best_value'], '-o', label=method, linewidth=2)

plt.xlabel('Cycle', fontsize=12)
plt.ylabel('Best Objective Value', fontsize=12)
plt.title('Algorithm Comparison', fontsize=14)
plt.legend(fontsize=10)
plt.grid(True, alpha=0.3)
plt.savefig('algorithm_comparison.png', dpi=150, bbox_inches='tight')
plt.show()

4.6 Using NIMO's Built-in Visualization

NIMO provides built-in visualization functions:

Code Example 7: NIMO Visualization Tools
import nimo
from nimo.visualization import plot_history, plot_distribution

# Plot optimization history
fig1 = plot_history(
    input_file="candidates.csv",
    num_objectives=1
)
fig1.savefig('nimo_history.png', dpi=150)

# Plot distribution of tested points
fig2 = plot_distribution(
    input_file="candidates.csv",
    num_objectives=1
)
fig2.savefig('nimo_distribution.png', dpi=150)

print("Saved: nimo_history.png, nimo_distribution.png")

4.7 Saving and Loading Optimization State

Code Example 8: Save and Resume Optimization
import shutil
import os

def save_checkpoint(checkpoint_name):
    """Save current optimization state"""
    checkpoint_dir = f"checkpoints/{checkpoint_name}"
    os.makedirs(checkpoint_dir, exist_ok=True)
    shutil.copy("candidates.csv", f"{checkpoint_dir}/candidates.csv")
    print(f"Checkpoint saved to {checkpoint_dir}")

def load_checkpoint(checkpoint_name):
    """Load optimization state from checkpoint"""
    checkpoint_dir = f"checkpoints/{checkpoint_name}"
    shutil.copy(f"{checkpoint_dir}/candidates.csv", "candidates.csv")
    print(f"Checkpoint loaded from {checkpoint_dir}")

# Example usage
# After 5 cycles:
save_checkpoint("cycle_5")

# To resume later:
# load_checkpoint("cycle_5")
# Continue optimization from cycle 6...

Exercises

Exercise 1: Run Your Own Optimization

Modify the optimization code to:

  1. Use a 20x20 grid instead of 16x16
  2. Run for 15 cycles instead of 10
  3. Select 5 proposals per cycle instead of 3

Compare the results: Does the higher resolution grid find better solutions?

Exercise 2: Different Acquisition Functions

Run the optimization three times using different PHYSBO acquisition functions:

  1. physbo_score="EI" (Expected Improvement)
  2. physbo_score="PI" (Probability of Improvement)
  3. physbo_score="TS" (Thompson Sampling)

Plot the optimization curves together. Which acquisition function performs best for this problem?

Summary

🎬 この章を動画で見る