From d7986922d55c4594396df40ee4221e86018100cd Mon Sep 17 00:00:00 2001 From: Anto01 Date: Thu, 29 Jan 2026 10:22:33 -0500 Subject: [PATCH] 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 --- tools/zernike_html_generator.py | 75 +++++++++++++++---------- tools/zernike_html_generator_annular.py | 39 +++++++------ 2 files changed, 66 insertions(+), 48 deletions(-) diff --git a/tools/zernike_html_generator.py b/tools/zernike_html_generator.py index fe606c47..b9e3e1b9 100644 --- a/tools/zernike_html_generator.py +++ b/tools/zernike_html_generator.py @@ -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(): diff --git a/tools/zernike_html_generator_annular.py b/tools/zernike_html_generator_annular.py index c7c8f5aa..afd11ee0 100644 --- a/tools/zernike_html_generator_annular.py +++ b/tools/zernike_html_generator_annular.py @@ -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