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 <noreply@anthropic.com>
This commit is contained in:
2026-01-15 22:39:38 -05:00
parent 2d4303bf22
commit 0eb5028d8f
2 changed files with 213 additions and 0 deletions

View File

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