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