Files
Atomizer/archive/scripts/run_calibration_loop.py
Anto01 eabcc4c3ca refactor: Major reorganization of optimization_engine module structure
BREAKING CHANGE: Module paths have been reorganized for better maintainability.
Backwards compatibility aliases with deprecation warnings are provided.

New Structure:
- core/           - Optimization runners (runner, intelligent_optimizer, etc.)
- processors/     - Data processing
  - surrogates/   - Neural network surrogates
- nx/             - NX/Nastran integration (solver, updater, session_manager)
- study/          - Study management (creator, wizard, state, reset)
- reporting/      - Reports and analysis (visualizer, report_generator)
- config/         - Configuration management (manager, builder)
- utils/          - Utilities (logger, auto_doc, etc.)
- future/         - Research/experimental code

Migration:
- ~200 import changes across 125 files
- All __init__.py files use lazy loading to avoid circular imports
- Backwards compatibility layer supports old import paths with warnings
- All existing functionality preserved

To migrate existing code:
  OLD: from optimization_engine.nx_solver import NXSolver
  NEW: from optimization_engine.nx.solver import NXSolver

  OLD: from optimization_engine.runner import OptimizationRunner
  NEW: from optimization_engine.core.runner import OptimizationRunner

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 12:30:59 -05:00

332 lines
12 KiB
Python

