feat: Add sub-part introspection and existing FEA results UI

Backend:
- GET /nx/parts - List all .prt files in model directory
- GET /nx/introspect/{part_name} - Introspect a specific part file
  (e.g., M1_Blank.prt instead of just the assembly)
- Each part gets its own cache file (_introspection_{stem}.json)

Frontend IntrospectionPanel:
- Add 'FEA Results' section showing existing OP2/F06 sources
- Green checkmark when results exist, shows recommended source
- Expand file_deps and fea_results sections by default
- Add CheckCircle2 and Database icons

This allows introspecting component parts that contain the actual
design variable expressions (e.g., M1_Blank has 56 expressions
while the assembly ASSY_M1 only has 5).
This commit is contained in:
2026-01-20 20:59:04 -05:00
parent 4749944a48
commit 0c252e3a65
2 changed files with 242 additions and 1 deletions

View File

@@ -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)}")