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:
@@ -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)}")
|
||||
|
||||
Reference in New Issue
Block a user