feat: Add Zernike GNN surrogate module and M1 mirror V12/V13 studies
This commit introduces the GNN-based surrogate for Zernike mirror optimization and the M1 mirror study progression from V12 (GNN validation) to V13 (pure NSGA-II). ## GNN Surrogate Module (optimization_engine/gnn/) New module for Graph Neural Network surrogate prediction of mirror deformations: - `polar_graph.py`: PolarMirrorGraph - fixed 3000-node polar grid structure - `zernike_gnn.py`: ZernikeGNN with design-conditioned message passing - `differentiable_zernike.py`: GPU-accelerated Zernike fitting and objectives - `train_zernike_gnn.py`: ZernikeGNNTrainer with multi-task loss - `gnn_optimizer.py`: ZernikeGNNOptimizer for turbo mode (~900k trials/hour) - `extract_displacement_field.py`: OP2 to HDF5 field extraction - `backfill_field_data.py`: Extract fields from existing FEA trials Key innovation: Design-conditioned convolutions that modulate message passing based on structural design parameters, enabling accurate field prediction. ## M1 Mirror Studies ### V12: GNN Field Prediction + FEA Validation - Zernike GNN trained on V10/V11 FEA data (238 samples) - Turbo mode: 5000 GNN predictions → top candidates → FEA validation - Calibration workflow for GNN-to-FEA error correction - Scripts: run_gnn_turbo.py, validate_gnn_best.py, compute_full_calibration.py ### V13: Pure NSGA-II FEA (Ground Truth) - Seeds 217 FEA trials from V11+V12 - Pure multi-objective NSGA-II without any surrogate - Establishes ground-truth Pareto front for GNN accuracy evaluation - Narrowed blank_backface_angle range to [4.0, 5.0] ## Documentation Updates - SYS_14: Added Zernike GNN section with architecture diagrams - CLAUDE.md: Added GNN module reference and quick start - V13 README: Study documentation with seeding strategy 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
35
optimization_engine/gnn/test_new_extraction.py
Normal file
35
optimization_engine/gnn/test_new_extraction.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Test the fixed extraction function directly on OP2."""
|
||||
import sys
|
||||
sys.path.insert(0, "C:/Users/Antoine/Atomizer")
|
||||
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from optimization_engine.gnn.extract_displacement_field import extract_displacement_field
|
||||
|
||||
# Test direct extraction from OP2
|
||||
op2_path = Path("C:/Users/Antoine/Atomizer/studies/m1_mirror_adaptive_V11/2_iterations/iter91/assy_m1_assyfem1_sim1-solution_1.op2")
|
||||
|
||||
print(f"Testing extraction from: {op2_path.name}")
|
||||
print(f"Exists: {op2_path.exists()}")
|
||||
|
||||
if op2_path.exists():
|
||||
field_data = extract_displacement_field(op2_path, r_inner=100.0, r_outer=650.0)
|
||||
|
||||
print(f"\n=== EXTRACTION RESULT ===")
|
||||
print(f"Total surface nodes: {len(field_data['node_ids'])}")
|
||||
|
||||
coords = field_data['node_coords']
|
||||
r = np.sqrt(coords[:, 0]**2 + coords[:, 1]**2)
|
||||
print(f"Radial range: [{r.min():.1f}, {r.max():.1f}] mm")
|
||||
print(f"Z range: [{coords[:, 2].min():.1f}, {coords[:, 2].max():.1f}] mm")
|
||||
|
||||
print(f"\nSubcases: {list(field_data['z_displacement'].keys())}")
|
||||
for sc, disp in field_data['z_displacement'].items():
|
||||
nan_count = np.sum(np.isnan(disp))
|
||||
if nan_count == 0:
|
||||
print(f" Subcase {sc}: Z-disp range [{disp.min():.6f}, {disp.max():.6f}] mm")
|
||||
else:
|
||||
valid = disp[~np.isnan(disp)]
|
||||
print(f" Subcase {sc}: {nan_count}/{len(disp)} NaN values, valid range: [{valid.min():.6f}, {valid.max():.6f}]")
|
||||
else:
|
||||
print("OP2 file not found!")
|
||||
Reference in New Issue
Block a user