🌐 EN | 🇯🇵 JP

Chapter 4: Hands-on Tutorial

Complete Materials Optimization Workflow

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

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

Disclaimer

This content is provided for educational purposes. NIMO is developed and maintained by NIMS.