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