fix(tools): make Zernike OPD tools robust to extra subcases

- Replace brittle order-based subcase mapping with name-based search
- Tools now directly search for required angles (20, 40, 60, 90) by label
- Ignores extra subcases (e.g., 30, 50 degrees) without errors
- Falls back to numeric IDs (1,2,3,4) if angle labels not found
- Clear error messages show exactly which subcases are missing

This allows running WFE analysis on simulations with >4 subcases
without manual file/code modifications.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-29 10:22:33 -05:00
parent a7039c5875
commit d7986922d5
2 changed files with 66 additions and 48 deletions

View File

@@ -655,22 +655,31 @@ def main_opd(op2_path: Path):
print(f"\nAvailable subcases: {list(extractor.displacements.keys())}")
# Map subcases (try common patterns)
# Map subcases - find required angles by name (robust to extra subcases)
displacements = extractor.displacements
required_angles = ['90', '20', '40', '60']
subcase_map = {}
if '1' in displacements and '2' in displacements:
subcase_map = {'90': '1', '20': '2', '40': '3', '60': '4'}
elif '90' in displacements and '20' in displacements:
subcase_map = {'90': '90', '20': '20', '40': '40', '60': '60'}
else:
available = sorted(displacements.keys(), key=lambda x: int(x) if x.isdigit() else 0)
if len(available) >= 4:
subcase_map = {'90': available[0], '20': available[1], '40': available[2], '60': available[3]}
print(f"[WARN] Using mapped subcases: {subcase_map}")
else:
print(f"[ERROR] Need 4 subcases, found: {available}")
return
# First, try direct angle matching (e.g., "20", "40", "60", "90")
for angle in required_angles:
if angle in displacements:
subcase_map[angle] = angle
# If we didn't find all angles, try numeric IDs (e.g., "1"=90, "2"=20, "3"=40, "4"=60)
if len(subcase_map) < 4:
if all(str(i) in displacements for i in range(1, 5)):
subcase_map = {'90': '1', '20': '2', '40': '3', '60': '4'}
print(f"[INFO] Using numeric subcases: 1=90°, 2=20°, 3=40°, 4=60°")
# Check if we found all required angles
if len(subcase_map) < 4:
missing = [a for a in required_angles if a not in subcase_map]
print(f"[ERROR] Missing required subcases: {missing}")
print(f"[ERROR] Available subcases: {list(displacements.keys())}")
print(f"[ERROR] Required subcases must be named: {required_angles} (or numeric 1,2,3,4)")
return
print(f"[INFO] Using subcase mapping: {subcase_map}")
output_dir = op2_path.parent
base = op2_path.stem
@@ -898,26 +907,30 @@ def main(op2_path: Path):
displacements = read_displacements(op2_path)
print(f"Found subcases: {list(displacements.keys())}")
# Map subcases (try common patterns)
# Pattern 1: Direct labels (1, 2, 3, 4)
# Pattern 2: Angle labels (90, 20, 40, 60)
# Map subcases - find required angles by name (robust to extra subcases)
required_angles = ['90', '20', '40', '60']
subcase_map = {}
if '1' in displacements and '2' in displacements:
# Standard NX pattern: 1=90deg, 2=20deg, 3=40deg, 4=60deg
subcase_map = {'90': '1', '20': '2', '40': '3', '60': '4'}
elif '90' in displacements and '20' in displacements:
# Direct angle labels
subcase_map = {'90': '90', '20': '20', '40': '40', '60': '60'}
else:
# Try to use whatever is available
available = sorted(displacements.keys(), key=lambda x: int(x) if x.isdigit() else 0)
if len(available) >= 4:
subcase_map = {'90': available[0], '20': available[1], '40': available[2], '60': available[3]}
print(f"[WARN] Using mapped subcases: {subcase_map}")
else:
print(f"[ERROR] Need 4 subcases, found: {available}")
return
# First, try direct angle matching (e.g., "20", "40", "60", "90")
for angle in required_angles:
if angle in displacements:
subcase_map[angle] = angle
# If we didn't find all angles, try numeric IDs (e.g., "1"=90, "2"=20, "3"=40, "4"=60)
if len(subcase_map) < 4:
if all(str(i) in displacements for i in range(1, 5)):
subcase_map = {'90': '1', '20': '2', '40': '3', '60': '4'}
print(f"[INFO] Using numeric subcases: 1=90°, 2=20°, 3=40°, 4=60°")
# Check if we found all required angles
if len(subcase_map) < 4:
missing = [a for a in required_angles if a not in subcase_map]
print(f"[ERROR] Missing required subcases: {missing}")
print(f"[ERROR] Available subcases: {list(displacements.keys())}")
print(f"[ERROR] Required subcases must be named: {required_angles} (or numeric 1,2,3,4)")
return
print(f"[INFO] Using subcase mapping: {subcase_map}")
# Check all required subcases exist
for angle, label in subcase_map.items():

View File

@@ -747,25 +747,30 @@ def main_annular(op2_path: Path, inner_radius_mm: float):
displacements = read_displacements(op2_path)
print(f"Found subcases: {list(displacements.keys())}")
# Map subcases
# Map subcases - find required angles by name (robust to extra subcases)
required_angles = ['90', '20', '40', '60']
subcase_map = {}
if '1' in displacements and '2' in displacements:
subcase_map = {'90': '1', '20': '2', '40': '3', '60': '4'}
elif '90' in displacements and '20' in displacements:
subcase_map = {'90': '90', '20': '20', '40': '40', '60': '60'}
else:
available = sorted(displacements.keys(), key=lambda x: int(x) if x.isdigit() else 0)
if len(available) >= 4:
subcase_map = {'90': available[0], '20': available[1], '40': available[2], '60': available[3]}
print(f"[WARN] Using mapped subcases: {subcase_map}")
else:
print(f"[ERROR] Need 4 subcases, found: {available}")
return
for angle, label in subcase_map.items():
if label not in displacements:
print(f"[ERROR] Subcase '{label}' (angle {angle}) not found")
return
# First, try direct angle matching (e.g., "20", "40", "60", "90")
for angle in required_angles:
if angle in displacements:
subcase_map[angle] = angle
# If we didn't find all angles, try numeric IDs (e.g., "1"=90, "2"=20, "3"=40, "4"=60)
if len(subcase_map) < 4:
if all(str(i) in displacements for i in range(1, 5)):
subcase_map = {'90': '1', '20': '2', '40': '3', '60': '4'}
print(f"[INFO] Using numeric subcases: 1=90°, 2=20°, 3=40°, 4=60°")
# Check if we found all required angles
if len(subcase_map) < 4:
missing = [a for a in required_angles if a not in subcase_map]
print(f"[ERROR] Missing required subcases: {missing}")
print(f"[ERROR] Available subcases: {list(displacements.keys())}")
print(f"[ERROR] Required subcases must be named: {required_angles} (or numeric 1,2,3,4)")
return
print(f"[INFO] Using subcase mapping: {subcase_map}")
output_dir = op2_path.parent
base = op2_path.stem