Learning Objectives
- Execute a complete NIMO optimization workflow from start to finish
- Create and prepare candidate files properly
- Run multiple optimization cycles with different algorithms
- Visualize and analyze optimization results
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$$
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.
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:
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:
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")
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:
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:
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
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:
- Use a 20x20 grid instead of 16x16
- Run for 15 cycles instead of 10
- 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:
- physbo_score="EI" (Expected Improvement)
- physbo_score="PI" (Probability of Improvement)
- physbo_score="TS" (Thompson Sampling)
Plot the optimization curves together. Which acquisition function performs best for this problem?
Summary
- NIMO optimization follows a clear workflow: Create candidates → Select → Execute → Update → Repeat
- Start with Random Exploration (RE) for initial data, then switch to PHYSBO
- The candidates.csv file tracks both tested and untested compositions
- Visualization helps understand the optimization progress and sampling patterns
- NIMO provides built-in tools for history plots and distribution analysis