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
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Failed to export data: {str(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}`);
|
if (!response.ok) throw new Error(`Failed to export ${format}`);
|
||||||
return response.json();
|
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();
|
export const apiClient = new ApiClient();
|
||||||
|
|||||||
Reference in New Issue
Block a user