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,23 +655,32 @@ 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")
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'} subcase_map = {'90': '1', '20': '2', '40': '3', '60': '4'}
elif '90' in displacements and '20' in displacements: print(f"[INFO] Using numeric subcases: 1=90°, 2=20°, 3=40°, 4=60°")
subcase_map = {'90': '90', '20': '20', '40': '40', '60': '60'}
else: # Check if we found all required angles
available = sorted(displacements.keys(), key=lambda x: int(x) if x.isdigit() else 0) if len(subcase_map) < 4:
if len(available) >= 4: missing = [a for a in required_angles if a not in subcase_map]
subcase_map = {'90': available[0], '20': available[1], '40': available[2], '60': available[3]} print(f"[ERROR] Missing required subcases: {missing}")
print(f"[WARN] Using mapped subcases: {subcase_map}") print(f"[ERROR] Available subcases: {list(displacements.keys())}")
else: print(f"[ERROR] Required subcases must be named: {required_angles} (or numeric 1,2,3,4)")
print(f"[ERROR] Need 4 subcases, found: {available}")
return 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
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
@@ -898,27 +907,31 @@ 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:
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'} subcase_map = {'90': '1', '20': '2', '40': '3', '60': '4'}
elif '90' in displacements and '20' in displacements: print(f"[INFO] Using numeric subcases: 1=90°, 2=20°, 3=40°, 4=60°")
# Direct angle labels
subcase_map = {'90': '90', '20': '20', '40': '40', '60': '60'} # Check if we found all required angles
else: if len(subcase_map) < 4:
# Try to use whatever is available missing = [a for a in required_angles if a not in subcase_map]
available = sorted(displacements.keys(), key=lambda x: int(x) if x.isdigit() else 0) print(f"[ERROR] Missing required subcases: {missing}")
if len(available) >= 4: print(f"[ERROR] Available subcases: {list(displacements.keys())}")
subcase_map = {'90': available[0], '20': available[1], '40': available[2], '60': available[3]} print(f"[ERROR] Required subcases must be named: {required_angles} (or numeric 1,2,3,4)")
print(f"[WARN] Using mapped subcases: {subcase_map}")
else:
print(f"[ERROR] Need 4 subcases, found: {available}")
return 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():
if label not in displacements: if label not in displacements:

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:
# 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'} subcase_map = {'90': '1', '20': '2', '40': '3', '60': '4'}
elif '90' in displacements and '20' in displacements: print(f"[INFO] Using numeric subcases: 1=90°, 2=20°, 3=40°, 4=60°")
subcase_map = {'90': '90', '20': '20', '40': '40', '60': '60'}
else: # Check if we found all required angles
available = sorted(displacements.keys(), key=lambda x: int(x) if x.isdigit() else 0) if len(subcase_map) < 4:
if len(available) >= 4: missing = [a for a in required_angles if a not in subcase_map]
subcase_map = {'90': available[0], '20': available[1], '40': available[2], '60': available[3]} print(f"[ERROR] Missing required subcases: {missing}")
print(f"[WARN] Using mapped subcases: {subcase_map}") print(f"[ERROR] Available subcases: {list(displacements.keys())}")
else: print(f"[ERROR] Required subcases must be named: {required_angles} (or numeric 1,2,3,4)")
print(f"[ERROR] Need 4 subcases, found: {available}")
return return
for angle, label in subcase_map.items(): print(f"[INFO] Using subcase mapping: {subcase_map}")
if label not in displacements:
print(f"[ERROR] Subcase '{label}' (angle {angle}) not found")
return
output_dir = op2_path.parent output_dir = op2_path.parent
base = op2_path.stem base = op2_path.stem