From 0eb5028d8f4b40e235c96aced74cbb98517ade74 Mon Sep 17 00:00:00 2001 From: Anto01 Date: Thu, 15 Jan 2026 22:39:38 -0500 Subject: [PATCH] feat(canvas): Add NX model introspection API endpoints Phase 4 of Canvas Professional Upgrade: - Add /studies/{id}/nx/introspect endpoint for full model introspection - Add /studies/{id}/nx/expressions endpoint for expression list - Add caching to avoid re-running NX journal on each request - Add frontend API client methods: introspectNxModel, getNxExpressions - Use existing introspect_part.py extractor Co-Authored-By: Claude Opus 4.5 --- .../backend/api/routes/optimization.py | 164 ++++++++++++++++++ atomizer-dashboard/frontend/src/api/client.ts | 49 ++++++ 2 files changed, 213 insertions(+) diff --git a/atomizer-dashboard/backend/api/routes/optimization.py b/atomizer-dashboard/backend/api/routes/optimization.py index e5d13f53..647ed2bf 100644 --- a/atomizer-dashboard/backend/api/routes/optimization.py +++ b/atomizer-dashboard/backend/api/routes/optimization.py @@ -3907,3 +3907,167 @@ async def export_study_data(study_id: str, format: str): raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to export data: {str(e)}") + + +# ============================================================================ +# NX Model Introspection Endpoints +# ============================================================================ + +@router.get("/studies/{study_id}/nx/introspect") +async def introspect_nx_model(study_id: str, force: bool = False): + """ + Introspect NX model to extract expressions and properties. + + Uses the optimization_engine.extractors.introspect_part module to run + NX journal that extracts expressions, mass properties, materials, etc. + + Args: + study_id: Study identifier + force: Force re-introspection even if cached results exist + + Returns: + JSON with introspection results including expressions list + """ + try: + study_dir = resolve_study_path(study_id) + + if not study_dir.exists(): + raise HTTPException(status_code=404, detail=f"Study {study_id} not found") + + # Find model directory + model_dir = study_dir / "1_model" + if not model_dir.exists(): + model_dir = study_dir / "0_model" + if not model_dir.exists(): + raise HTTPException(status_code=404, detail=f"Model directory not found for {study_id}") + + # Find .prt file + prt_files = list(model_dir.glob("*.prt")) + # Exclude idealized parts + prt_files = [f for f in prt_files if '_i.prt' not in f.name.lower()] + + if not prt_files: + raise HTTPException(status_code=404, detail=f"No .prt files found in {model_dir}") + + prt_file = prt_files[0] # Use first non-idealized part + + # Check for cached results + cache_file = model_dir / "_introspection_cache.json" + if cache_file.exists() and not force: + try: + with open(cache_file, 'r') as f: + cached = json.load(f) + # Check if cache is for same file + if cached.get('part_file') == str(prt_file): + return { + "study_id": study_id, + "cached": True, + "introspection": cached + } + except: + pass # Invalid cache, re-run + + # Run introspection + try: + from optimization_engine.extractors.introspect_part import introspect_part + result = introspect_part(str(prt_file), str(model_dir), verbose=False) + + # Cache results + with open(cache_file, 'w') as f: + json.dump(result, f, indent=2) + + return { + "study_id": study_id, + "cached": False, + "introspection": result + } + + except ImportError: + raise HTTPException( + status_code=500, + detail="introspect_part module not available" + ) + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Introspection failed: {str(e)}" + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to introspect: {str(e)}") + + +@router.get("/studies/{study_id}/nx/expressions") +async def get_nx_expressions(study_id: str): + """ + Get just the expressions from NX model (simplified endpoint). + + Returns list of user expressions suitable for design variable selection. + + Args: + study_id: Study identifier + + Returns: + JSON with expressions array containing name, value, units, formula + """ + try: + study_dir = resolve_study_path(study_id) + + if not study_dir.exists(): + raise HTTPException(status_code=404, detail=f"Study {study_id} not found") + + # Check cache first + model_dir = study_dir / "1_model" + if not model_dir.exists(): + model_dir = study_dir / "0_model" + + cache_file = model_dir / "_introspection_cache.json" if model_dir.exists() else None + + if cache_file and cache_file.exists(): + with open(cache_file, 'r') as f: + cached = json.load(f) + + expressions = cached.get('expressions', {}).get('user', []) + return { + "study_id": study_id, + "expressions": expressions, + "count": len(expressions) + } + + # No cache, need to run introspection + # Find .prt file + if not model_dir or not model_dir.exists(): + raise HTTPException(status_code=404, detail=f"Model directory not found for {study_id}") + + prt_files = list(model_dir.glob("*.prt")) + prt_files = [f for f in prt_files if '_i.prt' not in f.name.lower()] + + if not prt_files: + raise HTTPException(status_code=404, detail=f"No .prt files found") + + prt_file = prt_files[0] + + try: + from optimization_engine.extractors.introspect_part import introspect_part + result = introspect_part(str(prt_file), str(model_dir), verbose=False) + + # Cache for future + with open(cache_file, 'w') as f: + json.dump(result, f, indent=2) + + expressions = result.get('expressions', {}).get('user', []) + return { + "study_id": study_id, + "expressions": expressions, + "count": len(expressions) + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to get expressions: {str(e)}") + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to get expressions: {str(e)}") diff --git a/atomizer-dashboard/frontend/src/api/client.ts b/atomizer-dashboard/frontend/src/api/client.ts index 7bb6a959..51d20ae7 100644 --- a/atomizer-dashboard/frontend/src/api/client.ts +++ b/atomizer-dashboard/frontend/src/api/client.ts @@ -348,6 +348,55 @@ class ApiClient { if (!response.ok) throw new Error(`Failed to export ${format}`); return response.json(); } + // NX Model introspection + async introspectNxModel(studyId: string, force: boolean = false): Promise<{ + study_id: string; + cached: boolean; + introspection: { + success: boolean; + part_file: string; + expressions: { + user: Array<{ + name: string; + value: number; + units?: string; + formula?: string; + }>; + internal: any[]; + user_count: number; + total_count: number; + }; + mass_properties: { + mass_kg: number; + mass_g: number; + volume_mm3: number; + surface_area_mm2: number; + center_of_gravity_mm: [number, number, number]; + }; + }; + }> { + const url = force + ? `${API_BASE}/optimization/studies/${studyId}/nx/introspect?force=true` + : `${API_BASE}/optimization/studies/${studyId}/nx/introspect`; + const response = await fetch(url); + if (!response.ok) throw new Error('Failed to introspect NX model'); + return response.json(); + } + + async getNxExpressions(studyId: string): Promise<{ + study_id: string; + expressions: Array<{ + name: string; + value: number; + units?: string; + formula?: string; + }>; + count: number; + }> { + const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/nx/expressions`); + if (!response.ok) throw new Error('Failed to get NX expressions'); + return response.json(); + } } export const apiClient = new ApiClient();