diff --git a/atomizer-dashboard/backend/api/routes/optimization.py b/atomizer-dashboard/backend/api/routes/optimization.py index 1568b70c..7610c64e 100644 --- a/atomizer-dashboard/backend/api/routes/optimization.py +++ b/atomizer-dashboard/backend/api/routes/optimization.py @@ -5280,3 +5280,155 @@ async def get_nx_expressions(study_id: str): raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get expressions: {str(e)}") + + +@router.get("/studies/{study_id}/nx/introspect/{part_name}") +async def introspect_specific_part(study_id: str, part_name: str, force: bool = False): + """ + Introspect a specific .prt file in the model directory. + + Use this to get expressions from component parts (e.g., M1_Blank.prt) + rather than just the main assembly. + + Args: + study_id: Study identifier + part_name: Name of the part file (e.g., "M1_Blank.prt" or "M1_Blank") + force: Force re-introspection even if cached + + Returns: + JSON with introspection results for the specific part + """ + 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 = None + for possible_dir in [ + study_dir / "1_setup" / "model", + study_dir / "1_model", + study_dir / "model", + study_dir / "1_setup", + ]: + if possible_dir.exists(): + model_dir = possible_dir + break + + if model_dir is None: + raise HTTPException(status_code=404, detail=f"No model directory found for {study_id}") + + # Normalize part name + if not part_name.lower().endswith(".prt"): + part_name = part_name + ".prt" + + # Find the part file (case-insensitive) + prt_file = None + for f in model_dir.glob("*.prt"): + if f.name.lower() == part_name.lower(): + prt_file = f + break + + if prt_file is None: + # List available parts for helpful error + available = [f.name for f in model_dir.glob("*.prt")] + raise HTTPException( + status_code=404, detail=f"Part '{part_name}' not found. Available: {available}" + ) + + # Check cache + cache_file = model_dir / f"_introspection_{prt_file.stem}.json" + if cache_file.exists() and not force: + try: + with open(cache_file, "r") as f: + cached = json.load(f) + return { + "study_id": study_id, + "part_name": prt_file.name, + "cached": True, + "introspection": cached, + } + except: + pass + + # 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, + "part_name": prt_file.name, + "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 part: {str(e)}") + + +@router.get("/studies/{study_id}/nx/parts") +async def list_model_parts(study_id: str): + """ + List all .prt files in the study's model directory. + + Returns: + JSON with list of available parts that can be introspected + """ + 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 = None + for possible_dir in [ + study_dir / "1_setup" / "model", + study_dir / "1_model", + study_dir / "model", + study_dir / "1_setup", + ]: + if possible_dir.exists(): + model_dir = possible_dir + break + + if model_dir is None: + raise HTTPException(status_code=404, detail=f"No model directory found for {study_id}") + + # Collect all part files + parts = [] + for f in sorted(model_dir.glob("*.prt")): + is_idealized = "_i.prt" in f.name.lower() + parts.append( + { + "name": f.name, + "stem": f.stem, + "is_idealized": is_idealized, + "size_kb": round(f.stat().st_size / 1024, 1), + "has_cache": (model_dir / f"_introspection_{f.stem}.json").exists(), + } + ) + + return { + "study_id": study_id, + "model_dir": str(model_dir), + "parts": parts, + "count": len(parts), + } + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to list parts: {str(e)}") diff --git a/atomizer-dashboard/frontend/src/components/canvas/panels/IntrospectionPanel.tsx b/atomizer-dashboard/frontend/src/components/canvas/panels/IntrospectionPanel.tsx index 1b44eaa9..09c55a50 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/panels/IntrospectionPanel.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/panels/IntrospectionPanel.tsx @@ -21,6 +21,8 @@ import { Settings2, GitBranch, File, + Database, + CheckCircle2, } from 'lucide-react'; import { useCanvasStore } from '../../../hooks/useCanvasStore'; @@ -101,6 +103,22 @@ interface IntrospectionResult { units?: Record; linked_parts?: Record; file_dependencies?: FileDependencies; + existing_fea_results?: { + has_results: boolean; + sources: Array<{ + location: string; + path: string; + op2: string[]; + f06: string[]; + bdf: string[]; + timestamp?: number; + }>; + recommended?: { + location: string; + path: string; + op2: string[]; + }; + }; } // Baseline run result interface @@ -125,7 +143,7 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [expandedSections, setExpandedSections] = useState>( - new Set(['expressions', 'extractors']) + new Set(['expressions', 'extractors', 'file_deps', 'fea_results']) ); const [searchTerm, setSearchTerm] = useState(''); @@ -783,6 +801,77 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection )} + {/* Existing FEA Results Section */} + {result.existing_fea_results && ( +
+ + + {expandedSections.has('fea_results') && ( +
+ {result.existing_fea_results.has_results ? ( + <> +

+ Existing results found - no solve needed for extraction +

+ {result.existing_fea_results.sources?.map((source, idx) => ( +
+
+ {source.location} + {source.location === result.existing_fea_results?.recommended?.location && ( + + recommended + + )} +
+
+ {source.op2?.length > 0 && ( +

OP2: {source.op2.join(', ')}

+ )} + {source.f06?.length > 0 && ( +

F06: {source.f06.join(', ')}

+ )} +
+
+ ))} + + ) : ( +

+ No FEA results found. Run baseline to generate results. +

+ )} +
+ )} +
+ )} + {/* Extractors Section - only show if available */} {(result.extractors_available?.length ?? 0) > 0 && (