"""
Active Learning Calibration Loop
This script implements the iterative calibration workflow:
1. Train initial NN on existing FEA data
2. Run NN optimization to find promising designs
3. Select high-uncertainty designs for FEA validation
4. Run FEA on selected designs (simulated here, needs real FEA integration)
5. Retrain NN with new data
6. Repeat until confidence threshold reached
Usage:
python run_calibration_loop.py --study uav_arm_optimization --iterations 5
Note: For actual FEA integration, replace the simulate_fea() function with real NX calls.
"""
import sys
from pathlib import Path
import argparse
import json
import numpy as np
import optuna
from optuna.samplers import NSGAIISampler
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
# Add project paths
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
from optimization_engine.processors.surrogates.active_learning_surrogate import (
ActiveLearningSurrogate,
extract_training_data_from_study
)
def simulate_fea(design: dict, surrogate: ActiveLearningSurrogate) -> dict:
"""
PLACEHOLDER: Simulate FEA results.
In production, this would:
1. Update NX model parameters
2. Run FEA solve
3. Extract results (mass, frequency, displacement, stress)
For now, we use the ensemble mean + noise to simulate "ground truth"
with systematic differences to test calibration.
"""
# Get NN prediction
pred = surrogate.predict(design)
# Add systematic bias + noise to simulate FEA
# This simulates the case where NN is systematically off
fea_mass = pred['mass'] * 0.95 + np.random.normal(0, 50) # NN overestimates mass by ~5%
fea_freq = pred['frequency'] * 0.6 + np.random.normal(0, 2) # NN overestimates freq significantly
return {
'mass': max(fea_mass, 1000), # Ensure positive
'frequency': max(fea_freq, 1),
'max_displacement': pred.get('max_displacement', 0),
'max_stress': pred.get('max_stress', 0)
}
def run_nn_optimization(
surrogate: ActiveLearningSurrogate,
bounds: dict,
n_trials: int = 500
) -> list:
"""Run NN-only optimization to generate candidate designs."""
study = optuna.create_study(
directions=["minimize", "minimize"], # mass, -frequency
sampler=NSGAIISampler()
)
def objective(trial):
params = {}
for name, (low, high) in bounds.items():
if name == 'hole_count':
params[name] = trial.suggest_int(name, int(low), int(high))
else:
params[name] = trial.suggest_float(name, low, high)
pred = surrogate.predict(params)
# Store uncertainty in user_attrs
trial.set_user_attr('uncertainty', pred['total_uncertainty'])
trial.set_user_attr('params', params)
return pred['mass'], -pred['frequency']
study.optimize(objective, n_trials=n_trials, show_progress_bar=False)
# Extract Pareto front designs with their uncertainty
pareto_designs = []
for trial in study.best_trials:
pareto_designs.append({
'params': trial.user_attrs['params'],
'uncertainty': trial.user_attrs['uncertainty'],
'mass': trial.values[0],
'frequency': -trial.values[1]
})
return pareto_designs
def plot_calibration_progress(history: list, save_path: str):
"""Plot calibration progress over iterations."""
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
iterations = [h['iteration'] for h in history]
# 1. Confidence score
ax = axes[0, 0]
confidence = [h['confidence_score'] for h in history]
ax.plot(iterations, confidence, 'b-o', linewidth=2)
ax.axhline(y=0.7, color='g', linestyle='--', label='Target (0.7)')
ax.set_xlabel('Iteration')
ax.set_ylabel('Confidence Score')
ax.set_title('Model Confidence Over Iterations')
ax.legend()
ax.grid(True, alpha=0.3)
# 2. MAPE
ax = axes[0, 1]
mass_mape = [h['mass_mape'] for h in history]
freq_mape = [h['freq_mape'] for h in history]
ax.plot(iterations, mass_mape, 'b-o', label='Mass MAPE')
ax.plot(iterations, freq_mape, 'r-s', label='Frequency MAPE')
ax.axhline(y=10, color='g', linestyle='--', label='Target (10%)')
ax.set_xlabel('Iteration')
ax.set_ylabel('MAPE (%)')
ax.set_title('Prediction Error Over Iterations')
ax.legend()
ax.grid(True, alpha=0.3)
# 3. Training samples
ax = axes[1, 0]
n_samples = [h['n_training_samples'] for h in history]
ax.plot(iterations, n_samples, 'g-o', linewidth=2)
ax.set_xlabel('Iteration')
ax.set_ylabel('Training Samples')
ax.set_title('Training Data Growth')
ax.grid(True, alpha=0.3)
# 4. Average uncertainty of selected designs
ax = axes[1, 1]
avg_uncertainty = [h['avg_selected_uncertainty'] for h in history]
ax.plot(iterations, avg_uncertainty, 'm-o', linewidth=2)
ax.set_xlabel('Iteration')
ax.set_ylabel('Average Uncertainty')
ax.set_title('Uncertainty of Selected Designs')
ax.grid(True, alpha=0.3)
plt.suptitle('Active Learning Calibration Progress', fontsize=14)
plt.tight_layout()
plt.savefig(save_path, dpi=150)
plt.close()
print(f"Saved calibration progress plot: {save_path}")
def main():
parser = argparse.ArgumentParser(description='Run Active Learning Calibration Loop')
parser.add_argument('--study', default='uav_arm_optimization', help='Study name')
parser.add_argument('--iterations', type=int, default=5, help='Number of calibration iterations')
parser.add_argument('--fea-per-iter', type=int, default=10, help='FEA evaluations per iteration')
parser.add_argument('--confidence-target', type=float, default=0.7, help='Target confidence')
parser.add_argument('--simulate', action='store_true', default=True, help='Simulate FEA (for testing)')
args = parser.parse_args()
print("="*70)
print("Active Learning Calibration Loop")
print("="*70)
print(f"Study: {args.study}")
print(f"Max iterations: {args.iterations}")
print(f"FEA per iteration: {args.fea_per_iter}")
print(f"Confidence target: {args.confidence_target}")
# Find database
db_path = project_root / f"studies/{args.study}/2_results/study.db"
study_name = args.study
if not db_path.exists():
db_path = project_root / "studies/uav_arm_atomizerfield_test/2_results/study.db"
study_name = "uav_arm_atomizerfield_test"
if not db_path.exists():
print(f"ERROR: Database not found: {db_path}")
return
# Design bounds (from UAV arm study)
bounds = {
'beam_half_core_thickness': (1.0, 10.0),
'beam_face_thickness': (0.5, 3.0),
'holes_diameter': (0.5, 50.0),
'hole_count': (6, 14)
}
# Load initial training data
print(f"\n[1] Loading initial training data from {db_path}")
design_params, objectives, design_var_names = extract_training_data_from_study(
str(db_path), study_name
)
print(f" Initial samples: {len(design_params)}")
# Calibration history
calibration_history = []
# Track accumulated training data
all_design_params = design_params.copy()
all_objectives = objectives.copy()
for iteration in range(args.iterations):
print(f"\n{'='*70}")
print(f"ITERATION {iteration + 1}/{args.iterations}")
print("="*70)
# Train ensemble surrogate
print(f"\n[2.{iteration+1}] Training ensemble surrogate...")
surrogate = ActiveLearningSurrogate(n_ensemble=5)
surrogate.train(
all_design_params, all_objectives, design_var_names,
epochs=200
)
# Run NN optimization to find candidate designs
print(f"\n[3.{iteration+1}] Running NN optimization (500 trials)...")
pareto_designs = run_nn_optimization(surrogate, bounds, n_trials=500)
print(f" Found {len(pareto_designs)} Pareto designs")
# Select designs for FEA validation (highest uncertainty)
print(f"\n[4.{iteration+1}] Selecting designs for FEA validation...")
candidate_params = [d['params'] for d in pareto_designs]
selected = surrogate.select_designs_for_validation(
candidate_params,
n_select=args.fea_per_iter,
strategy='diverse' # Mix of high uncertainty + diversity
)
print(f" Selected {len(selected)} designs:")
avg_uncertainty = np.mean([s[2] for s in selected])
for i, (idx, params, uncertainty) in enumerate(selected[:5]):
print(f" {i+1}. Uncertainty={uncertainty:.3f}, params={params}")
# Run FEA (simulated)
print(f"\n[5.{iteration+1}] Running FEA validation...")
new_params = []
new_objectives = []
for idx, params, uncertainty in selected:
if args.simulate:
fea_result = simulate_fea(params, surrogate)
else:
# TODO: Call actual FEA here
# fea_result = run_actual_fea(params)
raise NotImplementedError("Real FEA not implemented")
# Record for retraining
param_array = [params.get(name, 0.0) for name in design_var_names]
new_params.append(param_array)
new_objectives.append([
fea_result['mass'],
fea_result['frequency'],
fea_result.get('max_displacement', 0),
fea_result.get('max_stress', 0)
])
# Update validation tracking
surrogate.update_with_validation([params], [fea_result])
# Add new data to training set
all_design_params = np.vstack([all_design_params, np.array(new_params, dtype=np.float32)])
all_objectives = np.vstack([all_objectives, np.array(new_objectives, dtype=np.float32)])
# Get confidence report
report = surrogate.get_confidence_report()
print(f"\n[6.{iteration+1}] Confidence Report:")
print(f" Confidence Score: {report['confidence_score']:.3f}")
print(f" Mass MAPE: {report['mass_mape']:.1f}%")
print(f" Freq MAPE: {report['freq_mape']:.1f}%")
print(f" Status: {report['status']}")
print(f" Recommendation: {report['recommendation']}")
# Record history
calibration_history.append({
'iteration': iteration + 1,
'n_training_samples': len(all_design_params),
'confidence_score': report['confidence_score'],
'mass_mape': report['mass_mape'],
'freq_mape': report['freq_mape'],
'avg_selected_uncertainty': avg_uncertainty,
'status': report['status']
})
# Check if we've reached target confidence
if report['confidence_score'] >= args.confidence_target:
print(f"\n*** TARGET CONFIDENCE REACHED ({report['confidence_score']:.3f} >= {args.confidence_target}) ***")
break
# Save final model
print("\n" + "="*70)
print("CALIBRATION COMPLETE")
print("="*70)
model_path = project_root / "calibrated_surrogate.pt"
surrogate.save(str(model_path))
print(f"Saved calibrated model to: {model_path}")
# Save calibration history
history_path = project_root / "calibration_history.json"
with open(history_path, 'w') as f:
json.dump(calibration_history, f, indent=2)
print(f"Saved calibration history to: {history_path}")
# Plot progress
plot_calibration_progress(calibration_history, str(project_root / "calibration_progress.png"))
# Final summary
final_report = surrogate.get_confidence_report()
print(f"\nFinal Results:")
print(f" Training samples: {len(all_design_params)}")
print(f" Confidence score: {final_report['confidence_score']:.3f}")
print(f" Mass MAPE: {final_report['mass_mape']:.1f}%")
print(f" Freq MAPE: {final_report['freq_mape']:.1f}%")
print(f" Ready for optimization: {surrogate.is_ready_for_optimization()}")
if __name__ == '__main__':
main()