4 Commits

Author SHA1 Message Date
27e78d3d56 feat(canvas): Custom extractor components, migrator, and MCP spec tools
Canvas Components:
- CustomExtractorNode.tsx: Node for custom Python extractors
- CustomExtractorPanel.tsx: Configuration panel for custom extractors
- ConnectionStatusIndicator.tsx: WebSocket status display
- atomizer-spec.ts: TypeScript types for AtomizerSpec v2.0

Config:
- migrator.py: Legacy config to AtomizerSpec v2.0 migration
- Updated __init__.py exports for config and extractors

MCP Tools:
- spec.ts: MCP tools for spec manipulation
- index.ts: Tool registration updates
2026-01-20 13:11:42 -05:00
cb6b130908 feat(config): Add AtomizerSpec v2.0 schema and migrate all studies
Added JSON Schema:
- optimization_engine/schemas/atomizer_spec_v2.json

Migrated 28 studies to AtomizerSpec v2.0 format:
- Drone_Gimbal studies (1)
- M1_Mirror studies (21)
- M2_Mirror studies (2)
- SheetMetal_Bracket studies (4)

Each atomizer_spec.json is the unified configuration containing:
- Design variables with bounds and expressions
- Extractors (standard and custom)
- Objectives and constraints
- Optimization algorithm settings
- Canvas layout information
2026-01-20 13:11:23 -05:00
f067497e08 refactor(dashboard): Remove unused Plotly components
Removed plotly/ directory with unused chart wrappers:
- PlotlyConvergencePlot, PlotlyCorrelationHeatmap
- PlotlyFeasibilityChart, PlotlyParallelCoordinates
- PlotlyParameterImportance, PlotlyParetoPlot
- PlotlyRunComparison, PlotlySurrogateQuality

These were replaced by Recharts-based implementations.
2026-01-20 13:11:02 -05:00
ba0b9a1fae feat(dashboard): Enhanced chat, spec management, and Claude integration
Backend:
- spec.py: New AtomizerSpec REST API endpoints
- spec_manager.py: SpecManager service for unified config
- interview_engine.py: Study creation interview logic
- claude.py: Enhanced Claude API with context
- optimization.py: Extended optimization endpoints
- context_builder.py, session_manager.py: Improved services

Frontend:
- Chat components: Enhanced message rendering, tool call cards
- Hooks: useClaudeCode, useSpecWebSocket, improved useChat
- Pages: Updated Dashboard, Analysis, Insights, Setup, Home
- Components: ParallelCoordinatesPlot, ParetoPlot improvements
- App.tsx: Route updates for canvas/studio

Infrastructure:
- vite.config.ts: Build configuration updates
- start/stop-dashboard.bat: Script improvements
2026-01-20 13:10:47 -05:00
81 changed files with 20245 additions and 2449 deletions

View File

@@ -13,7 +13,7 @@ import sys
# Add parent directory to path to import optimization_engine # Add parent directory to path to import optimization_engine
sys.path.append(str(Path(__file__).parent.parent.parent.parent)) sys.path.append(str(Path(__file__).parent.parent.parent.parent))
from api.routes import optimization, claude, terminal, insights, context, files, nx from api.routes import optimization, claude, terminal, insights, context, files, nx, claude_code, spec
from api.websocket import optimization_stream from api.websocket import optimization_stream
@@ -60,6 +60,9 @@ app.include_router(insights.router, prefix="/api/insights", tags=["insights"])
app.include_router(context.router, prefix="/api/context", tags=["context"]) app.include_router(context.router, prefix="/api/context", tags=["context"])
app.include_router(files.router, prefix="/api/files", tags=["files"]) app.include_router(files.router, prefix="/api/files", tags=["files"])
app.include_router(nx.router, prefix="/api/nx", tags=["nx"]) app.include_router(nx.router, prefix="/api/nx", tags=["nx"])
app.include_router(claude_code.router, prefix="/api", tags=["claude-code"])
app.include_router(spec.router, prefix="/api", tags=["spec"])
app.include_router(spec.validate_router, prefix="/api", tags=["spec"])
@app.get("/") @app.get("/")
async def root(): async def root():

View File

@@ -187,7 +187,15 @@ async def session_websocket(websocket: WebSocket, session_id: str):
continue continue
# Get canvas state from message or use stored state # Get canvas state from message or use stored state
canvas_state = data.get("canvas_state") or current_canvas_state msg_canvas = data.get("canvas_state")
canvas_state = msg_canvas if msg_canvas is not None else current_canvas_state
# Debug logging
if canvas_state:
node_count = len(canvas_state.get("nodes", []))
print(f"[Claude WS] Sending message with canvas state: {node_count} nodes")
else:
print("[Claude WS] Sending message WITHOUT canvas state")
async for chunk in manager.send_message( async for chunk in manager.send_message(
session_id, session_id,
@@ -401,6 +409,175 @@ async def websocket_chat(websocket: WebSocket):
pass pass
# ========== POWER MODE: Direct API with Write Tools ==========
@router.websocket("/sessions/{session_id}/ws/power")
async def power_mode_websocket(websocket: WebSocket, session_id: str):
"""
WebSocket for power mode chat using direct Anthropic API with write tools.
Unlike the regular /ws endpoint which uses Claude CLI + MCP,
this uses AtomizerClaudeAgent directly with built-in write tools.
This allows immediate modifications without permission prompts.
Message formats (client -> server):
{"type": "message", "content": "user message"}
{"type": "set_study", "study_id": "study_name"}
{"type": "ping"}
Message formats (server -> client):
{"type": "text", "content": "..."}
{"type": "tool_call", "tool": "...", "input": {...}}
{"type": "tool_result", "result": "..."}
{"type": "done", "tool_calls": [...]}
{"type": "error", "message": "..."}
{"type": "spec_modified", "changes": [...]}
{"type": "pong"}
"""
await websocket.accept()
manager = get_session_manager()
session = manager.get_session(session_id)
if not session:
await websocket.send_json({"type": "error", "message": "Session not found"})
await websocket.close()
return
# Import AtomizerClaudeAgent for direct API access
from api.services.claude_agent import AtomizerClaudeAgent
# Create agent with study context
agent = AtomizerClaudeAgent(study_id=session.study_id)
conversation_history: List[Dict[str, Any]] = []
# Load initial spec and set canvas state so Claude sees current canvas
initial_spec = agent.load_current_spec()
if initial_spec:
# Send initial spec to frontend
await websocket.send_json({
"type": "spec_updated",
"spec": initial_spec,
"reason": "initial_load"
})
try:
while True:
data = await websocket.receive_json()
if data.get("type") == "message":
content = data.get("content", "")
if not content:
continue
try:
# Use streaming API with tool support for real-time response
last_tool_calls = []
async for event in agent.chat_stream_with_tools(content, conversation_history):
event_type = event.get("type")
if event_type == "text":
# Stream text tokens to frontend immediately
await websocket.send_json({
"type": "text",
"content": event.get("content", ""),
})
elif event_type == "tool_call":
# Tool is being called
tool_info = event.get("tool", {})
await websocket.send_json({
"type": "tool_call",
"tool": tool_info,
})
elif event_type == "tool_result":
# Tool finished executing
tool_name = event.get("tool", "")
await websocket.send_json({
"type": "tool_result",
"tool": tool_name,
"result": event.get("result", ""),
})
# If it was a write tool, send full updated spec
if tool_name in ["add_design_variable", "add_extractor",
"add_objective", "add_constraint",
"update_spec_field", "remove_node",
"create_study"]:
# Load updated spec and update agent's canvas state
updated_spec = agent.load_current_spec()
if updated_spec:
await websocket.send_json({
"type": "spec_updated",
"tool": tool_name,
"spec": updated_spec, # Full spec for direct canvas update
})
elif event_type == "done":
# Streaming complete
last_tool_calls = event.get("tool_calls", [])
await websocket.send_json({
"type": "done",
"tool_calls": last_tool_calls,
})
# Update conversation history for next message
# Note: For proper history tracking, we'd need to store messages properly
# For now, we append the user message and response
conversation_history.append({"role": "user", "content": content})
conversation_history.append({"role": "assistant", "content": event.get("response", "")})
except Exception as e:
import traceback
traceback.print_exc()
await websocket.send_json({
"type": "error",
"message": str(e),
})
elif data.get("type") == "canvas_edit":
# User made a manual edit to the canvas - update Claude's context
spec = data.get("spec")
if spec:
agent.set_canvas_state(spec)
await websocket.send_json({
"type": "canvas_edit_received",
"acknowledged": True
})
elif data.get("type") == "set_study":
study_id = data.get("study_id")
if study_id:
await manager.set_study_context(session_id, study_id)
# Recreate agent with new study context
agent = AtomizerClaudeAgent(study_id=study_id)
conversation_history = [] # Clear history on study change
# Load spec for new study
new_spec = agent.load_current_spec()
await websocket.send_json({
"type": "context_updated",
"study_id": study_id,
})
if new_spec:
await websocket.send_json({
"type": "spec_updated",
"spec": new_spec,
"reason": "study_change"
})
elif data.get("type") == "ping":
await websocket.send_json({"type": "pong"})
except WebSocketDisconnect:
pass
except Exception as e:
try:
await websocket.send_json({"type": "error", "message": str(e)})
except:
pass
@router.get("/suggestions") @router.get("/suggestions")
async def get_chat_suggestions(study_id: Optional[str] = None): async def get_chat_suggestions(study_id: Optional[str] = None):
""" """

View File

@@ -38,16 +38,30 @@ def resolve_study_path(study_id: str) -> Path:
"""Find study folder by scanning all topic directories. """Find study folder by scanning all topic directories.
Supports nested folder structure: studies/Topic/study_name/ Supports nested folder structure: studies/Topic/study_name/
Study ID is the short name (e.g., 'm1_mirror_adaptive_V14') Study ID can be:
- Short name (e.g., 'm1_mirror_adaptive_V14') - scans all topic folders
- Full nested path (e.g., 'M1_Mirror/m1_mirror_cost_reduction_lateral')
Returns the full path to the study directory. Returns the full path to the study directory.
Raises HTTPException 404 if not found. Raises HTTPException 404 if not found.
""" """
# Handle nested path format (e.g., "M1_Mirror/m1_mirror_cost_reduction_lateral")
if "/" in study_id:
# Try with forward slashes
nested_path = STUDIES_DIR / study_id
if nested_path.exists() and nested_path.is_dir():
if _is_valid_study_dir(nested_path):
return nested_path
# Try with backslashes (Windows path)
nested_path = STUDIES_DIR / study_id.replace("/", "\\")
if nested_path.exists() and nested_path.is_dir():
if _is_valid_study_dir(nested_path):
return nested_path
# First check direct path (backwards compatibility for flat structure) # First check direct path (backwards compatibility for flat structure)
direct_path = STUDIES_DIR / study_id direct_path = STUDIES_DIR / study_id
if direct_path.exists() and direct_path.is_dir(): if direct_path.exists() and direct_path.is_dir():
# Verify it's actually a study (has 1_setup or config) if _is_valid_study_dir(direct_path):
if (direct_path / "1_setup").exists() or (direct_path / "optimization_config.json").exists():
return direct_path return direct_path
# Scan topic folders for nested structure # Scan topic folders for nested structure
@@ -55,13 +69,21 @@ def resolve_study_path(study_id: str) -> Path:
if topic_dir.is_dir() and not topic_dir.name.startswith('.'): if topic_dir.is_dir() and not topic_dir.name.startswith('.'):
study_dir = topic_dir / study_id study_dir = topic_dir / study_id
if study_dir.exists() and study_dir.is_dir(): if study_dir.exists() and study_dir.is_dir():
# Verify it's actually a study if _is_valid_study_dir(study_dir):
if (study_dir / "1_setup").exists() or (study_dir / "optimization_config.json").exists():
return study_dir return study_dir
raise HTTPException(status_code=404, detail=f"Study not found: {study_id}") raise HTTPException(status_code=404, detail=f"Study not found: {study_id}")
def _is_valid_study_dir(study_dir: Path) -> bool:
"""Check if a directory is a valid study directory."""
return (
(study_dir / "1_setup").exists() or
(study_dir / "optimization_config.json").exists() or
(study_dir / "atomizer_spec.json").exists()
)
def get_study_topic(study_dir: Path) -> Optional[str]: def get_study_topic(study_dir: Path) -> Optional[str]:
"""Get the topic folder name for a study, or None if in root.""" """Get the topic folder name for a study, or None if in root."""
# Check if parent is a topic folder (not the root studies dir) # Check if parent is a topic folder (not the root studies dir)
@@ -1542,16 +1564,17 @@ async def get_study_image(study_id: str, image_path: str):
raise HTTPException(status_code=500, detail=f"Failed to serve image: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to serve image: {str(e)}")
@router.get("/studies/{study_id}/config") @router.get("/studies/{study_id:path}/config")
async def get_study_config(study_id: str): async def get_study_config(study_id: str):
""" """
Get the full optimization_config.json for a study Get the study configuration - reads from atomizer_spec.json (v2.0) first,
falls back to legacy optimization_config.json if not found.
Args: Args:
study_id: Study identifier study_id: Study identifier
Returns: Returns:
JSON with the complete configuration JSON with the complete configuration in a unified format
""" """
try: try:
study_dir = resolve_study_path(study_id) study_dir = resolve_study_path(study_id)
@@ -1559,7 +1582,22 @@ async def get_study_config(study_id: str):
if not study_dir.exists(): if not study_dir.exists():
raise HTTPException(status_code=404, detail=f"Study {study_id} not found") raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
# Look for config in various locations # Priority 1: atomizer_spec.json (v2.0 unified format)
spec_file = study_dir / "atomizer_spec.json"
if spec_file.exists():
with open(spec_file) as f:
spec = json.load(f)
# Transform AtomizerSpec to the expected config format
config = _transform_spec_to_config(spec, study_id)
return {
"config": config,
"path": str(spec_file),
"study_id": study_id,
"source": "atomizer_spec"
}
# Priority 2: Legacy optimization_config.json
config_file = study_dir / "1_setup" / "optimization_config.json" config_file = study_dir / "1_setup" / "optimization_config.json"
if not config_file.exists(): if not config_file.exists():
config_file = study_dir / "optimization_config.json" config_file = study_dir / "optimization_config.json"
@@ -1573,7 +1611,8 @@ async def get_study_config(study_id: str):
return { return {
"config": config, "config": config,
"path": str(config_file), "path": str(config_file),
"study_id": study_id "study_id": study_id,
"source": "legacy_config"
} }
except HTTPException: except HTTPException:
@@ -1582,6 +1621,118 @@ async def get_study_config(study_id: str):
raise HTTPException(status_code=500, detail=f"Failed to read config: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to read config: {str(e)}")
def _transform_spec_to_config(spec: dict, study_id: str) -> dict:
"""Transform AtomizerSpec v2.0 format to legacy config format for backwards compatibility."""
meta = spec.get("meta", {})
model = spec.get("model", {})
optimization = spec.get("optimization", {})
# Transform design variables
design_variables = []
for dv in spec.get("design_variables", []):
bounds = dv.get("bounds", {})
design_variables.append({
"name": dv.get("name"),
"expression_name": dv.get("expression_name"),
"type": "float" if dv.get("type") == "continuous" else dv.get("type", "float"),
"min": bounds.get("min"),
"max": bounds.get("max"),
"low": bounds.get("min"), # Alias for compatibility
"high": bounds.get("max"), # Alias for compatibility
"baseline": dv.get("baseline"),
"unit": dv.get("units"),
"units": dv.get("units"),
"enabled": dv.get("enabled", True)
})
# Transform objectives
objectives = []
for obj in spec.get("objectives", []):
source = obj.get("source", {})
objectives.append({
"name": obj.get("name"),
"direction": obj.get("direction", "minimize"),
"weight": obj.get("weight", 1.0),
"target": obj.get("target"),
"unit": obj.get("units"),
"units": obj.get("units"),
"extractor_id": source.get("extractor_id"),
"output_key": source.get("output_key")
})
# Transform constraints
constraints = []
for con in spec.get("constraints", []):
constraints.append({
"name": con.get("name"),
"type": _operator_to_type(con.get("operator", "<=")),
"operator": con.get("operator"),
"max_value": con.get("threshold") if con.get("operator") in ["<=", "<"] else None,
"min_value": con.get("threshold") if con.get("operator") in [">=", ">"] else None,
"bound": con.get("threshold"),
"unit": con.get("units"),
"units": con.get("units")
})
# Transform extractors
extractors = []
for ext in spec.get("extractors", []):
extractors.append({
"name": ext.get("name"),
"type": ext.get("type"),
"builtin": ext.get("builtin", True),
"config": ext.get("config", {}),
"outputs": ext.get("outputs", [])
})
# Get algorithm info
algorithm = optimization.get("algorithm", {})
budget = optimization.get("budget", {})
# Build the config in legacy format
config = {
"study_name": meta.get("study_name", study_id),
"description": meta.get("description", ""),
"version": meta.get("version", "2.0"),
"design_variables": design_variables,
"objectives": objectives,
"constraints": constraints,
"extractors": extractors,
"optimization": {
"algorithm": algorithm.get("type", "TPE"),
"n_trials": budget.get("max_trials", 100),
"max_time_hours": budget.get("max_time_hours"),
"convergence_patience": budget.get("convergence_patience")
},
"optimization_settings": {
"sampler": algorithm.get("type", "TPE"),
"n_trials": budget.get("max_trials", 100)
},
"algorithm": {
"name": "Optuna",
"sampler": algorithm.get("type", "TPE"),
"n_trials": budget.get("max_trials", 100)
},
"model": model,
"sim_file": model.get("sim", {}).get("path") if isinstance(model.get("sim"), dict) else None
}
return config
def _operator_to_type(operator: str) -> str:
"""Convert constraint operator to legacy type string."""
mapping = {
"<=": "le",
"<": "le",
">=": "ge",
">": "ge",
"==": "eq",
"=": "eq"
}
return mapping.get(operator, "le")
# ============================================================================ # ============================================================================
# Process Control Endpoints # Process Control Endpoints
# ============================================================================ # ============================================================================
@@ -2851,7 +3002,162 @@ async def get_study_runs(study_id: str):
class UpdateConfigRequest(BaseModel): class UpdateConfigRequest(BaseModel):
config: dict config: Optional[dict] = None
intent: Optional[dict] = None
def intent_to_config(intent: dict, existing_config: Optional[dict] = None) -> dict:
"""
Convert canvas intent format to optimization_config.json format.
Preserves existing config fields that aren't in the intent.
"""
# Start with existing config or empty
config = existing_config.copy() if existing_config else {}
# Metadata
if intent.get('model', {}).get('path'):
model_path = Path(intent['model']['path']).name
if 'simulation' not in config:
config['simulation'] = {}
config['simulation']['model_file'] = model_path
# Try to infer other files from model name
base_name = model_path.replace('.prt', '')
if not config['simulation'].get('fem_file'):
config['simulation']['fem_file'] = f"{base_name}_fem1.fem"
if not config['simulation'].get('sim_file'):
config['simulation']['sim_file'] = f"{base_name}_sim1.sim"
# Solver
if intent.get('solver', {}).get('type'):
solver_type = intent['solver']['type']
if 'simulation' not in config:
config['simulation'] = {}
config['simulation']['solver'] = 'nastran'
# Map SOL types to analysis_types
sol_to_analysis = {
'SOL101': ['static'],
'SOL103': ['modal'],
'SOL105': ['buckling'],
'SOL106': ['nonlinear'],
'SOL111': ['modal', 'frequency_response'],
'SOL112': ['modal', 'transient'],
}
config['simulation']['analysis_types'] = sol_to_analysis.get(solver_type, ['static'])
# Design Variables
if intent.get('design_variables'):
config['design_variables'] = []
for dv in intent['design_variables']:
config['design_variables'].append({
'parameter': dv.get('name', dv.get('expression_name', '')),
'bounds': [dv.get('min', 0), dv.get('max', 100)],
'description': dv.get('description', f"Design variable: {dv.get('name', '')}"),
})
# Extractors → used for objectives/constraints extraction
extractor_map = {}
if intent.get('extractors'):
for ext in intent['extractors']:
ext_id = ext.get('id', '')
ext_name = ext.get('name', '')
extractor_map[ext_name] = ext
# Objectives
if intent.get('objectives'):
config['objectives'] = []
for obj in intent['objectives']:
obj_config = {
'name': obj.get('name', 'objective'),
'goal': obj.get('direction', 'minimize'),
'weight': obj.get('weight', 1.0),
'description': obj.get('description', f"Objective: {obj.get('name', '')}"),
}
# Add extraction config if extractor referenced
extractor_name = obj.get('extractor')
if extractor_name and extractor_name in extractor_map:
ext = extractor_map[extractor_name]
ext_config = ext.get('config', {})
obj_config['extraction'] = {
'action': _extractor_id_to_action(ext.get('id', '')),
'domain': 'result_extraction',
'params': ext_config,
}
config['objectives'].append(obj_config)
# Constraints
if intent.get('constraints'):
config['constraints'] = []
for con in intent['constraints']:
op = con.get('operator', '<=')
con_type = 'less_than' if '<' in op else 'greater_than' if '>' in op else 'equal_to'
con_config = {
'name': con.get('name', 'constraint'),
'type': con_type,
'threshold': con.get('value', 0),
'description': con.get('description', f"Constraint: {con.get('name', '')}"),
}
# Add extraction config if extractor referenced
extractor_name = con.get('extractor')
if extractor_name and extractor_name in extractor_map:
ext = extractor_map[extractor_name]
ext_config = ext.get('config', {})
con_config['extraction'] = {
'action': _extractor_id_to_action(ext.get('id', '')),
'domain': 'result_extraction',
'params': ext_config,
}
config['constraints'].append(con_config)
# Optimization settings
if intent.get('optimization'):
opt = intent['optimization']
if 'optimization_settings' not in config:
config['optimization_settings'] = {}
if opt.get('max_trials'):
config['optimization_settings']['n_trials'] = opt['max_trials']
if opt.get('method'):
# Map method names to Optuna sampler names
method_map = {
'TPE': 'TPESampler',
'CMA-ES': 'CmaEsSampler',
'NSGA-II': 'NSGAIISampler',
'RandomSearch': 'RandomSampler',
'GP-BO': 'GPSampler',
}
config['optimization_settings']['sampler'] = method_map.get(opt['method'], opt['method'])
# Surrogate
if intent.get('surrogate', {}).get('enabled'):
config['surrogate'] = {
'type': intent['surrogate'].get('type', 'MLP'),
'min_trials': intent['surrogate'].get('min_trials', 20),
}
return config
def _extractor_id_to_action(ext_id: str) -> str:
"""Map extractor IDs (E1, E2, etc.) to extraction action names."""
action_map = {
'E1': 'extract_displacement',
'E2': 'extract_frequency',
'E3': 'extract_stress',
'E4': 'extract_mass',
'E5': 'extract_mass',
'E8': 'extract_zernike',
'E9': 'extract_zernike',
'E10': 'extract_zernike',
'displacement': 'extract_displacement',
'frequency': 'extract_frequency',
'stress': 'extract_stress',
'mass': 'extract_mass',
'mass_bdf': 'extract_mass',
'mass_cad': 'extract_mass',
'zernike': 'extract_zernike',
'zernike_opd': 'extract_zernike',
}
return action_map.get(ext_id, 'extract_displacement')
@router.put("/studies/{study_id}/config") @router.put("/studies/{study_id}/config")
@@ -2859,9 +3165,13 @@ async def update_study_config(study_id: str, request: UpdateConfigRequest):
""" """
Update the optimization_config.json for a study Update the optimization_config.json for a study
Accepts either:
- {"config": {...}} - Direct config object (overwrites)
- {"intent": {...}} - Canvas intent (converted and merged with existing)
Args: Args:
study_id: Study identifier study_id: Study identifier
request: New configuration data request: New configuration data (config or intent)
Returns: Returns:
JSON with success status JSON with success status
@@ -2891,9 +3201,24 @@ async def update_study_config(study_id: str, request: UpdateConfigRequest):
backup_file = config_file.with_suffix('.json.backup') backup_file = config_file.with_suffix('.json.backup')
shutil.copy(config_file, backup_file) shutil.copy(config_file, backup_file)
# Determine which format was provided
if request.config is not None:
# Direct config update
new_config = request.config
elif request.intent is not None:
# Convert intent to config, merging with existing
with open(config_file, 'r') as f:
existing_config = json.load(f)
new_config = intent_to_config(request.intent, existing_config)
else:
raise HTTPException(
status_code=400,
detail="Request must include either 'config' or 'intent' field"
)
# Write new config # Write new config
with open(config_file, 'w') as f: with open(config_file, 'w') as f:
json.dump(request.config, f, indent=2) json.dump(new_config, f, indent=2)
return { return {
"success": True, "success": True,

View File

@@ -0,0 +1,646 @@
"""
AtomizerSpec v2.0 API Endpoints
REST API for managing AtomizerSpec configurations.
All spec modifications flow through these endpoints.
Endpoints:
- GET /studies/{study_id}/spec - Get full spec
- PUT /studies/{study_id}/spec - Replace entire spec
- PATCH /studies/{study_id}/spec - Partial update
- POST /studies/{study_id}/spec/validate - Validate spec
- POST /studies/{study_id}/spec/nodes - Add node
- PATCH /studies/{study_id}/spec/nodes/{node_id} - Update node
- DELETE /studies/{study_id}/spec/nodes/{node_id} - Delete node
- POST /studies/{study_id}/spec/custom-functions - Add custom extractor
- WebSocket /studies/{study_id}/spec/sync - Real-time sync
"""
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect, Query
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
import json
import sys
import asyncio
# Add project root to path
sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent))
from api.services.spec_manager import (
SpecManager,
SpecManagerError,
SpecNotFoundError,
SpecConflictError,
get_spec_manager,
)
from optimization_engine.config.spec_models import (
AtomizerSpec,
ValidationReport,
)
from optimization_engine.config.spec_validator import SpecValidationError
router = APIRouter(prefix="/studies/{study_id:path}/spec", tags=["spec"])
# Base studies directory
STUDIES_DIR = Path(__file__).parent.parent.parent.parent.parent / "studies"
# ============================================================================
# Request/Response Models
# ============================================================================
class SpecPatchRequest(BaseModel):
"""Request for patching a spec field."""
path: str = Field(..., description="JSONPath to the field (e.g., 'objectives[0].weight')")
value: Any = Field(..., description="New value")
modified_by: str = Field(default="api", description="Who is making the change")
class NodeAddRequest(BaseModel):
"""Request for adding a node."""
type: str = Field(..., description="Node type: designVar, extractor, objective, constraint")
data: Dict[str, Any] = Field(..., description="Node data")
modified_by: str = Field(default="canvas", description="Who is making the change")
class NodeUpdateRequest(BaseModel):
"""Request for updating a node."""
updates: Dict[str, Any] = Field(..., description="Fields to update")
modified_by: str = Field(default="canvas", description="Who is making the change")
class CustomFunctionRequest(BaseModel):
"""Request for adding a custom extractor function."""
name: str = Field(..., description="Function name")
code: str = Field(..., description="Python source code")
outputs: List[str] = Field(..., description="Output names")
description: Optional[str] = Field(default=None, description="Human-readable description")
modified_by: str = Field(default="claude", description="Who is making the change")
class ExtractorValidationRequest(BaseModel):
"""Request for validating custom extractor code."""
function_name: str = Field(default="extract", description="Expected function name")
source: str = Field(..., description="Python source code to validate")
class SpecUpdateResponse(BaseModel):
"""Response for spec modification operations."""
success: bool
hash: str
modified: str
modified_by: str
class NodeAddResponse(BaseModel):
"""Response for node add operation."""
success: bool
node_id: str
message: str
class ValidationResponse(BaseModel):
"""Response for validation endpoint."""
valid: bool
errors: List[Dict[str, Any]]
warnings: List[Dict[str, Any]]
summary: Dict[str, int]
# ============================================================================
# Helper Functions
# ============================================================================
def resolve_study_path(study_id: str) -> Path:
"""Find study folder by scanning all topic directories.
Supports both formats:
- "study_name" - Will scan topic folders to find it
- "Topic/study_name" - Direct nested path (e.g., "M1_Mirror/m1_mirror_v1")
"""
# Handle nested paths (e.g., "M1_Mirror/m1_mirror_cost_reduction_lateral")
if "/" in study_id:
nested_path = STUDIES_DIR / study_id.replace("/", "\\") # Handle Windows paths
if nested_path.exists() and nested_path.is_dir():
return nested_path
# Also try with forward slashes (Path handles both)
nested_path = STUDIES_DIR / study_id
if nested_path.exists() and nested_path.is_dir():
return nested_path
# Direct path (flat structure)
direct_path = STUDIES_DIR / study_id
if direct_path.exists() and direct_path.is_dir():
return direct_path
# Scan topic folders (nested structure)
for topic_dir in STUDIES_DIR.iterdir():
if topic_dir.is_dir() and not topic_dir.name.startswith('.'):
study_dir = topic_dir / study_id
if study_dir.exists() and study_dir.is_dir():
return study_dir
raise HTTPException(status_code=404, detail=f"Study not found: {study_id}")
def get_manager(study_id: str) -> SpecManager:
"""Get SpecManager for a study."""
study_path = resolve_study_path(study_id)
return get_spec_manager(study_path)
# ============================================================================
# REST Endpoints
# ============================================================================
@router.get("", response_model=None)
async def get_spec(study_id: str):
"""
Get the full AtomizerSpec for a study.
Returns the complete spec JSON with all design variables, extractors,
objectives, constraints, and canvas state.
"""
manager = get_manager(study_id)
if not manager.exists():
raise HTTPException(
status_code=404,
detail=f"No AtomizerSpec found for study '{study_id}'. Use migration or create new spec."
)
try:
spec = manager.load()
return spec.model_dump(mode='json')
except SpecValidationError as e:
# Return spec even if invalid, but include validation info
raw = manager.load_raw()
return JSONResponse(
status_code=200,
content={
**raw,
"_validation_error": str(e)
}
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/raw")
async def get_spec_raw(study_id: str):
"""
Get the raw spec JSON without validation.
Useful for debugging or when spec is invalid.
"""
manager = get_manager(study_id)
if not manager.exists():
raise HTTPException(status_code=404, detail=f"No spec found for study '{study_id}'")
try:
return manager.load_raw()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/hash")
async def get_spec_hash(study_id: str):
"""Get the current spec hash for conflict detection."""
manager = get_manager(study_id)
if not manager.exists():
raise HTTPException(status_code=404, detail=f"No spec found for study '{study_id}'")
return {"hash": manager.get_hash()}
@router.put("", response_model=SpecUpdateResponse)
async def replace_spec(
study_id: str,
spec: Dict[str, Any],
modified_by: str = Query(default="api"),
expected_hash: Optional[str] = Query(default=None)
):
"""
Replace the entire spec.
Validates the new spec before saving. Optionally check for conflicts
using expected_hash parameter.
"""
manager = get_manager(study_id)
try:
new_hash = manager.save(spec, modified_by=modified_by, expected_hash=expected_hash)
reloaded = manager.load()
return SpecUpdateResponse(
success=True,
hash=new_hash,
modified=reloaded.meta.modified or "",
modified_by=modified_by
)
except SpecConflictError as e:
raise HTTPException(
status_code=409,
detail={
"message": str(e),
"current_hash": e.current_hash
}
)
except SpecValidationError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.patch("", response_model=SpecUpdateResponse)
async def patch_spec(study_id: str, request: SpecPatchRequest):
"""
Partial update to spec using JSONPath.
Example paths:
- "objectives[0].weight" - Update objective weight
- "design_variables[1].bounds.max" - Update DV bound
- "meta.description" - Update description
"""
manager = get_manager(study_id)
if not manager.exists():
raise HTTPException(status_code=404, detail=f"No spec found for study '{study_id}'")
try:
spec = manager.patch(request.path, request.value, modified_by=request.modified_by)
return SpecUpdateResponse(
success=True,
hash=manager.get_hash(),
modified=spec.meta.modified or "",
modified_by=request.modified_by
)
except SpecValidationError as e:
raise HTTPException(status_code=400, detail=str(e))
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/validate", response_model=ValidationResponse)
async def validate_spec(study_id: str):
"""
Validate the spec and return detailed report.
Returns errors, warnings, and summary of the spec contents.
"""
manager = get_manager(study_id)
if not manager.exists():
raise HTTPException(status_code=404, detail=f"No spec found for study '{study_id}'")
try:
report = manager.validate_and_report()
return ValidationResponse(
valid=report.valid,
errors=[e.model_dump() for e in report.errors],
warnings=[w.model_dump() for w in report.warnings],
summary=report.summary.model_dump()
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# Node CRUD Endpoints
# ============================================================================
@router.post("/nodes", response_model=NodeAddResponse)
async def add_node(study_id: str, request: NodeAddRequest):
"""
Add a new node to the spec.
Supported types: designVar, extractor, objective, constraint
"""
manager = get_manager(study_id)
if not manager.exists():
raise HTTPException(status_code=404, detail=f"No spec found for study '{study_id}'")
valid_types = ["designVar", "extractor", "objective", "constraint"]
if request.type not in valid_types:
raise HTTPException(
status_code=400,
detail=f"Invalid node type '{request.type}'. Valid: {valid_types}"
)
try:
node_id = manager.add_node(request.type, request.data, modified_by=request.modified_by)
return NodeAddResponse(
success=True,
node_id=node_id,
message=f"Added {request.type} node: {node_id}"
)
except SpecValidationError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.patch("/nodes/{node_id}")
async def update_node(study_id: str, node_id: str, request: NodeUpdateRequest):
"""Update an existing node's properties."""
manager = get_manager(study_id)
if not manager.exists():
raise HTTPException(status_code=404, detail=f"No spec found for study '{study_id}'")
try:
manager.update_node(node_id, request.updates, modified_by=request.modified_by)
return {"success": True, "message": f"Updated node {node_id}"}
except SpecManagerError as e:
raise HTTPException(status_code=404, detail=str(e))
except SpecValidationError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/nodes/{node_id}")
async def delete_node(
study_id: str,
node_id: str,
modified_by: str = Query(default="canvas")
):
"""
Delete a node and all edges referencing it.
Use with caution - this will also remove any objectives or constraints
that reference a deleted extractor.
"""
manager = get_manager(study_id)
if not manager.exists():
raise HTTPException(status_code=404, detail=f"No spec found for study '{study_id}'")
try:
manager.remove_node(node_id, modified_by=modified_by)
return {"success": True, "message": f"Removed node {node_id}"}
except SpecManagerError as e:
raise HTTPException(status_code=404, detail=str(e))
except SpecValidationError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# Custom Function Endpoint
# ============================================================================
@router.post("/custom-functions", response_model=NodeAddResponse)
async def add_custom_function(study_id: str, request: CustomFunctionRequest):
"""
Add a custom Python function as an extractor.
The function will be available in the optimization workflow.
Claude can use this to add new physics extraction logic.
"""
manager = get_manager(study_id)
if not manager.exists():
raise HTTPException(status_code=404, detail=f"No spec found for study '{study_id}'")
try:
extractor_id = manager.add_custom_function(
name=request.name,
code=request.code,
outputs=request.outputs,
description=request.description,
modified_by=request.modified_by
)
return NodeAddResponse(
success=True,
node_id=extractor_id,
message=f"Added custom extractor: {request.name}"
)
except SpecValidationError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# Separate router for non-study-specific endpoints
validate_router = APIRouter(prefix="/spec", tags=["spec"])
@validate_router.post("/validate-extractor")
async def validate_custom_extractor(request: ExtractorValidationRequest):
"""
Validate custom extractor Python code.
Checks syntax, security patterns, and function signature.
Does not require a study - can be used before adding to spec.
"""
try:
from optimization_engine.extractors.custom_extractor_loader import (
validate_extractor_code,
ExtractorSecurityError,
)
try:
is_valid, errors = validate_extractor_code(request.source, request.function_name)
return {
"valid": is_valid,
"errors": errors
}
except ExtractorSecurityError as e:
return {
"valid": False,
"errors": [str(e)]
}
except ImportError as e:
raise HTTPException(
status_code=500,
detail=f"Custom extractor loader not available: {e}"
)
# ============================================================================
# Edge Endpoints
# ============================================================================
@router.post("/edges")
async def add_edge(
study_id: str,
source: str = Query(..., description="Source node ID"),
target: str = Query(..., description="Target node ID"),
modified_by: str = Query(default="canvas")
):
"""Add a canvas edge between two nodes."""
manager = get_manager(study_id)
if not manager.exists():
raise HTTPException(status_code=404, detail=f"No spec found for study '{study_id}'")
try:
manager.add_edge(source, target, modified_by=modified_by)
return {"success": True, "message": f"Added edge {source} -> {target}"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/edges")
async def delete_edge(
study_id: str,
source: str = Query(..., description="Source node ID"),
target: str = Query(..., description="Target node ID"),
modified_by: str = Query(default="canvas")
):
"""Remove a canvas edge."""
manager = get_manager(study_id)
if not manager.exists():
raise HTTPException(status_code=404, detail=f"No spec found for study '{study_id}'")
try:
manager.remove_edge(source, target, modified_by=modified_by)
return {"success": True, "message": f"Removed edge {source} -> {target}"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# WebSocket Sync Endpoint
# ============================================================================
class WebSocketSubscriber:
"""WebSocket subscriber adapter."""
def __init__(self, websocket: WebSocket):
self.websocket = websocket
async def send_json(self, data: Dict[str, Any]) -> None:
await self.websocket.send_json(data)
@router.websocket("/sync")
async def websocket_sync(websocket: WebSocket, study_id: str):
"""
WebSocket endpoint for real-time spec sync.
Clients receive notifications when spec changes:
- spec_updated: Spec was modified
- node_added: New node added
- node_removed: Node removed
- validation_error: Validation failed
"""
await websocket.accept()
manager = get_manager(study_id)
subscriber = WebSocketSubscriber(websocket)
# Subscribe to updates
manager.subscribe(subscriber)
try:
# Send initial connection ack
await websocket.send_json({
"type": "connection_ack",
"study_id": study_id,
"hash": manager.get_hash() if manager.exists() else None,
"message": "Connected to spec sync"
})
# Keep connection alive and handle client messages
while True:
try:
data = await asyncio.wait_for(
websocket.receive_json(),
timeout=30.0 # Heartbeat interval
)
# Handle client messages
msg_type = data.get("type")
if msg_type == "ping":
await websocket.send_json({"type": "pong"})
elif msg_type == "patch_node":
# Client requests node update
try:
manager.update_node(
data["node_id"],
data.get("data", {}),
modified_by=data.get("modified_by", "canvas")
)
except Exception as e:
await websocket.send_json({
"type": "error",
"message": str(e)
})
elif msg_type == "update_position":
# Client updates node position
try:
manager.update_node_position(
data["node_id"],
data["position"],
modified_by=data.get("modified_by", "canvas")
)
except Exception as e:
await websocket.send_json({
"type": "error",
"message": str(e)
})
except asyncio.TimeoutError:
# Send heartbeat
await websocket.send_json({"type": "heartbeat"})
except WebSocketDisconnect:
pass
finally:
manager.unsubscribe(subscriber)
# ============================================================================
# Create/Initialize Spec
# ============================================================================
@router.post("/create")
async def create_spec(
study_id: str,
spec: Dict[str, Any],
modified_by: str = Query(default="api")
):
"""
Create a new spec for a study.
Use this when migrating from old config or creating a new study.
Will fail if spec already exists (use PUT to replace).
"""
manager = get_manager(study_id)
if manager.exists():
raise HTTPException(
status_code=409,
detail=f"Spec already exists for '{study_id}'. Use PUT to replace."
)
try:
# Ensure meta fields are set
if "meta" not in spec:
spec["meta"] = {}
spec["meta"]["created_by"] = modified_by
new_hash = manager.save(spec, modified_by=modified_by)
return {
"success": True,
"hash": new_hash,
"message": f"Created spec for {study_id}"
}
except SpecValidationError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -3,5 +3,13 @@ Atomizer Dashboard Services
""" """
from .claude_agent import AtomizerClaudeAgent from .claude_agent import AtomizerClaudeAgent
from .spec_manager import SpecManager, SpecManagerError, SpecNotFoundError, SpecConflictError, get_spec_manager
__all__ = ['AtomizerClaudeAgent'] __all__ = [
'AtomizerClaudeAgent',
'SpecManager',
'SpecManagerError',
'SpecNotFoundError',
'SpecConflictError',
'get_spec_manager',
]

File diff suppressed because it is too large Load Diff

View File

@@ -43,7 +43,11 @@ class ContextBuilder:
# Canvas context takes priority - if user is working on a canvas, include it # Canvas context takes priority - if user is working on a canvas, include it
if canvas_state: if canvas_state:
node_count = len(canvas_state.get("nodes", []))
print(f"[ContextBuilder] Including canvas context with {node_count} nodes")
parts.append(self._canvas_context(canvas_state)) parts.append(self._canvas_context(canvas_state))
else:
print("[ContextBuilder] No canvas state provided")
if study_id: if study_id:
parts.append(self._study_context(study_id)) parts.append(self._study_context(study_id))
@@ -91,7 +95,117 @@ Important guidelines:
context = f"# Current Study: {study_id}\n\n" context = f"# Current Study: {study_id}\n\n"
# Load configuration # Check for AtomizerSpec v2.0 first (preferred)
spec_path = study_dir / "1_setup" / "atomizer_spec.json"
if not spec_path.exists():
spec_path = study_dir / "atomizer_spec.json"
if spec_path.exists():
context += self._spec_context(spec_path)
else:
# Fall back to legacy optimization_config.json
context += self._legacy_config_context(study_dir)
# Check for results
db_path = study_dir / "3_results" / "study.db"
if db_path.exists():
try:
conn = sqlite3.connect(db_path)
count = conn.execute(
"SELECT COUNT(*) FROM trials WHERE state = 'COMPLETE'"
).fetchone()[0]
best = conn.execute("""
SELECT MIN(tv.value) FROM trial_values tv
JOIN trials t ON tv.trial_id = t.trial_id
WHERE t.state = 'COMPLETE'
""").fetchone()[0]
context += f"\n## Results Status\n\n"
context += f"- **Trials completed**: {count}\n"
if best is not None:
context += f"- **Best objective**: {best:.6f}\n"
conn.close()
except Exception:
pass
return context
def _spec_context(self, spec_path: Path) -> str:
"""Build context from AtomizerSpec v2.0 file"""
context = "**Format**: AtomizerSpec v2.0\n\n"
try:
with open(spec_path) as f:
spec = json.load(f)
context += "## Configuration\n\n"
# Design variables
dvs = spec.get("design_variables", [])
if dvs:
context += "**Design Variables:**\n"
for dv in dvs[:10]:
bounds = dv.get("bounds", {})
bound_str = f"[{bounds.get('min', '?')}, {bounds.get('max', '?')}]"
enabled = "" if dv.get("enabled", True) else ""
context += f"- {dv.get('name', 'unnamed')}: {bound_str} {enabled}\n"
if len(dvs) > 10:
context += f"- ... and {len(dvs) - 10} more\n"
# Extractors
extractors = spec.get("extractors", [])
if extractors:
context += "\n**Extractors:**\n"
for ext in extractors:
ext_type = ext.get("type", "unknown")
outputs = ext.get("outputs", [])
output_names = [o.get("name", "?") for o in outputs[:3]]
builtin = "builtin" if ext.get("builtin", True) else "custom"
context += f"- {ext.get('name', 'unnamed')} ({ext_type}, {builtin}): outputs {output_names}\n"
# Objectives
objs = spec.get("objectives", [])
if objs:
context += "\n**Objectives:**\n"
for obj in objs:
direction = obj.get("direction", "minimize")
weight = obj.get("weight", 1.0)
context += f"- {obj.get('name', 'unnamed')} ({direction}, weight={weight})\n"
# Constraints
constraints = spec.get("constraints", [])
if constraints:
context += "\n**Constraints:**\n"
for c in constraints:
op = c.get("operator", "<=")
thresh = c.get("threshold", "?")
context += f"- {c.get('name', 'unnamed')}: {op} {thresh}\n"
# Optimization settings
opt = spec.get("optimization", {})
algo = opt.get("algorithm", {})
budget = opt.get("budget", {})
method = algo.get("type", "TPE")
max_trials = budget.get("max_trials", "not set")
context += f"\n**Optimization**: {method}, max_trials: {max_trials}\n"
# Surrogate
surrogate = opt.get("surrogate", {})
if surrogate.get("enabled"):
sur_type = surrogate.get("type", "gaussian_process")
context += f"**Surrogate**: {sur_type} enabled\n"
except (json.JSONDecodeError, IOError) as e:
context += f"\n*Spec file exists but could not be parsed: {e}*\n"
return context
def _legacy_config_context(self, study_dir: Path) -> str:
"""Build context from legacy optimization_config.json"""
context = "**Format**: Legacy optimization_config.json\n\n"
config_path = study_dir / "1_setup" / "optimization_config.json" config_path = study_dir / "1_setup" / "optimization_config.json"
if not config_path.exists(): if not config_path.exists():
config_path = study_dir / "optimization_config.json" config_path = study_dir / "optimization_config.json"
@@ -135,30 +249,8 @@ Important guidelines:
except (json.JSONDecodeError, IOError) as e: except (json.JSONDecodeError, IOError) as e:
context += f"\n*Config file exists but could not be parsed: {e}*\n" context += f"\n*Config file exists but could not be parsed: {e}*\n"
else:
# Check for results context += "*No configuration file found.*\n"
db_path = study_dir / "3_results" / "study.db"
if db_path.exists():
try:
conn = sqlite3.connect(db_path)
count = conn.execute(
"SELECT COUNT(*) FROM trials WHERE state = 'COMPLETE'"
).fetchone()[0]
best = conn.execute("""
SELECT MIN(tv.value) FROM trial_values tv
JOIN trials t ON tv.trial_id = t.trial_id
WHERE t.state = 'COMPLETE'
""").fetchone()[0]
context += f"\n## Results Status\n\n"
context += f"- **Trials completed**: {count}\n"
if best is not None:
context += f"- **Best objective**: {best:.6f}\n"
conn.close()
except Exception:
pass
return context return context
@@ -349,19 +441,26 @@ Important guidelines:
# Canvas modification instructions # Canvas modification instructions
context += """## Canvas Modification Tools context += """## Canvas Modification Tools
When the user asks to modify the canvas (add/remove nodes, change values), use these MCP tools: **For AtomizerSpec v2.0 studies (preferred):**
Use spec tools when working with v2.0 studies (check if study uses `atomizer_spec.json`):
- `spec_modify` - Modify spec values using JSONPath (e.g., "design_variables[0].bounds.min")
- `spec_add_node` - Add design variables, extractors, objectives, or constraints
- `spec_remove_node` - Remove nodes from the spec
- `spec_add_custom_extractor` - Add a Python-based custom extractor function
**For Legacy Canvas (optimization_config.json):**
- `canvas_add_node` - Add a new node (designVar, extractor, objective, constraint) - `canvas_add_node` - Add a new node (designVar, extractor, objective, constraint)
- `canvas_update_node` - Update node properties (bounds, weights, names) - `canvas_update_node` - Update node properties (bounds, weights, names)
- `canvas_remove_node` - Remove a node from the canvas - `canvas_remove_node` - Remove a node from the canvas
- `canvas_connect_nodes` - Create an edge between nodes - `canvas_connect_nodes` - Create an edge between nodes
**Example user requests you can handle:** **Example user requests you can handle:**
- "Add a design variable called hole_diameter with range 5-15 mm" → Use canvas_add_node - "Add a design variable called hole_diameter with range 5-15 mm" → Use spec_add_node or canvas_add_node
- "Change the weight of wfe_40_20 to 8" → Use canvas_update_node - "Change the weight of wfe_40_20 to 8" → Use spec_modify or canvas_update_node
- "Remove the constraint node" → Use canvas_remove_node - "Remove the constraint node" → Use spec_remove_node or canvas_remove_node
- "Connect the new extractor to the objective" → Use canvas_connect_nodes - "Add a custom extractor that computes stress ratio" → Use spec_add_custom_extractor
Always respond with confirmation of changes made to the canvas. Always respond with confirmation of changes made to the canvas/spec.
""" """
return context return context
@@ -371,17 +470,28 @@ Always respond with confirmation of changes made to the canvas.
if mode == "power": if mode == "power":
return """# Power Mode Instructions return """# Power Mode Instructions
You have **full access** to Atomizer's codebase. You can: You have **FULL ACCESS** to modify Atomizer studies. **DO NOT ASK FOR PERMISSION** - just do it.
- Edit any file using `edit_file` tool
- Create new files with `create_file` tool
- Create new extractors with `create_extractor` tool
- Run shell commands with `run_shell_command` tool
- Search codebase with `search_codebase` tool
- Commit and push changes
**Use these powers responsibly.** Always explain what you're doing and why. ## Direct Actions (no confirmation needed):
- **Add design variables**: Use `canvas_add_node` or `spec_add_node` with node_type="designVar"
- **Add extractors**: Use `canvas_add_node` with node_type="extractor"
- **Add objectives**: Use `canvas_add_node` with node_type="objective"
- **Add constraints**: Use `canvas_add_node` with node_type="constraint"
- **Update node properties**: Use `canvas_update_node` or `spec_modify`
- **Remove nodes**: Use `canvas_remove_node`
- **Edit atomizer_spec.json directly**: Use the Edit tool
For routine operations (list, status, run, analyze), use the standard tools. ## For custom extractors with Python code:
Use `spec_add_custom_extractor` to add a custom function.
## IMPORTANT:
- You have --dangerously-skip-permissions enabled
- The user has explicitly granted you power mode access
- **ACT IMMEDIATELY** when asked to add/modify/remove things
- Explain what you did AFTER doing it, not before
- Do NOT say "I need permission" - you already have it
Example: If user says "add a volume extractor", immediately use canvas_add_node to add it.
""" """
else: else:
return """# User Mode Instructions return """# User Mode Instructions
@@ -402,6 +512,15 @@ Available tools:
- `generate_report`, `export_data` - `generate_report`, `export_data`
- `explain_physics`, `recommend_method`, `query_extractors` - `explain_physics`, `recommend_method`, `query_extractors`
**AtomizerSpec v2.0 Tools (preferred for new studies):**
- `spec_get` - Get the full AtomizerSpec for a study
- `spec_modify` - Modify spec values using JSONPath (e.g., "design_variables[0].bounds.min")
- `spec_add_node` - Add design variables, extractors, objectives, or constraints
- `spec_remove_node` - Remove nodes from the spec
- `spec_validate` - Validate spec against JSON Schema
- `spec_add_custom_extractor` - Add a Python-based custom extractor function
- `spec_create_from_description` - Create a new study from natural language description
**Canvas Tools (for visual workflow builder):** **Canvas Tools (for visual workflow builder):**
- `validate_canvas_intent` - Validate a canvas-generated optimization intent - `validate_canvas_intent` - Validate a canvas-generated optimization intent
- `execute_canvas_intent` - Create a study from a canvas intent - `execute_canvas_intent` - Create a study from a canvas intent

View File

@@ -0,0 +1,454 @@
"""
Interview Engine - Guided Study Creation through Conversation
Provides a structured interview flow for creating optimization studies.
Claude uses this to gather information step-by-step, building a complete
atomizer_spec.json through natural conversation.
"""
from typing import Dict, Any, List, Optional, Literal
from dataclasses import dataclass, field
from enum import Enum
from datetime import datetime
import json
class InterviewState(str, Enum):
"""Current phase of the interview"""
NOT_STARTED = "not_started"
GATHERING_BASICS = "gathering_basics" # Name, description, goals
GATHERING_MODEL = "gathering_model" # Model file, solver type
GATHERING_VARIABLES = "gathering_variables" # Design variables
GATHERING_EXTRACTORS = "gathering_extractors" # Physics extractors
GATHERING_OBJECTIVES = "gathering_objectives" # Objectives
GATHERING_CONSTRAINTS = "gathering_constraints" # Constraints
GATHERING_SETTINGS = "gathering_settings" # Algorithm, trials
REVIEW = "review" # Review before creation
COMPLETED = "completed"
@dataclass
class InterviewData:
"""Accumulated data from the interview"""
# Basics
study_name: Optional[str] = None
category: Optional[str] = None
description: Optional[str] = None
goals: List[str] = field(default_factory=list)
# Model
sim_file: Optional[str] = None
prt_file: Optional[str] = None
solver_type: str = "nastran"
# Design variables
design_variables: List[Dict[str, Any]] = field(default_factory=list)
# Extractors
extractors: List[Dict[str, Any]] = field(default_factory=list)
# Objectives
objectives: List[Dict[str, Any]] = field(default_factory=list)
# Constraints
constraints: List[Dict[str, Any]] = field(default_factory=list)
# Settings
algorithm: str = "TPE"
max_trials: int = 100
def to_spec(self) -> Dict[str, Any]:
"""Convert interview data to atomizer_spec.json format"""
# Generate IDs for each element
dvs_with_ids = []
for i, dv in enumerate(self.design_variables):
dv_copy = dv.copy()
dv_copy['id'] = f"dv_{i+1:03d}"
dv_copy['canvas_position'] = {'x': 50, 'y': 100 + i * 80}
dvs_with_ids.append(dv_copy)
exts_with_ids = []
for i, ext in enumerate(self.extractors):
ext_copy = ext.copy()
ext_copy['id'] = f"ext_{i+1:03d}"
ext_copy['canvas_position'] = {'x': 400, 'y': 100 + i * 80}
exts_with_ids.append(ext_copy)
objs_with_ids = []
for i, obj in enumerate(self.objectives):
obj_copy = obj.copy()
obj_copy['id'] = f"obj_{i+1:03d}"
obj_copy['canvas_position'] = {'x': 750, 'y': 100 + i * 80}
objs_with_ids.append(obj_copy)
cons_with_ids = []
for i, con in enumerate(self.constraints):
con_copy = con.copy()
con_copy['id'] = f"con_{i+1:03d}"
con_copy['canvas_position'] = {'x': 750, 'y': 400 + i * 80}
cons_with_ids.append(con_copy)
return {
"meta": {
"version": "2.0",
"study_name": self.study_name or "untitled_study",
"description": self.description or "",
"created_at": datetime.now().isoformat(),
"created_by": "interview",
"modified_at": datetime.now().isoformat(),
"modified_by": "interview"
},
"model": {
"sim": {
"path": self.sim_file or "",
"solver": self.solver_type
}
},
"design_variables": dvs_with_ids,
"extractors": exts_with_ids,
"objectives": objs_with_ids,
"constraints": cons_with_ids,
"optimization": {
"algorithm": {
"type": self.algorithm
},
"budget": {
"max_trials": self.max_trials
}
},
"canvas": {
"edges": [],
"layout_version": "2.0"
}
}
class InterviewEngine:
"""
Manages the interview flow for study creation.
Usage:
1. Create engine: engine = InterviewEngine()
2. Start interview: engine.start()
3. Record answers: engine.record_answer("study_name", "bracket_opt")
4. Check progress: engine.get_progress()
5. Generate spec: engine.finalize()
"""
def __init__(self):
self.state = InterviewState.NOT_STARTED
self.data = InterviewData()
self.questions_asked: List[str] = []
self.errors: List[str] = []
def start(self) -> Dict[str, Any]:
"""Start the interview process"""
self.state = InterviewState.GATHERING_BASICS
return {
"state": self.state.value,
"message": "Let's create a new optimization study! I'll guide you through the process.",
"next_questions": self.get_current_questions()
}
def get_current_questions(self) -> List[Dict[str, Any]]:
"""Get the questions for the current interview state"""
questions = {
InterviewState.GATHERING_BASICS: [
{
"field": "study_name",
"question": "What would you like to name this study?",
"hint": "Use snake_case, e.g., 'bracket_mass_optimization'",
"required": True
},
{
"field": "category",
"question": "What category should this study be in?",
"hint": "e.g., 'Simple_Bracket', 'M1_Mirror', or leave blank for root",
"required": False
},
{
"field": "description",
"question": "Briefly describe what you're trying to optimize",
"hint": "e.g., 'Minimize bracket mass while maintaining stiffness'",
"required": True
}
],
InterviewState.GATHERING_MODEL: [
{
"field": "sim_file",
"question": "What is the path to your simulation (.sim) file?",
"hint": "Relative path from the study folder, e.g., '1_setup/Model_sim1.sim'",
"required": True
}
],
InterviewState.GATHERING_VARIABLES: [
{
"field": "design_variable",
"question": "What parameters do you want to optimize?",
"hint": "Tell me the NX expression names and their bounds",
"required": True,
"multi": True
}
],
InterviewState.GATHERING_EXTRACTORS: [
{
"field": "extractor",
"question": "What physics quantities do you want to extract from FEA?",
"hint": "e.g., mass, max displacement, max stress, frequency, Zernike WFE",
"required": True,
"multi": True
}
],
InterviewState.GATHERING_OBJECTIVES: [
{
"field": "objective",
"question": "What do you want to optimize?",
"hint": "Tell me which extracted quantities to minimize or maximize",
"required": True,
"multi": True
}
],
InterviewState.GATHERING_CONSTRAINTS: [
{
"field": "constraint",
"question": "Do you have any constraints? (e.g., max stress, min frequency)",
"hint": "You can say 'none' if you don't have any",
"required": False,
"multi": True
}
],
InterviewState.GATHERING_SETTINGS: [
{
"field": "algorithm",
"question": "Which optimization algorithm would you like to use?",
"hint": "Options: TPE (default), CMA-ES, NSGA-II, RandomSearch",
"required": False
},
{
"field": "max_trials",
"question": "How many trials (FEA evaluations) should we run?",
"hint": "Default is 100. More trials = better results but longer runtime",
"required": False
}
],
InterviewState.REVIEW: [
{
"field": "confirm",
"question": "Does this configuration look correct? (yes/no)",
"required": True
}
]
}
return questions.get(self.state, [])
def record_answer(self, field: str, value: Any) -> Dict[str, Any]:
"""Record an answer and potentially advance the state"""
self.questions_asked.append(field)
# Handle different field types
if field == "study_name":
self.data.study_name = value
elif field == "category":
self.data.category = value if value else None
elif field == "description":
self.data.description = value
elif field == "sim_file":
self.data.sim_file = value
elif field == "design_variable":
# Value should be a dict with name, min, max, etc.
if isinstance(value, dict):
self.data.design_variables.append(value)
elif isinstance(value, list):
self.data.design_variables.extend(value)
elif field == "extractor":
if isinstance(value, dict):
self.data.extractors.append(value)
elif isinstance(value, list):
self.data.extractors.extend(value)
elif field == "objective":
if isinstance(value, dict):
self.data.objectives.append(value)
elif isinstance(value, list):
self.data.objectives.extend(value)
elif field == "constraint":
if value and value.lower() not in ["none", "no", "skip"]:
if isinstance(value, dict):
self.data.constraints.append(value)
elif isinstance(value, list):
self.data.constraints.extend(value)
elif field == "algorithm":
if value in ["TPE", "CMA-ES", "NSGA-II", "RandomSearch"]:
self.data.algorithm = value
elif field == "max_trials":
try:
self.data.max_trials = int(value)
except (ValueError, TypeError):
pass
elif field == "confirm":
if value.lower() in ["yes", "y", "confirm", "ok"]:
self.state = InterviewState.COMPLETED
return {
"state": self.state.value,
"recorded": {field: value},
"data_so_far": self.get_summary()
}
def advance_state(self) -> Dict[str, Any]:
"""Advance to the next interview state"""
state_order = [
InterviewState.NOT_STARTED,
InterviewState.GATHERING_BASICS,
InterviewState.GATHERING_MODEL,
InterviewState.GATHERING_VARIABLES,
InterviewState.GATHERING_EXTRACTORS,
InterviewState.GATHERING_OBJECTIVES,
InterviewState.GATHERING_CONSTRAINTS,
InterviewState.GATHERING_SETTINGS,
InterviewState.REVIEW,
InterviewState.COMPLETED
]
current_idx = state_order.index(self.state)
if current_idx < len(state_order) - 1:
self.state = state_order[current_idx + 1]
return {
"state": self.state.value,
"next_questions": self.get_current_questions()
}
def get_summary(self) -> Dict[str, Any]:
"""Get a summary of collected data"""
return {
"study_name": self.data.study_name,
"category": self.data.category,
"description": self.data.description,
"model": self.data.sim_file,
"design_variables": len(self.data.design_variables),
"extractors": len(self.data.extractors),
"objectives": len(self.data.objectives),
"constraints": len(self.data.constraints),
"algorithm": self.data.algorithm,
"max_trials": self.data.max_trials
}
def get_progress(self) -> Dict[str, Any]:
"""Get interview progress information"""
state_progress = {
InterviewState.NOT_STARTED: 0,
InterviewState.GATHERING_BASICS: 15,
InterviewState.GATHERING_MODEL: 25,
InterviewState.GATHERING_VARIABLES: 40,
InterviewState.GATHERING_EXTRACTORS: 55,
InterviewState.GATHERING_OBJECTIVES: 70,
InterviewState.GATHERING_CONSTRAINTS: 80,
InterviewState.GATHERING_SETTINGS: 90,
InterviewState.REVIEW: 95,
InterviewState.COMPLETED: 100
}
return {
"state": self.state.value,
"progress_percent": state_progress.get(self.state, 0),
"summary": self.get_summary(),
"current_questions": self.get_current_questions()
}
def validate(self) -> Dict[str, Any]:
"""Validate the collected data before finalizing"""
errors = []
warnings = []
# Required fields
if not self.data.study_name:
errors.append("Study name is required")
if not self.data.design_variables:
errors.append("At least one design variable is required")
if not self.data.extractors:
errors.append("At least one extractor is required")
if not self.data.objectives:
errors.append("At least one objective is required")
# Warnings
if not self.data.sim_file:
warnings.append("No simulation file specified - you'll need to add one manually")
if not self.data.constraints:
warnings.append("No constraints defined - optimization will be unconstrained")
return {
"valid": len(errors) == 0,
"errors": errors,
"warnings": warnings
}
def finalize(self) -> Dict[str, Any]:
"""Generate the final atomizer_spec.json"""
validation = self.validate()
if not validation["valid"]:
return {
"success": False,
"errors": validation["errors"]
}
spec = self.data.to_spec()
return {
"success": True,
"spec": spec,
"warnings": validation.get("warnings", [])
}
def to_dict(self) -> Dict[str, Any]:
"""Serialize engine state for persistence"""
return {
"state": self.state.value,
"data": {
"study_name": self.data.study_name,
"category": self.data.category,
"description": self.data.description,
"goals": self.data.goals,
"sim_file": self.data.sim_file,
"prt_file": self.data.prt_file,
"solver_type": self.data.solver_type,
"design_variables": self.data.design_variables,
"extractors": self.data.extractors,
"objectives": self.data.objectives,
"constraints": self.data.constraints,
"algorithm": self.data.algorithm,
"max_trials": self.data.max_trials
},
"questions_asked": self.questions_asked,
"errors": self.errors
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "InterviewEngine":
"""Restore engine from serialized state"""
engine = cls()
engine.state = InterviewState(data.get("state", "not_started"))
d = data.get("data", {})
engine.data.study_name = d.get("study_name")
engine.data.category = d.get("category")
engine.data.description = d.get("description")
engine.data.goals = d.get("goals", [])
engine.data.sim_file = d.get("sim_file")
engine.data.prt_file = d.get("prt_file")
engine.data.solver_type = d.get("solver_type", "nastran")
engine.data.design_variables = d.get("design_variables", [])
engine.data.extractors = d.get("extractors", [])
engine.data.objectives = d.get("objectives", [])
engine.data.constraints = d.get("constraints", [])
engine.data.algorithm = d.get("algorithm", "TPE")
engine.data.max_trials = d.get("max_trials", 100)
engine.questions_asked = data.get("questions_asked", [])
engine.errors = data.get("errors", [])
return engine

View File

@@ -219,6 +219,18 @@ class SessionManager:
full_response = result["stdout"] or "" full_response = result["stdout"] or ""
if full_response: if full_response:
# Check if response contains canvas modifications (from MCP tools)
import logging
logger = logging.getLogger(__name__)
modifications = self._extract_canvas_modifications(full_response)
logger.info(f"[SEND_MSG] Found {len(modifications)} canvas modifications to send")
for mod in modifications:
logger.info(f"[SEND_MSG] Sending canvas_modification: {mod.get('action')} {mod.get('nodeType')}")
yield {"type": "canvas_modification", "modification": mod}
# Always send the text response
yield {"type": "text", "content": full_response} yield {"type": "text", "content": full_response}
if result["returncode"] != 0 and result["stderr"]: if result["returncode"] != 0 and result["stderr"]:
@@ -292,6 +304,90 @@ class SessionManager:
**({} if not db_record else {"db_record": db_record}), **({} if not db_record else {"db_record": db_record}),
} }
def _extract_canvas_modifications(self, response: str) -> List[Dict]:
"""
Extract canvas modification objects from Claude's response.
MCP tools like canvas_add_node return JSON with a 'modification' field.
This method finds and extracts those modifications so the frontend can apply them.
"""
import re
import logging
logger = logging.getLogger(__name__)
modifications = []
# Debug: log what we're searching
logger.info(f"[CANVAS_MOD] Searching response ({len(response)} chars) for modifications")
# Check if "modification" even exists in the response
if '"modification"' not in response:
logger.info("[CANVAS_MOD] No 'modification' key found in response")
return modifications
try:
# Method 1: Look for JSON in code fences
code_block_pattern = r'```(?:json)?\s*([\s\S]*?)```'
for match in re.finditer(code_block_pattern, response):
block_content = match.group(1).strip()
try:
obj = json.loads(block_content)
if isinstance(obj, dict) and 'modification' in obj:
logger.info(f"[CANVAS_MOD] Found modification in code fence: {obj['modification']}")
modifications.append(obj['modification'])
except json.JSONDecodeError:
continue
# Method 2: Find JSON objects using proper brace matching
# This handles nested objects correctly
i = 0
while i < len(response):
if response[i] == '{':
# Found a potential JSON start, find matching close
brace_count = 1
j = i + 1
in_string = False
escape_next = False
while j < len(response) and brace_count > 0:
char = response[j]
if escape_next:
escape_next = False
elif char == '\\':
escape_next = True
elif char == '"' and not escape_next:
in_string = not in_string
elif not in_string:
if char == '{':
brace_count += 1
elif char == '}':
brace_count -= 1
j += 1
if brace_count == 0:
potential_json = response[i:j]
try:
obj = json.loads(potential_json)
if isinstance(obj, dict) and 'modification' in obj:
mod = obj['modification']
# Avoid duplicates
if mod not in modifications:
logger.info(f"[CANVAS_MOD] Found inline modification: action={mod.get('action')}, nodeType={mod.get('nodeType')}")
modifications.append(mod)
except json.JSONDecodeError as e:
# Not valid JSON, skip
pass
i = j
else:
i += 1
except Exception as e:
logger.error(f"[CANVAS_MOD] Error extracting modifications: {e}")
logger.info(f"[CANVAS_MOD] Extracted {len(modifications)} modification(s)")
return modifications
def _build_mcp_config(self, mode: Literal["user", "power"]) -> dict: def _build_mcp_config(self, mode: Literal["user", "power"]) -> dict:
"""Build MCP configuration for Claude""" """Build MCP configuration for Claude"""
return { return {

View File

@@ -0,0 +1,747 @@
"""
SpecManager Service
Central service for managing AtomizerSpec v2.0.
All spec modifications flow through this service.
Features:
- Load/save specs with validation
- Atomic writes with conflict detection
- Patch operations with JSONPath support
- Node CRUD operations
- Custom function support
- WebSocket broadcast integration
"""
import hashlib
import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
# Add optimization_engine to path if needed
ATOMIZER_ROOT = Path(__file__).parent.parent.parent.parent.parent
if str(ATOMIZER_ROOT) not in sys.path:
sys.path.insert(0, str(ATOMIZER_ROOT))
from optimization_engine.config.spec_models import (
AtomizerSpec,
DesignVariable,
Extractor,
Objective,
Constraint,
CanvasPosition,
CanvasEdge,
ExtractorType,
CustomFunction,
ExtractorOutput,
ValidationReport,
)
from optimization_engine.config.spec_validator import (
SpecValidator,
SpecValidationError,
)
class SpecManagerError(Exception):
"""Base error for SpecManager operations."""
pass
class SpecNotFoundError(SpecManagerError):
"""Raised when spec file doesn't exist."""
pass
class SpecConflictError(SpecManagerError):
"""Raised when spec has been modified by another client."""
def __init__(self, message: str, current_hash: str):
super().__init__(message)
self.current_hash = current_hash
class WebSocketSubscriber:
"""Protocol for WebSocket subscribers."""
async def send_json(self, data: Dict[str, Any]) -> None:
"""Send JSON data to subscriber."""
raise NotImplementedError
class SpecManager:
"""
Central service for managing AtomizerSpec.
All modifications go through this service to ensure:
- Validation on every change
- Atomic file writes
- Conflict detection via hashing
- WebSocket broadcast to all clients
"""
SPEC_FILENAME = "atomizer_spec.json"
def __init__(self, study_path: Union[str, Path]):
"""
Initialize SpecManager for a study.
Args:
study_path: Path to the study directory
"""
self.study_path = Path(study_path)
self.spec_path = self.study_path / self.SPEC_FILENAME
self.validator = SpecValidator()
self._subscribers: List[WebSocketSubscriber] = []
self._last_hash: Optional[str] = None
# =========================================================================
# Core CRUD Operations
# =========================================================================
def load(self, validate: bool = True) -> AtomizerSpec:
"""
Load and optionally validate the spec.
Args:
validate: Whether to validate the spec
Returns:
AtomizerSpec instance
Raises:
SpecNotFoundError: If spec file doesn't exist
SpecValidationError: If validation fails
"""
if not self.spec_path.exists():
raise SpecNotFoundError(f"Spec not found: {self.spec_path}")
with open(self.spec_path, 'r', encoding='utf-8') as f:
data = json.load(f)
if validate:
self.validator.validate(data, strict=True)
spec = AtomizerSpec.model_validate(data)
self._last_hash = self._compute_hash(data)
return spec
def load_raw(self) -> Dict[str, Any]:
"""
Load spec as raw dict without parsing.
Returns:
Raw spec dict
Raises:
SpecNotFoundError: If spec file doesn't exist
"""
if not self.spec_path.exists():
raise SpecNotFoundError(f"Spec not found: {self.spec_path}")
with open(self.spec_path, 'r', encoding='utf-8') as f:
return json.load(f)
def save(
self,
spec: Union[AtomizerSpec, Dict[str, Any]],
modified_by: str = "api",
expected_hash: Optional[str] = None
) -> str:
"""
Save spec with validation and broadcast.
Args:
spec: Spec to save (AtomizerSpec or dict)
modified_by: Who/what is making the change
expected_hash: If provided, verify current file hash matches
Returns:
New spec hash
Raises:
SpecValidationError: If validation fails
SpecConflictError: If expected_hash doesn't match current
"""
# Convert to dict if needed
if isinstance(spec, AtomizerSpec):
data = spec.model_dump(mode='json')
else:
data = spec
# Check for conflicts if expected_hash provided
if expected_hash and self.spec_path.exists():
current_hash = self.get_hash()
if current_hash != expected_hash:
raise SpecConflictError(
"Spec was modified by another client",
current_hash=current_hash
)
# Update metadata
now = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
data["meta"]["modified"] = now
data["meta"]["modified_by"] = modified_by
# Validate
self.validator.validate(data, strict=True)
# Compute new hash
new_hash = self._compute_hash(data)
# Atomic write (write to temp, then rename)
temp_path = self.spec_path.with_suffix('.tmp')
with open(temp_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
temp_path.replace(self.spec_path)
# Update cached hash
self._last_hash = new_hash
# Broadcast to subscribers
self._broadcast({
"type": "spec_updated",
"hash": new_hash,
"modified_by": modified_by,
"timestamp": now
})
return new_hash
def exists(self) -> bool:
"""Check if spec file exists."""
return self.spec_path.exists()
def get_hash(self) -> str:
"""Get current spec hash."""
if not self.spec_path.exists():
return ""
with open(self.spec_path, 'r', encoding='utf-8') as f:
data = json.load(f)
return self._compute_hash(data)
def validate_and_report(self) -> ValidationReport:
"""
Run full validation and return detailed report.
Returns:
ValidationReport with errors, warnings, summary
"""
if not self.spec_path.exists():
raise SpecNotFoundError(f"Spec not found: {self.spec_path}")
data = self.load_raw()
return self.validator.validate(data, strict=False)
# =========================================================================
# Patch Operations
# =========================================================================
def patch(
self,
path: str,
value: Any,
modified_by: str = "api"
) -> AtomizerSpec:
"""
Apply a JSONPath-style modification.
Args:
path: JSONPath like "design_variables[0].bounds.max"
value: New value to set
modified_by: Who/what is making the change
Returns:
Updated AtomizerSpec
"""
data = self.load_raw()
# Validate the partial update
spec = AtomizerSpec.model_validate(data)
is_valid, errors = self.validator.validate_partial(path, value, spec)
if not is_valid:
raise SpecValidationError(f"Invalid update: {'; '.join(errors)}")
# Apply the patch
self._apply_patch(data, path, value)
# Save and return
self.save(data, modified_by)
return self.load(validate=False)
def _apply_patch(self, data: Dict, path: str, value: Any) -> None:
"""
Apply a patch to the data dict.
Supports paths like:
- "meta.description"
- "design_variables[0].bounds.max"
- "objectives[1].weight"
"""
parts = self._parse_path(path)
if not parts:
raise ValueError(f"Invalid path: {path}")
# Navigate to parent
current = data
for part in parts[:-1]:
if isinstance(current, list):
idx = int(part)
current = current[idx]
else:
current = current[part]
# Set final value
final_key = parts[-1]
if isinstance(current, list):
idx = int(final_key)
current[idx] = value
else:
current[final_key] = value
def _parse_path(self, path: str) -> List[str]:
"""Parse JSONPath into parts."""
# Handle both dot notation and bracket notation
parts = []
for part in re.split(r'\.|\[|\]', path):
if part:
parts.append(part)
return parts
# =========================================================================
# Node Operations
# =========================================================================
def add_node(
self,
node_type: str,
node_data: Dict[str, Any],
modified_by: str = "canvas"
) -> str:
"""
Add a new node (design var, extractor, objective, constraint).
Args:
node_type: One of 'designVar', 'extractor', 'objective', 'constraint'
node_data: Node data without ID
modified_by: Who/what is making the change
Returns:
Generated node ID
"""
data = self.load_raw()
# Generate ID
node_id = self._generate_id(node_type, data)
node_data["id"] = node_id
# Add canvas position if not provided
if "canvas_position" not in node_data:
node_data["canvas_position"] = self._auto_position(node_type, data)
# Add to appropriate section
section = self._get_section_for_type(node_type)
if section not in data or data[section] is None:
data[section] = []
data[section].append(node_data)
self.save(data, modified_by)
# Broadcast node addition
self._broadcast({
"type": "node_added",
"node_type": node_type,
"node_id": node_id,
"modified_by": modified_by
})
return node_id
def update_node(
self,
node_id: str,
updates: Dict[str, Any],
modified_by: str = "canvas"
) -> None:
"""
Update an existing node.
Args:
node_id: ID of the node to update
updates: Dict of fields to update
modified_by: Who/what is making the change
"""
data = self.load_raw()
# Find and update the node
found = False
for section in ["design_variables", "extractors", "objectives", "constraints"]:
if section not in data or data[section] is None:
continue
for node in data[section]:
if node.get("id") == node_id:
node.update(updates)
found = True
break
if found:
break
if not found:
raise SpecManagerError(f"Node not found: {node_id}")
self.save(data, modified_by)
def remove_node(
self,
node_id: str,
modified_by: str = "canvas"
) -> None:
"""
Remove a node and all edges referencing it.
Args:
node_id: ID of the node to remove
modified_by: Who/what is making the change
"""
data = self.load_raw()
# Find and remove node
removed = False
for section in ["design_variables", "extractors", "objectives", "constraints"]:
if section not in data or data[section] is None:
continue
original_len = len(data[section])
data[section] = [n for n in data[section] if n.get("id") != node_id]
if len(data[section]) < original_len:
removed = True
break
if not removed:
raise SpecManagerError(f"Node not found: {node_id}")
# Remove edges referencing this node
if "canvas" in data and data["canvas"] and "edges" in data["canvas"]:
data["canvas"]["edges"] = [
e for e in data["canvas"]["edges"]
if e.get("source") != node_id and e.get("target") != node_id
]
self.save(data, modified_by)
# Broadcast node removal
self._broadcast({
"type": "node_removed",
"node_id": node_id,
"modified_by": modified_by
})
def update_node_position(
self,
node_id: str,
position: Dict[str, float],
modified_by: str = "canvas"
) -> None:
"""
Update a node's canvas position.
Args:
node_id: ID of the node
position: Dict with x, y coordinates
modified_by: Who/what is making the change
"""
self.update_node(node_id, {"canvas_position": position}, modified_by)
def add_edge(
self,
source: str,
target: str,
modified_by: str = "canvas"
) -> None:
"""
Add a canvas edge between nodes.
Args:
source: Source node ID
target: Target node ID
modified_by: Who/what is making the change
"""
data = self.load_raw()
# Initialize canvas section if needed
if "canvas" not in data or data["canvas"] is None:
data["canvas"] = {}
if "edges" not in data["canvas"] or data["canvas"]["edges"] is None:
data["canvas"]["edges"] = []
# Check for duplicate
for edge in data["canvas"]["edges"]:
if edge.get("source") == source and edge.get("target") == target:
return # Already exists
data["canvas"]["edges"].append({
"source": source,
"target": target
})
self.save(data, modified_by)
def remove_edge(
self,
source: str,
target: str,
modified_by: str = "canvas"
) -> None:
"""
Remove a canvas edge.
Args:
source: Source node ID
target: Target node ID
modified_by: Who/what is making the change
"""
data = self.load_raw()
if "canvas" in data and data["canvas"] and "edges" in data["canvas"]:
data["canvas"]["edges"] = [
e for e in data["canvas"]["edges"]
if not (e.get("source") == source and e.get("target") == target)
]
self.save(data, modified_by)
# =========================================================================
# Custom Function Support
# =========================================================================
def add_custom_function(
self,
name: str,
code: str,
outputs: List[str],
description: Optional[str] = None,
modified_by: str = "claude"
) -> str:
"""
Add a custom extractor function.
Args:
name: Function name
code: Python source code
outputs: List of output names
description: Optional description
modified_by: Who/what is making the change
Returns:
Generated extractor ID
Raises:
SpecValidationError: If Python syntax is invalid
"""
# Validate Python syntax
try:
compile(code, f"<custom:{name}>", "exec")
except SyntaxError as e:
raise SpecValidationError(
f"Invalid Python syntax: {e.msg} at line {e.lineno}"
)
data = self.load_raw()
# Generate extractor ID
ext_id = self._generate_id("extractor", data)
# Create extractor
extractor = {
"id": ext_id,
"name": description or f"Custom: {name}",
"type": "custom_function",
"builtin": False,
"function": {
"name": name,
"module": "custom_extractors.dynamic",
"source_code": code
},
"outputs": [{"name": o, "metric": "custom"} for o in outputs],
"canvas_position": self._auto_position("extractor", data)
}
data["extractors"].append(extractor)
self.save(data, modified_by)
return ext_id
def update_custom_function(
self,
extractor_id: str,
code: Optional[str] = None,
outputs: Optional[List[str]] = None,
modified_by: str = "claude"
) -> None:
"""
Update an existing custom function.
Args:
extractor_id: ID of the custom extractor
code: New Python code (optional)
outputs: New outputs (optional)
modified_by: Who/what is making the change
"""
data = self.load_raw()
# Find the extractor
extractor = None
for ext in data.get("extractors", []):
if ext.get("id") == extractor_id:
extractor = ext
break
if not extractor:
raise SpecManagerError(f"Extractor not found: {extractor_id}")
if extractor.get("type") != "custom_function":
raise SpecManagerError(f"Extractor {extractor_id} is not a custom function")
# Update code
if code is not None:
try:
compile(code, f"<custom:{extractor_id}>", "exec")
except SyntaxError as e:
raise SpecValidationError(
f"Invalid Python syntax: {e.msg} at line {e.lineno}"
)
if "function" not in extractor:
extractor["function"] = {}
extractor["function"]["source_code"] = code
# Update outputs
if outputs is not None:
extractor["outputs"] = [{"name": o, "metric": "custom"} for o in outputs]
self.save(data, modified_by)
# =========================================================================
# WebSocket Subscription
# =========================================================================
def subscribe(self, subscriber: WebSocketSubscriber) -> None:
"""Subscribe to spec changes."""
if subscriber not in self._subscribers:
self._subscribers.append(subscriber)
def unsubscribe(self, subscriber: WebSocketSubscriber) -> None:
"""Unsubscribe from spec changes."""
if subscriber in self._subscribers:
self._subscribers.remove(subscriber)
def _broadcast(self, message: Dict[str, Any]) -> None:
"""Broadcast message to all subscribers."""
import asyncio
for subscriber in self._subscribers:
try:
# Handle both sync and async contexts
try:
loop = asyncio.get_running_loop()
loop.create_task(subscriber.send_json(message))
except RuntimeError:
# No running loop, try direct call if possible
pass
except Exception:
# Subscriber may have disconnected
pass
# =========================================================================
# Helper Methods
# =========================================================================
def _compute_hash(self, data: Dict) -> str:
"""Compute hash of spec data for conflict detection."""
# Sort keys for consistent hashing
json_str = json.dumps(data, sort_keys=True, ensure_ascii=False)
return hashlib.sha256(json_str.encode()).hexdigest()[:16]
def _generate_id(self, node_type: str, data: Dict) -> str:
"""Generate unique ID for a node type."""
prefix_map = {
"designVar": "dv",
"design_variable": "dv",
"extractor": "ext",
"objective": "obj",
"constraint": "con"
}
prefix = prefix_map.get(node_type, node_type[:3])
# Find existing IDs
section = self._get_section_for_type(node_type)
existing_ids: Set[str] = set()
if section in data and data[section]:
existing_ids = {n.get("id", "") for n in data[section]}
# Generate next available ID
for i in range(1, 1000):
new_id = f"{prefix}_{i:03d}"
if new_id not in existing_ids:
return new_id
raise SpecManagerError(f"Cannot generate ID for {node_type}: too many nodes")
def _get_section_for_type(self, node_type: str) -> str:
"""Map node type to spec section name."""
section_map = {
"designVar": "design_variables",
"design_variable": "design_variables",
"extractor": "extractors",
"objective": "objectives",
"constraint": "constraints"
}
return section_map.get(node_type, node_type + "s")
def _auto_position(self, node_type: str, data: Dict) -> Dict[str, float]:
"""Calculate auto position for a new node."""
# Default x positions by type
x_positions = {
"designVar": 50,
"design_variable": 50,
"extractor": 740,
"objective": 1020,
"constraint": 1020
}
x = x_positions.get(node_type, 400)
# Find max y position for this type
section = self._get_section_for_type(node_type)
max_y = 0
if section in data and data[section]:
for node in data[section]:
pos = node.get("canvas_position", {})
y = pos.get("y", 0)
if y > max_y:
max_y = y
# Place below existing nodes
y = max_y + 100 if max_y > 0 else 100
return {"x": x, "y": y}
# =========================================================================
# Factory Function
# =========================================================================
def get_spec_manager(study_path: Union[str, Path]) -> SpecManager:
"""
Get a SpecManager instance for a study.
Args:
study_path: Path to the study directory
Returns:
SpecManager instance
"""
return SpecManager(study_path)

View File

@@ -30,6 +30,7 @@ function App() {
{/* Canvas page - full screen, no sidebar */} {/* Canvas page - full screen, no sidebar */}
<Route path="canvas" element={<CanvasView />} /> <Route path="canvas" element={<CanvasView />} />
<Route path="canvas/*" element={<CanvasView />} />
{/* Study pages - with sidebar layout */} {/* Study pages - with sidebar layout */}
<Route element={<MainLayout />}> <Route element={<MainLayout />}>

View File

@@ -26,8 +26,8 @@ interface DesignVariable {
name: string; name: string;
parameter?: string; // Optional: the actual parameter name if different from name parameter?: string; // Optional: the actual parameter name if different from name
unit?: string; unit?: string;
min: number; min?: number;
max: number; max?: number;
} }
interface Constraint { interface Constraint {

View File

@@ -8,14 +8,15 @@ import { ScatterChart, Scatter, Line, XAxis, YAxis, CartesianGrid, Tooltip, Cell
interface ParetoTrial { interface ParetoTrial {
trial_number: number; trial_number: number;
values: [number, number]; values: number[]; // Support variable number of objectives
params: Record<string, number>; params: Record<string, number>;
constraint_satisfied?: boolean; constraint_satisfied?: boolean;
} }
interface Objective { interface Objective {
name: string; name: string;
type: 'minimize' | 'maximize'; type?: 'minimize' | 'maximize';
direction?: 'minimize' | 'maximize'; // Alternative field used by some configs
unit?: string; unit?: string;
} }

View File

@@ -0,0 +1,49 @@
/**
* ConnectionStatusIndicator - Visual indicator for WebSocket connection status.
*/
import { ConnectionStatus } from '../../hooks/useSpecWebSocket';
interface ConnectionStatusIndicatorProps {
status: ConnectionStatus;
className?: string;
}
/**
* Visual indicator for WebSocket connection status.
* Can be used in the canvas UI to show sync state.
*/
export function ConnectionStatusIndicator({
status,
className = '',
}: ConnectionStatusIndicatorProps) {
const statusConfig = {
disconnected: {
color: 'bg-gray-500',
label: 'Disconnected',
},
connecting: {
color: 'bg-yellow-500 animate-pulse',
label: 'Connecting...',
},
connected: {
color: 'bg-green-500',
label: 'Connected',
},
reconnecting: {
color: 'bg-yellow-500 animate-pulse',
label: 'Reconnecting...',
},
};
const config = statusConfig[status];
return (
<div className={`flex items-center gap-2 ${className}`}>
<div className={`w-2 h-2 rounded-full ${config.color}`} />
<span className="text-xs text-dark-400">{config.label}</span>
</div>
);
}
export default ConnectionStatusIndicator;

View File

@@ -1,5 +1,6 @@
// Main Canvas Component // Main Canvas Component
export { AtomizerCanvas } from './AtomizerCanvas'; export { AtomizerCanvas } from './AtomizerCanvas';
export { SpecRenderer } from './SpecRenderer';
// Palette // Palette
export { NodePalette } from './palette/NodePalette'; export { NodePalette } from './palette/NodePalette';

View File

@@ -0,0 +1,58 @@
/**
* CustomExtractorNode - Canvas node for custom Python extractors
*
* Displays custom extractors defined with inline Python code.
* Visually distinct from builtin extractors with a code icon.
*
* P3.11: Custom extractor UI component
*/
import { memo } from 'react';
import { NodeProps } from 'reactflow';
import { Code2 } from 'lucide-react';
import { BaseNode } from './BaseNode';
export interface CustomExtractorNodeData {
type: 'customExtractor';
label: string;
configured: boolean;
extractorId?: string;
extractorName?: string;
functionName?: string;
functionSource?: string;
outputs?: Array<{ name: string; units?: string }>;
dependencies?: string[];
}
function CustomExtractorNodeComponent(props: NodeProps<CustomExtractorNodeData>) {
const { data } = props;
// Show validation status
const hasCode = !!data.functionSource?.trim();
const hasOutputs = (data.outputs?.length ?? 0) > 0;
const isConfigured = hasCode && hasOutputs;
return (
<BaseNode
{...props}
icon={<Code2 size={16} />}
iconColor={isConfigured ? 'text-violet-400' : 'text-dark-500'}
>
<div className="flex flex-col">
<span className={isConfigured ? 'text-white' : 'text-dark-400'}>
{data.extractorName || data.functionName || 'Custom Extractor'}
</span>
{!isConfigured && (
<span className="text-xs text-amber-400">Needs configuration</span>
)}
{isConfigured && data.outputs && (
<span className="text-xs text-dark-400">
{data.outputs.length} output{data.outputs.length !== 1 ? 's' : ''}
</span>
)}
</div>
</BaseNode>
);
}
export const CustomExtractorNode = memo(CustomExtractorNodeComponent);

View File

@@ -0,0 +1,360 @@
/**
* CustomExtractorPanel - Panel for editing custom Python extractors
*
* Provides a code editor for writing custom extraction functions,
* output definitions, and validation.
*
* P3.12: Custom extractor UI component
*/
import { useState, useCallback } from 'react';
import { X, Play, AlertCircle, CheckCircle, Plus, Trash2, HelpCircle } from 'lucide-react';
interface CustomExtractorOutput {
name: string;
units?: string;
description?: string;
}
interface CustomExtractorPanelProps {
isOpen: boolean;
onClose: () => void;
initialName?: string;
initialFunctionName?: string;
initialSource?: string;
initialOutputs?: CustomExtractorOutput[];
initialDependencies?: string[];
onSave: (data: {
name: string;
functionName: string;
source: string;
outputs: CustomExtractorOutput[];
dependencies: string[];
}) => void;
}
// Common styling classes
const inputClass =
'w-full px-3 py-2 bg-dark-800 border border-dark-600 text-white placeholder-dark-400 rounded-lg focus:border-primary-500 focus:outline-none transition-colors';
const labelClass = 'block text-sm font-medium text-dark-300 mb-1';
// Default extractor template
const DEFAULT_SOURCE = `def extract(op2_path, bdf_path=None, params=None, working_dir=None):
"""
Custom extractor function.
Args:
op2_path: Path to the OP2 results file
bdf_path: Optional path to the BDF model file
params: Dictionary of current design parameters
working_dir: Path to the current trial directory
Returns:
Dictionary of output_name -> value
OR a single float value
OR a list/tuple of values (mapped to outputs in order)
"""
import numpy as np
from pyNastran.op2.op2 import OP2
# Load OP2 results
op2 = OP2(op2_path, debug=False)
# Example: compute custom metric
# ... your extraction logic here ...
result = 0.0
return {"custom_output": result}
`;
export function CustomExtractorPanel({
isOpen,
onClose,
initialName = '',
initialFunctionName = 'extract',
initialSource = DEFAULT_SOURCE,
initialOutputs = [{ name: 'custom_output', units: '' }],
initialDependencies = [],
onSave,
}: CustomExtractorPanelProps) {
const [name, setName] = useState(initialName);
const [functionName, setFunctionName] = useState(initialFunctionName);
const [source, setSource] = useState(initialSource);
const [outputs, setOutputs] = useState<CustomExtractorOutput[]>(initialOutputs);
const [dependencies] = useState<string[]>(initialDependencies);
const [validation, setValidation] = useState<{
valid: boolean;
errors: string[];
} | null>(null);
const [isValidating, setIsValidating] = useState(false);
const [showHelp, setShowHelp] = useState(false);
// Add a new output
const addOutput = useCallback(() => {
setOutputs((prev) => [...prev, { name: '', units: '' }]);
}, []);
// Remove an output
const removeOutput = useCallback((index: number) => {
setOutputs((prev) => prev.filter((_, i) => i !== index));
}, []);
// Update an output
const updateOutput = useCallback(
(index: number, field: keyof CustomExtractorOutput, value: string) => {
setOutputs((prev) =>
prev.map((out, i) => (i === index ? { ...out, [field]: value } : out))
);
},
[]
);
// Validate the code
const validateCode = useCallback(async () => {
setIsValidating(true);
setValidation(null);
try {
const response = await fetch('/api/spec/validate-extractor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
function_name: functionName,
source: source,
}),
});
const result = await response.json();
setValidation({
valid: result.valid,
errors: result.errors || [],
});
} catch (error) {
setValidation({
valid: false,
errors: ['Failed to validate: ' + (error instanceof Error ? error.message : 'Unknown error')],
});
} finally {
setIsValidating(false);
}
}, [functionName, source]);
// Handle save
const handleSave = useCallback(() => {
// Filter out empty outputs
const validOutputs = outputs.filter((o) => o.name.trim());
if (!name.trim()) {
setValidation({ valid: false, errors: ['Name is required'] });
return;
}
if (validOutputs.length === 0) {
setValidation({ valid: false, errors: ['At least one output is required'] });
return;
}
onSave({
name: name.trim(),
functionName: functionName.trim() || 'extract',
source,
outputs: validOutputs,
dependencies: dependencies.filter((d) => d.trim()),
});
onClose();
}, [name, functionName, source, outputs, dependencies, onSave, onClose]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-dark-850 rounded-xl shadow-2xl w-[900px] max-h-[90vh] flex flex-col border border-dark-700">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-dark-700">
<h2 className="text-lg font-semibold text-white">Custom Extractor</h2>
<div className="flex items-center gap-2">
<button
onClick={() => setShowHelp(!showHelp)}
className="p-2 text-dark-400 hover:text-white hover:bg-dark-700 rounded-lg transition-colors"
title="Show help"
>
<HelpCircle size={20} />
</button>
<button
onClick={onClose}
className="p-2 text-dark-400 hover:text-white hover:bg-dark-700 rounded-lg transition-colors"
>
<X size={20} />
</button>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-auto p-6">
{/* Help Section */}
{showHelp && (
<div className="mb-4 p-4 bg-primary-900/20 border border-primary-700 rounded-lg">
<h3 className="text-sm font-semibold text-primary-400 mb-2">How Custom Extractors Work</h3>
<ul className="text-sm text-dark-300 space-y-1">
<li> Your function receives the path to OP2 results and optional BDF/params</li>
<li> Use pyNastran, numpy, scipy for data extraction and analysis</li>
<li> Return a dictionary mapping output names to numeric values</li>
<li> Outputs can be used as objectives or constraints in optimization</li>
<li> Code runs in a sandboxed environment (no file I/O beyond OP2/BDF)</li>
</ul>
</div>
)}
<div className="grid grid-cols-2 gap-6">
{/* Left Column - Basic Info & Outputs */}
<div className="space-y-4">
{/* Name */}
<div>
<label className={labelClass}>Extractor Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="My Custom Extractor"
className={inputClass}
/>
</div>
{/* Function Name */}
<div>
<label className={labelClass}>Function Name</label>
<input
type="text"
value={functionName}
onChange={(e) => setFunctionName(e.target.value)}
placeholder="extract"
className={`${inputClass} font-mono`}
/>
<p className="text-xs text-dark-500 mt-1">
Name of the Python function in your code
</p>
</div>
{/* Outputs */}
<div>
<label className={labelClass}>Outputs</label>
<div className="space-y-2">
{outputs.map((output, index) => (
<div key={index} className="flex gap-2">
<input
type="text"
value={output.name}
onChange={(e) => updateOutput(index, 'name', e.target.value)}
placeholder="output_name"
className={`${inputClass} font-mono flex-1`}
/>
<input
type="text"
value={output.units || ''}
onChange={(e) => updateOutput(index, 'units', e.target.value)}
placeholder="units"
className={`${inputClass} w-24`}
/>
<button
onClick={() => removeOutput(index)}
className="p-2 text-red-400 hover:text-red-300 hover:bg-red-900/20 rounded-lg transition-colors"
disabled={outputs.length === 1}
>
<Trash2 size={16} />
</button>
</div>
))}
<button
onClick={addOutput}
className="flex items-center gap-1 text-sm text-primary-400 hover:text-primary-300 transition-colors"
>
<Plus size={14} />
Add Output
</button>
</div>
</div>
{/* Validation Status */}
{validation && (
<div
className={`p-3 rounded-lg border ${
validation.valid
? 'bg-green-900/20 border-green-700'
: 'bg-red-900/20 border-red-700'
}`}
>
<div className="flex items-center gap-2">
{validation.valid ? (
<CheckCircle size={16} className="text-green-400" />
) : (
<AlertCircle size={16} className="text-red-400" />
)}
<span
className={`text-sm font-medium ${
validation.valid ? 'text-green-400' : 'text-red-400'
}`}
>
{validation.valid ? 'Code is valid' : 'Validation failed'}
</span>
</div>
{validation.errors.length > 0 && (
<ul className="mt-2 text-sm text-red-300 space-y-1">
{validation.errors.map((err, i) => (
<li key={i}> {err}</li>
))}
</ul>
)}
</div>
)}
</div>
{/* Right Column - Code Editor */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className={labelClass}>Python Code</label>
<button
onClick={validateCode}
disabled={isValidating}
className="flex items-center gap-1 px-3 py-1 bg-primary-600 hover:bg-primary-500
text-white text-sm rounded-lg transition-colors disabled:opacity-50"
>
<Play size={14} />
{isValidating ? 'Validating...' : 'Validate'}
</button>
</div>
<textarea
value={source}
onChange={(e) => {
setSource(e.target.value);
setValidation(null);
}}
className={`${inputClass} h-[400px] font-mono text-sm resize-none`}
spellCheck={false}
/>
<p className="text-xs text-dark-500">
Available modules: numpy, scipy, pyNastran, math, statistics
</p>
</div>
</div>
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-dark-700">
<button
onClick={onClose}
className="px-4 py-2 text-dark-300 hover:text-white hover:bg-dark-700 rounded-lg transition-colors"
>
Cancel
</button>
<button
onClick={handleSave}
className="px-4 py-2 bg-primary-600 hover:bg-primary-500 text-white rounded-lg transition-colors"
>
Save Extractor
</button>
</div>
</div>
</div>
);
}

View File

@@ -5,7 +5,7 @@ import { ToolCallCard, ToolCall } from './ToolCallCard';
export interface Message { export interface Message {
id: string; id: string;
role: 'user' | 'assistant'; role: 'user' | 'assistant' | 'system';
content: string; content: string;
timestamp: Date; timestamp: Date;
isStreaming?: boolean; isStreaming?: boolean;
@@ -18,6 +18,18 @@ interface ChatMessageProps {
export const ChatMessage: React.FC<ChatMessageProps> = ({ message }) => { export const ChatMessage: React.FC<ChatMessageProps> = ({ message }) => {
const isAssistant = message.role === 'assistant'; const isAssistant = message.role === 'assistant';
const isSystem = message.role === 'system';
// System messages are displayed centered with special styling
if (isSystem) {
return (
<div className="flex justify-center my-2">
<div className="px-3 py-1 bg-dark-700/50 rounded-full text-xs text-dark-400 border border-dark-600">
{message.content}
</div>
</div>
);
}
return ( return (
<div <div

View File

@@ -1,4 +1,4 @@
import React, { useRef, useEffect, useState } from 'react'; import React, { useRef, useEffect, useState, useMemo } from 'react';
import { import {
MessageSquare, MessageSquare,
ChevronRight, ChevronRight,
@@ -13,8 +13,10 @@ import { ChatMessage } from './ChatMessage';
import { ChatInput } from './ChatInput'; import { ChatInput } from './ChatInput';
import { ThinkingIndicator } from './ThinkingIndicator'; import { ThinkingIndicator } from './ThinkingIndicator';
import { ModeToggle } from './ModeToggle'; import { ModeToggle } from './ModeToggle';
import { useChat } from '../../hooks/useChat'; import { useChat, CanvasState, CanvasModification } from '../../hooks/useChat';
import { useStudy } from '../../context/StudyContext'; import { useStudy } from '../../context/StudyContext';
import { useCanvasStore } from '../../hooks/useCanvasStore';
import { NodeType } from '../../lib/canvas/schema';
interface ChatPaneProps { interface ChatPaneProps {
isOpen: boolean; isOpen: boolean;
@@ -31,6 +33,76 @@ export const ChatPane: React.FC<ChatPaneProps> = ({
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
// Get canvas state and modification functions from the store
const { nodes, edges, addNode, updateNodeData, selectNode, deleteSelected } = useCanvasStore();
// Build canvas state for chat context
const canvasState: CanvasState | null = useMemo(() => {
if (nodes.length === 0) return null;
return {
nodes: nodes.map(n => ({
id: n.id,
type: n.type,
data: n.data,
position: n.position,
})),
edges: edges.map(e => ({
id: e.id,
source: e.source,
target: e.target,
})),
studyName: selectedStudy?.name || selectedStudy?.id,
};
}, [nodes, edges, selectedStudy]);
// Track position offset for multiple node additions
const nodeAddCountRef = useRef(0);
// Handle canvas modifications from the assistant
const handleCanvasModification = React.useCallback((modification: CanvasModification) => {
console.log('Canvas modification from assistant:', modification);
switch (modification.action) {
case 'add_node':
if (modification.nodeType) {
const nodeType = modification.nodeType as NodeType;
// Calculate position: offset each new node so they don't stack
const basePosition = modification.position || { x: 100, y: 100 };
const offset = nodeAddCountRef.current * 120;
const position = {
x: basePosition.x,
y: basePosition.y + offset,
};
nodeAddCountRef.current += 1;
// Reset counter after a delay (for batch operations)
setTimeout(() => { nodeAddCountRef.current = 0; }, 2000);
addNode(nodeType, position, modification.data);
console.log(`Added ${nodeType} node at position:`, position);
}
break;
case 'update_node':
if (modification.nodeId && modification.data) {
updateNodeData(modification.nodeId, modification.data);
}
break;
case 'remove_node':
if (modification.nodeId) {
selectNode(modification.nodeId);
deleteSelected();
}
break;
// Edge operations would need additional store methods
case 'add_edge':
case 'remove_edge':
console.warn('Edge modification not yet implemented:', modification);
break;
}
}, [addNode, updateNodeData, selectNode, deleteSelected]);
const { const {
messages, messages,
isThinking, isThinking,
@@ -41,22 +113,38 @@ export const ChatPane: React.FC<ChatPaneProps> = ({
sendMessage, sendMessage,
clearMessages, clearMessages,
switchMode, switchMode,
updateCanvasState,
} = useChat({ } = useChat({
studyId: selectedStudy?.id, studyId: selectedStudy?.id,
mode: 'user', mode: 'user',
useWebSocket: true, useWebSocket: true,
canvasState,
onError: (err) => console.error('Chat error:', err), onError: (err) => console.error('Chat error:', err),
onCanvasModification: handleCanvasModification,
}); });
// Keep canvas state synced with chat
useEffect(() => {
updateCanvasState(canvasState);
}, [canvasState, updateCanvasState]);
// Auto-scroll to bottom when new messages arrive // Auto-scroll to bottom when new messages arrive
useEffect(() => { useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, isThinking]); }, [messages, isThinking]);
// Welcome message based on study context // Welcome message based on study and canvas context
const welcomeMessage = selectedStudy const welcomeMessage = useMemo(() => {
? `Ready to help with **${selectedStudy.name || selectedStudy.id}**. Ask me about optimization progress, results analysis, or how to improve your design.` if (selectedStudy) {
: 'Select a study to get started, or ask me to help you create a new one.'; return `Ready to help with **${selectedStudy.name || selectedStudy.id}**. Ask me about optimization progress, results analysis, or how to improve your design.`;
}
if (nodes.length > 0) {
const dvCount = nodes.filter(n => n.type === 'designVar').length;
const objCount = nodes.filter(n => n.type === 'objective').length;
return `I can see your canvas with ${dvCount} design variables and ${objCount} objectives. Ask me to analyze, validate, or create a study from this setup.`;
}
return 'Select a study to get started, or build an optimization in the Canvas Builder.';
}, [selectedStudy, nodes]);
// Collapsed state - just show toggle button // Collapsed state - just show toggle button
if (!isOpen) { if (!isOpen) {

View File

@@ -30,22 +30,25 @@ interface ToolCallCardProps {
} }
// Map tool names to friendly labels and icons // Map tool names to friendly labels and icons
const TOOL_INFO: Record<string, { label: string; icon: React.ComponentType<{ className?: string }> }> = { const TOOL_INFO: Record<string, { label: string; icon: React.ComponentType<{ className?: string }>; color?: string }> = {
// Study tools // Study tools
list_studies: { label: 'Listing Studies', icon: Database }, list_studies: { label: 'Listing Studies', icon: Database },
get_study_status: { label: 'Getting Status', icon: FileSearch }, get_study_status: { label: 'Getting Status', icon: FileSearch },
create_study: { label: 'Creating Study', icon: Settings }, create_study: { label: 'Creating Study', icon: Settings, color: 'text-green-400' },
// Optimization tools // Optimization tools
run_optimization: { label: 'Starting Optimization', icon: Play }, run_optimization: { label: 'Starting Optimization', icon: Play, color: 'text-blue-400' },
stop_optimization: { label: 'Stopping Optimization', icon: XCircle }, stop_optimization: { label: 'Stopping Optimization', icon: XCircle },
get_optimization_status: { label: 'Checking Progress', icon: BarChart2 }, get_optimization_status: { label: 'Checking Progress', icon: BarChart2 },
// Analysis tools // Analysis tools
get_trial_data: { label: 'Querying Trials', icon: Database }, get_trial_data: { label: 'Querying Trials', icon: Database },
query_trials: { label: 'Querying Trials', icon: Database },
get_trial_details: { label: 'Getting Trial Details', icon: FileSearch },
analyze_convergence: { label: 'Analyzing Convergence', icon: BarChart2 }, analyze_convergence: { label: 'Analyzing Convergence', icon: BarChart2 },
compare_trials: { label: 'Comparing Trials', icon: BarChart2 }, compare_trials: { label: 'Comparing Trials', icon: BarChart2 },
get_best_design: { label: 'Getting Best Design', icon: CheckCircle }, get_best_design: { label: 'Getting Best Design', icon: CheckCircle },
get_optimization_summary: { label: 'Getting Summary', icon: BarChart2 },
// Reporting tools // Reporting tools
generate_report: { label: 'Generating Report', icon: FileText }, generate_report: { label: 'Generating Report', icon: FileText },
@@ -56,6 +59,25 @@ const TOOL_INFO: Record<string, { label: string; icon: React.ComponentType<{ cla
recommend_method: { label: 'Recommending Method', icon: Settings }, recommend_method: { label: 'Recommending Method', icon: Settings },
query_extractors: { label: 'Listing Extractors', icon: Database }, query_extractors: { label: 'Listing Extractors', icon: Database },
// Config tools (read)
read_study_config: { label: 'Reading Config', icon: FileSearch },
read_study_readme: { label: 'Reading README', icon: FileText },
// === WRITE TOOLS (Power Mode) ===
add_design_variable: { label: 'Adding Design Variable', icon: Settings, color: 'text-amber-400' },
add_extractor: { label: 'Adding Extractor', icon: Settings, color: 'text-amber-400' },
add_objective: { label: 'Adding Objective', icon: Settings, color: 'text-amber-400' },
add_constraint: { label: 'Adding Constraint', icon: Settings, color: 'text-amber-400' },
update_spec_field: { label: 'Updating Field', icon: Settings, color: 'text-amber-400' },
remove_node: { label: 'Removing Node', icon: XCircle, color: 'text-red-400' },
// === INTERVIEW TOOLS ===
start_interview: { label: 'Starting Interview', icon: HelpCircle, color: 'text-purple-400' },
interview_record: { label: 'Recording Answer', icon: CheckCircle, color: 'text-purple-400' },
interview_advance: { label: 'Advancing Interview', icon: Play, color: 'text-purple-400' },
interview_status: { label: 'Checking Progress', icon: BarChart2, color: 'text-purple-400' },
interview_finalize: { label: 'Creating Study', icon: CheckCircle, color: 'text-green-400' },
// Admin tools (power mode) // Admin tools (power mode)
edit_file: { label: 'Editing File', icon: FileText }, edit_file: { label: 'Editing File', icon: FileText },
create_file: { label: 'Creating File', icon: FileText }, create_file: { label: 'Creating File', icon: FileText },
@@ -104,7 +126,7 @@ export const ToolCallCard: React.FC<ToolCallCardProps> = ({ toolCall }) => {
)} )}
{/* Tool icon */} {/* Tool icon */}
<Icon className="w-4 h-4 text-dark-400 flex-shrink-0" /> <Icon className={`w-4 h-4 flex-shrink-0 ${info.color || 'text-dark-400'}`} />
{/* Label */} {/* Label */}
<span className="flex-1 text-sm text-dark-200 truncate">{info.label}</span> <span className="flex-1 text-sm text-dark-200 truncate">{info.label}</span>

View File

@@ -1,260 +0,0 @@
/**
* PlotlyConvergencePlot - Interactive convergence plot using Plotly
*
* Features:
* - Line plot showing objective vs trial number
* - Best-so-far trace overlay
* - FEA vs NN trial differentiation
* - Hover tooltips with trial details
* - Range slider for zooming
* - Log scale toggle
* - Export to PNG/SVG
*/
import { useMemo, useState } from 'react';
import Plot from 'react-plotly.js';
interface Trial {
trial_number: number;
values: number[];
params: Record<string, number>;
user_attrs?: Record<string, any>;
source?: 'FEA' | 'NN' | 'V10_FEA';
constraint_satisfied?: boolean;
}
// Penalty threshold - objectives above this are considered failed/penalty trials
const PENALTY_THRESHOLD = 100000;
interface PlotlyConvergencePlotProps {
trials: Trial[];
objectiveIndex?: number;
objectiveName?: string;
direction?: 'minimize' | 'maximize';
height?: number;
showRangeSlider?: boolean;
showLogScaleToggle?: boolean;
}
export function PlotlyConvergencePlot({
trials,
objectiveIndex = 0,
objectiveName = 'Objective',
direction = 'minimize',
height = 400,
showRangeSlider = true,
showLogScaleToggle = true
}: PlotlyConvergencePlotProps) {
const [useLogScale, setUseLogScale] = useState(false);
// Process trials and calculate best-so-far
const { feaData, nnData, bestSoFar, allX, allY } = useMemo(() => {
if (!trials.length) return { feaData: { x: [], y: [], text: [] }, nnData: { x: [], y: [], text: [] }, bestSoFar: { x: [], y: [] }, allX: [], allY: [] };
// Sort by trial number
const sorted = [...trials].sort((a, b) => a.trial_number - b.trial_number);
const fea: { x: number[]; y: number[]; text: string[] } = { x: [], y: [], text: [] };
const nn: { x: number[]; y: number[]; text: string[] } = { x: [], y: [], text: [] };
const best: { x: number[]; y: number[] } = { x: [], y: [] };
const xs: number[] = [];
const ys: number[] = [];
let bestValue = direction === 'minimize' ? Infinity : -Infinity;
sorted.forEach(t => {
const val = t.values?.[objectiveIndex] ?? t.user_attrs?.[objectiveName] ?? null;
if (val === null || !isFinite(val)) return;
// Filter out failed/penalty trials:
// 1. Objective above penalty threshold (e.g., 1000000 = solver failure)
// 2. constraint_satisfied explicitly false
// 3. user_attrs indicates pruned/failed
const isPenalty = val >= PENALTY_THRESHOLD;
const isFailed = t.constraint_satisfied === false;
const isPruned = t.user_attrs?.pruned === true || t.user_attrs?.fail_reason;
if (isPenalty || isFailed || isPruned) return;
const source = t.source || t.user_attrs?.source || 'FEA';
const hoverText = `Trial #${t.trial_number}<br>${objectiveName}: ${val.toFixed(4)}<br>Source: ${source}`;
xs.push(t.trial_number);
ys.push(val);
if (source === 'NN') {
nn.x.push(t.trial_number);
nn.y.push(val);
nn.text.push(hoverText);
} else {
fea.x.push(t.trial_number);
fea.y.push(val);
fea.text.push(hoverText);
}
// Update best-so-far
if (direction === 'minimize') {
if (val < bestValue) bestValue = val;
} else {
if (val > bestValue) bestValue = val;
}
best.x.push(t.trial_number);
best.y.push(bestValue);
});
return { feaData: fea, nnData: nn, bestSoFar: best, allX: xs, allY: ys };
}, [trials, objectiveIndex, objectiveName, direction]);
if (!trials.length || allX.length === 0) {
return (
<div className="flex items-center justify-center h-64 text-gray-500">
No trial data available
</div>
);
}
const traces: any[] = [];
// FEA trials scatter
if (feaData.x.length > 0) {
traces.push({
type: 'scatter',
mode: 'markers',
name: `FEA (${feaData.x.length})`,
x: feaData.x,
y: feaData.y,
text: feaData.text,
hoverinfo: 'text',
marker: {
color: '#3B82F6',
size: 8,
opacity: 0.7,
line: { color: '#1E40AF', width: 1 }
}
});
}
// NN trials scatter
if (nnData.x.length > 0) {
traces.push({
type: 'scatter',
mode: 'markers',
name: `NN (${nnData.x.length})`,
x: nnData.x,
y: nnData.y,
text: nnData.text,
hoverinfo: 'text',
marker: {
color: '#F97316',
size: 6,
symbol: 'cross',
opacity: 0.6
}
});
}
// Best-so-far line
if (bestSoFar.x.length > 0) {
traces.push({
type: 'scatter',
mode: 'lines',
name: 'Best So Far',
x: bestSoFar.x,
y: bestSoFar.y,
line: {
color: '#10B981',
width: 3,
shape: 'hv' // Step line
},
hoverinfo: 'y'
});
}
const layout: any = {
height,
margin: { l: 60, r: 30, t: 30, b: showRangeSlider ? 80 : 50 },
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
xaxis: {
title: 'Trial Number',
gridcolor: '#E5E7EB',
zerolinecolor: '#D1D5DB',
rangeslider: showRangeSlider ? { visible: true } : undefined
},
yaxis: {
title: useLogScale ? `log₁₀(${objectiveName})` : objectiveName,
gridcolor: '#E5E7EB',
zerolinecolor: '#D1D5DB',
type: useLogScale ? 'log' : 'linear'
},
legend: {
x: 1,
y: 1,
xanchor: 'right',
bgcolor: 'rgba(255,255,255,0.8)',
bordercolor: '#E5E7EB',
borderwidth: 1
},
font: { family: 'Inter, system-ui, sans-serif' },
hovermode: 'closest'
};
// Best value annotation
const bestVal = direction === 'minimize'
? Math.min(...allY)
: Math.max(...allY);
const bestIdx = allY.indexOf(bestVal);
const bestTrial = allX[bestIdx];
return (
<div className="w-full">
{/* Summary stats and controls */}
<div className="flex items-center justify-between mb-3">
<div className="flex gap-6 text-sm">
<div className="text-gray-600">
Best: <span className="font-semibold text-green-600">{bestVal.toFixed(4)}</span>
<span className="text-gray-400 ml-1">(Trial #{bestTrial})</span>
</div>
<div className="text-gray-600">
Current: <span className="font-semibold">{allY[allY.length - 1].toFixed(4)}</span>
</div>
<div className="text-gray-600">
Trials: <span className="font-semibold">{allX.length}</span>
</div>
</div>
{/* Log scale toggle */}
{showLogScaleToggle && (
<button
onClick={() => setUseLogScale(!useLogScale)}
className={`px-3 py-1 text-xs rounded transition-colors ${
useLogScale
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
title="Toggle logarithmic scale - better for viewing early improvements"
>
{useLogScale ? 'Log Scale' : 'Linear Scale'}
</button>
)}
</div>
<Plot
data={traces}
layout={layout}
config={{
displayModeBar: true,
displaylogo: false,
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
toImageButtonOptions: {
format: 'png',
filename: 'convergence_plot',
height: 600,
width: 1200,
scale: 2
}
}}
style={{ width: '100%' }}
/>
</div>
);
}

View File

@@ -1,161 +0,0 @@
import { useMemo } from 'react';
import Plot from 'react-plotly.js';
interface TrialData {
trial_number: number;
values: number[];
params: Record<string, number>;
}
interface PlotlyCorrelationHeatmapProps {
trials: TrialData[];
objectiveName?: string;
height?: number;
}
// Calculate Pearson correlation coefficient
function pearsonCorrelation(x: number[], y: number[]): number {
const n = x.length;
if (n === 0 || n !== y.length) return 0;
const meanX = x.reduce((a, b) => a + b, 0) / n;
const meanY = y.reduce((a, b) => a + b, 0) / n;
let numerator = 0;
let denomX = 0;
let denomY = 0;
for (let i = 0; i < n; i++) {
const dx = x[i] - meanX;
const dy = y[i] - meanY;
numerator += dx * dy;
denomX += dx * dx;
denomY += dy * dy;
}
const denominator = Math.sqrt(denomX) * Math.sqrt(denomY);
return denominator === 0 ? 0 : numerator / denominator;
}
export function PlotlyCorrelationHeatmap({
trials,
objectiveName = 'Objective',
height = 500
}: PlotlyCorrelationHeatmapProps) {
const { matrix, labels, annotations } = useMemo(() => {
if (trials.length < 3) {
return { matrix: [], labels: [], annotations: [] };
}
// Get parameter names
const paramNames = Object.keys(trials[0].params);
const allLabels = [...paramNames, objectiveName];
// Extract data columns
const columns: Record<string, number[]> = {};
paramNames.forEach(name => {
columns[name] = trials.map(t => t.params[name]).filter(v => v !== undefined && !isNaN(v));
});
columns[objectiveName] = trials.map(t => t.values[0]).filter(v => v !== undefined && !isNaN(v));
// Calculate correlation matrix
const n = allLabels.length;
const correlationMatrix: number[][] = [];
const annotationData: any[] = [];
for (let i = 0; i < n; i++) {
const row: number[] = [];
for (let j = 0; j < n; j++) {
const col1 = columns[allLabels[i]];
const col2 = columns[allLabels[j]];
// Ensure same length
const minLen = Math.min(col1.length, col2.length);
const corr = pearsonCorrelation(col1.slice(0, minLen), col2.slice(0, minLen));
row.push(corr);
// Add annotation
annotationData.push({
x: allLabels[j],
y: allLabels[i],
text: corr.toFixed(2),
showarrow: false,
font: {
color: Math.abs(corr) > 0.5 ? '#fff' : '#888',
size: 11
}
});
}
correlationMatrix.push(row);
}
return {
matrix: correlationMatrix,
labels: allLabels,
annotations: annotationData
};
}, [trials, objectiveName]);
if (trials.length < 3) {
return (
<div className="h-64 flex items-center justify-center text-dark-400">
<p>Need at least 3 trials to compute correlations</p>
</div>
);
}
return (
<Plot
data={[
{
z: matrix,
x: labels,
y: labels,
type: 'heatmap',
colorscale: [
[0, '#ef4444'], // -1: strong negative (red)
[0.25, '#f87171'], // -0.5: moderate negative
[0.5, '#1a1b26'], // 0: no correlation (dark)
[0.75, '#60a5fa'], // 0.5: moderate positive
[1, '#3b82f6'] // 1: strong positive (blue)
],
zmin: -1,
zmax: 1,
showscale: true,
colorbar: {
title: { text: 'Correlation', font: { color: '#888' } },
tickfont: { color: '#888' },
len: 0.8
},
hovertemplate: '%{y} vs %{x}<br>Correlation: %{z:.3f}<extra></extra>'
}
]}
layout={{
title: {
text: 'Parameter-Objective Correlation Matrix',
font: { color: '#fff', size: 14 }
},
height,
margin: { l: 120, r: 60, t: 60, b: 120 },
paper_bgcolor: 'transparent',
plot_bgcolor: 'transparent',
xaxis: {
tickangle: 45,
tickfont: { color: '#888', size: 10 },
gridcolor: 'rgba(255,255,255,0.05)'
},
yaxis: {
tickfont: { color: '#888', size: 10 },
gridcolor: 'rgba(255,255,255,0.05)'
},
annotations: annotations
}}
config={{
displayModeBar: true,
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
displaylogo: false
}}
style={{ width: '100%' }}
/>
);
}

View File

@@ -1,120 +0,0 @@
import { useMemo } from 'react';
import Plot from 'react-plotly.js';
interface TrialData {
trial_number: number;
values: number[];
constraint_satisfied?: boolean;
}
interface PlotlyFeasibilityChartProps {
trials: TrialData[];
height?: number;
}
export function PlotlyFeasibilityChart({
trials,
height = 350
}: PlotlyFeasibilityChartProps) {
const { trialNumbers, cumulativeFeasibility, windowedFeasibility } = useMemo(() => {
if (trials.length === 0) {
return { trialNumbers: [], cumulativeFeasibility: [], windowedFeasibility: [] };
}
// Sort trials by number
const sorted = [...trials].sort((a, b) => a.trial_number - b.trial_number);
const numbers: number[] = [];
const cumulative: number[] = [];
const windowed: number[] = [];
let feasibleCount = 0;
const windowSize = Math.min(20, Math.floor(sorted.length / 5) || 1);
sorted.forEach((trial, idx) => {
numbers.push(trial.trial_number);
// Cumulative feasibility
if (trial.constraint_satisfied !== false) {
feasibleCount++;
}
cumulative.push((feasibleCount / (idx + 1)) * 100);
// Windowed (rolling) feasibility
const windowStart = Math.max(0, idx - windowSize + 1);
const windowTrials = sorted.slice(windowStart, idx + 1);
const windowFeasible = windowTrials.filter(t => t.constraint_satisfied !== false).length;
windowed.push((windowFeasible / windowTrials.length) * 100);
});
return { trialNumbers: numbers, cumulativeFeasibility: cumulative, windowedFeasibility: windowed };
}, [trials]);
if (trials.length === 0) {
return (
<div className="h-64 flex items-center justify-center text-dark-400">
<p>No trials to display</p>
</div>
);
}
return (
<Plot
data={[
{
x: trialNumbers,
y: cumulativeFeasibility,
type: 'scatter',
mode: 'lines',
name: 'Cumulative Feasibility',
line: { color: '#22c55e', width: 2 },
hovertemplate: 'Trial %{x}<br>Cumulative: %{y:.1f}%<extra></extra>'
},
{
x: trialNumbers,
y: windowedFeasibility,
type: 'scatter',
mode: 'lines',
name: 'Rolling (20-trial)',
line: { color: '#60a5fa', width: 2, dash: 'dot' },
hovertemplate: 'Trial %{x}<br>Rolling: %{y:.1f}%<extra></extra>'
}
]}
layout={{
height,
margin: { l: 60, r: 30, t: 30, b: 50 },
paper_bgcolor: 'transparent',
plot_bgcolor: 'transparent',
xaxis: {
title: { text: 'Trial Number', font: { color: '#888' } },
tickfont: { color: '#888' },
gridcolor: 'rgba(255,255,255,0.05)',
zeroline: false
},
yaxis: {
title: { text: 'Feasibility Rate (%)', font: { color: '#888' } },
tickfont: { color: '#888' },
gridcolor: 'rgba(255,255,255,0.1)',
zeroline: false,
range: [0, 105]
},
legend: {
font: { color: '#888' },
bgcolor: 'rgba(0,0,0,0.5)',
x: 0.02,
y: 0.98,
xanchor: 'left',
yanchor: 'top'
},
showlegend: true,
hovermode: 'x unified'
}}
config={{
displayModeBar: true,
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
displaylogo: false
}}
style={{ width: '100%' }}
/>
);
}

View File

@@ -1,221 +0,0 @@
/**
* PlotlyParallelCoordinates - Interactive parallel coordinates plot using Plotly
*
* Features:
* - Native zoom, pan, and selection
* - Hover tooltips with trial details
* - Brush filtering on each axis
* - FEA vs NN color differentiation
* - Export to PNG/SVG
*/
import { useMemo } from 'react';
import Plot from 'react-plotly.js';
interface Trial {
trial_number: number;
values: number[];
params: Record<string, number>;
user_attrs?: Record<string, any>;
constraint_satisfied?: boolean;
source?: 'FEA' | 'NN' | 'V10_FEA';
}
interface Objective {
name: string;
direction?: 'minimize' | 'maximize';
unit?: string;
}
interface DesignVariable {
name: string;
unit?: string;
min?: number;
max?: number;
}
interface PlotlyParallelCoordinatesProps {
trials: Trial[];
objectives: Objective[];
designVariables: DesignVariable[];
paretoFront?: Trial[];
height?: number;
}
export function PlotlyParallelCoordinates({
trials,
objectives,
designVariables,
paretoFront = [],
height = 500
}: PlotlyParallelCoordinatesProps) {
// Create set of Pareto front trial numbers
const paretoSet = useMemo(() => new Set(paretoFront.map(t => t.trial_number)), [paretoFront]);
// Build dimensions array for parallel coordinates
const { dimensions, colorValues, colorScale } = useMemo(() => {
if (!trials.length) return { dimensions: [], colorValues: [], colorScale: [] };
const dims: any[] = [];
const colors: number[] = [];
// Get all design variable names
const dvNames = designVariables.map(dv => dv.name);
const objNames = objectives.map(obj => obj.name);
// Add design variable dimensions
dvNames.forEach((name, idx) => {
const dv = designVariables[idx];
const values = trials.map(t => t.params[name] ?? 0);
const validValues = values.filter(v => v !== null && v !== undefined && isFinite(v));
if (validValues.length === 0) return;
dims.push({
label: name,
values: values,
range: [
dv?.min ?? Math.min(...validValues),
dv?.max ?? Math.max(...validValues)
],
constraintrange: undefined
});
});
// Add objective dimensions
objNames.forEach((name, idx) => {
const obj = objectives[idx];
const values = trials.map(t => {
// Try to get from values array first, then user_attrs
if (t.values && t.values[idx] !== undefined) {
return t.values[idx];
}
return t.user_attrs?.[name] ?? 0;
});
const validValues = values.filter(v => v !== null && v !== undefined && isFinite(v));
if (validValues.length === 0) return;
dims.push({
label: `${name}${obj.unit ? ` (${obj.unit})` : ''}`,
values: values,
range: [Math.min(...validValues) * 0.95, Math.max(...validValues) * 1.05]
});
});
// Build color array: 0 = V10_FEA, 1 = FEA, 2 = NN, 3 = Pareto
trials.forEach(t => {
const source = t.source || t.user_attrs?.source || 'FEA';
const isPareto = paretoSet.has(t.trial_number);
if (isPareto) {
colors.push(3); // Pareto - special color
} else if (source === 'NN') {
colors.push(2); // NN trials
} else if (source === 'V10_FEA') {
colors.push(0); // V10 FEA
} else {
colors.push(1); // V11 FEA
}
});
// Color scale: V10_FEA (light blue), FEA (blue), NN (orange), Pareto (green)
const scale: [number, string][] = [
[0, '#93C5FD'], // V10_FEA - light blue
[0.33, '#2563EB'], // FEA - blue
[0.66, '#F97316'], // NN - orange
[1, '#10B981'] // Pareto - green
];
return { dimensions: dims, colorValues: colors, colorScale: scale };
}, [trials, objectives, designVariables, paretoSet]);
if (!trials.length || dimensions.length === 0) {
return (
<div className="flex items-center justify-center h-64 text-gray-500">
No trial data available for parallel coordinates
</div>
);
}
// Count trial types for legend
const feaCount = trials.filter(t => {
const source = t.source || t.user_attrs?.source || 'FEA';
return source === 'FEA' || source === 'V10_FEA';
}).length;
const nnCount = trials.filter(t => {
const source = t.source || t.user_attrs?.source || 'FEA';
return source === 'NN';
}).length;
return (
<div className="w-full">
{/* Legend */}
<div className="flex gap-4 justify-center mb-2 text-sm">
<div className="flex items-center gap-1.5">
<div className="w-4 h-1 rounded" style={{ backgroundColor: '#2563EB' }} />
<span className="text-gray-600">FEA ({feaCount})</span>
</div>
<div className="flex items-center gap-1.5">
<div className="w-4 h-1 rounded" style={{ backgroundColor: '#F97316' }} />
<span className="text-gray-600">NN ({nnCount})</span>
</div>
{paretoFront.length > 0 && (
<div className="flex items-center gap-1.5">
<div className="w-4 h-1 rounded" style={{ backgroundColor: '#10B981' }} />
<span className="text-gray-600">Pareto ({paretoFront.length})</span>
</div>
)}
</div>
<Plot
data={[
{
type: 'parcoords',
line: {
color: colorValues,
colorscale: colorScale as any,
showscale: false
},
dimensions: dimensions,
labelangle: -30,
labelfont: {
size: 11,
color: '#374151'
},
tickfont: {
size: 10,
color: '#6B7280'
}
} as any
]}
layout={{
height: height,
margin: { l: 80, r: 80, t: 30, b: 30 },
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
font: {
family: 'Inter, system-ui, sans-serif'
}
}}
config={{
displayModeBar: true,
displaylogo: false,
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
toImageButtonOptions: {
format: 'png',
filename: 'parallel_coordinates',
height: 800,
width: 1400,
scale: 2
}
}}
style={{ width: '100%' }}
/>
<p className="text-xs text-gray-500 text-center mt-2">
Drag along axes to filter. Double-click to reset.
</p>
</div>
);
}

View File

@@ -1,209 +0,0 @@
/**
* PlotlyParameterImportance - Interactive parameter importance chart using Plotly
*
* Features:
* - Horizontal bar chart showing correlation/importance
* - Color coding by positive/negative correlation
* - Hover tooltips with details
* - Sortable by importance
*/
import { useMemo, useState } from 'react';
import Plot from 'react-plotly.js';
interface Trial {
trial_number: number;
values: number[];
params: Record<string, number>;
user_attrs?: Record<string, any>;
}
interface DesignVariable {
name: string;
unit?: string;
}
interface PlotlyParameterImportanceProps {
trials: Trial[];
designVariables: DesignVariable[];
objectiveIndex?: number;
objectiveName?: string;
height?: number;
}
// Calculate Pearson correlation coefficient
function pearsonCorrelation(x: number[], y: number[]): number {
const n = x.length;
if (n === 0) return 0;
const sumX = x.reduce((a, b) => a + b, 0);
const sumY = y.reduce((a, b) => a + b, 0);
const sumXY = x.reduce((acc, xi, i) => acc + xi * y[i], 0);
const sumX2 = x.reduce((acc, xi) => acc + xi * xi, 0);
const sumY2 = y.reduce((acc, yi) => acc + yi * yi, 0);
const numerator = n * sumXY - sumX * sumY;
const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
if (denominator === 0) return 0;
return numerator / denominator;
}
export function PlotlyParameterImportance({
trials,
designVariables,
objectiveIndex = 0,
objectiveName = 'Objective',
height = 400
}: PlotlyParameterImportanceProps) {
const [sortBy, setSortBy] = useState<'importance' | 'name'>('importance');
// Calculate correlations for each parameter
const correlations = useMemo(() => {
if (!trials.length || !designVariables.length) return [];
// Get objective values
const objValues = trials.map(t => {
if (t.values && t.values[objectiveIndex] !== undefined) {
return t.values[objectiveIndex];
}
return t.user_attrs?.[objectiveName] ?? null;
}).filter((v): v is number => v !== null && isFinite(v));
if (objValues.length < 3) return []; // Need at least 3 points for correlation
const results: { name: string; correlation: number; absCorrelation: number }[] = [];
designVariables.forEach(dv => {
const paramValues = trials
.map((t) => {
const objVal = t.values?.[objectiveIndex] ?? t.user_attrs?.[objectiveName];
if (objVal === null || objVal === undefined || !isFinite(objVal)) return null;
return { param: t.params[dv.name], obj: objVal };
})
.filter((v): v is { param: number; obj: number } => v !== null && v.param !== undefined);
if (paramValues.length < 3) return;
const x = paramValues.map(v => v.param);
const y = paramValues.map(v => v.obj);
const corr = pearsonCorrelation(x, y);
results.push({
name: dv.name,
correlation: corr,
absCorrelation: Math.abs(corr)
});
});
// Sort by absolute correlation or name
if (sortBy === 'importance') {
results.sort((a, b) => b.absCorrelation - a.absCorrelation);
} else {
results.sort((a, b) => a.name.localeCompare(b.name));
}
return results;
}, [trials, designVariables, objectiveIndex, objectiveName, sortBy]);
if (!correlations.length) {
return (
<div className="flex items-center justify-center h-64 text-gray-500">
Not enough data to calculate parameter importance
</div>
);
}
// Build bar chart data
const names = correlations.map(c => c.name);
const values = correlations.map(c => c.correlation);
const colors = values.map(v => v > 0 ? '#EF4444' : '#22C55E'); // Red for positive (worse), Green for negative (better) when minimizing
const hoverTexts = correlations.map(c =>
`${c.name}<br>Correlation: ${c.correlation.toFixed(4)}<br>|r|: ${c.absCorrelation.toFixed(4)}<br>${c.correlation > 0 ? 'Higher → Higher objective' : 'Higher → Lower objective'}`
);
return (
<div className="w-full">
{/* Controls */}
<div className="flex justify-between items-center mb-3">
<div className="text-sm text-gray-600">
Correlation with <span className="font-semibold">{objectiveName}</span>
</div>
<div className="flex gap-2">
<button
onClick={() => setSortBy('importance')}
className={`px-3 py-1 text-xs rounded ${sortBy === 'importance' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700'}`}
>
By Importance
</button>
<button
onClick={() => setSortBy('name')}
className={`px-3 py-1 text-xs rounded ${sortBy === 'name' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700'}`}
>
By Name
</button>
</div>
</div>
<Plot
data={[
{
type: 'bar',
orientation: 'h',
y: names,
x: values,
text: hoverTexts,
hoverinfo: 'text',
marker: {
color: colors,
line: { color: '#fff', width: 1 }
}
}
]}
layout={{
height: Math.max(height, correlations.length * 30 + 80),
margin: { l: 150, r: 30, t: 10, b: 50 },
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
xaxis: {
title: { text: 'Correlation Coefficient' },
range: [-1, 1],
gridcolor: '#E5E7EB',
zerolinecolor: '#9CA3AF',
zerolinewidth: 2
},
yaxis: {
automargin: true
},
font: { family: 'Inter, system-ui, sans-serif', size: 11 },
bargap: 0.3
}}
config={{
displayModeBar: true,
displaylogo: false,
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
toImageButtonOptions: {
format: 'png',
filename: 'parameter_importance',
height: 600,
width: 800,
scale: 2
}
}}
style={{ width: '100%' }}
/>
{/* Legend */}
<div className="flex gap-6 justify-center mt-3 text-xs">
<div className="flex items-center gap-1.5">
<div className="w-4 h-3 rounded" style={{ backgroundColor: '#EF4444' }} />
<span className="text-gray-600">Positive correlation (higher param higher objective)</span>
</div>
<div className="flex items-center gap-1.5">
<div className="w-4 h-3 rounded" style={{ backgroundColor: '#22C55E' }} />
<span className="text-gray-600">Negative correlation (higher param lower objective)</span>
</div>
</div>
</div>
);
}

View File

@@ -1,448 +0,0 @@
/**
* PlotlyParetoPlot - Interactive Pareto front visualization using Plotly
*
* Features:
* - 2D scatter with Pareto front highlighted
* - 3D scatter for 3-objective problems
* - Hover tooltips with trial details
* - Pareto front connection line
* - FEA vs NN differentiation
* - Constraint satisfaction highlighting
* - Dark mode styling
* - Zoom, pan, and export
*/
import { useMemo, useState } from 'react';
import Plot from 'react-plotly.js';
interface Trial {
trial_number: number;
values: number[];
params: Record<string, number>;
user_attrs?: Record<string, any>;
source?: 'FEA' | 'NN' | 'V10_FEA';
constraint_satisfied?: boolean;
}
interface Objective {
name: string;
direction?: 'minimize' | 'maximize';
unit?: string;
}
interface PlotlyParetoPlotProps {
trials: Trial[];
paretoFront: Trial[];
objectives: Objective[];
height?: number;
showParetoLine?: boolean;
showInfeasible?: boolean;
}
export function PlotlyParetoPlot({
trials,
paretoFront,
objectives,
height = 500,
showParetoLine = true,
showInfeasible = true
}: PlotlyParetoPlotProps) {
const [viewMode, setViewMode] = useState<'2d' | '3d'>(objectives.length >= 3 ? '3d' : '2d');
const [selectedObjectives, setSelectedObjectives] = useState<[number, number, number]>([0, 1, 2]);
const paretoSet = useMemo(() => new Set(paretoFront.map(t => t.trial_number)), [paretoFront]);
// Separate trials by source, Pareto status, and constraint satisfaction
const { feaTrials, nnTrials, paretoTrials, infeasibleTrials, stats } = useMemo(() => {
const fea: Trial[] = [];
const nn: Trial[] = [];
const pareto: Trial[] = [];
const infeasible: Trial[] = [];
trials.forEach(t => {
const source = t.source || t.user_attrs?.source || 'FEA';
const isFeasible = t.constraint_satisfied !== false && t.user_attrs?.constraint_satisfied !== false;
if (!isFeasible && showInfeasible) {
infeasible.push(t);
} else if (paretoSet.has(t.trial_number)) {
pareto.push(t);
} else if (source === 'NN') {
nn.push(t);
} else {
fea.push(t);
}
});
// Calculate statistics
const stats = {
totalTrials: trials.length,
paretoCount: pareto.length,
feaCount: fea.length + pareto.filter(t => (t.source || 'FEA') !== 'NN').length,
nnCount: nn.length + pareto.filter(t => t.source === 'NN').length,
infeasibleCount: infeasible.length,
hypervolume: 0 // Could calculate if needed
};
return { feaTrials: fea, nnTrials: nn, paretoTrials: pareto, infeasibleTrials: infeasible, stats };
}, [trials, paretoSet, showInfeasible]);
// Helper to get objective value
const getObjValue = (trial: Trial, idx: number): number => {
if (trial.values && trial.values[idx] !== undefined) {
return trial.values[idx];
}
const objName = objectives[idx]?.name;
return trial.user_attrs?.[objName] ?? 0;
};
// Build hover text
const buildHoverText = (trial: Trial): string => {
const lines = [`Trial #${trial.trial_number}`];
objectives.forEach((obj, i) => {
const val = getObjValue(trial, i);
lines.push(`${obj.name}: ${val.toFixed(4)}${obj.unit ? ` ${obj.unit}` : ''}`);
});
const source = trial.source || trial.user_attrs?.source || 'FEA';
lines.push(`Source: ${source}`);
return lines.join('<br>');
};
// Create trace data
const createTrace = (
trialList: Trial[],
name: string,
color: string,
symbol: string,
size: number,
opacity: number
) => {
const [i, j, k] = selectedObjectives;
if (viewMode === '3d' && objectives.length >= 3) {
return {
type: 'scatter3d' as const,
mode: 'markers' as const,
name,
x: trialList.map(t => getObjValue(t, i)),
y: trialList.map(t => getObjValue(t, j)),
z: trialList.map(t => getObjValue(t, k)),
text: trialList.map(buildHoverText),
hoverinfo: 'text' as const,
marker: {
color,
size,
symbol,
opacity,
line: { color: '#fff', width: 1 }
}
};
} else {
return {
type: 'scatter' as const,
mode: 'markers' as const,
name,
x: trialList.map(t => getObjValue(t, i)),
y: trialList.map(t => getObjValue(t, j)),
text: trialList.map(buildHoverText),
hoverinfo: 'text' as const,
marker: {
color,
size,
symbol,
opacity,
line: { color: '#fff', width: 1 }
}
};
}
};
// Sort Pareto trials by first objective for line connection
const sortedParetoTrials = useMemo(() => {
const [i] = selectedObjectives;
return [...paretoTrials].sort((a, b) => getObjValue(a, i) - getObjValue(b, i));
}, [paretoTrials, selectedObjectives]);
// Create Pareto front line trace (2D only)
const createParetoLine = () => {
if (!showParetoLine || viewMode === '3d' || sortedParetoTrials.length < 2) return null;
const [i, j] = selectedObjectives;
return {
type: 'scatter' as const,
mode: 'lines' as const,
name: 'Pareto Front',
x: sortedParetoTrials.map(t => getObjValue(t, i)),
y: sortedParetoTrials.map(t => getObjValue(t, j)),
line: {
color: '#10B981',
width: 2,
dash: 'dot'
},
hoverinfo: 'skip' as const,
showlegend: false
};
};
const traces = [
// Infeasible trials (background, red X)
...(showInfeasible && infeasibleTrials.length > 0 ? [
createTrace(infeasibleTrials, `Infeasible (${infeasibleTrials.length})`, '#EF4444', 'x', 7, 0.4)
] : []),
// FEA trials (blue circles)
createTrace(feaTrials, `FEA (${feaTrials.length})`, '#3B82F6', 'circle', 8, 0.6),
// NN trials (purple diamonds)
createTrace(nnTrials, `NN (${nnTrials.length})`, '#A855F7', 'diamond', 8, 0.5),
// Pareto front line (2D only)
createParetoLine(),
// Pareto front points (highlighted)
createTrace(sortedParetoTrials, `Pareto (${sortedParetoTrials.length})`, '#10B981', 'star', 14, 1.0)
].filter(trace => trace && (trace.x as number[]).length > 0);
const [i, j, k] = selectedObjectives;
// Dark mode color scheme
const colors = {
text: '#E5E7EB',
textMuted: '#9CA3AF',
grid: 'rgba(255,255,255,0.1)',
zeroline: 'rgba(255,255,255,0.2)',
legendBg: 'rgba(30,30,30,0.9)',
legendBorder: 'rgba(255,255,255,0.1)'
};
const layout: any = viewMode === '3d' && objectives.length >= 3
? {
height,
margin: { l: 50, r: 50, t: 30, b: 50 },
paper_bgcolor: 'transparent',
plot_bgcolor: 'transparent',
scene: {
xaxis: {
title: { text: objectives[i]?.name || 'Objective 1', font: { color: colors.text } },
gridcolor: colors.grid,
zerolinecolor: colors.zeroline,
tickfont: { color: colors.textMuted }
},
yaxis: {
title: { text: objectives[j]?.name || 'Objective 2', font: { color: colors.text } },
gridcolor: colors.grid,
zerolinecolor: colors.zeroline,
tickfont: { color: colors.textMuted }
},
zaxis: {
title: { text: objectives[k]?.name || 'Objective 3', font: { color: colors.text } },
gridcolor: colors.grid,
zerolinecolor: colors.zeroline,
tickfont: { color: colors.textMuted }
},
bgcolor: 'transparent'
},
legend: {
x: 1,
y: 1,
font: { color: colors.text },
bgcolor: colors.legendBg,
bordercolor: colors.legendBorder,
borderwidth: 1
},
font: { family: 'Inter, system-ui, sans-serif', color: colors.text }
}
: {
height,
margin: { l: 60, r: 30, t: 30, b: 60 },
paper_bgcolor: 'transparent',
plot_bgcolor: 'transparent',
xaxis: {
title: { text: objectives[i]?.name || 'Objective 1', font: { color: colors.text } },
gridcolor: colors.grid,
zerolinecolor: colors.zeroline,
tickfont: { color: colors.textMuted }
},
yaxis: {
title: { text: objectives[j]?.name || 'Objective 2', font: { color: colors.text } },
gridcolor: colors.grid,
zerolinecolor: colors.zeroline,
tickfont: { color: colors.textMuted }
},
legend: {
x: 1,
y: 1,
xanchor: 'right',
font: { color: colors.text },
bgcolor: colors.legendBg,
bordercolor: colors.legendBorder,
borderwidth: 1
},
font: { family: 'Inter, system-ui, sans-serif', color: colors.text },
hovermode: 'closest' as const
};
if (!trials.length) {
return (
<div className="flex items-center justify-center h-64 text-dark-400">
No trial data available
</div>
);
}
return (
<div className="w-full">
{/* Stats Bar */}
<div className="flex gap-4 mb-4 text-sm">
<div className="flex items-center gap-2 px-3 py-1.5 bg-dark-700 rounded-lg">
<div className="w-3 h-3 bg-green-500 rounded-full" />
<span className="text-dark-300">Pareto:</span>
<span className="text-green-400 font-medium">{stats.paretoCount}</span>
</div>
<div className="flex items-center gap-2 px-3 py-1.5 bg-dark-700 rounded-lg">
<div className="w-3 h-3 bg-blue-500 rounded-full" />
<span className="text-dark-300">FEA:</span>
<span className="text-blue-400 font-medium">{stats.feaCount}</span>
</div>
<div className="flex items-center gap-2 px-3 py-1.5 bg-dark-700 rounded-lg">
<div className="w-3 h-3 bg-purple-500 rounded-full" />
<span className="text-dark-300">NN:</span>
<span className="text-purple-400 font-medium">{stats.nnCount}</span>
</div>
{stats.infeasibleCount > 0 && (
<div className="flex items-center gap-2 px-3 py-1.5 bg-dark-700 rounded-lg">
<div className="w-3 h-3 bg-red-500 rounded-full" />
<span className="text-dark-300">Infeasible:</span>
<span className="text-red-400 font-medium">{stats.infeasibleCount}</span>
</div>
)}
</div>
{/* Controls */}
<div className="flex gap-4 items-center justify-between mb-3">
<div className="flex gap-2 items-center">
{objectives.length >= 3 && (
<div className="flex rounded-lg overflow-hidden border border-dark-600">
<button
onClick={() => setViewMode('2d')}
className={`px-3 py-1.5 text-sm font-medium transition-colors ${
viewMode === '2d'
? 'bg-primary-600 text-white'
: 'bg-dark-700 text-dark-300 hover:bg-dark-600 hover:text-white'
}`}
>
2D
</button>
<button
onClick={() => setViewMode('3d')}
className={`px-3 py-1.5 text-sm font-medium transition-colors ${
viewMode === '3d'
? 'bg-primary-600 text-white'
: 'bg-dark-700 text-dark-300 hover:bg-dark-600 hover:text-white'
}`}
>
3D
</button>
</div>
)}
</div>
{/* Objective selectors */}
<div className="flex gap-2 items-center text-sm">
<label className="text-dark-400">X:</label>
<select
value={selectedObjectives[0]}
onChange={(e) => setSelectedObjectives([parseInt(e.target.value), selectedObjectives[1], selectedObjectives[2]])}
className="px-2 py-1.5 bg-dark-700 border border-dark-600 rounded text-white text-sm"
>
{objectives.map((obj, idx) => (
<option key={idx} value={idx}>{obj.name}</option>
))}
</select>
<label className="text-dark-400 ml-2">Y:</label>
<select
value={selectedObjectives[1]}
onChange={(e) => setSelectedObjectives([selectedObjectives[0], parseInt(e.target.value), selectedObjectives[2]])}
className="px-2 py-1.5 bg-dark-700 border border-dark-600 rounded text-white text-sm"
>
{objectives.map((obj, idx) => (
<option key={idx} value={idx}>{obj.name}</option>
))}
</select>
{viewMode === '3d' && objectives.length >= 3 && (
<>
<label className="text-dark-400 ml-2">Z:</label>
<select
value={selectedObjectives[2]}
onChange={(e) => setSelectedObjectives([selectedObjectives[0], selectedObjectives[1], parseInt(e.target.value)])}
className="px-2 py-1.5 bg-dark-700 border border-dark-600 rounded text-white text-sm"
>
{objectives.map((obj, idx) => (
<option key={idx} value={idx}>{obj.name}</option>
))}
</select>
</>
)}
</div>
</div>
<Plot
data={traces as any}
layout={layout}
config={{
displayModeBar: true,
displaylogo: false,
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
toImageButtonOptions: {
format: 'png',
filename: 'pareto_front',
height: 800,
width: 1200,
scale: 2
}
}}
style={{ width: '100%' }}
/>
{/* Pareto Front Table for 2D view */}
{viewMode === '2d' && sortedParetoTrials.length > 0 && (
<div className="mt-4 max-h-48 overflow-auto">
<table className="w-full text-sm">
<thead className="sticky top-0 bg-dark-800">
<tr className="border-b border-dark-600">
<th className="text-left py-2 px-3 text-dark-400 font-medium">Trial</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">{objectives[i]?.name || 'Obj 1'}</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">{objectives[j]?.name || 'Obj 2'}</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Source</th>
</tr>
</thead>
<tbody>
{sortedParetoTrials.slice(0, 10).map(trial => (
<tr key={trial.trial_number} className="border-b border-dark-700 hover:bg-dark-750">
<td className="py-2 px-3 font-mono text-white">#{trial.trial_number}</td>
<td className="py-2 px-3 font-mono text-green-400">
{getObjValue(trial, i).toExponential(4)}
</td>
<td className="py-2 px-3 font-mono text-green-400">
{getObjValue(trial, j).toExponential(4)}
</td>
<td className="py-2 px-3">
<span className={`px-2 py-0.5 rounded text-xs ${
(trial.source || trial.user_attrs?.source) === 'NN'
? 'bg-purple-500/20 text-purple-400'
: 'bg-blue-500/20 text-blue-400'
}`}>
{trial.source || trial.user_attrs?.source || 'FEA'}
</span>
</td>
</tr>
))}
</tbody>
</table>
{sortedParetoTrials.length > 10 && (
<div className="text-center py-2 text-dark-500 text-xs">
Showing 10 of {sortedParetoTrials.length} Pareto-optimal solutions
</div>
)}
</div>
)}
</div>
);
}

View File

@@ -1,247 +0,0 @@
import { useMemo } from 'react';
import Plot from 'react-plotly.js';
import { TrendingUp, TrendingDown, Minus } from 'lucide-react';
interface Run {
run_id: number;
name: string;
source: 'FEA' | 'NN';
trial_count: number;
best_value: number | null;
avg_value: number | null;
first_trial: string | null;
last_trial: string | null;
}
interface PlotlyRunComparisonProps {
runs: Run[];
height?: number;
}
export function PlotlyRunComparison({ runs, height = 400 }: PlotlyRunComparisonProps) {
const chartData = useMemo(() => {
if (runs.length === 0) return null;
// Separate FEA and NN runs
const feaRuns = runs.filter(r => r.source === 'FEA');
const nnRuns = runs.filter(r => r.source === 'NN');
// Create bar chart for trial counts
const trialCountData = {
x: runs.map(r => r.name),
y: runs.map(r => r.trial_count),
type: 'bar' as const,
name: 'Trial Count',
marker: {
color: runs.map(r => r.source === 'NN' ? 'rgba(147, 51, 234, 0.8)' : 'rgba(59, 130, 246, 0.8)'),
line: { color: runs.map(r => r.source === 'NN' ? 'rgb(147, 51, 234)' : 'rgb(59, 130, 246)'), width: 1 }
},
hovertemplate: '<b>%{x}</b><br>Trials: %{y}<extra></extra>'
};
// Create line chart for best values
const bestValueData = {
x: runs.map(r => r.name),
y: runs.map(r => r.best_value),
type: 'scatter' as const,
mode: 'lines+markers' as const,
name: 'Best Value',
yaxis: 'y2',
line: { color: 'rgba(16, 185, 129, 1)', width: 2 },
marker: { size: 8, color: 'rgba(16, 185, 129, 1)' },
hovertemplate: '<b>%{x}</b><br>Best: %{y:.4e}<extra></extra>'
};
return { trialCountData, bestValueData, feaRuns, nnRuns };
}, [runs]);
// Calculate statistics
const stats = useMemo(() => {
if (runs.length === 0) return null;
const totalTrials = runs.reduce((sum, r) => sum + r.trial_count, 0);
const feaTrials = runs.filter(r => r.source === 'FEA').reduce((sum, r) => sum + r.trial_count, 0);
const nnTrials = runs.filter(r => r.source === 'NN').reduce((sum, r) => sum + r.trial_count, 0);
const bestValues = runs.map(r => r.best_value).filter((v): v is number => v !== null);
const overallBest = bestValues.length > 0 ? Math.min(...bestValues) : null;
// Calculate improvement from first FEA run to overall best
const feaRuns = runs.filter(r => r.source === 'FEA');
const firstFEA = feaRuns.length > 0 ? feaRuns[0].best_value : null;
const improvement = firstFEA && overallBest ? ((firstFEA - overallBest) / Math.abs(firstFEA)) * 100 : null;
return {
totalTrials,
feaTrials,
nnTrials,
overallBest,
improvement,
totalRuns: runs.length,
feaRuns: runs.filter(r => r.source === 'FEA').length,
nnRuns: runs.filter(r => r.source === 'NN').length
};
}, [runs]);
if (!chartData || !stats) {
return (
<div className="flex items-center justify-center h-64 text-dark-400">
No run data available
</div>
);
}
return (
<div className="space-y-4">
{/* Stats Summary */}
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
<div className="bg-dark-750 rounded-lg p-3">
<div className="text-xs text-dark-400 mb-1">Total Runs</div>
<div className="text-xl font-bold text-white">{stats.totalRuns}</div>
</div>
<div className="bg-dark-750 rounded-lg p-3">
<div className="text-xs text-dark-400 mb-1">Total Trials</div>
<div className="text-xl font-bold text-white">{stats.totalTrials}</div>
</div>
<div className="bg-dark-750 rounded-lg p-3">
<div className="text-xs text-dark-400 mb-1">FEA Trials</div>
<div className="text-xl font-bold text-blue-400">{stats.feaTrials}</div>
</div>
<div className="bg-dark-750 rounded-lg p-3">
<div className="text-xs text-dark-400 mb-1">NN Trials</div>
<div className="text-xl font-bold text-purple-400">{stats.nnTrials}</div>
</div>
<div className="bg-dark-750 rounded-lg p-3">
<div className="text-xs text-dark-400 mb-1">Best Value</div>
<div className="text-xl font-bold text-green-400">
{stats.overallBest !== null ? stats.overallBest.toExponential(3) : 'N/A'}
</div>
</div>
<div className="bg-dark-750 rounded-lg p-3">
<div className="text-xs text-dark-400 mb-1">Improvement</div>
<div className="text-xl font-bold text-primary-400 flex items-center gap-1">
{stats.improvement !== null ? (
<>
{stats.improvement > 0 ? <TrendingDown className="w-4 h-4" /> :
stats.improvement < 0 ? <TrendingUp className="w-4 h-4" /> :
<Minus className="w-4 h-4" />}
{Math.abs(stats.improvement).toFixed(1)}%
</>
) : 'N/A'}
</div>
</div>
</div>
{/* Chart */}
<Plot
data={[chartData.trialCountData, chartData.bestValueData]}
layout={{
height,
margin: { l: 60, r: 60, t: 40, b: 100 },
paper_bgcolor: 'transparent',
plot_bgcolor: 'transparent',
font: { color: '#9ca3af', size: 11 },
showlegend: true,
legend: {
orientation: 'h',
y: 1.12,
x: 0.5,
xanchor: 'center',
bgcolor: 'transparent'
},
xaxis: {
tickangle: -45,
gridcolor: 'rgba(75, 85, 99, 0.3)',
linecolor: 'rgba(75, 85, 99, 0.5)',
tickfont: { size: 10 }
},
yaxis: {
title: { text: 'Trial Count' },
gridcolor: 'rgba(75, 85, 99, 0.3)',
linecolor: 'rgba(75, 85, 99, 0.5)',
zeroline: false
},
yaxis2: {
title: { text: 'Best Value' },
overlaying: 'y',
side: 'right',
gridcolor: 'rgba(75, 85, 99, 0.1)',
linecolor: 'rgba(75, 85, 99, 0.5)',
zeroline: false,
tickformat: '.2e'
},
bargap: 0.3,
hovermode: 'x unified'
}}
config={{
displayModeBar: true,
displaylogo: false,
modeBarButtonsToRemove: ['select2d', 'lasso2d', 'autoScale2d']
}}
className="w-full"
useResizeHandler
style={{ width: '100%' }}
/>
{/* Runs Table */}
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-dark-600">
<th className="text-left py-2 px-3 text-dark-400 font-medium">Run Name</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Source</th>
<th className="text-right py-2 px-3 text-dark-400 font-medium">Trials</th>
<th className="text-right py-2 px-3 text-dark-400 font-medium">Best Value</th>
<th className="text-right py-2 px-3 text-dark-400 font-medium">Avg Value</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Duration</th>
</tr>
</thead>
<tbody>
{runs.map((run) => {
// Calculate duration if times available
let duration = '-';
if (run.first_trial && run.last_trial) {
const start = new Date(run.first_trial);
const end = new Date(run.last_trial);
const diffMs = end.getTime() - start.getTime();
const diffMins = Math.round(diffMs / 60000);
if (diffMins < 60) {
duration = `${diffMins}m`;
} else {
const hours = Math.floor(diffMins / 60);
const mins = diffMins % 60;
duration = `${hours}h ${mins}m`;
}
}
return (
<tr key={run.run_id} className="border-b border-dark-700 hover:bg-dark-750">
<td className="py-2 px-3 font-mono text-white">{run.name}</td>
<td className="py-2 px-3">
<span className={`px-2 py-0.5 rounded text-xs ${
run.source === 'NN'
? 'bg-purple-500/20 text-purple-400'
: 'bg-blue-500/20 text-blue-400'
}`}>
{run.source}
</span>
</td>
<td className="py-2 px-3 text-right font-mono text-white">{run.trial_count}</td>
<td className="py-2 px-3 text-right font-mono text-green-400">
{run.best_value !== null ? run.best_value.toExponential(4) : '-'}
</td>
<td className="py-2 px-3 text-right font-mono text-dark-300">
{run.avg_value !== null ? run.avg_value.toExponential(4) : '-'}
</td>
<td className="py-2 px-3 text-dark-400">{duration}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}
export default PlotlyRunComparison;

View File

@@ -1,202 +0,0 @@
import { useMemo } from 'react';
import Plot from 'react-plotly.js';
interface TrialData {
trial_number: number;
values: number[];
source?: 'FEA' | 'NN' | 'V10_FEA';
user_attrs?: Record<string, any>;
}
interface PlotlySurrogateQualityProps {
trials: TrialData[];
height?: number;
}
export function PlotlySurrogateQuality({
trials,
height = 400
}: PlotlySurrogateQualityProps) {
const { feaTrials, nnTrials, timeline } = useMemo(() => {
const fea = trials.filter(t => t.source === 'FEA' || t.source === 'V10_FEA');
const nn = trials.filter(t => t.source === 'NN');
// Sort by trial number for timeline
const sorted = [...trials].sort((a, b) => a.trial_number - b.trial_number);
// Calculate source distribution over time
const timeline: { trial: number; feaCount: number; nnCount: number }[] = [];
let feaCount = 0;
let nnCount = 0;
sorted.forEach(t => {
if (t.source === 'NN') nnCount++;
else feaCount++;
timeline.push({
trial: t.trial_number,
feaCount,
nnCount
});
});
return {
feaTrials: fea,
nnTrials: nn,
timeline
};
}, [trials]);
if (nnTrials.length === 0) {
return (
<div className="h-64 flex items-center justify-center text-dark-400">
<p>No neural network evaluations in this study</p>
</div>
);
}
// Objective distribution by source
const feaObjectives = feaTrials.map(t => t.values[0]).filter(v => v !== undefined && !isNaN(v));
const nnObjectives = nnTrials.map(t => t.values[0]).filter(v => v !== undefined && !isNaN(v));
return (
<div className="space-y-6">
{/* Source Distribution Over Time */}
<Plot
data={[
{
x: timeline.map(t => t.trial),
y: timeline.map(t => t.feaCount),
type: 'scatter',
mode: 'lines',
name: 'FEA Cumulative',
line: { color: '#3b82f6', width: 2 },
fill: 'tozeroy',
fillcolor: 'rgba(59, 130, 246, 0.2)'
},
{
x: timeline.map(t => t.trial),
y: timeline.map(t => t.nnCount),
type: 'scatter',
mode: 'lines',
name: 'NN Cumulative',
line: { color: '#a855f7', width: 2 },
fill: 'tozeroy',
fillcolor: 'rgba(168, 85, 247, 0.2)'
}
]}
layout={{
title: {
text: 'Evaluation Source Over Time',
font: { color: '#fff', size: 14 }
},
height: height * 0.6,
margin: { l: 60, r: 30, t: 50, b: 50 },
paper_bgcolor: 'transparent',
plot_bgcolor: 'transparent',
xaxis: {
title: { text: 'Trial Number', font: { color: '#888' } },
tickfont: { color: '#888' },
gridcolor: 'rgba(255,255,255,0.05)'
},
yaxis: {
title: { text: 'Cumulative Count', font: { color: '#888' } },
tickfont: { color: '#888' },
gridcolor: 'rgba(255,255,255,0.1)'
},
legend: {
font: { color: '#888' },
bgcolor: 'rgba(0,0,0,0.5)',
orientation: 'h',
y: 1.1
},
showlegend: true
}}
config={{
displayModeBar: true,
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
displaylogo: false
}}
style={{ width: '100%' }}
/>
{/* Objective Distribution by Source */}
<Plot
data={[
{
x: feaObjectives,
type: 'histogram',
name: 'FEA',
marker: { color: 'rgba(59, 130, 246, 0.7)' },
opacity: 0.8
} as any,
{
x: nnObjectives,
type: 'histogram',
name: 'NN',
marker: { color: 'rgba(168, 85, 247, 0.7)' },
opacity: 0.8
} as any
]}
layout={{
title: {
text: 'Objective Distribution by Source',
font: { color: '#fff', size: 14 }
},
height: height * 0.5,
margin: { l: 60, r: 30, t: 50, b: 50 },
paper_bgcolor: 'transparent',
plot_bgcolor: 'transparent',
xaxis: {
title: { text: 'Objective Value', font: { color: '#888' } },
tickfont: { color: '#888' },
gridcolor: 'rgba(255,255,255,0.05)'
},
yaxis: {
title: { text: 'Count', font: { color: '#888' } },
tickfont: { color: '#888' },
gridcolor: 'rgba(255,255,255,0.1)'
},
barmode: 'overlay',
legend: {
font: { color: '#888' },
bgcolor: 'rgba(0,0,0,0.5)',
orientation: 'h',
y: 1.1
},
showlegend: true
}}
config={{
displayModeBar: true,
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
displaylogo: false
}}
style={{ width: '100%' }}
/>
{/* FEA vs NN Best Values Comparison */}
{feaObjectives.length > 0 && nnObjectives.length > 0 && (
<div className="grid grid-cols-2 gap-4 mt-4">
<div className="bg-dark-750 rounded-lg p-4 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-2">FEA Best</div>
<div className="text-xl font-mono text-blue-400">
{Math.min(...feaObjectives).toExponential(4)}
</div>
<div className="text-xs text-dark-500 mt-1">
from {feaObjectives.length} evaluations
</div>
</div>
<div className="bg-dark-750 rounded-lg p-4 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-2">NN Best</div>
<div className="text-xl font-mono text-purple-400">
{Math.min(...nnObjectives).toExponential(4)}
</div>
<div className="text-xs text-dark-500 mt-1">
from {nnObjectives.length} predictions
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,217 +0,0 @@
# Plotly Chart Components
Interactive visualization components using Plotly.js for the Atomizer Dashboard.
## Overview
These components provide enhanced interactivity compared to Recharts:
- Native zoom, pan, and selection
- Export to PNG/SVG
- Hover tooltips with detailed information
- Brush filtering (parallel coordinates)
- 3D visualization support
## Components
### PlotlyParallelCoordinates
Multi-dimensional data visualization showing relationships between all variables.
```tsx
import { PlotlyParallelCoordinates } from '../components/plotly';
<PlotlyParallelCoordinates
trials={allTrials}
objectives={studyMetadata.objectives}
designVariables={studyMetadata.design_variables}
paretoFront={paretoFront}
height={450}
/>
```
**Props:**
| Prop | Type | Description |
|------|------|-------------|
| trials | Trial[] | All trial data |
| objectives | Objective[] | Objective definitions |
| designVariables | DesignVariable[] | Design variable definitions |
| paretoFront | Trial[] | Pareto-optimal trials (optional) |
| height | number | Chart height in pixels |
**Features:**
- Drag on axes to filter data
- Double-click to reset filters
- Color coding: FEA (blue), NN (orange), Pareto (green)
### PlotlyParetoPlot
2D/3D scatter plot for Pareto front visualization.
```tsx
<PlotlyParetoPlot
trials={allTrials}
paretoFront={paretoFront}
objectives={studyMetadata.objectives}
height={350}
/>
```
**Props:**
| Prop | Type | Description |
|------|------|-------------|
| trials | Trial[] | All trial data |
| paretoFront | Trial[] | Pareto-optimal trials |
| objectives | Objective[] | Objective definitions |
| height | number | Chart height in pixels |
**Features:**
- Toggle between 2D and 3D views
- Axis selector for multi-objective problems
- Click to select trials
- Hover for trial details
### PlotlyConvergencePlot
Optimization progress over trials.
```tsx
<PlotlyConvergencePlot
trials={allTrials}
objectiveIndex={0}
objectiveName="weighted_objective"
direction="minimize"
height={350}
/>
```
**Props:**
| Prop | Type | Description |
|------|------|-------------|
| trials | Trial[] | All trial data |
| objectiveIndex | number | Which objective to plot |
| objectiveName | string | Objective display name |
| direction | 'minimize' \| 'maximize' | Optimization direction |
| height | number | Chart height |
| showRangeSlider | boolean | Show zoom slider |
**Features:**
- Scatter points for each trial
- Best-so-far step line
- Range slider for zooming
- FEA vs NN differentiation
### PlotlyParameterImportance
Correlation-based parameter sensitivity analysis.
```tsx
<PlotlyParameterImportance
trials={allTrials}
designVariables={studyMetadata.design_variables}
objectiveIndex={0}
objectiveName="weighted_objective"
height={350}
/>
```
**Props:**
| Prop | Type | Description |
|------|------|-------------|
| trials | Trial[] | All trial data |
| designVariables | DesignVariable[] | Design variables |
| objectiveIndex | number | Which objective |
| objectiveName | string | Objective display name |
| height | number | Chart height |
**Features:**
- Horizontal bar chart of correlations
- Sort by importance or name
- Color: Red (positive), Green (negative)
- Pearson correlation coefficient
## Bundle Optimization
To minimize bundle size, we use:
1. **plotly.js-basic-dist**: Smaller bundle (~1MB vs 3.5MB)
- Includes: scatter, bar, parcoords
- Excludes: 3D plots, maps, animations
2. **Lazy Loading**: Components loaded on demand
```tsx
const PlotlyParetoPlot = lazy(() =>
import('./plotly/PlotlyParetoPlot')
.then(m => ({ default: m.PlotlyParetoPlot }))
);
```
3. **Code Splitting**: Vite config separates Plotly into its own chunk
```ts
manualChunks: {
plotly: ['plotly.js-basic-dist', 'react-plotly.js']
}
```
## Usage with Suspense
Always wrap Plotly components with Suspense:
```tsx
<Suspense fallback={<ChartLoading />}>
<PlotlyParetoPlot {...props} />
</Suspense>
```
## Type Definitions
```typescript
interface Trial {
trial_number: number;
values: number[];
params: Record<string, number>;
user_attrs?: Record<string, any>;
source?: 'FEA' | 'NN' | 'V10_FEA';
}
interface Objective {
name: string;
direction?: 'minimize' | 'maximize';
unit?: string;
}
interface DesignVariable {
name: string;
unit?: string;
min?: number;
max?: number;
}
```
## Styling
Components use transparent backgrounds for dark theme compatibility:
- `paper_bgcolor: 'rgba(0,0,0,0)'`
- `plot_bgcolor: 'rgba(0,0,0,0)'`
- Font: Inter, system-ui, sans-serif
- Grid colors: Tailwind gray palette
## Export Options
All Plotly charts include a mode bar with:
- Download PNG
- Download SVG (via menu)
- Zoom, Pan, Reset
- Auto-scale
Configure export in the `config` prop:
```tsx
config={{
toImageButtonOptions: {
format: 'png',
filename: 'my_chart',
height: 600,
width: 1200,
scale: 2
}
}}
```

View File

@@ -1,15 +0,0 @@
/**
* Plotly-based interactive chart components
*
* These components provide enhanced interactivity compared to Recharts:
* - Native zoom/pan
* - Brush selection on axes
* - 3D views for multi-objective problems
* - Export to PNG/SVG
* - Detailed hover tooltips
*/
export { PlotlyParallelCoordinates } from './PlotlyParallelCoordinates';
export { PlotlyParetoPlot } from './PlotlyParetoPlot';
export { PlotlyConvergencePlot } from './PlotlyConvergencePlot';
export { PlotlyParameterImportance } from './PlotlyParameterImportance';

View File

@@ -3,3 +3,27 @@ export { useCanvasStore } from './useCanvasStore';
export type { OptimizationConfig } from './useCanvasStore'; export type { OptimizationConfig } from './useCanvasStore';
export { useCanvasChat } from './useCanvasChat'; export { useCanvasChat } from './useCanvasChat';
export { useIntentParser } from './useIntentParser'; export { useIntentParser } from './useIntentParser';
// Spec Store (AtomizerSpec v2.0)
export {
useSpecStore,
useSpec,
useSpecLoading,
useSpecError,
useSpecValidation,
useSelectedNodeId,
useSelectedEdgeId,
useSpecHash,
useSpecIsDirty,
useDesignVariables,
useExtractors,
useObjectives,
useConstraints,
useCanvasEdges,
useSelectedNode,
} from './useSpecStore';
// WebSocket Sync
export { useSpecWebSocket } from './useSpecWebSocket';
export type { ConnectionStatus } from './useSpecWebSocket';
export { ConnectionStatusIndicator } from '../components/canvas/ConnectionStatusIndicator';

View File

@@ -11,12 +11,25 @@ export interface CanvasState {
studyPath?: string; studyPath?: string;
} }
export interface CanvasModification {
action: 'add_node' | 'update_node' | 'remove_node' | 'add_edge' | 'remove_edge';
nodeType?: string;
nodeId?: string;
edgeId?: string;
data?: Record<string, any>;
source?: string;
target?: string;
position?: { x: number; y: number };
}
interface UseChatOptions { interface UseChatOptions {
studyId?: string | null; studyId?: string | null;
mode?: ChatMode; mode?: ChatMode;
useWebSocket?: boolean; useWebSocket?: boolean;
canvasState?: CanvasState | null; canvasState?: CanvasState | null;
onError?: (error: string) => void; onError?: (error: string) => void;
onCanvasModification?: (modification: CanvasModification) => void;
onSpecUpdated?: (spec: any) => void; // Called when Claude modifies the spec
} }
interface ChatState { interface ChatState {
@@ -35,6 +48,8 @@ export function useChat({
useWebSocket = true, useWebSocket = true,
canvasState: initialCanvasState, canvasState: initialCanvasState,
onError, onError,
onCanvasModification,
onSpecUpdated,
}: UseChatOptions = {}) { }: UseChatOptions = {}) {
const [state, setState] = useState<ChatState>({ const [state, setState] = useState<ChatState>({
messages: [], messages: [],
@@ -49,6 +64,23 @@ export function useChat({
// Track canvas state for sending with messages // Track canvas state for sending with messages
const canvasStateRef = useRef<CanvasState | null>(initialCanvasState || null); const canvasStateRef = useRef<CanvasState | null>(initialCanvasState || null);
// Sync mode prop changes to internal state (triggers WebSocket reconnect)
useEffect(() => {
if (mode !== state.mode) {
console.log(`[useChat] Mode prop changed from ${state.mode} to ${mode}, triggering reconnect`);
// Close existing WebSocket
wsRef.current?.close();
wsRef.current = null;
// Update internal state to trigger reconnect
setState((prev) => ({
...prev,
mode,
sessionId: null,
isConnected: false,
}));
}
}, [mode]);
const abortControllerRef = useRef<AbortController | null>(null); const abortControllerRef = useRef<AbortController | null>(null);
const conversationHistoryRef = useRef<Array<{ role: string; content: string }>>([]); const conversationHistoryRef = useRef<Array<{ role: string; content: string }>>([]);
const wsRef = useRef<WebSocket | null>(null); const wsRef = useRef<WebSocket | null>(null);
@@ -82,9 +114,16 @@ export function useChat({
const data = await response.json(); const data = await response.json();
setState((prev) => ({ ...prev, sessionId: data.session_id })); setState((prev) => ({ ...prev, sessionId: data.session_id }));
// Connect WebSocket // Connect WebSocket - use backend directly in dev mode
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/api/claude/sessions/${data.session_id}/ws`; // Use port 8001 to match start-dashboard.bat
const backendHost = import.meta.env.DEV ? 'localhost:8001' : window.location.host;
// Both modes use the same WebSocket - mode is handled by session config
// Power mode uses --dangerously-skip-permissions in CLI
// User mode uses --allowedTools to restrict access
const wsPath = `/api/claude/sessions/${data.session_id}/ws`;
const wsUrl = `${protocol}//${backendHost}${wsPath}`;
console.log(`[useChat] Connecting to WebSocket (${state.mode} mode): ${wsUrl}`);
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);
ws.onopen = () => { ws.onopen = () => {
@@ -126,6 +165,9 @@ export function useChat({
// Handle WebSocket messages // Handle WebSocket messages
const handleWebSocketMessage = useCallback((data: any) => { const handleWebSocketMessage = useCallback((data: any) => {
// Debug: log all incoming WebSocket messages
console.log('[useChat] WebSocket message received:', data.type, data);
switch (data.type) { switch (data.type) {
case 'text': case 'text':
currentMessageRef.current += data.content || ''; currentMessageRef.current += data.content || '';
@@ -212,11 +254,51 @@ export function useChat({
// Canvas state was updated - could show notification // Canvas state was updated - could show notification
break; break;
case 'canvas_modification':
// Assistant wants to modify the canvas (from MCP tools in user mode)
console.log('[useChat] Received canvas_modification:', data.modification);
if (onCanvasModification && data.modification) {
console.log('[useChat] Calling onCanvasModification callback');
onCanvasModification(data.modification);
} else {
console.warn('[useChat] canvas_modification received but no handler or modification:', {
hasCallback: !!onCanvasModification,
modification: data.modification
});
}
break;
case 'spec_updated':
// Assistant modified the spec - we receive the full updated spec
console.log('[useChat] Spec updated by assistant:', data.tool, data.reason);
if (onSpecUpdated && data.spec) {
// Directly update the canvas with the new spec
onSpecUpdated(data.spec);
}
break;
case 'spec_modified':
// Legacy: Assistant modified the spec directly (from power mode write tools)
console.log('[useChat] Spec was modified by assistant (legacy):', data.tool, data.changes);
// Treat this as a canvas modification to trigger reload
if (onCanvasModification) {
// Create a synthetic modification event to trigger canvas refresh
onCanvasModification({
action: 'add_node', // Use add_node as it triggers refresh
data: {
_refresh: true,
tool: data.tool,
changes: data.changes,
},
});
}
break;
case 'pong': case 'pong':
// Heartbeat response - ignore // Heartbeat response - ignore
break; break;
} }
}, [onError]); }, [onError, onCanvasModification]);
// Switch mode (requires new session) // Switch mode (requires new session)
const switchMode = useCallback(async (newMode: ChatMode) => { const switchMode = useCallback(async (newMode: ChatMode) => {
@@ -462,6 +544,18 @@ export function useChat({
} }
}, [useWebSocket]); }, [useWebSocket]);
// Notify backend when user edits canvas (so Claude sees the changes)
const notifyCanvasEdit = useCallback((spec: any) => {
if (useWebSocket && wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(
JSON.stringify({
type: 'canvas_edit',
spec: spec,
})
);
}
}, [useWebSocket]);
return { return {
messages: state.messages, messages: state.messages,
isThinking: state.isThinking, isThinking: state.isThinking,
@@ -475,5 +569,6 @@ export function useChat({
cancelRequest, cancelRequest,
switchMode, switchMode,
updateCanvasState, updateCanvasState,
notifyCanvasEdit,
}; };
} }

View File

@@ -0,0 +1,349 @@
/**
* Hook for Claude Code CLI integration
*
* Connects to backend that spawns actual Claude Code CLI processes.
* This gives full power: file editing, command execution, etc.
*
* Unlike useChat (which uses MCP tools), this hook:
* - Spawns actual Claude Code CLI in the backend
* - Has full file system access
* - Can edit files directly (not just return instructions)
* - Uses Opus 4.5 model
* - Has all Claude Code capabilities
*/
import { useState, useCallback, useRef, useEffect } from 'react';
import { Message } from '../components/chat/ChatMessage';
import { useCanvasStore } from './useCanvasStore';
export interface CanvasState {
nodes: any[];
edges: any[];
studyName?: string;
studyPath?: string;
}
interface UseClaudeCodeOptions {
studyId?: string | null;
canvasState?: CanvasState | null;
onError?: (error: string) => void;
onCanvasRefresh?: (studyId: string) => void;
}
interface ClaudeCodeState {
messages: Message[];
isThinking: boolean;
error: string | null;
sessionId: string | null;
isConnected: boolean;
workingDir: string | null;
}
export function useClaudeCode({
studyId,
canvasState: initialCanvasState,
onError,
onCanvasRefresh,
}: UseClaudeCodeOptions = {}) {
const [state, setState] = useState<ClaudeCodeState>({
messages: [],
isThinking: false,
error: null,
sessionId: null,
isConnected: false,
workingDir: null,
});
// Track canvas state for sending with messages
const canvasStateRef = useRef<CanvasState | null>(initialCanvasState || null);
const wsRef = useRef<WebSocket | null>(null);
const currentMessageRef = useRef<string>('');
const reconnectAttempts = useRef(0);
const maxReconnectAttempts = 3;
// Keep canvas state in sync with prop changes
useEffect(() => {
if (initialCanvasState) {
canvasStateRef.current = initialCanvasState;
}
}, [initialCanvasState]);
// Get canvas store for auto-refresh
const { loadFromConfig } = useCanvasStore();
// Connect to Claude Code WebSocket
useEffect(() => {
const connect = () => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// In development, connect directly to backend (bypass Vite proxy for WebSockets)
// Use port 8001 to match start-dashboard.bat
const backendHost = import.meta.env.DEV ? 'localhost:8001' : window.location.host;
// Use study-specific endpoint if studyId provided
const wsUrl = studyId
? `${protocol}//${backendHost}/api/claude-code/ws/${encodeURIComponent(studyId)}`
: `${protocol}//${backendHost}/api/claude-code/ws`;
console.log('[ClaudeCode] Connecting to:', wsUrl);
const ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('[ClaudeCode] Connected');
setState((prev) => ({ ...prev, isConnected: true, error: null }));
reconnectAttempts.current = 0;
// If no studyId in URL, send init message
if (!studyId) {
ws.send(JSON.stringify({ type: 'init', study_id: null }));
}
};
ws.onclose = () => {
console.log('[ClaudeCode] Disconnected');
setState((prev) => ({ ...prev, isConnected: false }));
// Attempt reconnection
if (reconnectAttempts.current < maxReconnectAttempts) {
reconnectAttempts.current++;
console.log(`[ClaudeCode] Reconnecting... attempt ${reconnectAttempts.current}`);
setTimeout(connect, 2000 * reconnectAttempts.current);
}
};
ws.onerror = (event) => {
console.error('[ClaudeCode] WebSocket error:', event);
setState((prev) => ({ ...prev, isConnected: false }));
onError?.('Claude Code connection error');
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
handleWebSocketMessage(data);
} catch (e) {
console.error('[ClaudeCode] Failed to parse message:', e);
}
};
wsRef.current = ws;
};
connect();
return () => {
reconnectAttempts.current = maxReconnectAttempts; // Prevent reconnection on unmount
wsRef.current?.close();
wsRef.current = null;
};
}, [studyId]);
// Handle WebSocket messages
const handleWebSocketMessage = useCallback(
(data: any) => {
switch (data.type) {
case 'initialized':
console.log('[ClaudeCode] Session initialized:', data.session_id);
setState((prev) => ({
...prev,
sessionId: data.session_id,
workingDir: data.working_dir || null,
}));
break;
case 'text':
currentMessageRef.current += data.content || '';
setState((prev) => ({
...prev,
messages: prev.messages.map((msg, idx) =>
idx === prev.messages.length - 1 && msg.role === 'assistant'
? { ...msg, content: currentMessageRef.current }
: msg
),
}));
break;
case 'done':
setState((prev) => ({
...prev,
isThinking: false,
messages: prev.messages.map((msg, idx) =>
idx === prev.messages.length - 1 && msg.role === 'assistant'
? { ...msg, isStreaming: false }
: msg
),
}));
currentMessageRef.current = '';
break;
case 'error':
console.error('[ClaudeCode] Error:', data.content);
setState((prev) => ({
...prev,
isThinking: false,
error: data.content || 'Unknown error',
}));
onError?.(data.content || 'Unknown error');
currentMessageRef.current = '';
break;
case 'refresh_canvas':
// Claude made file changes - trigger canvas refresh
console.log('[ClaudeCode] Canvas refresh requested:', data.reason);
if (data.study_id) {
onCanvasRefresh?.(data.study_id);
reloadCanvasFromStudy(data.study_id);
}
break;
case 'canvas_updated':
console.log('[ClaudeCode] Canvas state updated');
break;
case 'pong':
// Heartbeat response
break;
default:
console.log('[ClaudeCode] Unknown message type:', data.type);
}
},
[onError, onCanvasRefresh]
);
// Reload canvas from study config
const reloadCanvasFromStudy = useCallback(
async (studyIdToReload: string) => {
try {
console.log('[ClaudeCode] Reloading canvas for study:', studyIdToReload);
// Fetch fresh config from backend
const response = await fetch(`/api/optimization/studies/${encodeURIComponent(studyIdToReload)}/config`);
if (!response.ok) {
throw new Error(`Failed to fetch config: ${response.status}`);
}
const data = await response.json();
const config = data.config; // API returns { config: ..., path: ..., study_id: ... }
// Reload canvas with new config
loadFromConfig(config);
// Add system message about refresh
const refreshMessage: Message = {
id: `msg_${Date.now()}_refresh`,
role: 'system',
content: `Canvas refreshed with latest changes from ${studyIdToReload}`,
timestamp: new Date(),
};
setState((prev) => ({
...prev,
messages: [...prev.messages, refreshMessage],
}));
} catch (error) {
console.error('[ClaudeCode] Failed to reload canvas:', error);
}
},
[loadFromConfig]
);
const generateMessageId = () => {
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
};
const sendMessage = useCallback(
async (content: string) => {
if (!content.trim() || state.isThinking) return;
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
onError?.('Not connected to Claude Code');
return;
}
// Add user message
const userMessage: Message = {
id: generateMessageId(),
role: 'user',
content: content.trim(),
timestamp: new Date(),
};
// Add assistant message placeholder
const assistantMessage: Message = {
id: generateMessageId(),
role: 'assistant',
content: '',
timestamp: new Date(),
isStreaming: true,
};
setState((prev) => ({
...prev,
messages: [...prev.messages, userMessage, assistantMessage],
isThinking: true,
error: null,
}));
// Reset current message tracking
currentMessageRef.current = '';
// Send message via WebSocket with canvas state
wsRef.current.send(
JSON.stringify({
type: 'message',
content: content.trim(),
canvas_state: canvasStateRef.current || undefined,
})
);
},
[state.isThinking, onError]
);
const clearMessages = useCallback(() => {
setState((prev) => ({
...prev,
messages: [],
error: null,
}));
currentMessageRef.current = '';
}, []);
// Update canvas state (call this when canvas changes)
const updateCanvasState = useCallback((newCanvasState: CanvasState | null) => {
canvasStateRef.current = newCanvasState;
// Also send to backend to update context
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(
JSON.stringify({
type: 'set_canvas',
canvas_state: newCanvasState,
})
);
}
}, []);
// Send ping to keep connection alive
useEffect(() => {
const pingInterval = setInterval(() => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({ type: 'ping' }));
}
}, 30000); // Every 30 seconds
return () => clearInterval(pingInterval);
}, []);
return {
messages: state.messages,
isThinking: state.isThinking,
error: state.error,
sessionId: state.sessionId,
isConnected: state.isConnected,
workingDir: state.workingDir,
sendMessage,
clearMessages,
updateCanvasState,
reloadCanvasFromStudy,
};
}

View File

@@ -0,0 +1,288 @@
/**
* useSpecWebSocket - WebSocket connection for real-time spec sync
*
* Connects to the backend WebSocket endpoint for live spec updates.
* Handles auto-reconnection, message parsing, and store updates.
*
* P2.11-P2.14: WebSocket sync implementation
*/
import { useEffect, useRef, useCallback, useState } from 'react';
import { useSpecStore } from './useSpecStore';
// ============================================================================
// Types
// ============================================================================
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
interface SpecWebSocketMessage {
type: 'modification' | 'full_sync' | 'error' | 'ping';
payload: unknown;
}
interface ModificationPayload {
operation: 'set' | 'add' | 'remove';
path: string;
value?: unknown;
modified_by: string;
timestamp: string;
hash: string;
}
interface ErrorPayload {
message: string;
code?: string;
}
interface UseSpecWebSocketOptions {
/**
* Enable auto-reconnect on disconnect (default: true)
*/
autoReconnect?: boolean;
/**
* Reconnect delay in ms (default: 3000)
*/
reconnectDelay?: number;
/**
* Max reconnect attempts (default: 10)
*/
maxReconnectAttempts?: number;
/**
* Client identifier for tracking modifications (default: 'canvas')
*/
clientId?: string;
}
interface UseSpecWebSocketReturn {
/**
* Current connection status
*/
status: ConnectionStatus;
/**
* Manually disconnect
*/
disconnect: () => void;
/**
* Manually reconnect
*/
reconnect: () => void;
/**
* Send a message to the WebSocket (for future use)
*/
send: (message: SpecWebSocketMessage) => void;
/**
* Last error message if any
*/
lastError: string | null;
}
// ============================================================================
// Hook
// ============================================================================
export function useSpecWebSocket(
studyId: string | null,
options: UseSpecWebSocketOptions = {}
): UseSpecWebSocketReturn {
const {
autoReconnect = true,
reconnectDelay = 3000,
maxReconnectAttempts = 10,
clientId = 'canvas',
} = options;
const wsRef = useRef<WebSocket | null>(null);
const reconnectAttemptsRef = useRef(0);
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [status, setStatus] = useState<ConnectionStatus>('disconnected');
const [lastError, setLastError] = useState<string | null>(null);
// Get store actions
const reloadSpec = useSpecStore((s) => s.reloadSpec);
const setError = useSpecStore((s) => s.setError);
// Build WebSocket URL
const getWsUrl = useCallback((id: string): string => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
return `${protocol}//${host}/api/studies/${encodeURIComponent(id)}/spec/sync?client_id=${clientId}`;
}, [clientId]);
// Handle incoming messages
const handleMessage = useCallback((event: MessageEvent) => {
try {
const message: SpecWebSocketMessage = JSON.parse(event.data);
switch (message.type) {
case 'modification': {
const payload = message.payload as ModificationPayload;
// Skip if this is our own modification
if (payload.modified_by === clientId) {
return;
}
// Reload spec to get latest state
// In a more sophisticated implementation, we could apply the patch locally
reloadSpec().catch((err) => {
console.error('Failed to reload spec after modification:', err);
});
break;
}
case 'full_sync': {
// Full spec sync requested (e.g., after reconnect)
reloadSpec().catch((err) => {
console.error('Failed to reload spec during full_sync:', err);
});
break;
}
case 'error': {
const payload = message.payload as ErrorPayload;
console.error('WebSocket error:', payload.message);
setLastError(payload.message);
setError(payload.message);
break;
}
case 'ping': {
// Keep-alive ping, respond with pong
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({ type: 'pong' }));
}
break;
}
default:
console.warn('Unknown WebSocket message type:', message.type);
}
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
}, [clientId, reloadSpec, setError]);
// Connect to WebSocket
const connect = useCallback(() => {
if (!studyId) return;
// Clean up existing connection
if (wsRef.current) {
wsRef.current.close();
}
setStatus('connecting');
setLastError(null);
const url = getWsUrl(studyId);
const ws = new WebSocket(url);
ws.onopen = () => {
setStatus('connected');
reconnectAttemptsRef.current = 0;
};
ws.onmessage = handleMessage;
ws.onerror = (event) => {
console.error('WebSocket error:', event);
setLastError('WebSocket connection error');
};
ws.onclose = (_event) => {
setStatus('disconnected');
// Check if we should reconnect
if (autoReconnect && reconnectAttemptsRef.current < maxReconnectAttempts) {
reconnectAttemptsRef.current++;
setStatus('reconnecting');
// Clear any existing reconnect timeout
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
// Schedule reconnect with exponential backoff
const delay = reconnectDelay * Math.min(reconnectAttemptsRef.current, 5);
reconnectTimeoutRef.current = setTimeout(() => {
connect();
}, delay);
} else if (reconnectAttemptsRef.current >= maxReconnectAttempts) {
setLastError('Max reconnection attempts reached');
}
};
wsRef.current = ws;
}, [studyId, getWsUrl, handleMessage, autoReconnect, reconnectDelay, maxReconnectAttempts]);
// Disconnect
const disconnect = useCallback(() => {
// Clear reconnect timeout
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
// Close WebSocket
if (wsRef.current) {
wsRef.current.close();
wsRef.current = null;
}
reconnectAttemptsRef.current = maxReconnectAttempts; // Prevent auto-reconnect
setStatus('disconnected');
}, [maxReconnectAttempts]);
// Reconnect
const reconnect = useCallback(() => {
reconnectAttemptsRef.current = 0;
connect();
}, [connect]);
// Send message
const send = useCallback((message: SpecWebSocketMessage) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify(message));
} else {
console.warn('WebSocket not connected, cannot send message');
}
}, []);
// Connect when studyId changes
useEffect(() => {
if (studyId) {
connect();
} else {
disconnect();
}
return () => {
// Cleanup on unmount or studyId change
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
if (wsRef.current) {
wsRef.current.close();
}
};
}, [studyId, connect, disconnect]);
return {
status,
disconnect,
reconnect,
send,
lastError,
};
}
export default useSpecWebSocket;

View File

@@ -18,7 +18,8 @@ export const useOptimizationWebSocket = ({ studyId, onMessage }: UseOptimization
const host = window.location.host; // This will be localhost:3000 in dev const host = window.location.host; // This will be localhost:3000 in dev
// If using proxy in vite.config.ts, this works. // If using proxy in vite.config.ts, this works.
// If not, we might need to hardcode backend URL for dev: // If not, we might need to hardcode backend URL for dev:
const backendHost = import.meta.env.DEV ? 'localhost:8000' : host; // Use port 8001 to match start-dashboard.bat
const backendHost = import.meta.env.DEV ? 'localhost:8001' : host;
setSocketUrl(`${protocol}//${backendHost}/api/ws/optimization/${studyId}`); setSocketUrl(`${protocol}//${backendHost}/api/ws/optimization/${studyId}`);
} else { } else {

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, lazy, Suspense, useMemo } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { import {
BarChart3, BarChart3,
@@ -14,25 +14,10 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { useStudy } from '../context/StudyContext'; import { useStudy } from '../context/StudyContext';
import { Card } from '../components/common/Card'; import { Card } from '../components/common/Card';
import { ConvergencePlot } from '../components/ConvergencePlot';
// Lazy load charts import { ParameterImportanceChart } from '../components/ParameterImportanceChart';
const PlotlyParetoPlot = lazy(() => import('../components/plotly/PlotlyParetoPlot').then(m => ({ default: m.PlotlyParetoPlot }))); import { ParallelCoordinatesPlot } from '../components/ParallelCoordinatesPlot';
const PlotlyParallelCoordinates = lazy(() => import('../components/plotly/PlotlyParallelCoordinates').then(m => ({ default: m.PlotlyParallelCoordinates }))); import { ParetoPlot } from '../components/ParetoPlot';
const PlotlyParameterImportance = lazy(() => import('../components/plotly/PlotlyParameterImportance').then(m => ({ default: m.PlotlyParameterImportance })));
const PlotlyConvergencePlot = lazy(() => import('../components/plotly/PlotlyConvergencePlot').then(m => ({ default: m.PlotlyConvergencePlot })));
const PlotlyCorrelationHeatmap = lazy(() => import('../components/plotly/PlotlyCorrelationHeatmap').then(m => ({ default: m.PlotlyCorrelationHeatmap })));
const PlotlyFeasibilityChart = lazy(() => import('../components/plotly/PlotlyFeasibilityChart').then(m => ({ default: m.PlotlyFeasibilityChart })));
const PlotlySurrogateQuality = lazy(() => import('../components/plotly/PlotlySurrogateQuality').then(m => ({ default: m.PlotlySurrogateQuality })));
const PlotlyRunComparison = lazy(() => import('../components/plotly/PlotlyRunComparison').then(m => ({ default: m.PlotlyRunComparison })));
const ChartLoading = () => (
<div className="flex items-center justify-center h-64 text-dark-400">
<div className="flex flex-col items-center gap-2">
<div className="animate-spin w-6 h-6 border-2 border-primary-500 border-t-transparent rounded-full"></div>
<span className="text-sm animate-pulse">Loading chart...</span>
</div>
</div>
);
const NoData = ({ message = 'No data available' }: { message?: string }) => ( const NoData = ({ message = 'No data available' }: { message?: string }) => (
<div className="flex items-center justify-center h-64 text-dark-500"> <div className="flex items-center justify-center h-64 text-dark-500">
@@ -383,15 +368,12 @@ export default function Analysis() {
{/* Convergence Plot */} {/* Convergence Plot */}
{trials.length > 0 && ( {trials.length > 0 && (
<Card title="Convergence Plot"> <Card title="Convergence Plot">
<Suspense fallback={<ChartLoading />}> <ConvergencePlot
<PlotlyConvergencePlot trials={trials}
trials={trials} objectiveIndex={0}
objectiveIndex={0} objectiveName={metadata?.objectives?.[0]?.name || 'Objective'}
objectiveName={metadata?.objectives?.[0]?.name || 'Objective'} direction="minimize"
direction="minimize" />
height={350}
/>
</Suspense>
</Card> </Card>
)} )}
@@ -455,30 +437,24 @@ export default function Analysis() {
{/* Parameter Importance */} {/* Parameter Importance */}
{trials.length > 0 && metadata?.design_variables && ( {trials.length > 0 && metadata?.design_variables && (
<Card title="Parameter Importance"> <Card title="Parameter Importance">
<Suspense fallback={<ChartLoading />}> <ParameterImportanceChart
<PlotlyParameterImportance trials={trials}
trials={trials} designVariables={metadata.design_variables}
designVariables={metadata.design_variables} objectiveIndex={0}
objectiveIndex={0} objectiveName={metadata?.objectives?.[0]?.name || 'Objective'}
objectiveName={metadata?.objectives?.[0]?.name || 'Objective'} />
height={400}
/>
</Suspense>
</Card> </Card>
)} )}
{/* Parallel Coordinates */} {/* Parallel Coordinates */}
{trials.length > 0 && metadata && ( {trials.length > 0 && metadata && (
<Card title="Parallel Coordinates"> <Card title="Parallel Coordinates">
<Suspense fallback={<ChartLoading />}> <ParallelCoordinatesPlot
<PlotlyParallelCoordinates paretoData={trials}
trials={trials} objectives={metadata.objectives || []}
objectives={metadata.objectives || []} designVariables={metadata.design_variables || []}
designVariables={metadata.design_variables || []} paretoFront={paretoFront}
paretoFront={paretoFront} />
height={450}
/>
</Suspense>
</Card> </Card>
)} )}
</div> </div>
@@ -508,14 +484,11 @@ export default function Analysis() {
{/* Pareto Front Plot */} {/* Pareto Front Plot */}
{paretoFront.length > 0 && ( {paretoFront.length > 0 && (
<Card title="Pareto Front"> <Card title="Pareto Front">
<Suspense fallback={<ChartLoading />}> <ParetoPlot
<PlotlyParetoPlot paretoData={paretoFront}
trials={trials} objectives={metadata?.objectives || []}
paretoFront={paretoFront} allTrials={trials}
objectives={metadata?.objectives || []} />
height={500}
/>
</Suspense>
</Card> </Card>
)} )}
@@ -550,16 +523,10 @@ export default function Analysis() {
{/* Correlations Tab */} {/* Correlations Tab */}
{activeTab === 'correlations' && ( {activeTab === 'correlations' && (
<div className="space-y-6"> <div className="space-y-6">
{/* Correlation Heatmap */} {/* Correlation Analysis */}
{trials.length > 2 && ( {trials.length > 2 && (
<Card title="Parameter-Objective Correlation Matrix"> <Card title="Parameter-Objective Correlation Analysis">
<Suspense fallback={<ChartLoading />}> <CorrelationTable trials={trials} objectiveName={metadata?.objectives?.[0]?.name || 'Objective'} />
<PlotlyCorrelationHeatmap
trials={trials}
objectiveName={metadata?.objectives?.[0]?.name || 'Objective'}
height={Math.min(500, 100 + Object.keys(trials[0]?.params || {}).length * 40)}
/>
</Suspense>
</Card> </Card>
)} )}
@@ -612,11 +579,22 @@ export default function Analysis() {
</Card> </Card>
</div> </div>
{/* Feasibility Over Time Chart */} {/* Feasibility Summary */}
<Card title="Feasibility Rate Over Time"> <Card title="Feasibility Analysis">
<Suspense fallback={<ChartLoading />}> <div className="p-4">
<PlotlyFeasibilityChart trials={trials} height={350} /> <div className="flex items-center gap-4 mb-4">
</Suspense> <div className="flex-1 bg-dark-700 rounded-full h-4 overflow-hidden">
<div
className="h-full bg-green-500 transition-all duration-500"
style={{ width: `${stats.feasibilityRate}%` }}
/>
</div>
<span className="text-lg font-bold text-green-400">{stats.feasibilityRate.toFixed(1)}%</span>
</div>
<p className="text-dark-400 text-sm">
{stats.feasible} of {stats.total} trials satisfy all constraints
</p>
</div>
</Card> </Card>
{/* Infeasible Trials List */} {/* Infeasible Trials List */}
@@ -683,11 +661,38 @@ export default function Analysis() {
</Card> </Card>
</div> </div>
{/* Surrogate Quality Charts */} {/* Surrogate Performance Summary */}
<Card title="Surrogate Model Analysis"> <Card title="Surrogate Model Performance">
<Suspense fallback={<ChartLoading />}> <div className="grid grid-cols-2 gap-6 p-4">
<PlotlySurrogateQuality trials={trials} height={400} /> <div>
</Suspense> <h4 className="text-sm font-semibold text-dark-300 mb-3">Trial Distribution</h4>
<div className="space-y-2">
<div className="flex items-center gap-3">
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
<span className="text-dark-200">FEA: {stats.feaTrials} trials</span>
<span className="text-dark-400 ml-auto">
{((stats.feaTrials / stats.total) * 100).toFixed(0)}%
</span>
</div>
<div className="flex items-center gap-3">
<div className="w-3 h-3 bg-purple-500 rounded-full"></div>
<span className="text-dark-200">NN: {stats.nnTrials} trials</span>
<span className="text-dark-400 ml-auto">
{((stats.nnTrials / stats.total) * 100).toFixed(0)}%
</span>
</div>
</div>
</div>
<div>
<h4 className="text-sm font-semibold text-dark-300 mb-3">Efficiency Gains</h4>
<div className="text-center p-4 bg-dark-750 rounded-lg">
<div className="text-3xl font-bold text-primary-400">
{stats.feaTrials > 0 ? `${(stats.total / stats.feaTrials).toFixed(1)}x` : '1.0x'}
</div>
<div className="text-xs text-dark-400 mt-1">Effective Speedup</div>
</div>
</div>
</div>
</Card> </Card>
</div> </div>
)} )}
@@ -700,9 +705,36 @@ export default function Analysis() {
Compare different optimization runs within this study. Studies with adaptive optimization Compare different optimization runs within this study. Studies with adaptive optimization
may have multiple runs (e.g., initial FEA exploration, NN-accelerated iterations). may have multiple runs (e.g., initial FEA exploration, NN-accelerated iterations).
</p> </p>
<Suspense fallback={<ChartLoading />}> <div className="overflow-x-auto">
<PlotlyRunComparison runs={runs} height={400} /> <table className="w-full text-sm">
</Suspense> <thead>
<tr className="border-b border-dark-600">
<th className="text-left py-2 px-3 text-dark-400 font-medium">Run</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Source</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Trials</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Best Value</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Avg Value</th>
</tr>
</thead>
<tbody>
{runs.map((run) => (
<tr key={run.run_id} className="border-b border-dark-700">
<td className="py-2 px-3 font-mono text-white">{run.name || `Run ${run.run_id}`}</td>
<td className="py-2 px-3">
<span className={`px-2 py-0.5 rounded text-xs ${
run.source === 'NN' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
}`}>
{run.source}
</span>
</td>
<td className="py-2 px-3 text-dark-200">{run.trial_count}</td>
<td className="py-2 px-3 font-mono text-green-400">{run.best_value?.toExponential(4) || 'N/A'}</td>
<td className="py-2 px-3 font-mono text-dark-300">{run.avg_value?.toExponential(4) || 'N/A'}</td>
</tr>
))}
</tbody>
</table>
</div>
</Card> </Card>
</div> </div>
)} )}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, lazy, Suspense, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Settings } from 'lucide-react'; import { Settings } from 'lucide-react';
import { useOptimizationWebSocket } from '../hooks/useWebSocket'; import { useOptimizationWebSocket } from '../hooks/useWebSocket';
@@ -21,19 +21,6 @@ import { CurrentTrialPanel, OptimizerStatePanel } from '../components/tracker';
import { NivoParallelCoordinates } from '../components/charts'; import { NivoParallelCoordinates } from '../components/charts';
import type { Trial } from '../types'; import type { Trial } from '../types';
// Lazy load Plotly components for better initial load performance
const PlotlyParallelCoordinates = lazy(() => import('../components/plotly/PlotlyParallelCoordinates').then(m => ({ default: m.PlotlyParallelCoordinates })));
const PlotlyParetoPlot = lazy(() => import('../components/plotly/PlotlyParetoPlot').then(m => ({ default: m.PlotlyParetoPlot })));
const PlotlyConvergencePlot = lazy(() => import('../components/plotly/PlotlyConvergencePlot').then(m => ({ default: m.PlotlyConvergencePlot })));
const PlotlyParameterImportance = lazy(() => import('../components/plotly/PlotlyParameterImportance').then(m => ({ default: m.PlotlyParameterImportance })));
// Loading placeholder for lazy components
const ChartLoading = () => (
<div className="flex items-center justify-center h-64 text-dark-400">
<div className="animate-pulse">Loading chart...</div>
</div>
);
export default function Dashboard() { export default function Dashboard() {
const navigate = useNavigate(); const navigate = useNavigate();
const { selectedStudy, refreshStudies, isInitialized } = useStudy(); const { selectedStudy, refreshStudies, isInitialized } = useStudy();
@@ -62,8 +49,8 @@ export default function Dashboard() {
const [paretoFront, setParetoFront] = useState<any[]>([]); const [paretoFront, setParetoFront] = useState<any[]>([]);
const [allTrialsRaw, setAllTrialsRaw] = useState<any[]>([]); // All trials for parallel coordinates const [allTrialsRaw, setAllTrialsRaw] = useState<any[]>([]); // All trials for parallel coordinates
// Chart library toggle: 'nivo' (dark theme, default), 'plotly' (more interactive), or 'recharts' (simple) // Chart library toggle: 'nivo' (dark theme, default) or 'recharts' (simple)
const [chartLibrary, setChartLibrary] = useState<'nivo' | 'plotly' | 'recharts'>('nivo'); const [chartLibrary, setChartLibrary] = useState<'nivo' | 'recharts'>('nivo');
// Process status for tracker panels // Process status for tracker panels
const [isRunning, setIsRunning] = useState(false); const [isRunning, setIsRunning] = useState(false);
@@ -464,18 +451,7 @@ export default function Dashboard() {
}`} }`}
title="Modern Nivo charts with dark theme (recommended)" title="Modern Nivo charts with dark theme (recommended)"
> >
Nivo Advanced
</button>
<button
onClick={() => setChartLibrary('plotly')}
className={`px-3 py-1.5 text-sm transition-colors ${
chartLibrary === 'plotly'
? 'bg-primary-500 text-white'
: 'bg-dark-600 text-dark-200 hover:bg-dark-500'
}`}
title="Interactive Plotly charts with zoom, pan, and export"
>
Plotly
</button> </button>
<button <button
onClick={() => setChartLibrary('recharts')} onClick={() => setChartLibrary('recharts')}
@@ -570,22 +546,11 @@ export default function Dashboard() {
title="Pareto Front" title="Pareto Front"
subtitle={`${paretoFront.length} Pareto-optimal solutions | ${studyMetadata.sampler || 'NSGA-II'} | ${studyMetadata.objectives?.length || 2} objectives`} subtitle={`${paretoFront.length} Pareto-optimal solutions | ${studyMetadata.sampler || 'NSGA-II'} | ${studyMetadata.objectives?.length || 2} objectives`}
> >
{chartLibrary === 'plotly' ? ( <ParetoPlot
<Suspense fallback={<ChartLoading />}> paretoData={paretoFront}
<PlotlyParetoPlot objectives={studyMetadata.objectives}
trials={allTrialsRaw} allTrials={allTrialsRaw}
paretoFront={paretoFront} />
objectives={studyMetadata.objectives}
height={300}
/>
</Suspense>
) : (
<ParetoPlot
paretoData={paretoFront}
objectives={studyMetadata.objectives}
allTrials={allTrialsRaw}
/>
)}
</ExpandableChart> </ExpandableChart>
</div> </div>
)} )}
@@ -605,16 +570,6 @@ export default function Dashboard() {
paretoFront={paretoFront} paretoFront={paretoFront}
height={380} height={380}
/> />
) : chartLibrary === 'plotly' ? (
<Suspense fallback={<ChartLoading />}>
<PlotlyParallelCoordinates
trials={allTrialsRaw}
objectives={studyMetadata.objectives}
designVariables={studyMetadata.design_variables}
paretoFront={paretoFront}
height={350}
/>
</Suspense>
) : ( ) : (
<ParallelCoordinatesPlot <ParallelCoordinatesPlot
paretoData={allTrialsRaw} paretoData={allTrialsRaw}
@@ -634,24 +589,12 @@ export default function Dashboard() {
title="Convergence" title="Convergence"
subtitle={`Best ${studyMetadata?.objectives?.[0]?.name || 'Objective'} over ${allTrialsRaw.length} trials`} subtitle={`Best ${studyMetadata?.objectives?.[0]?.name || 'Objective'} over ${allTrialsRaw.length} trials`}
> >
{chartLibrary === 'plotly' ? ( <ConvergencePlot
<Suspense fallback={<ChartLoading />}> trials={allTrialsRaw}
<PlotlyConvergencePlot objectiveIndex={0}
trials={allTrialsRaw} objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'}
objectiveIndex={0} direction="minimize"
objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'} />
direction="minimize"
height={280}
/>
</Suspense>
) : (
<ConvergencePlot
trials={allTrialsRaw}
objectiveIndex={0}
objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'}
direction="minimize"
/>
)}
</ExpandableChart> </ExpandableChart>
</div> </div>
)} )}
@@ -663,32 +606,16 @@ export default function Dashboard() {
title="Parameter Importance" title="Parameter Importance"
subtitle={`Correlation with ${studyMetadata?.objectives?.[0]?.name || 'Objective'}`} subtitle={`Correlation with ${studyMetadata?.objectives?.[0]?.name || 'Objective'}`}
> >
{chartLibrary === 'plotly' ? ( <ParameterImportanceChart
<Suspense fallback={<ChartLoading />}> trials={allTrialsRaw}
<PlotlyParameterImportance designVariables={
trials={allTrialsRaw} studyMetadata?.design_variables?.length > 0
designVariables={ ? studyMetadata.design_variables
studyMetadata?.design_variables?.length > 0 : Object.keys(allTrialsRaw[0]?.params || {}).map(name => ({ name }))
? studyMetadata.design_variables }
: Object.keys(allTrialsRaw[0]?.params || {}).map(name => ({ name })) objectiveIndex={0}
} objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'}
objectiveIndex={0} />
objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'}
height={280}
/>
</Suspense>
) : (
<ParameterImportanceChart
trials={allTrialsRaw}
designVariables={
studyMetadata?.design_variables?.length > 0
? studyMetadata.design_variables
: Object.keys(allTrialsRaw[0]?.params || {}).map(name => ({ name }))
}
objectiveIndex={0}
objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'}
/>
)}
</ExpandableChart> </ExpandableChart>
</div> </div>
)} )}

View File

@@ -394,18 +394,32 @@ const Home: React.FC = () => {
<p className="text-dark-400 text-sm">Study Documentation</p> <p className="text-dark-400 text-sm">Study Documentation</p>
</div> </div>
</div> </div>
<button <div className="flex items-center gap-2">
onClick={() => handleSelectStudy(selectedPreview)} <button
className="flex items-center gap-2 px-5 py-2.5 rounded-lg transition-all font-semibold whitespace-nowrap hover:-translate-y-0.5" onClick={() => navigate(`/canvas/${selectedPreview.id}`)}
style={{ className="flex items-center gap-2 px-4 py-2.5 rounded-lg transition-all font-medium whitespace-nowrap hover:-translate-y-0.5"
background: 'linear-gradient(135deg, #00d4e6 0%, #0891b2 100%)', style={{
color: '#000', background: 'rgba(8, 15, 26, 0.85)',
boxShadow: '0 4px 15px rgba(0, 212, 230, 0.3)' border: '1px solid rgba(0, 212, 230, 0.3)',
}} color: '#00d4e6'
> }}
Open >
<ArrowRight className="w-4 h-4" /> <Layers className="w-4 h-4" />
</button> Canvas
</button>
<button
onClick={() => handleSelectStudy(selectedPreview)}
className="flex items-center gap-2 px-5 py-2.5 rounded-lg transition-all font-semibold whitespace-nowrap hover:-translate-y-0.5"
style={{
background: 'linear-gradient(135deg, #00d4e6 0%, #0891b2 100%)',
color: '#000',
boxShadow: '0 4px 15px rgba(0, 212, 230, 0.3)'
}}
>
Open
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div> </div>
{/* Study Quick Stats */} {/* Study Quick Stats */}

View File

@@ -20,11 +20,11 @@ import {
ExternalLink, ExternalLink,
Zap, Zap,
List, List,
LucideIcon LucideIcon,
FileText
} from 'lucide-react'; } from 'lucide-react';
import { useStudy } from '../context/StudyContext'; import { useStudy } from '../context/StudyContext';
import { Card } from '../components/common/Card'; import { Card } from '../components/common/Card';
import Plot from 'react-plotly.js';
// ============================================================================ // ============================================================================
// Types // Types
@@ -642,13 +642,15 @@ export default function Insights() {
Open Full View Open Full View
</button> </button>
)} )}
<button {activeInsight.html_path && (
onClick={() => setFullscreen(true)} <button
className="p-2 bg-dark-700 hover:bg-dark-600 text-white rounded-lg transition-colors" onClick={() => setFullscreen(true)}
title="Fullscreen" className="p-2 bg-dark-700 hover:bg-dark-600 text-white rounded-lg transition-colors"
> title="Fullscreen"
<Maximize2 className="w-5 h-5" /> >
</button> <Maximize2 className="w-5 h-5" />
</button>
)}
</div> </div>
</div> </div>
@@ -674,49 +676,43 @@ export default function Insights() {
</div> </div>
)} )}
{/* Plotly Figure */} {/* Insight Result */}
<Card className="p-0 overflow-hidden"> <Card className="p-0 overflow-hidden">
{activeInsight.plotly_figure ? ( <div className="flex flex-col items-center justify-center h-64 text-dark-400 p-8">
<div className="bg-dark-900" style={{ height: '600px' }}> <CheckCircle className="w-12 h-12 text-green-400 mb-4" />
<Plot <p className="text-lg font-medium text-white mb-2">Insight Generated Successfully</p>
data={activeInsight.plotly_figure.data} {activeInsight.html_path ? (
layout={{ <>
...activeInsight.plotly_figure.layout, <p className="text-sm text-center mb-4">
autosize: true, Click the button below to view the interactive visualization.
margin: { l: 60, r: 60, t: 60, b: 60 }, </p>
paper_bgcolor: '#111827', <button
plot_bgcolor: '#1f2937', onClick={() => window.open(`/api/insights/studies/${selectedStudy?.id}/view/${activeInsight.insight_type}`, '_blank')}
font: { color: 'white' } className="flex items-center gap-2 px-6 py-3 bg-primary-600 hover:bg-primary-500 text-white rounded-lg font-medium transition-colors"
}} >
config={{ <ExternalLink className="w-5 h-5" />
responsive: true, Open Interactive Visualization
displayModeBar: true, </button>
displaylogo: false </>
}} ) : (
style={{ width: '100%', height: '100%' }}
/>
</div>
) : (
<div className="flex flex-col items-center justify-center h-64 text-dark-400 p-8">
<CheckCircle className="w-12 h-12 text-green-400 mb-4" />
<p className="text-lg font-medium text-white mb-2">Insight Generated Successfully</p>
<p className="text-sm text-center"> <p className="text-sm text-center">
This insight generates HTML files. Click "Open Full View" to see the visualization. The visualization has been generated. Check the study's insights folder.
</p> </p>
{activeInsight.summary?.html_files && ( )}
<div className="mt-4 text-sm"> {activeInsight.summary?.html_files && (
<p className="text-dark-400 mb-2">Generated files:</p> <div className="mt-4 text-sm">
<ul className="space-y-1"> <p className="text-dark-400 mb-2">Generated files:</p>
{(activeInsight.summary.html_files as string[]).slice(0, 4).map((f: string, i: number) => ( <ul className="space-y-1">
<li key={i} className="text-dark-300"> {(activeInsight.summary.html_files as string[]).slice(0, 4).map((f: string, i: number) => (
{f.split(/[/\\]/).pop()} <li key={i} className="text-dark-300 flex items-center gap-2">
</li> <FileText className="w-3 h-3" />
))} {f.split(/[/\\]/).pop()}
</ul> </li>
</div> ))}
)} </ul>
</div> </div>
)} )}
</div>
</Card> </Card>
{/* Generate Another */} {/* Generate Another */}
@@ -736,8 +732,8 @@ export default function Insights() {
</div> </div>
)} )}
{/* Fullscreen Modal */} {/* Fullscreen Modal - now opens external HTML */}
{fullscreen && activeInsight?.plotly_figure && ( {fullscreen && activeInsight && (
<div className="fixed inset-0 z-50 bg-dark-900 flex flex-col"> <div className="fixed inset-0 z-50 bg-dark-900 flex flex-col">
<div className="flex items-center justify-between p-4 border-b border-dark-600"> <div className="flex items-center justify-between p-4 border-b border-dark-600">
<h2 className="text-xl font-bold text-white"> <h2 className="text-xl font-bold text-white">
@@ -750,23 +746,24 @@ export default function Insights() {
<X className="w-6 h-6" /> <X className="w-6 h-6" />
</button> </button>
</div> </div>
<div className="flex-1 p-4"> <div className="flex-1 p-4 flex items-center justify-center">
<Plot {activeInsight.html_path ? (
data={activeInsight.plotly_figure.data} <iframe
layout={{ src={`/api/insights/studies/${selectedStudy?.id}/view/${activeInsight.insight_type}`}
...activeInsight.plotly_figure.layout, className="w-full h-full border-0 rounded-lg"
autosize: true, title={activeInsight.insight_name || activeInsight.insight_type}
paper_bgcolor: '#111827', />
plot_bgcolor: '#1f2937', ) : (
font: { color: 'white' } <div className="text-center text-dark-400">
}} <p className="text-lg mb-4">No interactive visualization available for this insight.</p>
config={{ <button
responsive: true, onClick={() => setFullscreen(false)}
displayModeBar: true, className="px-4 py-2 bg-dark-700 hover:bg-dark-600 text-white rounded-lg"
displaylogo: false >
}} Close
style={{ width: '100%', height: '100%' }} </button>
/> </div>
)}
</div> </div>
</div> </div>
)} )}

View File

@@ -278,7 +278,7 @@ export default function Setup() {
Configuration Configuration
</button> </button>
<button <button
onClick={() => setActiveTab('canvas')} onClick={() => navigate(`/canvas/${selectedStudy?.id || ''}`)}
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-colors bg-primary-600 text-white" className="flex items-center gap-2 px-4 py-2 rounded-lg transition-colors bg-primary-600 text-white"
> >
<Grid3X3 className="w-4 h-4" /> <Grid3X3 className="w-4 h-4" />
@@ -333,7 +333,7 @@ export default function Setup() {
Configuration Configuration
</button> </button>
<button <button
onClick={() => setActiveTab('canvas')} onClick={() => navigate(`/canvas/${selectedStudy?.id || ''}`)}
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-colors bg-dark-800 text-dark-300 hover:text-white hover:bg-dark-700" className="flex items-center gap-2 px-4 py-2 rounded-lg transition-colors bg-dark-800 text-dark-300 hover:text-white hover:bg-dark-700"
> >
<Grid3X3 className="w-4 h-4" /> <Grid3X3 className="w-4 h-4" />

View File

@@ -0,0 +1,572 @@
/**
* AtomizerSpec v2.0 TypeScript Types
*
* These types match the JSON Schema at optimization_engine/schemas/atomizer_spec_v2.json
* This is the single source of truth for optimization configuration.
*/
// ============================================================================
// Position Types
// ============================================================================
export interface CanvasPosition {
x: number;
y: number;
}
// ============================================================================
// Meta Types
// ============================================================================
export type SpecCreatedBy = 'canvas' | 'claude' | 'api' | 'migration' | 'manual';
export interface SpecMeta {
/** Schema version (e.g., "2.0") */
version: string;
/** When the spec was created (ISO 8601) */
created?: string;
/** When the spec was last modified (ISO 8601) */
modified?: string;
/** Who/what created the spec */
created_by?: SpecCreatedBy;
/** Who/what last modified the spec */
modified_by?: string;
/** Unique study identifier (snake_case) */
study_name: string;
/** Human-readable description */
description?: string;
/** Tags for categorization */
tags?: string[];
/** Real-world engineering context */
engineering_context?: string;
}
// ============================================================================
// Model Types
// ============================================================================
export interface NxPartConfig {
/** Path to .prt file */
path?: string;
/** File hash for change detection */
hash?: string;
/** Idealized part filename (_i.prt) */
idealized_part?: string;
}
export interface FemConfig {
/** Path to .fem file */
path?: string;
/** Number of elements */
element_count?: number;
/** Number of nodes */
node_count?: number;
}
export type SolverType = 'nastran' | 'NX_Nastran' | 'abaqus';
export type SubcaseType = 'static' | 'modal' | 'thermal' | 'buckling';
export interface Subcase {
id: number;
name?: string;
type?: SubcaseType;
}
export interface SimConfig {
/** Path to .sim file */
path: string;
/** Solver type */
solver: SolverType;
/** Solution type (e.g., SOL101) */
solution_type?: string;
/** Defined subcases */
subcases?: Subcase[];
}
export interface NxSettings {
nx_install_path?: string;
simulation_timeout_s?: number;
auto_start_nx?: boolean;
}
export interface ModelConfig {
nx_part?: NxPartConfig;
fem?: FemConfig;
sim: SimConfig;
nx_settings?: NxSettings;
}
// ============================================================================
// Design Variable Types
// ============================================================================
export type DesignVariableType = 'continuous' | 'integer' | 'categorical';
export interface DesignVariableBounds {
min: number;
max: number;
}
export interface DesignVariable {
/** Unique identifier (pattern: dv_XXX) */
id: string;
/** Human-readable name */
name: string;
/** NX expression name (must match model) */
expression_name: string;
/** Variable type */
type: DesignVariableType;
/** Value bounds */
bounds: DesignVariableBounds;
/** Current/initial value */
baseline?: number;
/** Physical units (mm, deg, etc.) */
units?: string;
/** Step size for integer/discrete */
step?: number;
/** Whether to include in optimization */
enabled?: boolean;
/** Description */
description?: string;
/** Canvas position */
canvas_position?: CanvasPosition;
}
// ============================================================================
// Extractor Types
// ============================================================================
export type ExtractorType =
| 'displacement'
| 'frequency'
| 'stress'
| 'mass'
| 'mass_expression'
| 'zernike_opd'
| 'zernike_csv'
| 'temperature'
| 'custom_function';
export interface ExtractorConfig {
/** Inner radius for Zernike (mm) */
inner_radius_mm?: number;
/** Outer radius for Zernike (mm) */
outer_radius_mm?: number;
/** Number of Zernike modes */
n_modes?: number;
/** Low-order modes to filter */
filter_low_orders?: number;
/** Displacement unit */
displacement_unit?: string;
/** Reference subcase ID */
reference_subcase?: number;
/** NX expression name (for mass_expression) */
expression_name?: string;
/** Mode number (for frequency) */
mode_number?: number;
/** Element type (for stress) */
element_type?: string;
/** Result type */
result_type?: string;
/** Metric type */
metric?: string;
/** Additional config properties */
[key: string]: unknown;
}
export interface CustomFunction {
/** Function name */
name?: string;
/** Python module path */
module?: string;
/** Function signature */
signature?: string;
/** Python source code */
source_code?: string;
}
export interface ExtractorOutput {
/** Output name (used by objectives/constraints) */
name: string;
/** Specific metric (max, total, rms, etc.) */
metric?: string;
/** Subcase ID for this output */
subcase?: number;
/** Units */
units?: string;
}
export interface Extractor {
/** Unique identifier (pattern: ext_XXX) */
id: string;
/** Human-readable name */
name: string;
/** Extractor type */
type: ExtractorType;
/** Whether this is a built-in extractor */
builtin?: boolean;
/** Type-specific configuration */
config?: ExtractorConfig;
/** Custom function definition (for custom_function type) */
function?: CustomFunction;
/** Output values this extractor produces */
outputs: ExtractorOutput[];
/** Canvas position */
canvas_position?: CanvasPosition;
}
// ============================================================================
// Objective Types
// ============================================================================
export type OptimizationDirection = 'minimize' | 'maximize';
export interface ObjectiveSource {
/** Reference to extractor */
extractor_id: string;
/** Which output from the extractor */
output_name: string;
}
export interface Objective {
/** Unique identifier (pattern: obj_XXX) */
id: string;
/** Human-readable name */
name: string;
/** Optimization direction */
direction: OptimizationDirection;
/** Weight for weighted sum (multi-objective) */
weight?: number;
/** Where the value comes from */
source: ObjectiveSource;
/** Target value (for goal programming) */
target?: number;
/** Units */
units?: string;
/** Description */
description?: string;
/** Canvas position */
canvas_position?: CanvasPosition;
}
// ============================================================================
// Constraint Types
// ============================================================================
export type ConstraintType = 'hard' | 'soft';
export type ConstraintOperator = '<=' | '>=' | '<' | '>' | '==';
export type PenaltyMethod = 'linear' | 'quadratic' | 'exponential';
export interface ConstraintSource {
extractor_id: string;
output_name: string;
}
export interface PenaltyConfig {
/** Penalty method */
method?: PenaltyMethod;
/** Penalty weight */
weight?: number;
/** Soft margin before penalty kicks in */
margin?: number;
}
export interface Constraint {
/** Unique identifier (pattern: con_XXX) */
id: string;
/** Human-readable name */
name: string;
/** Constraint type */
type: ConstraintType;
/** Comparison operator */
operator: ConstraintOperator;
/** Constraint threshold value */
threshold: number;
/** Where the value comes from */
source: ConstraintSource;
/** Penalty method configuration */
penalty_config?: PenaltyConfig;
/** Description */
description?: string;
/** Canvas position */
canvas_position?: CanvasPosition;
}
// ============================================================================
// Optimization Types
// ============================================================================
export type AlgorithmType = 'TPE' | 'CMA-ES' | 'NSGA-II' | 'RandomSearch' | 'SAT_v3' | 'GP-BO';
export type SurrogateType = 'MLP' | 'GNN' | 'ensemble';
export interface AlgorithmConfig {
/** Population size (evolutionary algorithms) */
population_size?: number;
/** Number of generations */
n_generations?: number;
/** Mutation probability */
mutation_prob?: number | null;
/** Crossover probability */
crossover_prob?: number;
/** Random seed */
seed?: number;
/** Number of startup trials (TPE) */
n_startup_trials?: number;
/** Initial sigma (CMA-ES) */
sigma0?: number;
/** Additional config properties */
[key: string]: unknown;
}
export interface Algorithm {
type: AlgorithmType;
config?: AlgorithmConfig;
}
export interface OptimizationBudget {
/** Maximum number of trials */
max_trials?: number;
/** Maximum time in hours */
max_time_hours?: number;
/** Stop if no improvement for N trials */
convergence_patience?: number;
}
export interface SurrogateConfig {
/** Number of models in ensemble */
n_models?: number;
/** Network architecture layers */
architecture?: number[];
/** Retrain every N trials */
train_every_n_trials?: number;
/** Minimum training samples */
min_training_samples?: number;
/** Acquisition function candidates */
acquisition_candidates?: number;
/** FEA validations per round */
fea_validations_per_round?: number;
}
export interface Surrogate {
enabled?: boolean;
type?: SurrogateType;
config?: SurrogateConfig;
}
export interface OptimizationConfig {
algorithm: Algorithm;
budget: OptimizationBudget;
surrogate?: Surrogate;
canvas_position?: CanvasPosition;
}
// ============================================================================
// Workflow Types
// ============================================================================
export interface WorkflowStage {
id: string;
name: string;
algorithm?: string;
trials?: number;
purpose?: string;
}
export interface WorkflowTransition {
from: string;
to: string;
condition?: string;
}
export interface Workflow {
stages?: WorkflowStage[];
transitions?: WorkflowTransition[];
}
// ============================================================================
// Reporting Types
// ============================================================================
export interface InsightConfig {
include_html?: boolean;
show_pareto_evolution?: boolean;
[key: string]: unknown;
}
export interface Insight {
type?: string;
for_trials?: string;
config?: InsightConfig;
}
export interface ReportingConfig {
auto_report?: boolean;
report_triggers?: string[];
insights?: Insight[];
}
// ============================================================================
// Canvas Types
// ============================================================================
export interface CanvasViewport {
x: number;
y: number;
zoom: number;
}
export interface CanvasEdge {
source: string;
target: string;
sourceHandle?: string;
targetHandle?: string;
}
export interface CanvasGroup {
id: string;
name: string;
node_ids: string[];
}
export interface CanvasConfig {
layout_version?: string;
viewport?: CanvasViewport;
edges?: CanvasEdge[];
groups?: CanvasGroup[];
}
// ============================================================================
// Main AtomizerSpec Type
// ============================================================================
/**
* AtomizerSpec v2.0 - The unified configuration schema for Atomizer optimization studies.
*
* This is the single source of truth used by:
* - Canvas UI (rendering and editing)
* - Backend API (validation and storage)
* - Claude Assistant (reading and modifying)
* - Optimization Engine (execution)
*/
export interface AtomizerSpec {
/** Metadata about the spec */
meta: SpecMeta;
/** NX model files and configuration */
model: ModelConfig;
/** Design variables (NX expressions) to optimize */
design_variables: DesignVariable[];
/** Physics extractors that compute outputs from FEA results */
extractors: Extractor[];
/** Optimization objectives (minimize/maximize) */
objectives: Objective[];
/** Hard and soft constraints */
constraints?: Constraint[];
/** Optimization algorithm configuration */
optimization: OptimizationConfig;
/** Multi-stage optimization workflow */
workflow?: Workflow;
/** Reporting configuration */
reporting?: ReportingConfig;
/** Canvas UI state (persisted for reconstruction) */
canvas?: CanvasConfig;
}
// ============================================================================
// Utility Types for API Responses
// ============================================================================
export interface SpecValidationError {
type: 'schema' | 'semantic' | 'reference';
path: string[];
message: string;
}
export interface SpecValidationWarning {
type: string;
path: string[];
message: string;
}
export interface SpecValidationReport {
valid: boolean;
errors: SpecValidationError[];
warnings: SpecValidationWarning[];
summary: {
design_variables: number;
extractors: number;
objectives: number;
constraints: number;
custom_functions: number;
};
}
export interface SpecModification {
operation: 'set' | 'add' | 'remove';
path: string;
value?: unknown;
}
export interface SpecUpdateResult {
success: boolean;
hash: string;
modified: string;
modified_by: string;
}
export interface SpecPatchRequest {
path: string;
value: unknown;
modified_by?: string;
}
// ============================================================================
// Node Types for Canvas
// ============================================================================
export type SpecNodeType =
| 'designVar'
| 'extractor'
| 'objective'
| 'constraint'
| 'model'
| 'solver'
| 'algorithm';
export interface SpecNodeBase {
id: string;
type: SpecNodeType;
position: CanvasPosition;
data: Record<string, unknown>;
}
// ============================================================================
// WebSocket Types
// ============================================================================
export type SpecSyncMessageType =
| 'spec_updated'
| 'validation_error'
| 'node_added'
| 'node_removed'
| 'connection_ack';
export interface SpecSyncMessage {
type: SpecSyncMessageType;
timestamp: string;
hash?: string;
modified_by?: string;
changes?: Array<{
path: string;
old: unknown;
new: unknown;
}>;
error?: string;
}
export interface SpecClientMessage {
type: 'subscribe' | 'patch_node' | 'add_node' | 'remove_node' | 'update_position';
study_id: string;
node_id?: string;
data?: Record<string, unknown>;
position?: CanvasPosition;
}

View File

@@ -1,3 +1,6 @@
// AtomizerSpec v2.0 types (unified configuration)
export * from './atomizer-spec';
// Study types // Study types
export interface Study { export interface Study {
id: string; id: string;

View File

@@ -17,18 +17,10 @@ export default defineConfig({
} }
} }
}, },
resolve: {
alias: {
// Use the smaller basic Plotly distribution
'plotly.js/dist/plotly': 'plotly.js-basic-dist'
}
},
build: { build: {
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks: { manualChunks: {
// Separate Plotly into its own chunk for better caching
plotly: ['plotly.js-basic-dist', 'react-plotly.js'],
// Separate React and core libs // Separate React and core libs
vendor: ['react', 'react-dom', 'react-router-dom'], vendor: ['react', 'react-dom', 'react-router-dom'],
// Recharts in its own chunk // Recharts in its own chunk
@@ -37,8 +29,5 @@ export default defineConfig({
} }
}, },
chunkSizeWarningLimit: 600 chunkSizeWarningLimit: 600
},
optimizeDeps: {
include: ['plotly.js-basic-dist']
} }
}) })

View File

@@ -25,6 +25,18 @@ if not exist "%CONDA_PATH%\Scripts\activate.bat" (
exit /b 1 exit /b 1
) )
:: Stop any existing dashboard processes first
echo [0/3] Stopping existing processes...
taskkill /F /FI "WINDOWTITLE eq Atomizer Backend*" >nul 2>&1
taskkill /F /FI "WINDOWTITLE eq Atomizer Frontend*" >nul 2>&1
for /f "tokens=5" %%a in ('netstat -ano ^| findstr :%BACKEND_PORT% ^| findstr LISTENING') do (
taskkill /F /PID %%a >nul 2>&1
)
for /f "tokens=5" %%a in ('netstat -ano ^| findstr :%FRONTEND_PORT% ^| findstr LISTENING') do (
taskkill /F /PID %%a >nul 2>&1
)
ping 127.0.0.1 -n 2 >nul
echo [1/3] Starting Backend Server (port %BACKEND_PORT%)... echo [1/3] Starting Backend Server (port %BACKEND_PORT%)...
start "Atomizer Backend" cmd /k "call %CONDA_PATH%\Scripts\activate.bat %CONDA_ENV% && cd /d %SCRIPT_DIR%backend && python -m uvicorn api.main:app --reload --port %BACKEND_PORT%" start "Atomizer Backend" cmd /k "call %CONDA_PATH%\Scripts\activate.bat %CONDA_ENV% && cd /d %SCRIPT_DIR%backend && python -m uvicorn api.main:app --reload --port %BACKEND_PORT%"

View File

@@ -10,11 +10,11 @@ echo.
taskkill /F /FI "WINDOWTITLE eq Atomizer Backend*" >nul 2>&1 taskkill /F /FI "WINDOWTITLE eq Atomizer Backend*" >nul 2>&1
taskkill /F /FI "WINDOWTITLE eq Atomizer Frontend*" >nul 2>&1 taskkill /F /FI "WINDOWTITLE eq Atomizer Frontend*" >nul 2>&1
:: Kill any remaining processes on the ports :: Kill any remaining processes on the ports (backend: 8001, frontend: 3003)
for /f "tokens=5" %%a in ('netstat -ano ^| findstr :8000 ^| findstr LISTENING') do ( for /f "tokens=5" %%a in ('netstat -ano ^| findstr :8001 ^| findstr LISTENING') do (
taskkill /F /PID %%a >nul 2>&1 taskkill /F /PID %%a >nul 2>&1
) )
for /f "tokens=5" %%a in ('netstat -ano ^| findstr :5173 ^| findstr LISTENING') do ( for /f "tokens=5" %%a in ('netstat -ano ^| findstr :3003 ^| findstr LISTENING') do (
taskkill /F /PID %%a >nul 2>&1 taskkill /F /PID %%a >nul 2>&1
) )

View File

@@ -22,6 +22,7 @@ import { analysisTools } from "./tools/analysis.js";
import { reportingTools } from "./tools/reporting.js"; import { reportingTools } from "./tools/reporting.js";
import { physicsTools } from "./tools/physics.js"; import { physicsTools } from "./tools/physics.js";
import { canvasTools } from "./tools/canvas.js"; import { canvasTools } from "./tools/canvas.js";
import { specTools } from "./tools/spec.js";
import { adminTools } from "./tools/admin.js"; import { adminTools } from "./tools/admin.js";
import { ATOMIZER_MODE } from "./utils/paths.js"; import { ATOMIZER_MODE } from "./utils/paths.js";
@@ -52,6 +53,7 @@ const userTools: AtomizerTool[] = [
...reportingTools, ...reportingTools,
...physicsTools, ...physicsTools,
...canvasTools, ...canvasTools,
...specTools,
]; ];
const powerTools: AtomizerTool[] = [ const powerTools: AtomizerTool[] = [

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ Modules:
- builder: OptimizationConfigBuilder for creating configs - builder: OptimizationConfigBuilder for creating configs
- setup_wizard: Interactive configuration setup - setup_wizard: Interactive configuration setup
- capability_matcher: Match capabilities to requirements - capability_matcher: Match capabilities to requirements
- spec_models: AtomizerSpec v2.0 Pydantic models (unified configuration)
""" """
# Lazy imports to avoid circular dependencies # Lazy imports to avoid circular dependencies
@@ -31,6 +32,27 @@ def __getattr__(name):
elif name == 'TemplateLoader': elif name == 'TemplateLoader':
from .template_loader import TemplateLoader from .template_loader import TemplateLoader
return TemplateLoader return TemplateLoader
elif name == 'AtomizerSpec':
from .spec_models import AtomizerSpec
return AtomizerSpec
elif name == 'SpecValidator':
from .spec_validator import SpecValidator
return SpecValidator
elif name == 'SpecValidationError':
from .spec_validator import SpecValidationError
return SpecValidationError
elif name == 'validate_spec':
from .spec_validator import validate_spec
return validate_spec
elif name == 'SpecMigrator':
from .migrator import SpecMigrator
return SpecMigrator
elif name == 'migrate_config':
from .migrator import migrate_config
return migrate_config
elif name == 'migrate_config_file':
from .migrator import migrate_config_file
return migrate_config_file
raise AttributeError(f"module 'optimization_engine.config' has no attribute '{name}'") raise AttributeError(f"module 'optimization_engine.config' has no attribute '{name}'")
__all__ = [ __all__ = [
@@ -40,4 +62,11 @@ __all__ = [
'SetupWizard', 'SetupWizard',
'CapabilityMatcher', 'CapabilityMatcher',
'TemplateLoader', 'TemplateLoader',
'AtomizerSpec',
'SpecValidator',
'SpecValidationError',
'validate_spec',
'SpecMigrator',
'migrate_config',
'migrate_config_file',
] ]

View File

@@ -0,0 +1,844 @@
"""
AtomizerSpec v2.0 Migrator
Converts legacy optimization_config.json files to AtomizerSpec v2.0 format.
Supports migration from:
- Mirror/Zernike configs (extraction_method, zernike_settings)
- Structural/Bracket configs (optimization_settings, simulation_settings)
- Canvas Intent format (simplified canvas output)
Migration Rules:
- bounds: [min, max] -> bounds: {min, max}
- parameter -> expression_name
- goal/type: "minimize"/"maximize" -> direction: "minimize"/"maximize"
- Infers extractors from objectives and extraction settings
- Generates canvas edges automatically
"""
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple, Union
import json
import re
class MigrationError(Exception):
"""Raised when migration fails."""
pass
class SpecMigrator:
"""
Migrate old optimization_config.json to AtomizerSpec v2.0.
Handles multiple legacy formats and infers missing information.
"""
# Extractor type inference based on objective names
EXTRACTOR_INFERENCE = {
# Zernike patterns
r"wfe|zernike|opd": "zernike_opd",
r"mfg|manufacturing": "zernike_opd",
r"rms": "zernike_opd",
# Structural patterns
r"displacement|deflection|deform": "displacement",
r"stress|von.?mises": "stress",
r"frequency|modal|eigen": "frequency",
r"mass|weight": "mass",
r"stiffness": "displacement", # Stiffness computed from displacement
r"temperature|thermal": "temperature",
}
def __init__(self, study_path: Optional[Path] = None):
"""
Initialize migrator.
Args:
study_path: Path to study directory (for inferring sim/fem paths)
"""
self.study_path = Path(study_path) if study_path else None
self._extractor_counter = 0
self._objective_counter = 0
self._constraint_counter = 0
self._dv_counter = 0
def migrate(
self,
old_config: Dict[str, Any],
study_name: Optional[str] = None
) -> Dict[str, Any]:
"""
Convert old config to AtomizerSpec v2.0.
Args:
old_config: Legacy config dict
study_name: Override study name (defaults to config value)
Returns:
AtomizerSpec v2.0 dict
"""
# Reset counters
self._extractor_counter = 0
self._objective_counter = 0
self._constraint_counter = 0
self._dv_counter = 0
# Detect config type
config_type = self._detect_config_type(old_config)
# Build spec
spec = {
"meta": self._migrate_meta(old_config, study_name),
"model": self._migrate_model(old_config, config_type),
"design_variables": self._migrate_design_variables(old_config),
"extractors": [],
"objectives": [],
"constraints": [],
"optimization": self._migrate_optimization(old_config, config_type),
"canvas": {"edges": [], "layout_version": "2.0"}
}
# Migrate extractors and objectives together (they're linked)
extractors, objectives = self._migrate_extractors_and_objectives(old_config, config_type)
spec["extractors"] = extractors
spec["objectives"] = objectives
# Migrate constraints
spec["constraints"] = self._migrate_constraints(old_config, spec["extractors"])
# Generate canvas edges
spec["canvas"]["edges"] = self._generate_edges(spec)
# Add workflow if SAT/turbo settings present
if self._has_sat_settings(old_config):
spec["workflow"] = self._migrate_workflow(old_config)
return spec
def migrate_file(
self,
config_path: Union[str, Path],
output_path: Optional[Union[str, Path]] = None
) -> Dict[str, Any]:
"""
Migrate a config file and optionally save the result.
Args:
config_path: Path to old config file
output_path: Path to save new spec (optional)
Returns:
AtomizerSpec v2.0 dict
"""
config_path = Path(config_path)
if not config_path.exists():
raise MigrationError(f"Config file not found: {config_path}")
with open(config_path, 'r', encoding='utf-8') as f:
old_config = json.load(f)
# Infer study path from config location
if self.study_path is None:
# Config is typically in study_dir/1_setup/ or study_dir/
if config_path.parent.name == "1_setup":
self.study_path = config_path.parent.parent
else:
self.study_path = config_path.parent
spec = self.migrate(old_config)
if output_path:
output_path = Path(output_path)
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(spec, f, indent=2, ensure_ascii=False)
return spec
# =========================================================================
# Detection
# =========================================================================
def _detect_config_type(self, config: Dict) -> str:
"""Detect the type of config format."""
if "extraction_method" in config or "zernike_settings" in config:
return "mirror"
elif "simulation_settings" in config or "extraction_settings" in config:
return "structural"
elif "optimization_settings" in config:
return "structural"
elif "extractors" in config:
# Already partially in new format (canvas intent)
return "canvas_intent"
else:
# Generic/minimal format
return "generic"
def _has_sat_settings(self, config: Dict) -> bool:
"""Check if config has SAT/turbo settings."""
return (
"sat_settings" in config or
config.get("optimization", {}).get("algorithm") in ["SAT_v3", "SAT", "turbo"]
)
# =========================================================================
# Meta Migration
# =========================================================================
def _migrate_meta(self, config: Dict, study_name: Optional[str]) -> Dict:
"""Migrate metadata section."""
now = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
name = study_name or config.get("study_name", "migrated_study")
# Ensure snake_case
name = re.sub(r'[^a-z0-9_]', '_', name.lower())
name = re.sub(r'_+', '_', name).strip('_')
meta = {
"version": "2.0",
"created": now,
"modified": now,
"created_by": "migration",
"modified_by": "migration",
"study_name": name,
"description": config.get("description", ""),
"tags": []
}
# Extract tags from various sources
if "study_tag" in config:
meta["tags"].append(config["study_tag"])
if "business_context" in config:
meta["engineering_context"] = config["business_context"].get("purpose", "")
# Infer tags from config type
if "zernike_settings" in config:
meta["tags"].extend(["mirror", "zernike"])
if "extraction_method" in config:
if config["extraction_method"].get("type") == "zernike_opd":
meta["tags"].append("opd")
return meta
# =========================================================================
# Model Migration
# =========================================================================
def _migrate_model(self, config: Dict, config_type: str) -> Dict:
"""Migrate model section (sim/fem/prt paths)."""
model = {
"sim": {
"path": "",
"solver": "nastran"
}
}
# Extract from nx_settings (mirror format)
if "nx_settings" in config:
nx = config["nx_settings"]
model["sim"]["path"] = nx.get("sim_file", "")
if "nx_install_path" in nx:
model["nx_settings"] = {
"nx_install_path": nx["nx_install_path"],
"simulation_timeout_s": nx.get("simulation_timeout_s", 600)
}
# Extract from simulation_settings (structural format)
elif "simulation_settings" in config:
sim = config["simulation_settings"]
model["sim"]["path"] = sim.get("sim_file", "")
solver = sim.get("solver", "nastran").lower()
# Normalize solver name - valid values: nastran, NX_Nastran, abaqus
solver_map = {"nx": "nastran", "nx_nastran": "NX_Nastran", "nxnastran": "NX_Nastran"}
model["sim"]["solver"] = solver_map.get(solver, "nastran" if solver not in ["nastran", "NX_Nastran", "abaqus"] else solver)
if sim.get("solution_type"):
model["sim"]["solution_type"] = sim["solution_type"]
if sim.get("model_file"):
model["nx_part"] = {"path": sim["model_file"]}
if sim.get("fem_file"):
model["fem"] = {"path": sim["fem_file"]}
# Try to infer from study path
if self.study_path and not model["sim"]["path"]:
setup_dir = self.study_path / "1_setup" / "model"
if setup_dir.exists():
for f in setup_dir.glob("*.sim"):
model["sim"]["path"] = str(f.relative_to(self.study_path))
break
return model
# =========================================================================
# Design Variables Migration
# =========================================================================
def _migrate_design_variables(self, config: Dict) -> List[Dict]:
"""Migrate design variables."""
dvs = []
for dv in config.get("design_variables", []):
self._dv_counter += 1
# Handle different bound formats
if "bounds" in dv:
if isinstance(dv["bounds"], list):
bounds = {"min": dv["bounds"][0], "max": dv["bounds"][1]}
else:
bounds = dv["bounds"]
else:
bounds = {"min": dv.get("min", 0), "max": dv.get("max", 1)}
# Ensure min < max (fix degenerate cases)
if bounds["min"] >= bounds["max"]:
# Expand bounds slightly around the value
val = bounds["min"]
if val == 0:
bounds = {"min": -0.001, "max": 0.001}
else:
bounds = {"min": val * 0.99, "max": val * 1.01}
# Determine type
dv_type = dv.get("type", "continuous")
if dv_type not in ["continuous", "integer", "categorical"]:
dv_type = "continuous"
new_dv = {
"id": f"dv_{self._dv_counter:03d}",
"name": dv.get("name", f"param_{self._dv_counter}"),
"expression_name": dv.get("expression_name", dv.get("parameter", dv.get("name", ""))),
"type": dv_type,
"bounds": bounds,
"baseline": dv.get("baseline", dv.get("initial")),
"units": dv.get("units", dv.get("unit", "")),
"enabled": dv.get("enabled", True),
"description": dv.get("description", dv.get("notes", "")),
"canvas_position": {"x": 50, "y": 100 + (self._dv_counter - 1) * 80}
}
dvs.append(new_dv)
return dvs
# =========================================================================
# Extractors and Objectives Migration
# =========================================================================
def _migrate_extractors_and_objectives(
self,
config: Dict,
config_type: str
) -> Tuple[List[Dict], List[Dict]]:
"""
Migrate extractors and objectives together.
Returns tuple of (extractors, objectives).
"""
extractors = []
objectives = []
# Handle mirror/zernike configs
if config_type == "mirror" and "zernike_settings" in config:
extractor = self._create_zernike_extractor(config)
extractors.append(extractor)
# Create objectives from config
for obj in config.get("objectives", []):
self._objective_counter += 1
objectives.append(self._create_objective(obj, extractor["id"]))
# Handle structural configs
elif config_type == "structural":
# Create extractors based on extraction_settings
if "extraction_settings" in config:
extractor = self._create_structural_extractor(config)
extractors.append(extractor)
ext_id = extractor["id"]
else:
# Infer extractors from objectives
ext_id = None
for obj in config.get("objectives", []):
self._objective_counter += 1
# Infer extractor if not yet created
if ext_id is None:
inferred_type = self._infer_extractor_type(obj.get("name", ""))
ext_id = self._get_or_create_extractor(extractors, inferred_type, obj.get("name", ""))
objectives.append(self._create_objective(obj, ext_id))
# Handle canvas intent or generic
else:
# Pass through existing extractors if present
for ext in config.get("extractors", []):
self._extractor_counter += 1
ext_copy = dict(ext)
if "id" not in ext_copy:
ext_copy["id"] = f"ext_{self._extractor_counter:03d}"
extractors.append(ext_copy)
# Create objectives
for obj in config.get("objectives", []):
self._objective_counter += 1
# Find or create extractor
ext_id = None
if extractors:
ext_id = extractors[0]["id"]
else:
inferred_type = self._infer_extractor_type(obj.get("name", ""))
ext_id = self._get_or_create_extractor(extractors, inferred_type, obj.get("name", ""))
objectives.append(self._create_objective(obj, ext_id))
return extractors, objectives
def _create_zernike_extractor(self, config: Dict) -> Dict:
"""Create a Zernike OPD extractor from config."""
self._extractor_counter += 1
zs = config.get("zernike_settings", {})
em = config.get("extraction_method", {})
# Collect all output names from objectives
outputs = []
for obj in config.get("objectives", []):
obj_name = obj.get("name", "")
outputs.append({
"name": obj_name,
"metric": "filtered_rms_nm"
})
# Get outer radius with sensible default for telescope mirrors
outer_radius = em.get("outer_radius", zs.get("outer_radius"))
if outer_radius is None:
# Default to typical M1 mirror outer radius
outer_radius = 500.0
extractor = {
"id": f"ext_{self._extractor_counter:03d}",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": True,
"config": {
"inner_radius_mm": em.get("inner_radius", zs.get("inner_radius", 0)),
"outer_radius_mm": outer_radius,
"n_modes": zs.get("n_modes", 40),
"filter_low_orders": zs.get("filter_low_orders", 4),
"displacement_unit": zs.get("displacement_unit", "mm"),
"reference_subcase": int(zs.get("reference_subcase", 1))
},
"outputs": outputs,
"canvas_position": {"x": 740, "y": 100}
}
return extractor
def _create_structural_extractor(self, config: Dict) -> Dict:
"""Create extractor from extraction_settings."""
self._extractor_counter += 1
es = config.get("extraction_settings", {})
# Infer type from extractor class name
extractor_class = es.get("extractor_class", "")
if "stiffness" in extractor_class.lower():
ext_type = "displacement"
elif "stress" in extractor_class.lower():
ext_type = "stress"
elif "frequency" in extractor_class.lower():
ext_type = "frequency"
else:
ext_type = "displacement"
# Create outputs from objectives
outputs = []
for obj in config.get("objectives", []):
outputs.append({
"name": obj.get("name", "output"),
"metric": es.get("displacement_aggregation", "max")
})
extractor = {
"id": f"ext_{self._extractor_counter:03d}",
"name": f"{extractor_class or 'Results'} Extractor",
"type": ext_type,
"builtin": True,
"config": {
"result_type": es.get("displacement_component", "z"),
"metric": es.get("displacement_aggregation", "max")
},
"outputs": outputs,
"canvas_position": {"x": 740, "y": 100}
}
return extractor
def _infer_extractor_type(self, objective_name: str) -> str:
"""Infer extractor type from objective name."""
name_lower = objective_name.lower()
for pattern, ext_type in self.EXTRACTOR_INFERENCE.items():
if re.search(pattern, name_lower):
return ext_type
return "displacement" # Default
def _get_or_create_extractor(
self,
extractors: List[Dict],
ext_type: str,
output_name: str
) -> str:
"""Get existing extractor of type or create new one."""
# Look for existing
for ext in extractors:
if ext.get("type") == ext_type:
# Add output if not present
output_names = {o["name"] for o in ext.get("outputs", [])}
if output_name not in output_names:
ext["outputs"].append({"name": output_name, "metric": "total"})
return ext["id"]
# Create new
self._extractor_counter += 1
ext_id = f"ext_{self._extractor_counter:03d}"
extractor = {
"id": ext_id,
"name": f"{ext_type.title()} Extractor",
"type": ext_type,
"builtin": True,
"outputs": [{"name": output_name, "metric": "total"}],
"canvas_position": {"x": 740, "y": 100 + (len(extractors)) * 150}
}
extractors.append(extractor)
return ext_id
def _create_objective(self, obj: Dict, extractor_id: str) -> Dict:
"""Create objective from old format."""
# Normalize direction
direction = obj.get("direction", obj.get("type", obj.get("goal", "minimize")))
if direction not in ["minimize", "maximize"]:
direction = "minimize" if "min" in direction.lower() else "maximize"
obj_name = obj.get("name", f"objective_{self._objective_counter}")
return {
"id": f"obj_{self._objective_counter:03d}",
"name": obj.get("description", obj_name),
"direction": direction,
"weight": obj.get("weight", 1.0),
"source": {
"extractor_id": extractor_id,
"output_name": obj_name
},
"target": obj.get("target"),
"units": obj.get("units", ""),
"canvas_position": {"x": 1020, "y": 100 + (self._objective_counter - 1) * 100}
}
# =========================================================================
# Constraints Migration
# =========================================================================
def _migrate_constraints(self, config: Dict, extractors: List[Dict]) -> List[Dict]:
"""Migrate constraints."""
constraints = []
for con in config.get("constraints", []):
self._constraint_counter += 1
# Determine constraint type
con_type = con.get("type", "hard")
if con_type not in ["hard", "soft"]:
# Infer from type field
if con_type in ["less_than", "greater_than", "less_equal", "greater_equal"]:
con_type = "hard"
# Determine operator
operator = con.get("operator", "<=")
old_type = con.get("type", "")
if "less" in old_type:
operator = "<=" if "equal" in old_type else "<"
elif "greater" in old_type:
operator = ">=" if "equal" in old_type else ">"
# Try to parse expression for threshold
threshold = con.get("threshold", con.get("value"))
if threshold is None and "expression" in con:
# Parse from expression like "mass_kg <= 120.0"
match = re.search(r'([<>=!]+)\s*([\d.]+)', con["expression"])
if match:
operator = match.group(1)
threshold = float(match.group(2))
# Find or create extractor for constraint
con_name = con.get("name", "constraint")
extractor_id = None
output_name = con_name
# Check if name matches existing objective (share extractor)
for ext in extractors:
for out in ext.get("outputs", []):
if con_name.replace("_max", "").replace("_min", "") in out["name"]:
extractor_id = ext["id"]
output_name = out["name"]
break
if extractor_id:
break
# If no match, use first extractor or create mass extractor for mass constraints
if extractor_id is None:
if "mass" in con_name.lower():
# Check if mass extractor exists
for ext in extractors:
if ext.get("type") == "mass":
extractor_id = ext["id"]
break
if extractor_id is None:
# Create mass extractor
ext_id = f"ext_{len(extractors) + 1:03d}"
extractors.append({
"id": ext_id,
"name": "Mass Extractor",
"type": "mass",
"builtin": True,
"outputs": [{"name": "mass_kg", "metric": "total"}],
"canvas_position": {"x": 740, "y": 100 + len(extractors) * 150}
})
extractor_id = ext_id
output_name = "mass_kg"
elif extractors:
extractor_id = extractors[0]["id"]
output_name = extractors[0]["outputs"][0]["name"] if extractors[0].get("outputs") else con_name
constraint = {
"id": f"con_{self._constraint_counter:03d}",
"name": con.get("description", con_name),
"type": con_type if con_type in ["hard", "soft"] else "hard",
"operator": operator,
"threshold": threshold or 0,
"source": {
"extractor_id": extractor_id or "ext_001",
"output_name": output_name
},
"penalty_config": {
"method": "quadratic",
"weight": con.get("penalty_weight", 1000.0)
},
"canvas_position": {"x": 1020, "y": 400 + (self._constraint_counter - 1) * 100}
}
constraints.append(constraint)
return constraints
# =========================================================================
# Optimization Migration
# =========================================================================
def _migrate_optimization(self, config: Dict, config_type: str) -> Dict:
"""Migrate optimization settings."""
# Extract from different locations
if "optimization" in config:
opt = config["optimization"]
elif "optimization_settings" in config:
opt = config["optimization_settings"]
else:
opt = {}
# Normalize algorithm name
algo = opt.get("algorithm", opt.get("sampler", "TPE"))
algo_map = {
"tpe": "TPE",
"tpesampler": "TPE",
"cma-es": "CMA-ES",
"cmaes": "CMA-ES",
"nsga-ii": "NSGA-II",
"nsgaii": "NSGA-II",
"nsga2": "NSGA-II",
"random": "RandomSearch",
"randomsampler": "RandomSearch",
"randomsearch": "RandomSearch",
"sat": "SAT_v3",
"sat_v3": "SAT_v3",
"turbo": "SAT_v3",
"gp": "GP-BO",
"gp-bo": "GP-BO",
"gpbo": "GP-BO",
"bo": "GP-BO",
"bayesian": "GP-BO"
}
# Valid algorithm types for schema
valid_algorithms = {"TPE", "CMA-ES", "NSGA-II", "RandomSearch", "SAT_v3", "GP-BO"}
algo = algo_map.get(algo.lower(), algo)
# Fallback to TPE if still invalid
if algo not in valid_algorithms:
algo = "TPE"
optimization = {
"algorithm": {
"type": algo,
"config": {}
},
"budget": {
"max_trials": opt.get("n_trials", 100)
},
"canvas_position": {"x": 1300, "y": 150}
}
# Algorithm-specific config
if algo == "CMA-ES":
optimization["algorithm"]["config"]["sigma0"] = opt.get("sigma0", 0.3)
elif algo == "NSGA-II":
optimization["algorithm"]["config"]["population_size"] = opt.get("population_size", 50)
elif algo == "TPE":
optimization["algorithm"]["config"]["n_startup_trials"] = opt.get("n_startup_trials", 10)
# Seed
if "seed" in opt:
optimization["algorithm"]["config"]["seed"] = opt["seed"]
# Timeout/patience
if opt.get("timeout"):
optimization["budget"]["max_time_hours"] = opt["timeout"] / 3600
# SAT/surrogate settings
if "sat_settings" in config:
sat = config["sat_settings"]
optimization["surrogate"] = {
"enabled": True,
"type": "ensemble",
"config": {
"n_models": sat.get("n_ensemble_models", 10),
"architecture": sat.get("hidden_dims", [256, 128]),
"train_every_n_trials": sat.get("retrain_frequency", 20),
"min_training_samples": sat.get("min_samples", 30)
}
}
return optimization
# =========================================================================
# Workflow Migration
# =========================================================================
def _migrate_workflow(self, config: Dict) -> Dict:
"""Migrate SAT/turbo workflow settings."""
sat = config.get("sat_settings", {})
exploration_trials = sat.get("min_samples", 30)
total_trials = config.get("optimization", {}).get("n_trials", 100)
return {
"stages": [
{
"id": "stage_exploration",
"name": "Design Space Exploration",
"algorithm": "RandomSearch",
"trials": exploration_trials,
"purpose": "Build initial training data for surrogate"
},
{
"id": "stage_optimization",
"name": "Surrogate-Assisted Optimization",
"algorithm": "SAT_v3",
"trials": total_trials - exploration_trials,
"purpose": "Neural-accelerated optimization"
}
],
"transitions": [
{
"from": "stage_exploration",
"to": "stage_optimization",
"condition": f"trial_count >= {exploration_trials}"
}
]
}
# =========================================================================
# Canvas Edge Generation
# =========================================================================
def _generate_edges(self, spec: Dict) -> List[Dict]:
"""Generate canvas edges connecting nodes."""
edges = []
# DVs -> model
for dv in spec.get("design_variables", []):
edges.append({"source": dv["id"], "target": "model"})
# model -> solver
edges.append({"source": "model", "target": "solver"})
# solver -> extractors
for ext in spec.get("extractors", []):
edges.append({"source": "solver", "target": ext["id"]})
# extractors -> objectives
for obj in spec.get("objectives", []):
ext_id = obj.get("source", {}).get("extractor_id")
if ext_id:
edges.append({"source": ext_id, "target": obj["id"]})
# extractors -> constraints
for con in spec.get("constraints", []):
ext_id = con.get("source", {}).get("extractor_id")
if ext_id:
edges.append({"source": ext_id, "target": con["id"]})
# objectives -> optimization
for obj in spec.get("objectives", []):
edges.append({"source": obj["id"], "target": "optimization"})
# constraints -> optimization
for con in spec.get("constraints", []):
edges.append({"source": con["id"], "target": "optimization"})
return edges
# ============================================================================
# Convenience Functions
# ============================================================================
def migrate_config(
old_config: Dict[str, Any],
study_name: Optional[str] = None
) -> Dict[str, Any]:
"""
Migrate old config dict to AtomizerSpec v2.0.
Args:
old_config: Legacy config dict
study_name: Override study name
Returns:
AtomizerSpec v2.0 dict
"""
migrator = SpecMigrator()
return migrator.migrate(old_config, study_name)
def migrate_config_file(
config_path: Union[str, Path],
output_path: Optional[Union[str, Path]] = None
) -> Dict[str, Any]:
"""
Migrate a config file to AtomizerSpec v2.0.
Args:
config_path: Path to old config file
output_path: Path to save new spec (optional)
Returns:
AtomizerSpec v2.0 dict
"""
migrator = SpecMigrator()
return migrator.migrate_file(config_path, output_path)

View File

@@ -11,6 +11,7 @@ Available extractors:
- SPC Forces: extract_spc_forces, extract_total_reaction_force - SPC Forces: extract_spc_forces, extract_total_reaction_force
- Zernike: extract_zernike_from_op2, ZernikeExtractor (telescope mirrors) - Zernike: extract_zernike_from_op2, ZernikeExtractor (telescope mirrors)
- Part Introspection: introspect_part (comprehensive NX .prt analysis) - Part Introspection: introspect_part (comprehensive NX .prt analysis)
- Custom: CustomExtractorLoader for user-defined Python extractors
Phase 2 Extractors (2025-12-06): Phase 2 Extractors (2025-12-06):
- Principal stress extraction (sigma1, sigma2, sigma3) - Principal stress extraction (sigma1, sigma2, sigma3)
@@ -25,6 +26,10 @@ Phase 3 Extractors (2025-12-06):
Phase 4 Extractors (2025-12-19): Phase 4 Extractors (2025-12-19):
- Part Introspection (E12): Comprehensive .prt analysis (expressions, mass, materials, attributes, groups, features) - Part Introspection (E12): Comprehensive .prt analysis (expressions, mass, materials, attributes, groups, features)
Phase 5 Extractors (2026-01-17):
- Custom Extractor Loader: Dynamic loading and execution of user-defined Python extractors
from AtomizerSpec v2.0 (sandboxed execution with security validation)
""" """
# Zernike extractor for telescope mirror optimization (standard Z-only method) # Zernike extractor for telescope mirror optimization (standard Z-only method)
@@ -119,6 +124,26 @@ from optimization_engine.extractors.introspect_part import (
print_introspection_summary, print_introspection_summary,
) )
# Custom extractor loader (Phase 5) - dynamic Python extractors from AtomizerSpec v2.0
from optimization_engine.extractors.custom_extractor_loader import (
CustomExtractor,
CustomExtractorLoader,
CustomExtractorContext,
ExtractorSecurityError,
ExtractorValidationError,
load_custom_extractors,
execute_custom_extractor,
validate_custom_extractor,
)
# Spec extractor builder - builds extractors from AtomizerSpec
from optimization_engine.extractors.spec_extractor_builder import (
SpecExtractorBuilder,
build_extractors_from_spec,
get_extractor_outputs,
list_available_builtin_extractors,
)
__all__ = [ __all__ = [
# Part mass & material (from .prt) # Part mass & material (from .prt)
'extract_part_mass_material', 'extract_part_mass_material',
@@ -174,4 +199,18 @@ __all__ = [
'get_expressions_dict', 'get_expressions_dict',
'get_expression_value', 'get_expression_value',
'print_introspection_summary', 'print_introspection_summary',
# Custom extractor loader (Phase 5)
'CustomExtractor',
'CustomExtractorLoader',
'CustomExtractorContext',
'ExtractorSecurityError',
'ExtractorValidationError',
'load_custom_extractors',
'execute_custom_extractor',
'validate_custom_extractor',
# Spec extractor builder
'SpecExtractorBuilder',
'build_extractors_from_spec',
'get_extractor_outputs',
'list_available_builtin_extractors',
] ]

View File

@@ -0,0 +1,800 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://atomizer.io/schemas/atomizer_spec_v2.json",
"title": "AtomizerSpec v2.0",
"description": "Unified configuration schema for Atomizer optimization studies. This is the single source of truth used by Canvas, Backend, Claude, and the Optimization Engine.",
"type": "object",
"required": ["meta", "model", "design_variables", "extractors", "objectives", "optimization"],
"properties": {
"meta": {
"$ref": "#/definitions/meta"
},
"model": {
"$ref": "#/definitions/model"
},
"design_variables": {
"type": "array",
"description": "Design variables (NX expressions) to optimize",
"minItems": 1,
"maxItems": 50,
"items": {
"$ref": "#/definitions/design_variable"
}
},
"extractors": {
"type": "array",
"description": "Physics extractors that compute outputs from FEA results",
"minItems": 1,
"items": {
"$ref": "#/definitions/extractor"
}
},
"objectives": {
"type": "array",
"description": "Optimization objectives (minimize/maximize)",
"minItems": 1,
"maxItems": 5,
"items": {
"$ref": "#/definitions/objective"
}
},
"constraints": {
"type": "array",
"description": "Hard and soft constraints",
"items": {
"$ref": "#/definitions/constraint"
}
},
"optimization": {
"$ref": "#/definitions/optimization"
},
"workflow": {
"$ref": "#/definitions/workflow"
},
"reporting": {
"$ref": "#/definitions/reporting"
},
"canvas": {
"$ref": "#/definitions/canvas"
}
},
"definitions": {
"meta": {
"type": "object",
"description": "Metadata about the spec",
"required": ["version", "study_name"],
"properties": {
"version": {
"type": "string",
"description": "Schema version",
"pattern": "^2\\.\\d+$",
"examples": ["2.0", "2.1"]
},
"created": {
"type": "string",
"format": "date-time",
"description": "When the spec was created"
},
"modified": {
"type": "string",
"format": "date-time",
"description": "When the spec was last modified"
},
"created_by": {
"type": "string",
"description": "Who/what created the spec",
"enum": ["canvas", "claude", "api", "migration", "manual"]
},
"modified_by": {
"type": "string",
"description": "Who/what last modified the spec"
},
"study_name": {
"type": "string",
"description": "Unique study identifier (snake_case)",
"pattern": "^[a-z0-9_]+$",
"minLength": 3,
"maxLength": 100
},
"description": {
"type": "string",
"description": "Human-readable description of the study",
"maxLength": 1000
},
"tags": {
"type": "array",
"description": "Tags for categorization",
"items": {
"type": "string",
"maxLength": 50
}
},
"engineering_context": {
"type": "string",
"description": "Real-world engineering scenario"
}
}
},
"model": {
"type": "object",
"description": "NX model files and configuration",
"required": ["sim"],
"properties": {
"nx_part": {
"type": "object",
"description": "NX geometry part file",
"properties": {
"path": {
"type": "string",
"description": "Path to .prt file"
},
"hash": {
"type": "string",
"description": "File hash for change detection"
},
"idealized_part": {
"type": "string",
"description": "Idealized part filename (_i.prt)"
}
}
},
"fem": {
"type": "object",
"description": "FEM mesh file",
"properties": {
"path": {
"type": "string",
"description": "Path to .fem file"
},
"element_count": {
"type": "integer",
"description": "Number of elements"
},
"node_count": {
"type": "integer",
"description": "Number of nodes"
}
}
},
"sim": {
"type": "object",
"description": "Simulation file",
"required": ["path", "solver"],
"properties": {
"path": {
"type": "string",
"description": "Path to .sim file"
},
"solver": {
"type": "string",
"description": "Solver type",
"enum": ["nastran", "NX_Nastran", "abaqus"]
},
"solution_type": {
"type": "string",
"description": "Solution type (e.g., SOL101)",
"pattern": "^SOL\\d+$"
},
"subcases": {
"type": "array",
"description": "Defined subcases",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["static", "modal", "thermal", "buckling"]
}
}
}
}
}
},
"nx_settings": {
"type": "object",
"description": "NX runtime settings",
"properties": {
"nx_install_path": {
"type": "string"
},
"simulation_timeout_s": {
"type": "integer",
"minimum": 60,
"maximum": 7200
},
"auto_start_nx": {
"type": "boolean"
}
}
}
}
},
"design_variable": {
"type": "object",
"description": "A design variable to optimize",
"required": ["id", "name", "expression_name", "type", "bounds"],
"properties": {
"id": {
"type": "string",
"description": "Unique identifier",
"pattern": "^dv_\\d{3}$"
},
"name": {
"type": "string",
"description": "Human-readable name"
},
"expression_name": {
"type": "string",
"description": "NX expression name (must match model)",
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
},
"type": {
"type": "string",
"description": "Variable type",
"enum": ["continuous", "integer", "categorical"]
},
"bounds": {
"type": "object",
"description": "Value bounds",
"required": ["min", "max"],
"properties": {
"min": {
"type": "number"
},
"max": {
"type": "number"
}
}
},
"baseline": {
"type": "number",
"description": "Current/initial value"
},
"units": {
"type": "string",
"description": "Physical units (mm, deg, etc.)"
},
"step": {
"type": "number",
"description": "Step size for integer/discrete"
},
"enabled": {
"type": "boolean",
"description": "Whether to include in optimization",
"default": true
},
"description": {
"type": "string"
},
"canvas_position": {
"$ref": "#/definitions/position"
}
}
},
"extractor": {
"type": "object",
"description": "Physics extractor that computes outputs from FEA",
"required": ["id", "name", "type", "outputs"],
"properties": {
"id": {
"type": "string",
"description": "Unique identifier",
"pattern": "^ext_\\d{3}$"
},
"name": {
"type": "string",
"description": "Human-readable name"
},
"type": {
"type": "string",
"description": "Extractor type",
"enum": [
"displacement",
"frequency",
"stress",
"mass",
"mass_expression",
"zernike_opd",
"zernike_csv",
"temperature",
"custom_function"
]
},
"builtin": {
"type": "boolean",
"description": "Whether this is a built-in extractor",
"default": true
},
"config": {
"type": "object",
"description": "Type-specific configuration",
"properties": {
"inner_radius_mm": {
"type": "number"
},
"outer_radius_mm": {
"type": "number"
},
"n_modes": {
"type": "integer"
},
"filter_low_orders": {
"type": "integer"
},
"displacement_unit": {
"type": "string"
},
"reference_subcase": {
"type": "integer"
},
"expression_name": {
"type": "string"
},
"mode_number": {
"type": "integer"
},
"element_type": {
"type": "string"
},
"result_type": {
"type": "string"
},
"metric": {
"type": "string"
}
}
},
"function": {
"type": "object",
"description": "Custom function definition (for custom_function type)",
"properties": {
"name": {
"type": "string",
"description": "Function name"
},
"module": {
"type": "string",
"description": "Python module path"
},
"signature": {
"type": "string",
"description": "Function signature"
},
"source_code": {
"type": "string",
"description": "Python source code"
}
}
},
"outputs": {
"type": "array",
"description": "Output values this extractor produces",
"minItems": 1,
"items": {
"type": "object",
"required": ["name"],
"properties": {
"name": {
"type": "string",
"description": "Output name (used by objectives/constraints)"
},
"metric": {
"type": "string",
"description": "Specific metric (max, total, rms, etc.)"
},
"subcase": {
"type": "integer",
"description": "Subcase ID for this output"
},
"units": {
"type": "string"
}
}
}
},
"canvas_position": {
"$ref": "#/definitions/position"
}
}
},
"objective": {
"type": "object",
"description": "Optimization objective",
"required": ["id", "name", "direction", "source"],
"properties": {
"id": {
"type": "string",
"description": "Unique identifier",
"pattern": "^obj_\\d{3}$"
},
"name": {
"type": "string",
"description": "Human-readable name"
},
"direction": {
"type": "string",
"description": "Optimization direction",
"enum": ["minimize", "maximize"]
},
"weight": {
"type": "number",
"description": "Weight for weighted sum (multi-objective)",
"minimum": 0,
"default": 1.0
},
"source": {
"type": "object",
"description": "Where the value comes from",
"required": ["extractor_id", "output_name"],
"properties": {
"extractor_id": {
"type": "string",
"description": "Reference to extractor"
},
"output_name": {
"type": "string",
"description": "Which output from the extractor"
}
}
},
"target": {
"type": "number",
"description": "Target value (for goal programming)"
},
"units": {
"type": "string"
},
"description": {
"type": "string"
},
"canvas_position": {
"$ref": "#/definitions/position"
}
}
},
"constraint": {
"type": "object",
"description": "Hard or soft constraint",
"required": ["id", "name", "type", "operator", "threshold", "source"],
"properties": {
"id": {
"type": "string",
"description": "Unique identifier",
"pattern": "^con_\\d{3}$"
},
"name": {
"type": "string"
},
"type": {
"type": "string",
"description": "Constraint type",
"enum": ["hard", "soft"]
},
"operator": {
"type": "string",
"description": "Comparison operator",
"enum": ["<=", ">=", "<", ">", "=="]
},
"threshold": {
"type": "number",
"description": "Constraint threshold value"
},
"source": {
"type": "object",
"description": "Where the value comes from",
"required": ["extractor_id", "output_name"],
"properties": {
"extractor_id": {
"type": "string"
},
"output_name": {
"type": "string"
}
}
},
"penalty_config": {
"type": "object",
"description": "Penalty method configuration",
"properties": {
"method": {
"type": "string",
"enum": ["linear", "quadratic", "exponential"]
},
"weight": {
"type": "number"
},
"margin": {
"type": "number",
"description": "Soft margin before penalty kicks in"
}
}
},
"description": {
"type": "string"
},
"canvas_position": {
"$ref": "#/definitions/position"
}
}
},
"optimization": {
"type": "object",
"description": "Optimization algorithm configuration",
"required": ["algorithm", "budget"],
"properties": {
"algorithm": {
"type": "object",
"required": ["type"],
"properties": {
"type": {
"type": "string",
"description": "Algorithm type",
"enum": ["TPE", "CMA-ES", "NSGA-II", "RandomSearch", "SAT_v3", "GP-BO"]
},
"config": {
"type": "object",
"description": "Algorithm-specific settings",
"properties": {
"population_size": {
"type": "integer"
},
"n_generations": {
"type": "integer"
},
"mutation_prob": {
"type": ["number", "null"]
},
"crossover_prob": {
"type": "number"
},
"seed": {
"type": "integer"
},
"n_startup_trials": {
"type": "integer"
},
"sigma0": {
"type": "number"
}
}
}
}
},
"budget": {
"type": "object",
"description": "Computational budget",
"properties": {
"max_trials": {
"type": "integer",
"minimum": 1,
"maximum": 10000
},
"max_time_hours": {
"type": "number"
},
"convergence_patience": {
"type": "integer",
"description": "Stop if no improvement for N trials"
}
}
},
"surrogate": {
"type": "object",
"description": "Neural surrogate configuration",
"properties": {
"enabled": {
"type": "boolean"
},
"type": {
"type": "string",
"enum": ["MLP", "GNN", "ensemble"]
},
"config": {
"type": "object",
"properties": {
"n_models": {
"type": "integer"
},
"architecture": {
"type": "array",
"items": {
"type": "integer"
}
},
"train_every_n_trials": {
"type": "integer"
},
"min_training_samples": {
"type": "integer"
},
"acquisition_candidates": {
"type": "integer"
},
"fea_validations_per_round": {
"type": "integer"
}
}
}
}
},
"canvas_position": {
"$ref": "#/definitions/position"
}
}
},
"workflow": {
"type": "object",
"description": "Multi-stage optimization workflow",
"properties": {
"stages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"algorithm": {
"type": "string"
},
"trials": {
"type": "integer"
},
"purpose": {
"type": "string"
}
}
}
},
"transitions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"from": {
"type": "string"
},
"to": {
"type": "string"
},
"condition": {
"type": "string"
}
}
}
}
}
},
"reporting": {
"type": "object",
"description": "Reporting configuration",
"properties": {
"auto_report": {
"type": "boolean"
},
"report_triggers": {
"type": "array",
"items": {
"type": "string"
}
},
"insights": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"for_trials": {
"type": "string"
},
"config": {
"type": "object"
}
}
}
}
}
},
"canvas": {
"type": "object",
"description": "Canvas UI state (persisted for reconstruction)",
"properties": {
"layout_version": {
"type": "string"
},
"viewport": {
"type": "object",
"properties": {
"x": {
"type": "number"
},
"y": {
"type": "number"
},
"zoom": {
"type": "number"
}
}
},
"edges": {
"type": "array",
"description": "Connections between nodes",
"items": {
"type": "object",
"required": ["source", "target"],
"properties": {
"source": {
"type": "string"
},
"target": {
"type": "string"
},
"sourceHandle": {
"type": "string"
},
"targetHandle": {
"type": "string"
}
}
}
},
"groups": {
"type": "array",
"description": "Node groupings for organization",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"node_ids": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
},
"position": {
"type": "object",
"description": "Canvas position",
"properties": {
"x": {
"type": "number"
},
"y": {
"type": "number"
}
}
}
}
}

View File

@@ -0,0 +1,287 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.010333Z",
"modified": "2026-01-17T15:35:12.010333Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "drone_gimbal_arm_optimization",
"description": "Drone Camera Gimbal Support Arm - Multi-Objective Lightweight Design",
"tags": []
},
"model": {
"sim": {
"path": "1_setup\\model\\Beam_sim1.sim",
"solver": "nastran"
}
},
"design_variables": [
{
"id": "dv_001",
"name": "param_1",
"expression_name": "beam_half_core_thickness",
"type": "continuous",
"bounds": {
"min": 5,
"max": 10
},
"baseline": null,
"units": "",
"enabled": true,
"description": "Half thickness of beam core (mm) - affects weight and stiffness",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "param_2",
"expression_name": "beam_face_thickness",
"type": "continuous",
"bounds": {
"min": 1,
"max": 3
},
"baseline": null,
"units": "",
"enabled": true,
"description": "Thickness of beam face sheets (mm) - bending resistance",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "param_3",
"expression_name": "holes_diameter",
"type": "continuous",
"bounds": {
"min": 10,
"max": 50
},
"baseline": null,
"units": "",
"enabled": true,
"description": "Diameter of lightening holes (mm) - weight reduction",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "param_4",
"expression_name": "hole_count",
"type": "continuous",
"bounds": {
"min": 8,
"max": 14
},
"baseline": null,
"units": "",
"enabled": true,
"description": "Number of lightening holes - balance weight vs strength",
"canvas_position": {
"x": 50,
"y": 340
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Total mass (grams) - minimize for longer flight time",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"target": 4000,
"units": "",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "First natural frequency (Hz) - avoid rotor resonance",
"direction": "maximize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "fundamental_frequency"
},
"target": 150,
"units": "",
"canvas_position": {
"x": 1020,
"y": 200
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum tip displacement under 850g camera load < 1.5mm for image stabilization",
"type": "hard",
"operator": "<",
"threshold": 1.5,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
},
{
"id": "con_002",
"name": "Maximum von Mises stress < 120 MPa (Al 6061-T6, SF=2.3)",
"type": "hard",
"operator": "<",
"threshold": 120,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 500
}
},
{
"id": "con_003",
"name": "Natural frequency > 150 Hz to avoid rotor frequencies (80-120 Hz safety margin)",
"type": "hard",
"operator": ">",
"threshold": 150,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 600
}
}
],
"optimization": {
"algorithm": {
"type": "TPE",
"config": {
"n_startup_trials": 10
}
},
"budget": {
"max_trials": 30
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "con_001"
},
{
"source": "ext_001",
"target": "con_002"
},
{
"source": "ext_001",
"target": "con_003"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
},
{
"source": "con_002",
"target": "optimization"
},
{
"source": "con_003",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,406 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.012625Z",
"modified": "2026-01-17T15:35:12.012625Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_adaptive_v11",
"description": "V11 - Adaptive surrogate optimization with real FEA validation. Cross-links V10 training data. NN/FEA trial differentiation for dashboard.",
"tags": [
"mirror",
"zernike"
]
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\NX2506",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 28.5
},
"baseline": 26.79,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 13.0,
"max": 17.0
},
"baseline": 14.64,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "lateral_outer_pivot",
"expression_name": "lateral_outer_pivot",
"type": "continuous",
"bounds": {
"min": 9.0,
"max": 12.0
},
"baseline": 10.4,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "lateral_inner_pivot",
"expression_name": "lateral_inner_pivot",
"type": "continuous",
"bounds": {
"min": 9.0,
"max": 12.0
},
"baseline": 10.07,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 18.0,
"max": 23.0
},
"baseline": 20.73,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "lateral_closeness",
"expression_name": "lateral_closeness",
"type": "continuous",
"bounds": {
"min": 9.5,
"max": 12.5
},
"baseline": 11.02,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 35.0,
"max": 55.0
},
"baseline": 40.55,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 580
}
},
{
"id": "dv_008",
"name": "whiffle_outer_to_vertical",
"expression_name": "whiffle_outer_to_vertical",
"type": "continuous",
"bounds": {
"min": 68.0,
"max": 80.0
},
"baseline": 75.67,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 660
}
},
{
"id": "dv_009",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 50.0,
"max": 65.0
},
"baseline": 60.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 740
}
},
{
"id": "dv_010",
"name": "blank_backface_angle",
"expression_name": "blank_backface_angle",
"type": "continuous",
"bounds": {
"min": 4,
"max": 5.0
},
"baseline": 4.23,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 820
}
},
{
"id": "dv_011",
"name": "inner_circular_rib_dia",
"expression_name": "inner_circular_rib_dia",
"type": "continuous",
"bounds": {
"min": 480.0,
"max": 620.0
},
"baseline": 534.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 900
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 0,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "rel_filtered_rms_40_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "rel_filtered_rms_60_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90_optician_workload",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg reference (operational tracking)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_40_vs_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg reference (operational tracking)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_60_vs_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered RMS)",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90_optician_workload"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
}
],
"constraints": [],
"optimization": {
"algorithm": {
"type": "TPE",
"config": {
"n_startup_trials": 10
}
},
"budget": {
"max_trials": 100
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "dv_008",
"target": "model"
},
{
"source": "dv_009",
"target": "model"
},
{
"source": "dv_010",
"target": "model"
},
{
"source": "dv_011",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,454 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.013676Z",
"modified": "2026-01-17T15:35:12.013676Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_adaptive_v12",
"description": "V12 - Adaptive optimization with tuned hyperparameters, ensemble surrogate, and mass constraint (<99kg).",
"tags": [
"mirror",
"zernike"
]
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\NX2506",
"simulation_timeout_s": 900
}
},
"design_variables": [
{
"id": "dv_001",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 28.5
},
"baseline": 26.79,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 13.0,
"max": 17.0
},
"baseline": 14.64,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "lateral_outer_pivot",
"expression_name": "lateral_outer_pivot",
"type": "continuous",
"bounds": {
"min": 9.0,
"max": 12.0
},
"baseline": 10.4,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "lateral_inner_pivot",
"expression_name": "lateral_inner_pivot",
"type": "continuous",
"bounds": {
"min": 9.0,
"max": 12.0
},
"baseline": 10.07,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 18.0,
"max": 23.0
},
"baseline": 20.73,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "lateral_closeness",
"expression_name": "lateral_closeness",
"type": "continuous",
"bounds": {
"min": 9.5,
"max": 12.5
},
"baseline": 11.02,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 35.0,
"max": 55.0
},
"baseline": 40.55,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 580
}
},
{
"id": "dv_008",
"name": "whiffle_outer_to_vertical",
"expression_name": "whiffle_outer_to_vertical",
"type": "continuous",
"bounds": {
"min": 68.0,
"max": 80.0
},
"baseline": 75.67,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 660
}
},
{
"id": "dv_009",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 50.0,
"max": 65.0
},
"baseline": 60.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 740
}
},
{
"id": "dv_010",
"name": "blank_backface_angle",
"expression_name": "blank_backface_angle",
"type": "continuous",
"bounds": {
"min": 4,
"max": 5.0
},
"baseline": 4.23,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 820
}
},
{
"id": "dv_011",
"name": "inner_circular_rib_dia",
"expression_name": "inner_circular_rib_dia",
"type": "continuous",
"bounds": {
"min": 480.0,
"max": 620.0
},
"baseline": 534.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 900
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 0,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "rel_filtered_rms_40_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "rel_filtered_rms_60_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90_optician_workload",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg reference (operational tracking)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_40_vs_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg reference (operational tracking)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_60_vs_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered RMS)",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90_optician_workload"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Mirror assembly mass must be under 99kg",
"type": "hard",
"operator": "<=",
"threshold": 0,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 100.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "TPE",
"config": {
"n_startup_trials": 10
}
},
"budget": {
"max_trials": 100
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "dv_008",
"target": "model"
},
{
"source": "dv_009",
"target": "model"
},
{
"source": "dv_010",
"target": "model"
},
{
"source": "dv_011",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,406 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.015282Z",
"modified": "2026-01-17T15:35:12.015282Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_adaptive_v13",
"description": "V13 - Pure NSGA-II multi-objective optimization with FEA only. No surrogate. Seeds from V11+V12 FEA data.",
"tags": [
"mirror",
"zernike"
]
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\NX2506",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 28.5
},
"baseline": 26.79,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 13.0,
"max": 17.0
},
"baseline": 14.64,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "lateral_outer_pivot",
"expression_name": "lateral_outer_pivot",
"type": "continuous",
"bounds": {
"min": 9.0,
"max": 12.0
},
"baseline": 10.4,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "lateral_inner_pivot",
"expression_name": "lateral_inner_pivot",
"type": "continuous",
"bounds": {
"min": 9.0,
"max": 12.0
},
"baseline": 10.07,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 18.0,
"max": 23.0
},
"baseline": 20.73,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "lateral_closeness",
"expression_name": "lateral_closeness",
"type": "continuous",
"bounds": {
"min": 9.5,
"max": 12.5
},
"baseline": 11.02,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 35.0,
"max": 55.0
},
"baseline": 40.55,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 580
}
},
{
"id": "dv_008",
"name": "whiffle_outer_to_vertical",
"expression_name": "whiffle_outer_to_vertical",
"type": "continuous",
"bounds": {
"min": 68.0,
"max": 80.0
},
"baseline": 75.67,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 660
}
},
{
"id": "dv_009",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 50.0,
"max": 65.0
},
"baseline": 60.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 740
}
},
{
"id": "dv_010",
"name": "blank_backface_angle",
"expression_name": "blank_backface_angle",
"type": "continuous",
"bounds": {
"min": 4,
"max": 5.0
},
"baseline": 4.23,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 820
}
},
{
"id": "dv_011",
"name": "inner_circular_rib_dia",
"expression_name": "inner_circular_rib_dia",
"type": "continuous",
"bounds": {
"min": 480.0,
"max": 620.0
},
"baseline": 534.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 900
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 0,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "rel_filtered_rms_40_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "rel_filtered_rms_60_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90_optician_workload",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg reference (operational tracking)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_40_vs_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg reference (operational tracking)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_60_vs_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered RMS)",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90_optician_workload"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
}
],
"constraints": [],
"optimization": {
"algorithm": {
"type": "TPE",
"config": {
"n_startup_trials": 10
}
},
"budget": {
"max_trials": 100
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "dv_008",
"target": "model"
},
{
"source": "dv_009",
"target": "model"
},
{
"source": "dv_010",
"target": "model"
},
{
"source": "dv_011",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,406 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.016335Z",
"modified": "2026-01-17T15:35:12.016335Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_adaptive_v14",
"description": "V14 continuation - TPE with expanded bounds based on V14 analysis. 4 params were at bounds: lateral_middle_pivot (99.4%), whiffle_min (96.3%), lateral_outer_angle (4.7%), lateral_inner_pivot (8.1%).",
"tags": [
"mirror",
"zernike"
]
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\NX2506",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 30.0
},
"baseline": 26.79,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 11.0,
"max": 17.0
},
"baseline": 14.64,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "lateral_outer_pivot",
"expression_name": "lateral_outer_pivot",
"type": "continuous",
"bounds": {
"min": 9.0,
"max": 12.0
},
"baseline": 10.4,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "lateral_inner_pivot",
"expression_name": "lateral_inner_pivot",
"type": "continuous",
"bounds": {
"min": 5.0,
"max": 12.0
},
"baseline": 10.07,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 15.0,
"max": 27.0
},
"baseline": 20.73,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "lateral_closeness",
"expression_name": "lateral_closeness",
"type": "continuous",
"bounds": {
"min": 9.5,
"max": 12.5
},
"baseline": 11.02,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 30.0,
"max": 72.0
},
"baseline": 40.55,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 580
}
},
{
"id": "dv_008",
"name": "whiffle_outer_to_vertical",
"expression_name": "whiffle_outer_to_vertical",
"type": "continuous",
"bounds": {
"min": 60.0,
"max": 80.0
},
"baseline": 75.67,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 660
}
},
{
"id": "dv_009",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 50.0,
"max": 80.0
},
"baseline": 60.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 740
}
},
{
"id": "dv_010",
"name": "blank_backface_angle",
"expression_name": "blank_backface_angle",
"type": "continuous",
"bounds": {
"min": 4.1,
"max": 4.5
},
"baseline": 4.15,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 820
}
},
{
"id": "dv_011",
"name": "inner_circular_rib_dia",
"expression_name": "inner_circular_rib_dia",
"type": "continuous",
"bounds": {
"min": 480.0,
"max": 620.0
},
"baseline": 534.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 900
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 0,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "rel_filtered_rms_40_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "rel_filtered_rms_60_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90_optician_workload",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg reference (operational tracking)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_40_vs_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg reference (operational tracking)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_60_vs_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered RMS)",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90_optician_workload"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
}
],
"constraints": [],
"optimization": {
"algorithm": {
"type": "TPE",
"config": {
"n_startup_trials": 10
}
},
"budget": {
"max_trials": 100
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "dv_008",
"target": "model"
},
{
"source": "dv_009",
"target": "model"
},
{
"source": "dv_010",
"target": "model"
},
{
"source": "dv_011",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,407 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.017994Z",
"modified": "2026-01-17T15:35:12.017994Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_adaptive_v15",
"description": "V15 NSGA-II multi-objective optimization. Seeds from 785 V14 trials. Explores Pareto trade-offs between 40deg tracking, 60deg tracking, and manufacturing workload.",
"tags": [
"mirror",
"zernike"
]
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\DesigncenterNX2512",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 30.0
},
"baseline": 26.79,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 11.0,
"max": 17.0
},
"baseline": 14.64,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "lateral_outer_pivot",
"expression_name": "lateral_outer_pivot",
"type": "continuous",
"bounds": {
"min": 9.0,
"max": 12.0
},
"baseline": 10.4,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "lateral_inner_pivot",
"expression_name": "lateral_inner_pivot",
"type": "continuous",
"bounds": {
"min": 5.0,
"max": 12.0
},
"baseline": 10.07,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 15.0,
"max": 27.0
},
"baseline": 20.73,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "lateral_closeness",
"expression_name": "lateral_closeness",
"type": "continuous",
"bounds": {
"min": 9.5,
"max": 12.5
},
"baseline": 11.02,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 30.0,
"max": 72.0
},
"baseline": 40.55,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 580
}
},
{
"id": "dv_008",
"name": "whiffle_outer_to_vertical",
"expression_name": "whiffle_outer_to_vertical",
"type": "continuous",
"bounds": {
"min": 60.0,
"max": 80.0
},
"baseline": 75.67,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 660
}
},
{
"id": "dv_009",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 50.0,
"max": 80.0
},
"baseline": 60.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 740
}
},
{
"id": "dv_010",
"name": "blank_backface_angle",
"expression_name": "blank_backface_angle",
"type": "continuous",
"bounds": {
"min": 4.1,
"max": 4.5
},
"baseline": 4.15,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 820
}
},
{
"id": "dv_011",
"name": "inner_circular_rib_dia",
"expression_name": "inner_circular_rib_dia",
"type": "continuous",
"bounds": {
"min": 480.0,
"max": 620.0
},
"baseline": 534.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 900
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 0,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "rel_filtered_rms_40_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "rel_filtered_rms_60_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90_optician_workload",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg reference (operational tracking)",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_40_vs_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg reference (operational tracking)",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_60_vs_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered RMS)",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90_optician_workload"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
}
],
"constraints": [],
"optimization": {
"algorithm": {
"type": "NSGA-II",
"config": {
"population_size": 50,
"seed": 42
}
},
"budget": {
"max_trials": 100
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "dv_008",
"target": "model"
},
{
"source": "dv_009",
"target": "model"
},
{
"source": "dv_010",
"target": "model"
},
{
"source": "dv_011",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,435 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.019028Z",
"modified": "2026-01-17T15:35:12.019028Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_cost_reduction",
"description": "TPE optimization for M1 mirror cost reduction. Starting from best V11-V15 design (Trial #725, WS=121.72). New geometry with modified behavior.",
"tags": [
"mirror",
"zernike"
]
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\DesigncenterNX2512",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 30.0
},
"baseline": 27.7,
"units": "degrees",
"enabled": false,
"description": "",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 11.0,
"max": 17.0
},
"baseline": 13.03,
"units": "degrees",
"enabled": false,
"description": "",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "lateral_outer_pivot",
"expression_name": "lateral_outer_pivot",
"type": "continuous",
"bounds": {
"min": 9.0,
"max": 12.0
},
"baseline": 11.24,
"units": "mm",
"enabled": false,
"description": "",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "lateral_inner_pivot",
"expression_name": "lateral_inner_pivot",
"type": "continuous",
"bounds": {
"min": 5.0,
"max": 12.0
},
"baseline": 8.16,
"units": "mm",
"enabled": false,
"description": "",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 15.0,
"max": 27.0
},
"baseline": 22.88,
"units": "mm",
"enabled": false,
"description": "",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "lateral_closeness",
"expression_name": "lateral_closeness",
"type": "continuous",
"bounds": {
"min": 9.5,
"max": 12.5
},
"baseline": 9.84,
"units": "mm",
"enabled": false,
"description": "",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 30.0,
"max": 72.0
},
"baseline": 58.63,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 580
}
},
{
"id": "dv_008",
"name": "whiffle_outer_to_vertical",
"expression_name": "whiffle_outer_to_vertical",
"type": "continuous",
"bounds": {
"min": 60.0,
"max": 80.0
},
"baseline": 77.96,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 660
}
},
{
"id": "dv_009",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 65,
"max": 80.0
},
"baseline": 67.33,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 740
}
},
{
"id": "dv_010",
"name": "blank_backface_angle",
"expression_name": "blank_backface_angle",
"type": "continuous",
"bounds": {
"min": 4.5,
"max": 5
},
"baseline": 4.5,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 820
}
},
{
"id": "dv_011",
"name": "inner_circular_rib_dia",
"expression_name": "inner_circular_rib_dia",
"type": "continuous",
"bounds": {
"min": 480.0,
"max": 620.0
},
"baseline": 537.86,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 900
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 0,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "rel_filtered_rms_40_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "rel_filtered_rms_60_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90_optician_workload",
"metric": "filtered_rms_nm"
},
{
"name": "mass_kg",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg reference (operational tracking)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_40_vs_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg reference (operational tracking)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_60_vs_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered RMS)",
"direction": "minimize",
"weight": 2.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90_optician_workload"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
},
{
"id": "obj_004",
"name": "Total mass of M1_Blank part extracted via NX MeasureManager",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mass_kg"
},
"target": null,
"units": "kg",
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"constraints": [],
"optimization": {
"algorithm": {
"type": "TPE",
"config": {
"n_startup_trials": 15,
"seed": 42
}
},
"budget": {
"max_trials": 100
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "dv_008",
"target": "model"
},
{
"source": "dv_009",
"target": "model"
},
{
"source": "dv_010",
"target": "model"
},
{
"source": "dv_011",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "ext_001",
"target": "obj_004"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
},
{
"source": "obj_004",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,528 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.025507Z",
"modified": "2026-01-17T15:35:12.025507Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_cost_reduction_v10",
"description": "CMA-ES lateral optimization with RIGOROUS ZernikeFigure extraction. Uses actual figure geometry to compute OPD with XY lateral displacement correction. Replaces V8 which used Z-only Zernike extraction. 6 active lateral variables.",
"tags": [
"mirror",
"zernike",
"opd"
]
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\DesigncenterNX2512",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 30.0,
"max": 72.0
},
"baseline": 61.9189,
"units": "mm",
"enabled": false,
"description": "FIXED at V7 best value",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "whiffle_outer_to_vertical",
"expression_name": "whiffle_outer_to_vertical",
"type": "continuous",
"bounds": {
"min": 70.0,
"max": 85.0
},
"baseline": 81.7479,
"units": "mm",
"enabled": false,
"description": "FIXED at V7 best value",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 65.0,
"max": 120.0
},
"baseline": 65.5418,
"units": "mm",
"enabled": false,
"description": "FIXED at V7 best value",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "blank_backface_angle",
"expression_name": "blank_backface_angle",
"type": "continuous",
"bounds": {
"min": 4.2,
"max": 4.5
},
"baseline": 4.4158,
"units": "degrees",
"enabled": false,
"description": "FIXED at V7 best value",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "Pocket_Radius",
"expression_name": "Pocket_Radius",
"type": "continuous",
"bounds": {
"min": 9.0,
"max": 12.0
},
"baseline": 10.05,
"units": "mm",
"enabled": false,
"description": "FIXED at 10.05mm (pushed to model each iteration)",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 30.0
},
"baseline": 27.7,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V10. Angle of inner lateral support constraint.",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 11.0,
"max": 17.0
},
"baseline": 13.03,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V10. Angle of outer lateral support constraint.",
"canvas_position": {
"x": 50,
"y": 580
}
},
{
"id": "dv_008",
"name": "lateral_outer_pivot",
"expression_name": "lateral_outer_pivot",
"type": "continuous",
"bounds": {
"min": 7,
"max": 12.0
},
"baseline": 10.0,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V10. Pivot position for outer lateral. Max constrained to 11.",
"canvas_position": {
"x": 50,
"y": 660
}
},
{
"id": "dv_009",
"name": "lateral_inner_pivot",
"expression_name": "lateral_inner_pivot",
"type": "continuous",
"bounds": {
"min": 5.0,
"max": 12.0
},
"baseline": 8.16,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V10. Pivot position for inner lateral.",
"canvas_position": {
"x": 50,
"y": 740
}
},
{
"id": "dv_010",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 15.0,
"max": 27.0
},
"baseline": 22.88,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V10. Pivot position for middle lateral.",
"canvas_position": {
"x": 50,
"y": 820
}
},
{
"id": "dv_011",
"name": "lateral_closeness",
"expression_name": "lateral_closeness",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 12.0
},
"baseline": 9.84,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V10. Closeness factor for lateral supports.",
"canvas_position": {
"x": 50,
"y": 900
}
},
{
"id": "dv_012",
"name": "inner_circular_rib_dia",
"expression_name": "inner_circular_rib_dia",
"type": "continuous",
"bounds": {
"min": 480.0,
"max": 620.0
},
"baseline": 537.86,
"units": "mm",
"enabled": false,
"description": "DISABLED. Fixed structural parameter.",
"canvas_position": {
"x": 50,
"y": 980
}
},
{
"id": "dv_013",
"name": "center_thickness",
"expression_name": "center_thickness",
"type": "continuous",
"bounds": {
"min": 60.0,
"max": 85.0
},
"baseline": 85.0,
"units": "mm",
"enabled": false,
"description": "DISABLED. Fixed at 85mm per sensitivity analysis.",
"canvas_position": {
"x": 50,
"y": 1060
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 0,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "rel_filtered_rms_40_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "rel_filtered_rms_60_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90_optician_workload",
"metric": "filtered_rms_nm"
},
{
"name": "mass_kg",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg reference (ZernikeOPD)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_40_vs_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg reference (ZernikeOPD)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_60_vs_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered RMS, ZernikeOPD)",
"direction": "minimize",
"weight": 3.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90_optician_workload"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
},
{
"id": "obj_004",
"name": "Total mass of M1_Blank part extracted via NX MeasureManager",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mass_kg"
},
"target": null,
"units": "kg",
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum blank mass constraint: must be <= 105 kg",
"type": "hard",
"operator": "<=",
"threshold": 105.0,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "CMA-ES",
"config": {
"sigma0": 0.3,
"seed": 42
}
},
"budget": {
"max_trials": 200
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "dv_008",
"target": "model"
},
{
"source": "dv_009",
"target": "model"
},
{
"source": "dv_010",
"target": "model"
},
{
"source": "dv_011",
"target": "model"
},
{
"source": "dv_012",
"target": "model"
},
{
"source": "dv_013",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "ext_001",
"target": "obj_004"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
},
{
"source": "obj_004",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,528 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.027110Z",
"modified": "2026-01-17T15:35:12.027110Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_cost_reduction_v11",
"description": "CMA-ES lateral optimization with CORRECT ZernikeOPD extraction using extract_relative() method. Uses actual figure geometry to compute OPD with XY lateral displacement correction. Clean restart from V10 (which had corrupted iterations).",
"tags": [
"mirror",
"zernike",
"opd"
]
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\DesigncenterNX2512",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 30.0,
"max": 72.0
},
"baseline": 61.9189,
"units": "mm",
"enabled": false,
"description": "FIXED at V7 best value",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "whiffle_outer_to_vertical",
"expression_name": "whiffle_outer_to_vertical",
"type": "continuous",
"bounds": {
"min": 70.0,
"max": 85.0
},
"baseline": 81.7479,
"units": "mm",
"enabled": false,
"description": "FIXED at V7 best value",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 65.0,
"max": 120.0
},
"baseline": 65.5418,
"units": "mm",
"enabled": false,
"description": "FIXED at V7 best value",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "blank_backface_angle",
"expression_name": "blank_backface_angle",
"type": "continuous",
"bounds": {
"min": 4.2,
"max": 4.5
},
"baseline": 4.4158,
"units": "degrees",
"enabled": false,
"description": "FIXED at V7 best value",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "Pocket_Radius",
"expression_name": "Pocket_Radius",
"type": "continuous",
"bounds": {
"min": 9.0,
"max": 12.0
},
"baseline": 10.05,
"units": "mm",
"enabled": false,
"description": "FIXED at 10.05mm (pushed to model each iteration)",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 30.0
},
"baseline": 27.7,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V11. Angle of inner lateral support constraint.",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 11.0,
"max": 17.0
},
"baseline": 13.03,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V11. Angle of outer lateral support constraint.",
"canvas_position": {
"x": 50,
"y": 580
}
},
{
"id": "dv_008",
"name": "lateral_outer_pivot",
"expression_name": "lateral_outer_pivot",
"type": "continuous",
"bounds": {
"min": 7,
"max": 12.0
},
"baseline": 10.0,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V11. Pivot position for outer lateral.",
"canvas_position": {
"x": 50,
"y": 660
}
},
{
"id": "dv_009",
"name": "lateral_inner_pivot",
"expression_name": "lateral_inner_pivot",
"type": "continuous",
"bounds": {
"min": 5.0,
"max": 12.0
},
"baseline": 8.16,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V11. Pivot position for inner lateral.",
"canvas_position": {
"x": 50,
"y": 740
}
},
{
"id": "dv_010",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 15.0,
"max": 27.0
},
"baseline": 22.88,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V11. Pivot position for middle lateral.",
"canvas_position": {
"x": 50,
"y": 820
}
},
{
"id": "dv_011",
"name": "lateral_closeness",
"expression_name": "lateral_closeness",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 12.0
},
"baseline": 9.84,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V11. Closeness factor for lateral supports.",
"canvas_position": {
"x": 50,
"y": 900
}
},
{
"id": "dv_012",
"name": "inner_circular_rib_dia",
"expression_name": "inner_circular_rib_dia",
"type": "continuous",
"bounds": {
"min": 480.0,
"max": 620.0
},
"baseline": 537.86,
"units": "mm",
"enabled": false,
"description": "DISABLED. Fixed structural parameter.",
"canvas_position": {
"x": 50,
"y": 980
}
},
{
"id": "dv_013",
"name": "center_thickness",
"expression_name": "center_thickness",
"type": "continuous",
"bounds": {
"min": 60.0,
"max": 85.0
},
"baseline": 85.0,
"units": "mm",
"enabled": false,
"description": "DISABLED. Fixed at 85mm per sensitivity analysis.",
"canvas_position": {
"x": 50,
"y": 1060
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 0,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "rel_filtered_rms_40_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "rel_filtered_rms_60_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90_optician_workload",
"metric": "filtered_rms_nm"
},
{
"name": "mass_kg",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg reference (ZernikeOPD)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_40_vs_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg reference (ZernikeOPD)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_60_vs_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered RMS, ZernikeOPD)",
"direction": "minimize",
"weight": 3.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90_optician_workload"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
},
{
"id": "obj_004",
"name": "Total mass of M1_Blank part extracted via NX MeasureManager",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mass_kg"
},
"target": null,
"units": "kg",
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum blank mass constraint: must be <= 105 kg",
"type": "hard",
"operator": "<=",
"threshold": 105.0,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "CMA-ES",
"config": {
"sigma0": 0.3,
"seed": 42
}
},
"budget": {
"max_trials": 200
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "dv_008",
"target": "model"
},
{
"source": "dv_009",
"target": "model"
},
{
"source": "dv_010",
"target": "model"
},
{
"source": "dv_011",
"target": "model"
},
{
"source": "dv_012",
"target": "model"
},
{
"source": "dv_013",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "ext_001",
"target": "obj_004"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
},
{
"source": "obj_004",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,528 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.028161Z",
"modified": "2026-01-17T15:35:12.028161Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_cost_reduction_v12",
"description": "CMA-ES combined Whiffle + Lateral optimization with ZernikeOPD extraction using extract_relative() method. All 10 design variables enabled for full fine-tuning. Starting from V11 best (trial 190, WS=284.19).",
"tags": [
"mirror",
"zernike",
"opd"
]
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\DesigncenterNX2512",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 30.0,
"max": 72.0
},
"baseline": 61.9189,
"units": "mm",
"enabled": true,
"description": "ACTIVE for V12. Inner whiffle tree contact radius. V7 optimum.",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "whiffle_outer_to_vertical",
"expression_name": "whiffle_outer_to_vertical",
"type": "continuous",
"bounds": {
"min": 70.0,
"max": 85.0
},
"baseline": 81.7479,
"units": "mm",
"enabled": true,
"description": "ACTIVE for V12. Outer whiffle arm length. V7 optimum.",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 65.0,
"max": 120.0
},
"baseline": 65.5418,
"units": "mm",
"enabled": true,
"description": "ACTIVE for V12. Whiffle triangle geometry. V7 optimum.",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "blank_backface_angle",
"expression_name": "blank_backface_angle",
"type": "continuous",
"bounds": {
"min": 4.2,
"max": 4.5
},
"baseline": 4.4158,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V12. Back face taper angle (affects mass). V7 optimum.",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "Pocket_Radius",
"expression_name": "Pocket_Radius",
"type": "continuous",
"bounds": {
"min": 9.0,
"max": 12.0
},
"baseline": 10.05,
"units": "mm",
"enabled": false,
"description": "FIXED at 10.05mm (pushed to model each iteration)",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 30.0
},
"baseline": 29.9005,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V12. V11 best value. Angle of inner lateral support constraint.",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 11.0,
"max": 17.0
},
"baseline": 11.759,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V12. V11 best value. Angle of outer lateral support constraint.",
"canvas_position": {
"x": 50,
"y": 580
}
},
{
"id": "dv_008",
"name": "lateral_outer_pivot",
"expression_name": "lateral_outer_pivot",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 12.0
},
"baseline": 11.1336,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V12. V11 best value. Pivot position for outer lateral.",
"canvas_position": {
"x": 50,
"y": 660
}
},
{
"id": "dv_009",
"name": "lateral_inner_pivot",
"expression_name": "lateral_inner_pivot",
"type": "continuous",
"bounds": {
"min": 5.0,
"max": 12.0
},
"baseline": 6.5186,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V12. V11 best value. Pivot position for inner lateral.",
"canvas_position": {
"x": 50,
"y": 740
}
},
{
"id": "dv_010",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 15.0,
"max": 27.0
},
"baseline": 22.3907,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V12. V11 best value. Pivot position for middle lateral.",
"canvas_position": {
"x": 50,
"y": 820
}
},
{
"id": "dv_011",
"name": "lateral_closeness",
"expression_name": "lateral_closeness",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 12.0
},
"baseline": 8.685,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V12. V11 best value. Closeness factor for lateral supports.",
"canvas_position": {
"x": 50,
"y": 900
}
},
{
"id": "dv_012",
"name": "inner_circular_rib_dia",
"expression_name": "inner_circular_rib_dia",
"type": "continuous",
"bounds": {
"min": 480.0,
"max": 620.0
},
"baseline": 537.86,
"units": "mm",
"enabled": false,
"description": "DISABLED. Fixed structural parameter.",
"canvas_position": {
"x": 50,
"y": 980
}
},
{
"id": "dv_013",
"name": "center_thickness",
"expression_name": "center_thickness",
"type": "continuous",
"bounds": {
"min": 60.0,
"max": 85.0
},
"baseline": 85.0,
"units": "mm",
"enabled": false,
"description": "DISABLED. Fixed at 85mm per sensitivity analysis.",
"canvas_position": {
"x": 50,
"y": 1060
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 0,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "rel_filtered_rms_40_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "rel_filtered_rms_60_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90_optician_workload",
"metric": "filtered_rms_nm"
},
{
"name": "mass_kg",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg reference (ZernikeOPD)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_40_vs_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg reference (ZernikeOPD)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_60_vs_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered RMS, ZernikeOPD)",
"direction": "minimize",
"weight": 3.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90_optician_workload"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
},
{
"id": "obj_004",
"name": "Total mass of M1_Blank part extracted via NX MeasureManager",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mass_kg"
},
"target": null,
"units": "kg",
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum blank mass constraint: must be <= 105 kg",
"type": "hard",
"operator": "<=",
"threshold": 105.0,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "CMA-ES",
"config": {
"sigma0": 0.15,
"seed": 42
}
},
"budget": {
"max_trials": 200
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "dv_008",
"target": "model"
},
{
"source": "dv_009",
"target": "model"
},
{
"source": "dv_010",
"target": "model"
},
{
"source": "dv_011",
"target": "model"
},
{
"source": "dv_012",
"target": "model"
},
{
"source": "dv_013",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "ext_001",
"target": "obj_004"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
},
{
"source": "obj_004",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,560 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.029754Z",
"modified": "2026-01-17T15:35:12.029754Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_cost_reduction_v13",
"description": "SAT v3 with conical backface (4-5 deg), stricter 100kg mass constraint, same cost function as flat_back (6*wfe_40 + 5*wfe_60 + 3*mfg). Uses all V6-V12 training data (1200+ samples).",
"tags": [
"SAT-v3-conical",
"mirror",
"zernike",
"opd"
],
"engineering_context": "Cost reduction with conical backface design"
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\DesigncenterNX2512",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 30.0,
"max": 72.0
},
"baseline": 56.54,
"units": "mm",
"enabled": true,
"description": "Inner whiffle tree contact radius - V12 range",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "whiffle_outer_to_vertical",
"expression_name": "whiffle_outer_to_vertical",
"type": "continuous",
"bounds": {
"min": 70.0,
"max": 85.0
},
"baseline": 82.28,
"units": "degrees",
"enabled": true,
"description": "Outer whiffle arm length - V12 range",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 65.0,
"max": 95.0
},
"baseline": 65.69,
"units": "mm",
"enabled": true,
"description": "CAD constraint - tightened from 120 to 95 based on failure analysis",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 32.0
},
"baseline": 29.14,
"units": "degrees",
"enabled": true,
"description": "Inner lateral support angle - V12 range",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 9.0,
"max": 15.0
},
"baseline": 12.57,
"units": "degrees",
"enabled": true,
"description": "Outer lateral support angle - V12 range",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "lateral_outer_pivot",
"expression_name": "lateral_outer_pivot",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 12.0
},
"baseline": 11.3,
"units": "mm",
"enabled": true,
"description": "CAD constraint",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "lateral_inner_pivot",
"expression_name": "lateral_inner_pivot",
"type": "continuous",
"bounds": {
"min": 5.0,
"max": 12.0
},
"baseline": 9.48,
"units": "mm",
"enabled": true,
"description": "Inner lateral pivot position",
"canvas_position": {
"x": 50,
"y": 580
}
},
{
"id": "dv_008",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 15.0,
"max": 24.0
},
"baseline": 20.09,
"units": "mm",
"enabled": true,
"description": "Middle lateral pivot position - tightened from 27 to 24",
"canvas_position": {
"x": 50,
"y": 660
}
},
{
"id": "dv_009",
"name": "lateral_closeness",
"expression_name": "lateral_closeness",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 11.0
},
"baseline": 9.03,
"units": "mm",
"enabled": true,
"description": "Lateral closeness factor - tightened from 12 to 11",
"canvas_position": {
"x": 50,
"y": 740
}
},
{
"id": "dv_010",
"name": "rib_thickness",
"expression_name": "rib_thickness",
"type": "continuous",
"bounds": {
"min": 6.0,
"max": 9.0
},
"baseline": 8.0,
"units": "mm",
"enabled": true,
"description": "Radial rib thickness - CAD constraint max 9mm (failures above 9.0)",
"canvas_position": {
"x": 50,
"y": 820
}
},
{
"id": "dv_011",
"name": "ribs_circular_thk",
"expression_name": "ribs_circular_thk",
"type": "continuous",
"bounds": {
"min": 5.0,
"max": 10.0
},
"baseline": 7.0,
"units": "mm",
"enabled": true,
"description": "Circular rib thickness",
"canvas_position": {
"x": 50,
"y": 900
}
},
{
"id": "dv_012",
"name": "rib_thickness_lateral_truss",
"expression_name": "rib_thickness_lateral_truss",
"type": "continuous",
"bounds": {
"min": 8.0,
"max": 14.0
},
"baseline": 11.0,
"units": "mm",
"enabled": true,
"description": "Lateral truss rib thickness",
"canvas_position": {
"x": 50,
"y": 980
}
},
{
"id": "dv_013",
"name": "center_thickness",
"expression_name": "center_thickness",
"type": "continuous",
"bounds": {
"min": 60.0,
"max": 85.0
},
"baseline": 85.0,
"units": "mm",
"enabled": true,
"description": "Center section thickness - from V12 at 85mm, can explore thinner",
"canvas_position": {
"x": 50,
"y": 1060
}
},
{
"id": "dv_014",
"name": "blank_backface_angle",
"expression_name": "blank_backface_angle",
"type": "continuous",
"bounds": {
"min": 4.0,
"max": 5.0
},
"baseline": 4.47,
"units": "degrees",
"enabled": true,
"description": "Backface taper angle (conical, not flat) - V12 best was 4.47",
"canvas_position": {
"x": 50,
"y": 1140
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 0,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "wfe_40_20",
"metric": "filtered_rms_nm"
},
{
"name": "wfe_60_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg",
"direction": "minimize",
"weight": 6.0,
"source": {
"extractor_id": "ext_001",
"output_name": "wfe_40_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "wfe_60_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing",
"direction": "minimize",
"weight": 3.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum blank mass constraint - 100kg strict limit",
"type": "hard",
"operator": "<=",
"threshold": 100.0,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "SAT_v3",
"config": {}
},
"budget": {
"max_trials": 300
},
"canvas_position": {
"x": 1300,
"y": 150
},
"surrogate": {
"enabled": true,
"type": "ensemble",
"config": {
"n_models": 5,
"architecture": [
128,
64,
32
],
"train_every_n_trials": 20,
"min_training_samples": 30
}
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "dv_008",
"target": "model"
},
{
"source": "dv_009",
"target": "model"
},
{
"source": "dv_010",
"target": "model"
},
{
"source": "dv_011",
"target": "model"
},
{
"source": "dv_012",
"target": "model"
},
{
"source": "dv_013",
"target": "model"
},
{
"source": "dv_014",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
},
"workflow": {
"stages": [
{
"id": "stage_exploration",
"name": "Design Space Exploration",
"algorithm": "RandomSearch",
"trials": 30,
"purpose": "Build initial training data for surrogate"
},
{
"id": "stage_optimization",
"name": "Surrogate-Assisted Optimization",
"algorithm": "SAT_v3",
"trials": 270,
"purpose": "Neural-accelerated optimization"
}
],
"transitions": [
{
"from": "stage_exploration",
"to": "stage_optimization",
"condition": "trial_count >= 30"
}
]
}
}

View File

@@ -0,0 +1,527 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.031312Z",
"modified": "2026-01-17T15:35:12.031312Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_cost_reduction_v9",
"description": "CMA-ES combined fine-tuning of all 10 parameters. Phase 3 of sequential optimization: V7 (whiffle) + V8 (lateral) results combined. Lower sigma for final polish around best known design.",
"tags": [
"mirror",
"zernike"
]
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\DesigncenterNX2512",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 30.0,
"max": 72.0
},
"baseline": 61.9189,
"units": "mm",
"enabled": true,
"description": "ACTIVE for V9. Value from V7 best (unchanged in V8).",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "whiffle_outer_to_vertical",
"expression_name": "whiffle_outer_to_vertical",
"type": "continuous",
"bounds": {
"min": 70.0,
"max": 85.0
},
"baseline": 81.7479,
"units": "mm",
"enabled": true,
"description": "ACTIVE for V9. Value from V7 best (unchanged in V8).",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 65.0,
"max": 120.0
},
"baseline": 65.5418,
"units": "mm",
"enabled": true,
"description": "ACTIVE for V9. Value from V7 best (unchanged in V8).",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "blank_backface_angle",
"expression_name": "blank_backface_angle",
"type": "continuous",
"bounds": {
"min": 4.2,
"max": 4.5
},
"baseline": 4.42,
"units": "degrees",
"enabled": false,
"description": "ACTIVE for V9. Value from V7 best (unchanged in V8).",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "Pocket_Radius",
"expression_name": "Pocket_Radius",
"type": "continuous",
"bounds": {
"min": 9.0,
"max": 12.0
},
"baseline": 10.05,
"units": "mm",
"enabled": false,
"description": "FIXED at 10.05mm (pushed to model each iteration)",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 32.0
},
"baseline": 29.827,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V9. Value from V8 best trial 189.",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 9,
"max": 15.0
},
"baseline": 11.206,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V9. Value from V8 best trial 189.",
"canvas_position": {
"x": 50,
"y": 580
}
},
{
"id": "dv_008",
"name": "lateral_outer_pivot",
"expression_name": "lateral_outer_pivot",
"type": "continuous",
"bounds": {
"min": 9.0,
"max": 11.0
},
"baseline": 10.828,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V9. Value from V8 best trial 189. Max constrained to 11.",
"canvas_position": {
"x": 50,
"y": 660
}
},
{
"id": "dv_009",
"name": "lateral_inner_pivot",
"expression_name": "lateral_inner_pivot",
"type": "continuous",
"bounds": {
"min": 5.0,
"max": 12.0
},
"baseline": 9.048,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V9. Value from V8 best trial 189.",
"canvas_position": {
"x": 50,
"y": 740
}
},
{
"id": "dv_010",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 18.0,
"max": 25.0
},
"baseline": 21.491,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V9. Value from V8 best trial 189.",
"canvas_position": {
"x": 50,
"y": 820
}
},
{
"id": "dv_011",
"name": "lateral_closeness",
"expression_name": "lateral_closeness",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 12.0
},
"baseline": 9.398,
"units": "degrees",
"enabled": true,
"description": "ACTIVE for V9. Value from V8 best trial 189.",
"canvas_position": {
"x": 50,
"y": 900
}
},
{
"id": "dv_012",
"name": "inner_circular_rib_dia",
"expression_name": "inner_circular_rib_dia",
"type": "continuous",
"bounds": {
"min": 480.0,
"max": 620.0
},
"baseline": 537.86,
"units": "mm",
"enabled": false,
"description": "DISABLED. Fixed structural parameter.",
"canvas_position": {
"x": 50,
"y": 980
}
},
{
"id": "dv_013",
"name": "center_thickness",
"expression_name": "center_thickness",
"type": "continuous",
"bounds": {
"min": 60.0,
"max": 85.0
},
"baseline": 85.0,
"units": "mm",
"enabled": false,
"description": "DISABLED. Fixed at 85mm per sensitivity analysis.",
"canvas_position": {
"x": 50,
"y": 1060
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 0,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "rel_filtered_rms_40_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "rel_filtered_rms_60_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90_optician_workload",
"metric": "filtered_rms_nm"
},
{
"name": "mass_kg",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg reference (operational tracking)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_40_vs_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg reference (operational tracking)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_60_vs_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered RMS)",
"direction": "minimize",
"weight": 3.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90_optician_workload"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
},
{
"id": "obj_004",
"name": "Total mass of M1_Blank part extracted via NX MeasureManager",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mass_kg"
},
"target": null,
"units": "kg",
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum blank mass constraint: must be <= 105 kg",
"type": "hard",
"operator": "<=",
"threshold": 105.0,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "CMA-ES",
"config": {
"sigma0": 0.15,
"seed": 42
}
},
"budget": {
"max_trials": 200
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "dv_008",
"target": "model"
},
{
"source": "dv_009",
"target": "model"
},
{
"source": "dv_010",
"target": "model"
},
{
"source": "dv_011",
"target": "model"
},
{
"source": "dv_012",
"target": "model"
},
{
"source": "dv_013",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "ext_001",
"target": "obj_004"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
},
{
"source": "obj_004",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,582 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.020603Z",
"modified": "2026-01-17T15:35:12.020603Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_cost_reduction_flat_back_v10",
"description": "SAT v3 with expanded bounds: center_thickness=60-75mm, thinner mirror face, expanded lateral angles. Uses all V5-V9 training data (800+ samples).",
"tags": [
"SAT-v3-300-thin",
"mirror",
"zernike",
"opd"
],
"engineering_context": "Cost reduction option C for Schott Quote revisions"
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\DesigncenterNX2512",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 60.0,
"max": 80.0
},
"baseline": 72.0,
"units": "mm",
"enabled": true,
"description": "Expanded range - top designs at 72mm",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "whiffle_outer_to_vertical",
"expression_name": "whiffle_outer_to_vertical",
"type": "continuous",
"bounds": {
"min": 70.0,
"max": 88.0
},
"baseline": 83.0,
"units": "degrees",
"enabled": true,
"description": "Slightly expanded upper bound",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 65.0,
"max": 120.0
},
"baseline": 65.0,
"units": "mm",
"enabled": true,
"description": "Keep V9 bounds - CAD constraint",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 28.0,
"max": 35.0
},
"baseline": 30.0,
"units": "degrees",
"enabled": true,
"description": "Expanded upper - top designs at 30 deg",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 8.0,
"max": 14.0
},
"baseline": 11.0,
"units": "degrees",
"enabled": true,
"description": "Lowered min - top designs at 11 deg",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "lateral_outer_pivot",
"expression_name": "lateral_outer_pivot",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 12.0
},
"baseline": 11.9,
"units": "mm",
"enabled": true,
"description": "Keep V9 bounds - CAD constraint",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "lateral_inner_pivot",
"expression_name": "lateral_inner_pivot",
"type": "continuous",
"bounds": {
"min": 5.0,
"max": 12.0
},
"baseline": 11.0,
"units": "mm",
"enabled": true,
"description": "Keep V9 bounds",
"canvas_position": {
"x": 50,
"y": 580
}
},
{
"id": "dv_008",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 15.0,
"max": 27.0
},
"baseline": 22.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 660
}
},
{
"id": "dv_009",
"name": "lateral_closeness",
"expression_name": "lateral_closeness",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 12.0
},
"baseline": 11.5,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 740
}
},
{
"id": "dv_010",
"name": "rib_thickness",
"expression_name": "rib_thickness",
"type": "continuous",
"bounds": {
"min": 8.0,
"max": 12.0
},
"baseline": 10.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 820
}
},
{
"id": "dv_011",
"name": "ribs_circular_thk",
"expression_name": "ribs_circular_thk",
"type": "continuous",
"bounds": {
"min": 5.0,
"max": 10.0
},
"baseline": 7.0,
"units": "mm",
"enabled": true,
"description": "Reduced min to 5mm - top designs at 7-8mm",
"canvas_position": {
"x": 50,
"y": 900
}
},
{
"id": "dv_012",
"name": "rib_thickness_lateral_truss",
"expression_name": "rib_thickness_lateral_truss",
"type": "continuous",
"bounds": {
"min": 8.0,
"max": 14.0
},
"baseline": 11.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 980
}
},
{
"id": "dv_013",
"name": "mirror_face_thickness",
"expression_name": "mirror_face_thickness",
"type": "continuous",
"bounds": {
"min": 15.0,
"max": 20.0
},
"baseline": 15.0,
"units": "mm",
"enabled": true,
"description": "Keep V9 bounds - CAD constraint",
"canvas_position": {
"x": 50,
"y": 1060
}
},
{
"id": "dv_014",
"name": "center_thickness",
"expression_name": "center_thickness",
"type": "continuous",
"bounds": {
"min": 60.0,
"max": 75.0
},
"baseline": 67.5,
"units": "mm",
"enabled": true,
"description": "REDUCED from 75-85 to 60-75 per user request",
"canvas_position": {
"x": 50,
"y": 1140
}
},
{
"id": "dv_015",
"name": "blank_backface_angle",
"expression_name": "blank_backface_angle",
"type": "continuous",
"bounds": {
"min": -0.001,
"max": 0.001
},
"baseline": 0.0,
"units": "degrees",
"enabled": false,
"description": "FIXED at 0 for flat backface",
"canvas_position": {
"x": 50,
"y": 1220
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 0,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "wfe_40_20",
"metric": "filtered_rms_nm"
},
{
"name": "wfe_60_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg",
"direction": "minimize",
"weight": 6.0,
"source": {
"extractor_id": "ext_001",
"output_name": "wfe_40_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "wfe_60_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing",
"direction": "minimize",
"weight": 3.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum blank mass constraint",
"type": "hard",
"operator": "<=",
"threshold": 120.0,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "SAT_v3",
"config": {}
},
"budget": {
"max_trials": 300
},
"canvas_position": {
"x": 1300,
"y": 150
},
"surrogate": {
"enabled": true,
"type": "ensemble",
"config": {
"n_models": 5,
"architecture": [
128,
64,
32
],
"train_every_n_trials": 20,
"min_training_samples": 30
}
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "dv_008",
"target": "model"
},
{
"source": "dv_009",
"target": "model"
},
{
"source": "dv_010",
"target": "model"
},
{
"source": "dv_011",
"target": "model"
},
{
"source": "dv_012",
"target": "model"
},
{
"source": "dv_013",
"target": "model"
},
{
"source": "dv_014",
"target": "model"
},
{
"source": "dv_015",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
},
"workflow": {
"stages": [
{
"id": "stage_exploration",
"name": "Design Space Exploration",
"algorithm": "RandomSearch",
"trials": 30,
"purpose": "Build initial training data for surrogate"
},
{
"id": "stage_optimization",
"name": "Surrogate-Assisted Optimization",
"algorithm": "SAT_v3",
"trials": 270,
"purpose": "Neural-accelerated optimization"
}
],
"transitions": [
{
"from": "stage_exploration",
"to": "stage_optimization",
"condition": "trial_count >= 30"
}
]
}
}

View File

@@ -0,0 +1,582 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.022867Z",
"modified": "2026-01-17T15:35:12.022867Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_cost_reduction_flat_back_v9",
"description": "Self-Aware Turbo v3 with ALL campaign data (601 samples), adaptive exploration schedule, optimal mass region targeting (115-120 kg), and L-BFGS polish phase.",
"tags": [
"SAT-v3-200",
"mirror",
"zernike",
"opd"
],
"engineering_context": "Cost reduction option C for Schott Quote revisions"
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\DesigncenterNX2512",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 30.0,
"max": 72.0
},
"baseline": 51.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "whiffle_outer_to_vertical",
"expression_name": "whiffle_outer_to_vertical",
"type": "continuous",
"bounds": {
"min": 70.0,
"max": 85.0
},
"baseline": 77.5,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 65.0,
"max": 120.0
},
"baseline": 92.5,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 30.0
},
"baseline": 27.5,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 11.0,
"max": 17.0
},
"baseline": 14.0,
"units": "degrees",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "lateral_outer_pivot",
"expression_name": "lateral_outer_pivot",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 12.0
},
"baseline": 9.5,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "lateral_inner_pivot",
"expression_name": "lateral_inner_pivot",
"type": "continuous",
"bounds": {
"min": 5.0,
"max": 12.0
},
"baseline": 8.5,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 580
}
},
{
"id": "dv_008",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 15.0,
"max": 27.0
},
"baseline": 21.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 660
}
},
{
"id": "dv_009",
"name": "lateral_closeness",
"expression_name": "lateral_closeness",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 12.0
},
"baseline": 9.5,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 740
}
},
{
"id": "dv_010",
"name": "rib_thickness",
"expression_name": "rib_thickness",
"type": "continuous",
"bounds": {
"min": 8.0,
"max": 12.0
},
"baseline": 10.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 820
}
},
{
"id": "dv_011",
"name": "ribs_circular_thk",
"expression_name": "ribs_circular_thk",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 12.0
},
"baseline": 9.5,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 900
}
},
{
"id": "dv_012",
"name": "rib_thickness_lateral_truss",
"expression_name": "rib_thickness_lateral_truss",
"type": "continuous",
"bounds": {
"min": 8.0,
"max": 14.0
},
"baseline": 12.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 980
}
},
{
"id": "dv_013",
"name": "mirror_face_thickness",
"expression_name": "mirror_face_thickness",
"type": "continuous",
"bounds": {
"min": 15.0,
"max": 20.0
},
"baseline": 17.5,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 1060
}
},
{
"id": "dv_014",
"name": "center_thickness",
"expression_name": "center_thickness",
"type": "continuous",
"bounds": {
"min": 75.0,
"max": 85.0
},
"baseline": 80.0,
"units": "mm",
"enabled": true,
"description": "",
"canvas_position": {
"x": 50,
"y": 1140
}
},
{
"id": "dv_015",
"name": "blank_backface_angle",
"expression_name": "blank_backface_angle",
"type": "continuous",
"bounds": {
"min": -0.001,
"max": 0.001
},
"baseline": 0.0,
"units": "degrees",
"enabled": false,
"description": "FIXED at 0 for flat backface",
"canvas_position": {
"x": 50,
"y": 1220
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 0,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "wfe_40_20",
"metric": "filtered_rms_nm"
},
{
"name": "wfe_60_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg",
"direction": "minimize",
"weight": 6.0,
"source": {
"extractor_id": "ext_001",
"output_name": "wfe_40_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "wfe_60_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing",
"direction": "minimize",
"weight": 3.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum blank mass constraint",
"type": "hard",
"operator": "<=",
"threshold": 120.0,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "SAT_v3",
"config": {}
},
"budget": {
"max_trials": 200
},
"canvas_position": {
"x": 1300,
"y": 150
},
"surrogate": {
"enabled": true,
"type": "ensemble",
"config": {
"n_models": 5,
"architecture": [
128,
64,
32
],
"train_every_n_trials": 20,
"min_training_samples": 30
}
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "dv_008",
"target": "model"
},
{
"source": "dv_009",
"target": "model"
},
{
"source": "dv_010",
"target": "model"
},
{
"source": "dv_011",
"target": "model"
},
{
"source": "dv_012",
"target": "model"
},
{
"source": "dv_013",
"target": "model"
},
{
"source": "dv_014",
"target": "model"
},
{
"source": "dv_015",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
},
"workflow": {
"stages": [
{
"id": "stage_exploration",
"name": "Design Space Exploration",
"algorithm": "RandomSearch",
"trials": 30,
"purpose": "Build initial training data for surrogate"
},
{
"id": "stage_optimization",
"name": "Surrogate-Assisted Optimization",
"algorithm": "SAT_v3",
"trials": 170,
"purpose": "Neural-accelerated optimization"
}
],
"transitions": [
{
"from": "stage_exploration",
"to": "stage_optimization",
"condition": "trial_count >= 30"
}
]
}
}

View File

@@ -0,0 +1,400 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.024432Z",
"modified": "2026-01-17T16:33:51.000000Z",
"created_by": "migration",
"modified_by": "claude",
"study_name": "m1_mirror_cost_reduction_lateral",
"description": "Lateral support optimization with new U-joint expressions (lateral_inner_u, lateral_outer_u) for cost reduction model. Focus on WFE and MFG only - no mass objective.",
"tags": [
"CMA-ES-100",
"mirror",
"zernike",
"opd"
],
"engineering_context": "Optimize lateral support geometry using new U-joint parameterization on cost reduction model"
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\DesigncenterNX2512",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "lateral_inner_u",
"expression_name": "lateral_inner_u",
"type": "continuous",
"bounds": {
"min": 0.2,
"max": 0.95
},
"baseline": 0.3,
"units": "unitless",
"enabled": true,
"description": "U-joint ratio for inner lateral support (replaces lateral_inner_pivot)",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "lateral_outer_u",
"expression_name": "lateral_outer_u",
"type": "continuous",
"bounds": {
"min": 0.2,
"max": 0.95
},
"baseline": 0.8,
"units": "unitless",
"enabled": true,
"description": "U-joint ratio for outer lateral support (replaces lateral_outer_pivot)",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 15.0,
"max": 27.0
},
"baseline": 21.07,
"units": "mm",
"enabled": true,
"description": "Middle pivot position on lateral support",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 35.0
},
"baseline": 31.93,
"units": "degrees",
"enabled": true,
"description": "Inner lateral support angle",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 8.0,
"max": 17.0
},
"baseline": 10.77,
"units": "degrees",
"enabled": true,
"description": "Outer lateral support angle",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "hole_dia1",
"expression_name": "hole_dia1",
"type": "continuous",
"bounds": {
"min": 5.0,
"max": 25.0
},
"baseline": 15.0,
"units": "mm",
"enabled": true,
"description": "Diameter of hole pattern 1",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "hole_dia2",
"expression_name": "hole_dia2",
"type": "continuous",
"bounds": {
"min": 5.0,
"max": 25.0
},
"baseline": 15.0,
"units": "mm",
"enabled": true,
"description": "Diameter of hole pattern 2",
"canvas_position": {
"x": 50,
"y": 580
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 135.75,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "wfe_40_20",
"metric": "filtered_rms_nm"
},
{
"name": "wfe_60_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
},
{
"id": "ext_003",
"name": "Volume Extractor",
"type": "custom",
"builtin": false,
"config": {
"density_kg_m3": 2530.0
},
"custom_function": {
"name": "extract_volume",
"code": "def extract_volume(trial_dir, config, context):\n \"\"\"\n Extract volume from mass using material density.\n Volume = Mass / Density\n \n For Zerodur glass-ceramic: density ~ 2530 kg/m³\n \"\"\"\n import json\n from pathlib import Path\n \n # Get mass from the mass extractor results\n results_file = Path(trial_dir) / 'results.json'\n if results_file.exists():\n with open(results_file) as f:\n results = json.load(f)\n mass_kg = results.get('mass_kg', 0)\n else:\n # If no results yet, try to get from context\n mass_kg = context.get('mass_kg', 0)\n \n density = config.get('density_kg_m3', 2530.0) # Zerodur default\n \n # Volume in m³\n volume_m3 = mass_kg / density if density > 0 else 0\n \n # Also calculate in liters for convenience (1 m³ = 1000 L)\n volume_liters = volume_m3 * 1000\n \n return {\n 'volume_m3': volume_m3,\n 'volume_liters': volume_liters\n }\n"
},
"outputs": [
{
"name": "volume_m3",
"metric": "total"
},
{
"name": "volume_liters",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 400
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg (annular)",
"direction": "minimize",
"weight": 6.0,
"source": {
"extractor_id": "ext_001",
"output_name": "wfe_40_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg (annular)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "wfe_60_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered, annular)",
"direction": "minimize",
"weight": 3.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum blank mass constraint (still enforced even though mass not optimized)",
"type": "hard",
"operator": "<=",
"threshold": 120.0,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "CMA-ES",
"config": {
"sigma0": 0.3
}
},
"budget": {
"max_trials": 100
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "solver",
"target": "ext_003"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,259 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.032823Z",
"modified": "2026-01-17T15:35:12.032823Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_flat_back_final",
"description": "Final whiffle refinement starting from V10 iter123 (best mass design). Only 2 variables: whiffle_min and whiffle_triangle_closeness. Uses CMA-ES for efficient 2D optimization.",
"tags": [
"CMA-ES-60",
"mirror",
"zernike",
"opd"
],
"engineering_context": "Final optimization of whiffle parameters for flat back design"
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\DesigncenterNX2512",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 35.0,
"max": 85.0
},
"baseline": 70.77,
"units": "mm",
"enabled": true,
"description": "Minimum whiffle tree arm length - affects support stiffness distribution",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 55.0,
"max": 110.0
},
"baseline": 69.02,
"units": "mm",
"enabled": true,
"description": "Closeness of whiffle triangle vertices - affects load distribution geometry",
"canvas_position": {
"x": 50,
"y": 180
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 135.75,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "wfe_40_20",
"metric": "filtered_rms_nm"
},
{
"name": "wfe_60_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg (annular)",
"direction": "minimize",
"weight": 6.0,
"source": {
"extractor_id": "ext_001",
"output_name": "wfe_40_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg (annular)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "wfe_60_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered, annular)",
"direction": "minimize",
"weight": 3.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum blank mass constraint",
"type": "hard",
"operator": "<=",
"threshold": 120.0,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "CMA-ES",
"config": {
"sigma0": 0.3
}
},
"budget": {
"max_trials": 60
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,325 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.034330Z",
"modified": "2026-01-17T15:35:12.034330Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_flatback_lateral",
"description": "Lateral support optimization with new U-joint expressions (lateral_inner_u, lateral_outer_u). Focus on WFE and MFG only - no mass objective.",
"tags": [
"CMA-ES-100",
"mirror",
"zernike",
"opd"
],
"engineering_context": "Optimize lateral support geometry using new U-joint parameterization"
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\DesigncenterNX2512",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "lateral_inner_u",
"expression_name": "lateral_inner_u",
"type": "continuous",
"bounds": {
"min": 0.2,
"max": 0.95
},
"baseline": 0.4,
"units": "unitless",
"enabled": true,
"description": "U-joint ratio for inner lateral support (replaces lateral_inner_pivot)",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "lateral_outer_u",
"expression_name": "lateral_outer_u",
"type": "continuous",
"bounds": {
"min": 0.2,
"max": 0.95
},
"baseline": 0.4,
"units": "unitless",
"enabled": true,
"description": "U-joint ratio for outer lateral support (replaces lateral_outer_pivot)",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 15.0,
"max": 27.0
},
"baseline": 22.42,
"units": "mm",
"enabled": true,
"description": "Middle pivot position on lateral support",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 35.0
},
"baseline": 31.96,
"units": "degrees",
"enabled": true,
"description": "Inner lateral support angle",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 8.0,
"max": 17.0
},
"baseline": 9.08,
"units": "degrees",
"enabled": true,
"description": "Outer lateral support angle",
"canvas_position": {
"x": 50,
"y": 420
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 135.75,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "wfe_40_20",
"metric": "filtered_rms_nm"
},
{
"name": "wfe_60_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg (annular)",
"direction": "minimize",
"weight": 6.0,
"source": {
"extractor_id": "ext_001",
"output_name": "wfe_40_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg (annular)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "wfe_60_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered, annular)",
"direction": "minimize",
"weight": 3.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum blank mass constraint (still enforced even though mass not optimized)",
"type": "hard",
"operator": "<=",
"threshold": 120.0,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "CMA-ES",
"config": {
"sigma0": 0.3
}
},
"budget": {
"max_trials": 100
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,461 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.035839Z",
"modified": "2026-01-17T15:35:12.035839Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_surrogate_turbo",
"description": "GNN surrogate turbo optimization trained on ~1000 FEA trials from V6-V9 (re-extracted) + V11-V12. Iterative refinement with 5 FEA validations per turbo cycle.",
"tags": [
"mirror",
"zernike",
"opd"
]
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\DesigncenterNX2512",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 30.0,
"max": 72.0
},
"baseline": 61.9189,
"units": "mm",
"enabled": true,
"description": "Inner whiffle tree contact radius",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "whiffle_outer_to_vertical",
"expression_name": "whiffle_outer_to_vertical",
"type": "continuous",
"bounds": {
"min": 70.0,
"max": 85.0
},
"baseline": 81.7479,
"units": "mm",
"enabled": true,
"description": "Outer whiffle arm length",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 65.0,
"max": 120.0
},
"baseline": 65.5418,
"units": "mm",
"enabled": true,
"description": "Whiffle triangle geometry",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "blank_backface_angle",
"expression_name": "blank_backface_angle",
"type": "continuous",
"bounds": {
"min": 4.2,
"max": 4.5
},
"baseline": 4.4158,
"units": "degrees",
"enabled": true,
"description": "Back face taper angle (affects mass)",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 30.0
},
"baseline": 29.9005,
"units": "degrees",
"enabled": true,
"description": "Angle of inner lateral support constraint",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 11.0,
"max": 17.0
},
"baseline": 11.759,
"units": "degrees",
"enabled": true,
"description": "Angle of outer lateral support constraint",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "lateral_outer_pivot",
"expression_name": "lateral_outer_pivot",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 12.0
},
"baseline": 11.1336,
"units": "degrees",
"enabled": true,
"description": "Pivot position for outer lateral",
"canvas_position": {
"x": 50,
"y": 580
}
},
{
"id": "dv_008",
"name": "lateral_inner_pivot",
"expression_name": "lateral_inner_pivot",
"type": "continuous",
"bounds": {
"min": 5.0,
"max": 12.0
},
"baseline": 6.5186,
"units": "degrees",
"enabled": true,
"description": "Pivot position for inner lateral",
"canvas_position": {
"x": 50,
"y": 660
}
},
{
"id": "dv_009",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 15.0,
"max": 27.0
},
"baseline": 22.3907,
"units": "degrees",
"enabled": true,
"description": "Pivot position for middle lateral",
"canvas_position": {
"x": 50,
"y": 740
}
},
{
"id": "dv_010",
"name": "lateral_closeness",
"expression_name": "lateral_closeness",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 12.0
},
"baseline": 8.685,
"units": "degrees",
"enabled": true,
"description": "Closeness factor for lateral supports",
"canvas_position": {
"x": 50,
"y": 820
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 0,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "rel_filtered_rms_40_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "rel_filtered_rms_60_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90_optician_workload",
"metric": "filtered_rms_nm"
},
{
"name": "mass_kg",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg reference (ZernikeOPD)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_40_vs_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg reference (ZernikeOPD)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_60_vs_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered RMS, ZernikeOPD)",
"direction": "minimize",
"weight": 3.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90_optician_workload"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
},
{
"id": "obj_004",
"name": "Total mass of M1_Blank part",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mass_kg"
},
"target": null,
"units": "kg",
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum blank mass constraint: must be <= 105 kg",
"type": "hard",
"operator": "<=",
"threshold": 105.0,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "TPE",
"config": {
"n_startup_trials": 10
}
},
"budget": {
"max_trials": 100
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "dv_008",
"target": "model"
},
{
"source": "dv_009",
"target": "model"
},
{
"source": "dv_010",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "ext_001",
"target": "obj_004"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
},
{
"source": "obj_004",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,462 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.036882Z",
"modified": "2026-01-17T15:35:12.036882Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_turbo_v1",
"description": "Self-improving turbo optimization: MLP surrogate explores design space, top candidates validated by FEA, surrogate retrains on new FEA data. Dashboard shows only FEA-validated iterations.",
"tags": [
"Surrogate",
"mirror",
"zernike",
"opd"
]
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\DesigncenterNX2512",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 30.0,
"max": 72.0
},
"baseline": 61.9189,
"units": "mm",
"enabled": true,
"description": "Inner whiffle tree contact radius",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "whiffle_outer_to_vertical",
"expression_name": "whiffle_outer_to_vertical",
"type": "continuous",
"bounds": {
"min": 70.0,
"max": 85.0
},
"baseline": 81.7479,
"units": "mm",
"enabled": true,
"description": "Outer whiffle arm length",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 65.0,
"max": 120.0
},
"baseline": 65.5418,
"units": "mm",
"enabled": true,
"description": "Whiffle triangle geometry",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "blank_backface_angle",
"expression_name": "blank_backface_angle",
"type": "continuous",
"bounds": {
"min": 4.2,
"max": 4.5
},
"baseline": 4.4158,
"units": "degrees",
"enabled": true,
"description": "Back face taper angle (affects mass)",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 30.0
},
"baseline": 29.9005,
"units": "degrees",
"enabled": true,
"description": "Angle of inner lateral support constraint",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 11.0,
"max": 17.0
},
"baseline": 11.759,
"units": "degrees",
"enabled": true,
"description": "Angle of outer lateral support constraint",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "lateral_outer_pivot",
"expression_name": "lateral_outer_pivot",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 12.0
},
"baseline": 11.1336,
"units": "degrees",
"enabled": true,
"description": "Pivot position for outer lateral",
"canvas_position": {
"x": 50,
"y": 580
}
},
{
"id": "dv_008",
"name": "lateral_inner_pivot",
"expression_name": "lateral_inner_pivot",
"type": "continuous",
"bounds": {
"min": 5.0,
"max": 12.0
},
"baseline": 6.5186,
"units": "degrees",
"enabled": true,
"description": "Pivot position for inner lateral",
"canvas_position": {
"x": 50,
"y": 660
}
},
{
"id": "dv_009",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 15.0,
"max": 27.0
},
"baseline": 22.3907,
"units": "degrees",
"enabled": true,
"description": "Pivot position for middle lateral",
"canvas_position": {
"x": 50,
"y": 740
}
},
{
"id": "dv_010",
"name": "lateral_closeness",
"expression_name": "lateral_closeness",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 12.0
},
"baseline": 8.685,
"units": "degrees",
"enabled": true,
"description": "Closeness factor for lateral supports",
"canvas_position": {
"x": 50,
"y": 820
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 0,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "rel_filtered_rms_40_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "rel_filtered_rms_60_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90_optician_workload",
"metric": "filtered_rms_nm"
},
{
"name": "mass_kg",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg reference (ZernikeOPD)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_40_vs_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg reference (ZernikeOPD)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_60_vs_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered RMS, ZernikeOPD)",
"direction": "minimize",
"weight": 3.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90_optician_workload"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
},
{
"id": "obj_004",
"name": "Total mass of M1_Blank part",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mass_kg"
},
"target": null,
"units": "kg",
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum blank mass constraint: must be <= 105 kg",
"type": "hard",
"operator": "<=",
"threshold": 105.0,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "TPE",
"config": {
"n_startup_trials": 10
}
},
"budget": {
"max_trials": 100
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "dv_008",
"target": "model"
},
{
"source": "dv_009",
"target": "model"
},
{
"source": "dv_010",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "ext_001",
"target": "obj_004"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
},
{
"source": "obj_004",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,462 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.037953Z",
"modified": "2026-01-17T15:35:12.037953Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_turbo_v2",
"description": "Turbo V2: Higher weight on 40-20 WFE (6.0 vs 5.0), mass objective disabled (weight 0). Uses pre-trained surrogate from V1 (45 FEA samples).",
"tags": [
"Surrogate",
"mirror",
"zernike",
"opd"
]
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\DesigncenterNX2512",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 30.0,
"max": 72.0
},
"baseline": 58.8,
"units": "mm",
"enabled": true,
"description": "Inner whiffle tree contact radius (V1 best value)",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "whiffle_outer_to_vertical",
"expression_name": "whiffle_outer_to_vertical",
"type": "continuous",
"bounds": {
"min": 70.0,
"max": 85.0
},
"baseline": 82.8,
"units": "mm",
"enabled": true,
"description": "Outer whiffle arm length (V1 best value)",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 65.0,
"max": 120.0
},
"baseline": 66.1,
"units": "mm",
"enabled": true,
"description": "Whiffle triangle geometry (V1 best value)",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "blank_backface_angle",
"expression_name": "blank_backface_angle",
"type": "continuous",
"bounds": {
"min": 4.2,
"max": 4.5
},
"baseline": 4.41,
"units": "degrees",
"enabled": true,
"description": "Back face taper angle (V1 best value)",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 30.0
},
"baseline": 30.0,
"units": "degrees",
"enabled": true,
"description": "Angle of inner lateral support constraint (V1 best value)",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 11.0,
"max": 17.0
},
"baseline": 11.7,
"units": "degrees",
"enabled": true,
"description": "Angle of outer lateral support constraint (V1 best value)",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "lateral_outer_pivot",
"expression_name": "lateral_outer_pivot",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 12.0
},
"baseline": 11.7,
"units": "degrees",
"enabled": true,
"description": "Pivot position for outer lateral (V1 best value)",
"canvas_position": {
"x": 50,
"y": 580
}
},
{
"id": "dv_008",
"name": "lateral_inner_pivot",
"expression_name": "lateral_inner_pivot",
"type": "continuous",
"bounds": {
"min": 5.0,
"max": 12.0
},
"baseline": 7.7,
"units": "degrees",
"enabled": true,
"description": "Pivot position for inner lateral (V1 best value)",
"canvas_position": {
"x": 50,
"y": 660
}
},
{
"id": "dv_009",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 15.0,
"max": 27.0
},
"baseline": 22.5,
"units": "degrees",
"enabled": true,
"description": "Pivot position for middle lateral (V1 best value)",
"canvas_position": {
"x": 50,
"y": 740
}
},
{
"id": "dv_010",
"name": "lateral_closeness",
"expression_name": "lateral_closeness",
"type": "continuous",
"bounds": {
"min": 7.0,
"max": 12.0
},
"baseline": 9.1,
"units": "degrees",
"enabled": true,
"description": "Closeness factor for lateral supports (V1 best value)",
"canvas_position": {
"x": 50,
"y": 820
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 0,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "rel_filtered_rms_40_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "rel_filtered_rms_60_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90_optician_workload",
"metric": "filtered_rms_nm"
},
{
"name": "mass_kg",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg reference (ZernikeOPD)",
"direction": "minimize",
"weight": 6.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_40_vs_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg reference (ZernikeOPD)",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_60_vs_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered RMS, ZernikeOPD)",
"direction": "minimize",
"weight": 3.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90_optician_workload"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
},
{
"id": "obj_004",
"name": "Total mass of M1_Blank part",
"direction": "minimize",
"weight": 0.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mass_kg"
},
"target": null,
"units": "kg",
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum blank mass constraint: must be <= 105 kg",
"type": "hard",
"operator": "<=",
"threshold": 105.0,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "CMA-ES",
"config": {
"sigma0": 0.3
}
},
"budget": {
"max_trials": 100
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "dv_008",
"target": "model"
},
{
"source": "dv_009",
"target": "model"
},
{
"source": "dv_010",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "ext_001",
"target": "obj_004"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
},
{
"source": "obj_004",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,435 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.038964Z",
"modified": "2026-01-17T15:35:12.038964Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "m1_mirror_zernike_optimization",
"description": "Telescope primary mirror support structure optimization using Zernike wavefront error metrics with neural acceleration",
"tags": [
"mirror",
"zernike"
]
},
"model": {
"sim": {
"path": "ASSY_M1_assyfem1_sim1.sim",
"solver": "nastran"
},
"nx_settings": {
"nx_install_path": "C:\\Program Files\\Siemens\\NX2506",
"simulation_timeout_s": 600
}
},
"design_variables": [
{
"id": "dv_001",
"name": "lateral_inner_angle",
"expression_name": "lateral_inner_angle",
"type": "continuous",
"bounds": {
"min": 25.0,
"max": 28.5
},
"baseline": 26.79,
"units": "degrees",
"enabled": false,
"description": "Lateral support inner angle",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "lateral_outer_angle",
"expression_name": "lateral_outer_angle",
"type": "continuous",
"bounds": {
"min": 13.0,
"max": 17.0
},
"baseline": 14.64,
"units": "degrees",
"enabled": false,
"description": "Lateral support outer angle",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "lateral_outer_pivot",
"expression_name": "lateral_outer_pivot",
"type": "continuous",
"bounds": {
"min": 9.0,
"max": 12.0
},
"baseline": 10.4,
"units": "mm",
"enabled": false,
"description": "Lateral outer pivot position",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "lateral_inner_pivot",
"expression_name": "lateral_inner_pivot",
"type": "continuous",
"bounds": {
"min": 9.0,
"max": 12.0
},
"baseline": 10.07,
"units": "mm",
"enabled": false,
"description": "Lateral inner pivot position",
"canvas_position": {
"x": 50,
"y": 340
}
},
{
"id": "dv_005",
"name": "lateral_middle_pivot",
"expression_name": "lateral_middle_pivot",
"type": "continuous",
"bounds": {
"min": 18.0,
"max": 23.0
},
"baseline": 20.73,
"units": "mm",
"enabled": false,
"description": "Lateral middle pivot position",
"canvas_position": {
"x": 50,
"y": 420
}
},
{
"id": "dv_006",
"name": "lateral_closeness",
"expression_name": "lateral_closeness",
"type": "continuous",
"bounds": {
"min": 9.5,
"max": 12.5
},
"baseline": 11.02,
"units": "mm",
"enabled": false,
"description": "Lateral support closeness parameter",
"canvas_position": {
"x": 50,
"y": 500
}
},
{
"id": "dv_007",
"name": "whiffle_min",
"expression_name": "whiffle_min",
"type": "continuous",
"bounds": {
"min": 35.0,
"max": 55.0
},
"baseline": 40.55,
"units": "mm",
"enabled": true,
"description": "Whiffle tree minimum parameter",
"canvas_position": {
"x": 50,
"y": 580
}
},
{
"id": "dv_008",
"name": "whiffle_outer_to_vertical",
"expression_name": "whiffle_outer_to_vertical",
"type": "continuous",
"bounds": {
"min": 68.0,
"max": 80.0
},
"baseline": 75.67,
"units": "degrees",
"enabled": true,
"description": "Whiffle tree outer to vertical angle",
"canvas_position": {
"x": 50,
"y": 660
}
},
{
"id": "dv_009",
"name": "whiffle_triangle_closeness",
"expression_name": "whiffle_triangle_closeness",
"type": "continuous",
"bounds": {
"min": 50.0,
"max": 65.0
},
"baseline": 60.0,
"units": "mm",
"enabled": false,
"description": "Whiffle tree triangle closeness",
"canvas_position": {
"x": 50,
"y": 740
}
},
{
"id": "dv_010",
"name": "blank_backface_angle",
"expression_name": "blank_backface_angle",
"type": "continuous",
"bounds": {
"min": 3.5,
"max": 5.0
},
"baseline": 4.23,
"units": "degrees",
"enabled": false,
"description": "Mirror blank backface angle",
"canvas_position": {
"x": 50,
"y": 820
}
},
{
"id": "dv_011",
"name": "inner_circular_rib_dia",
"expression_name": "inner_circular_rib_dia",
"type": "continuous",
"bounds": {
"min": 480.0,
"max": 620.0
},
"baseline": 534.0,
"units": "mm",
"enabled": true,
"description": "Inner circular rib diameter",
"canvas_position": {
"x": 50,
"y": 900
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 0,
"outer_radius_mm": 500.0,
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"reference_subcase": 2
},
"outputs": [
{
"name": "rel_filtered_rms_40_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "rel_filtered_rms_60_vs_20",
"metric": "filtered_rms_nm"
},
{
"name": "mfg_90_optician_workload",
"metric": "filtered_rms_nm"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Filtered RMS WFE at 40 deg relative to 20 deg reference",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_40_vs_20"
},
"target": 4.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Filtered RMS WFE at 60 deg relative to 20 deg reference",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_60_vs_20"
},
"target": 10.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Optician workload at 90 deg polishing orientation (filtered RMS with J1-J3)",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mfg_90_optician_workload"
},
"target": 20.0,
"units": "nm",
"canvas_position": {
"x": 1020,
"y": 300
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum von Mises stress in mirror assembly",
"type": "hard",
"operator": "<=",
"threshold": 10.0,
"source": {
"extractor_id": "ext_001",
"output_name": "rel_filtered_rms_40_vs_20"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "TPE",
"config": {
"n_startup_trials": 15,
"seed": 42
}
},
"budget": {
"max_trials": 100
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "dv_005",
"target": "model"
},
{
"source": "dv_006",
"target": "model"
},
{
"source": "dv_007",
"target": "model"
},
{
"source": "dv_008",
"target": "model"
},
{
"source": "dv_009",
"target": "model"
},
{
"source": "dv_010",
"target": "model"
},
{
"source": "dv_011",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "ext_001",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,213 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.039975Z",
"modified": "2026-01-17T15:35:12.039975Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "bracket_pareto_3obj",
"description": "Three-objective Pareto optimization: minimize mass, minimize stress, maximize stiffness",
"tags": []
},
"model": {
"sim": {
"path": "1_setup\\model\\Bracket_sim1.sim",
"solver": "nastran"
}
},
"design_variables": [
{
"id": "dv_001",
"name": "param_1",
"expression_name": "support_angle",
"type": "continuous",
"bounds": {
"min": 20,
"max": 70
},
"baseline": null,
"units": "degrees",
"enabled": true,
"description": "Angle of support arm relative to base",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "param_2",
"expression_name": "tip_thickness",
"type": "continuous",
"bounds": {
"min": 30,
"max": 60
},
"baseline": null,
"units": "mm",
"enabled": true,
"description": "Thickness at bracket tip where load is applied",
"canvas_position": {
"x": 50,
"y": 180
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Total bracket mass (kg)",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"target": null,
"units": "",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Maximum von Mises stress (MPa)",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "stress"
},
"target": null,
"units": "",
"canvas_position": {
"x": 1020,
"y": 200
}
},
{
"id": "obj_003",
"name": "Structural stiffness = Force/Displacement (N/mm)",
"direction": "maximize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "stiffness"
},
"target": null,
"units": "",
"canvas_position": {
"x": 1020,
"y": 300
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Keep stress below 300 MPa for safety margin",
"type": "hard",
"operator": "<",
"threshold": 300,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "TPE",
"config": {
"n_startup_trials": 10
}
},
"budget": {
"max_trials": 100
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "obj_003"
},
{
"source": "ext_001",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "obj_003",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,225 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.041483Z",
"modified": "2026-01-17T15:35:12.041483Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "bracket_stiffness_optimization",
"description": "Maximize bracket stiffness while minimizing mass (constraint: mass ≤ 0.2 kg)",
"tags": []
},
"model": {
"sim": {
"path": "1_setup/model/Bracket_sim1.sim",
"solver": "NX_Nastran",
"solution_type": "SOL101"
},
"nx_part": {
"path": "1_setup/model/Bracket.prt"
},
"fem": {
"path": "1_setup/model/Bracket_fem1.fem"
}
},
"design_variables": [
{
"id": "dv_001",
"name": "support_angle",
"expression_name": "support_angle",
"type": "continuous",
"bounds": {
"min": 20.0,
"max": 70.0
},
"baseline": 60.0,
"units": "degrees",
"enabled": true,
"description": "Angle of support arm relative to base",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "tip_thickness",
"expression_name": "tip_thickness",
"type": "continuous",
"bounds": {
"min": 30.0,
"max": 60.0
},
"baseline": 30.0,
"units": "mm",
"enabled": true,
"description": "Thickness of bracket tip where load is applied",
"canvas_position": {
"x": 50,
"y": 180
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "BracketStiffnessExtractor Extractor",
"type": "displacement",
"builtin": true,
"config": {
"result_type": "z",
"metric": "max_abs"
},
"outputs": [
{
"name": "stiffness",
"metric": "max_abs"
},
{
"name": "mass",
"metric": "max_abs"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Structural stiffness (N/mm) calculated as Force/Displacement",
"direction": "maximize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "stiffness"
},
"target": null,
"units": "",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Total mass (kg) - secondary objective with hard constraint",
"direction": "minimize",
"weight": 0.1,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"target": null,
"units": "",
"canvas_position": {
"x": 1020,
"y": 200
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum allowable mass: 200 grams",
"type": "hard",
"operator": "<",
"threshold": 0.2,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "NSGA-II",
"config": {
"population_size": 50,
"seed": 42
}
},
"budget": {
"max_trials": 50
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,225 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.043512Z",
"modified": "2026-01-17T15:35:12.043512Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "bracket_stiffness_optimization_v2",
"description": "Maximize bracket stiffness while minimizing mass (constraint: mass ≤ 0.2 kg)",
"tags": []
},
"model": {
"sim": {
"path": "1_setup/model/Bracket_sim1.sim",
"solver": "NX_Nastran",
"solution_type": "SOL101"
},
"nx_part": {
"path": "1_setup/model/Bracket.prt"
},
"fem": {
"path": "1_setup/model/Bracket_fem1.fem"
}
},
"design_variables": [
{
"id": "dv_001",
"name": "support_angle",
"expression_name": "support_angle",
"type": "continuous",
"bounds": {
"min": 20.0,
"max": 70.0
},
"baseline": 60.0,
"units": "degrees",
"enabled": true,
"description": "Angle of support arm relative to base",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "tip_thickness",
"expression_name": "tip_thickness",
"type": "continuous",
"bounds": {
"min": 30.0,
"max": 60.0
},
"baseline": 30.0,
"units": "mm",
"enabled": true,
"description": "Thickness of bracket tip where load is applied",
"canvas_position": {
"x": 50,
"y": 180
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "BracketStiffnessExtractor Extractor",
"type": "displacement",
"builtin": true,
"config": {
"result_type": "z",
"metric": "max_abs"
},
"outputs": [
{
"name": "stiffness",
"metric": "max_abs"
},
{
"name": "mass",
"metric": "max_abs"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Structural stiffness (N/mm) calculated as Force/Displacement",
"direction": "maximize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "stiffness"
},
"target": null,
"units": "",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Total mass (kg) - secondary objective with hard constraint",
"direction": "minimize",
"weight": 0.1,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"target": null,
"units": "",
"canvas_position": {
"x": 1020,
"y": 200
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum allowable mass: 200 grams",
"type": "hard",
"operator": "<",
"threshold": 0.2,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "NSGA-II",
"config": {
"population_size": 50,
"seed": 42
}
},
"budget": {
"max_trials": 50
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,225 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.044509Z",
"modified": "2026-01-17T15:35:12.044509Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "bracket_stiffness_optimization_v3",
"description": "Maximize bracket stiffness while minimizing mass (constraint: mass ≤ 0.2 kg)",
"tags": []
},
"model": {
"sim": {
"path": "1_setup/model/Bracket_sim1.sim",
"solver": "NX_Nastran",
"solution_type": "SOL101"
},
"nx_part": {
"path": "1_setup/model/Bracket.prt"
},
"fem": {
"path": "1_setup/model/Bracket_fem1.fem"
}
},
"design_variables": [
{
"id": "dv_001",
"name": "support_angle",
"expression_name": "support_angle",
"type": "continuous",
"bounds": {
"min": 20.0,
"max": 70.0
},
"baseline": 60.0,
"units": "degrees",
"enabled": true,
"description": "Angle of support arm relative to base",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "tip_thickness",
"expression_name": "tip_thickness",
"type": "continuous",
"bounds": {
"min": 30.0,
"max": 60.0
},
"baseline": 30.0,
"units": "mm",
"enabled": true,
"description": "Thickness of bracket tip where load is applied",
"canvas_position": {
"x": 50,
"y": 180
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "BracketStiffnessExtractor Extractor",
"type": "displacement",
"builtin": true,
"config": {
"result_type": "z",
"metric": "max_abs"
},
"outputs": [
{
"name": "stiffness",
"metric": "max_abs"
},
{
"name": "mass",
"metric": "max_abs"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Structural stiffness (N/mm) calculated as Force/Displacement",
"direction": "maximize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "stiffness"
},
"target": null,
"units": "",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Total mass (kg) - secondary objective with hard constraint",
"direction": "minimize",
"weight": 0.1,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"target": null,
"units": "",
"canvas_position": {
"x": 1020,
"y": 200
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum allowable mass: 200 grams",
"type": "hard",
"operator": "<",
"threshold": 0.2,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "NSGA-II",
"config": {
"population_size": 50,
"seed": 42
}
},
"budget": {
"max_trials": 50
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,209 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.042505Z",
"modified": "2026-01-17T15:35:12.042505Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "bracket_stiffness_optimization_atomizerfield",
"description": "Bracket Stiffness Optimization with AtomizerField Neural Acceleration - Multi-objective optimization of bracket geometry for maximum stiffness and minimum mass",
"tags": []
},
"model": {
"sim": {
"path": "1_setup\\model\\Bracket_sim1.sim",
"solver": "nastran"
}
},
"design_variables": [
{
"id": "dv_001",
"name": "param_1",
"expression_name": "support_angle",
"type": "continuous",
"bounds": {
"min": 20,
"max": 70
},
"baseline": null,
"units": "degrees",
"enabled": true,
"description": "Angle of the support arm (degrees)",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "param_2",
"expression_name": "tip_thickness",
"type": "continuous",
"bounds": {
"min": 30,
"max": 60
},
"baseline": null,
"units": "mm",
"enabled": true,
"description": "Thickness at the bracket tip (mm)",
"canvas_position": {
"x": 50,
"y": 180
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Displacement Extractor",
"type": "displacement",
"builtin": true,
"outputs": [
{
"name": "stiffness",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass_kg",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 250
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Structural stiffness (inverse of max displacement)",
"direction": "maximize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "stiffness"
},
"target": null,
"units": "",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "Total bracket mass (kg)",
"direction": "minimize",
"weight": 0.1,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"target": null,
"units": "",
"canvas_position": {
"x": 1020,
"y": 200
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum mass constraint (kg)",
"type": "hard",
"operator": "<",
"threshold": 0.2,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
}
],
"optimization": {
"algorithm": {
"type": "TPE",
"config": {
"n_startup_trials": 10
}
},
"budget": {
"max_trials": 100
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "solver",
"target": "ext_002"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_002",
"target": "con_001"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,287 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.045018Z",
"modified": "2026-01-17T15:35:12.045018Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "uav_arm_atomizerfield_test",
"description": "UAV Camera Support Arm - AtomizerField Neural Surrogate Integration Test",
"tags": []
},
"model": {
"sim": {
"path": "1_setup\\model\\Beam_sim1.sim",
"solver": "nastran"
}
},
"design_variables": [
{
"id": "dv_001",
"name": "param_1",
"expression_name": "beam_half_core_thickness",
"type": "continuous",
"bounds": {
"min": 5,
"max": 10
},
"baseline": null,
"units": "",
"enabled": true,
"description": "Half thickness of beam core (mm) - affects weight and stiffness",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "param_2",
"expression_name": "beam_face_thickness",
"type": "continuous",
"bounds": {
"min": 1,
"max": 3
},
"baseline": null,
"units": "",
"enabled": true,
"description": "Thickness of beam face sheets (mm) - bending resistance",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "param_3",
"expression_name": "holes_diameter",
"type": "continuous",
"bounds": {
"min": 10,
"max": 50
},
"baseline": null,
"units": "",
"enabled": true,
"description": "Diameter of lightening holes (mm) - weight reduction",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "param_4",
"expression_name": "hole_count",
"type": "continuous",
"bounds": {
"min": 8,
"max": 14
},
"baseline": null,
"units": "",
"enabled": true,
"description": "Number of lightening holes - balance weight vs strength",
"canvas_position": {
"x": 50,
"y": 340
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Total mass (grams) - minimize for longer flight time",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"target": 4000,
"units": "",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "First natural frequency (Hz) - avoid rotor resonance",
"direction": "maximize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "fundamental_frequency"
},
"target": 150,
"units": "",
"canvas_position": {
"x": 1020,
"y": 200
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum tip displacement under 850g camera load < 1.5mm for image stabilization",
"type": "hard",
"operator": "<",
"threshold": 1.5,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
},
{
"id": "con_002",
"name": "Maximum von Mises stress < 120 MPa (Al 6061-T6, SF=2.3)",
"type": "hard",
"operator": "<",
"threshold": 120,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 500
}
},
{
"id": "con_003",
"name": "Natural frequency > 150 Hz to avoid rotor frequencies (80-120 Hz safety margin)",
"type": "hard",
"operator": ">",
"threshold": 150,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 600
}
}
],
"optimization": {
"algorithm": {
"type": "TPE",
"config": {
"n_startup_trials": 10
}
},
"budget": {
"max_trials": 200
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "con_001"
},
{
"source": "ext_001",
"target": "con_002"
},
{
"source": "ext_001",
"target": "con_003"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
},
{
"source": "con_002",
"target": "optimization"
},
{
"source": "con_003",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,287 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.046025Z",
"modified": "2026-01-17T15:35:12.046025Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "uav_arm_optimization",
"description": "UAV Camera Support Arm - Multi-Objective Lightweight Design",
"tags": []
},
"model": {
"sim": {
"path": "1_setup\\model\\Beam_sim1.sim",
"solver": "nastran"
}
},
"design_variables": [
{
"id": "dv_001",
"name": "param_1",
"expression_name": "beam_half_core_thickness",
"type": "continuous",
"bounds": {
"min": 5,
"max": 10
},
"baseline": null,
"units": "",
"enabled": true,
"description": "Half thickness of beam core (mm) - affects weight and stiffness",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "param_2",
"expression_name": "beam_face_thickness",
"type": "continuous",
"bounds": {
"min": 1,
"max": 3
},
"baseline": null,
"units": "",
"enabled": true,
"description": "Thickness of beam face sheets (mm) - bending resistance",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "param_3",
"expression_name": "holes_diameter",
"type": "continuous",
"bounds": {
"min": 10,
"max": 50
},
"baseline": null,
"units": "",
"enabled": true,
"description": "Diameter of lightening holes (mm) - weight reduction",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "param_4",
"expression_name": "hole_count",
"type": "continuous",
"bounds": {
"min": 8,
"max": 14
},
"baseline": null,
"units": "",
"enabled": true,
"description": "Number of lightening holes - balance weight vs strength",
"canvas_position": {
"x": 50,
"y": 340
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Total mass (grams) - minimize for longer flight time",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"target": 4000,
"units": "",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "First natural frequency (Hz) - avoid rotor resonance",
"direction": "maximize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "fundamental_frequency"
},
"target": 150,
"units": "",
"canvas_position": {
"x": 1020,
"y": 200
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum tip displacement under 850g camera load < 1.5mm for image stabilization",
"type": "hard",
"operator": "<",
"threshold": 1.5,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
},
{
"id": "con_002",
"name": "Maximum von Mises stress < 120 MPa (Al 6061-T6, SF=2.3)",
"type": "hard",
"operator": "<",
"threshold": 120,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 500
}
},
{
"id": "con_003",
"name": "Natural frequency > 150 Hz to avoid rotor frequencies (80-120 Hz safety margin)",
"type": "hard",
"operator": ">",
"threshold": 150,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 600
}
}
],
"optimization": {
"algorithm": {
"type": "TPE",
"config": {
"n_startup_trials": 10
}
},
"budget": {
"max_trials": 30
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "con_001"
},
{
"source": "ext_001",
"target": "con_002"
},
{
"source": "ext_001",
"target": "con_003"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
},
{
"source": "con_002",
"target": "optimization"
},
{
"source": "con_003",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,287 @@
{
"meta": {
"version": "2.0",
"created": "2026-01-17T15:35:12.047031Z",
"modified": "2026-01-17T15:35:12.047031Z",
"created_by": "migration",
"modified_by": "migration",
"study_name": "uav_arm_optimization_v2",
"description": "UAV Camera Support Arm V2 - Testing Solution Monitor Control",
"tags": []
},
"model": {
"sim": {
"path": "1_setup\\model\\Beam_sim1.sim",
"solver": "nastran"
}
},
"design_variables": [
{
"id": "dv_001",
"name": "param_1",
"expression_name": "beam_half_core_thickness",
"type": "continuous",
"bounds": {
"min": 5,
"max": 10
},
"baseline": null,
"units": "",
"enabled": true,
"description": "Half thickness of beam core (mm) - affects weight and stiffness",
"canvas_position": {
"x": 50,
"y": 100
}
},
{
"id": "dv_002",
"name": "param_2",
"expression_name": "beam_face_thickness",
"type": "continuous",
"bounds": {
"min": 1,
"max": 3
},
"baseline": null,
"units": "",
"enabled": true,
"description": "Thickness of beam face sheets (mm) - bending resistance",
"canvas_position": {
"x": 50,
"y": 180
}
},
{
"id": "dv_003",
"name": "param_3",
"expression_name": "holes_diameter",
"type": "continuous",
"bounds": {
"min": 10,
"max": 50
},
"baseline": null,
"units": "",
"enabled": true,
"description": "Diameter of lightening holes (mm) - weight reduction",
"canvas_position": {
"x": 50,
"y": 260
}
},
{
"id": "dv_004",
"name": "param_4",
"expression_name": "hole_count",
"type": "continuous",
"bounds": {
"min": 8,
"max": 14
},
"baseline": null,
"units": "",
"enabled": true,
"description": "Number of lightening holes - balance weight vs strength",
"canvas_position": {
"x": 50,
"y": 340
}
}
],
"extractors": [
{
"id": "ext_001",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"outputs": [
{
"name": "mass",
"metric": "total"
}
],
"canvas_position": {
"x": 740,
"y": 100
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "Total mass (grams) - minimize for longer flight time",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"target": 4000,
"units": "",
"canvas_position": {
"x": 1020,
"y": 100
}
},
{
"id": "obj_002",
"name": "First natural frequency (Hz) - avoid rotor resonance",
"direction": "maximize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "fundamental_frequency"
},
"target": 150,
"units": "",
"canvas_position": {
"x": 1020,
"y": 200
}
}
],
"constraints": [
{
"id": "con_001",
"name": "Maximum tip displacement under 850g camera load < 1.5mm for image stabilization",
"type": "hard",
"operator": "<",
"threshold": 1.5,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 400
}
},
{
"id": "con_002",
"name": "Maximum von Mises stress < 120 MPa (Al 6061-T6, SF=2.3)",
"type": "hard",
"operator": "<",
"threshold": 120,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 500
}
},
{
"id": "con_003",
"name": "Natural frequency > 150 Hz to avoid rotor frequencies (80-120 Hz safety margin)",
"type": "hard",
"operator": ">",
"threshold": 150,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0
},
"canvas_position": {
"x": 1020,
"y": 600
}
}
],
"optimization": {
"algorithm": {
"type": "TPE",
"config": {
"n_startup_trials": 10
}
},
"budget": {
"max_trials": 10
},
"canvas_position": {
"x": 1300,
"y": 150
}
},
"canvas": {
"edges": [
{
"source": "dv_001",
"target": "model"
},
{
"source": "dv_002",
"target": "model"
},
{
"source": "dv_003",
"target": "model"
},
{
"source": "dv_004",
"target": "model"
},
{
"source": "model",
"target": "solver"
},
{
"source": "solver",
"target": "ext_001"
},
{
"source": "ext_001",
"target": "obj_001"
},
{
"source": "ext_001",
"target": "obj_002"
},
{
"source": "ext_001",
"target": "con_001"
},
{
"source": "ext_001",
"target": "con_002"
},
{
"source": "ext_001",
"target": "con_003"
},
{
"source": "obj_001",
"target": "optimization"
},
{
"source": "obj_002",
"target": "optimization"
},
{
"source": "con_001",
"target": "optimization"
},
{
"source": "con_002",
"target": "optimization"
},
{
"source": "con_003",
"target": "optimization"
}
],
"layout_version": "2.0"
}
}

View File

@@ -0,0 +1,83 @@
{
"meta": {
"version": "2.0",
"study_name": "support_mass_v1",
"description": "Minimize mass of structural support",
"created_by": "claude",
"created_at": "2026-01-18",
"modified_by": "claude",
"modified_at": "2026-01-18"
},
"model": {
"sim": {
"path": "",
"solver": "nastran",
"description": "Structural support model - to be configured in Canvas"
}
},
"design_variables": [],
"extractors": [
{
"id": "ext_001",
"name": "mass",
"type": "mass_bdf",
"description": "Total structural mass from BDF",
"config": {
"unit": "kg"
},
"enabled": true,
"canvas_position": {
"x": 400,
"y": 200
}
}
],
"objectives": [
{
"id": "obj_001",
"name": "minimize_mass",
"direction": "minimize",
"weight": 1.0,
"extractor_id": "ext_001",
"enabled": true,
"canvas_position": {
"x": 600,
"y": 200
}
}
],
"constraints": [],
"optimization": {
"algorithm": {
"type": "TPE",
"config": {
"n_startup_trials": 10,
"multivariate": true
}
},
"budget": {
"max_trials": 100,
"timeout_hours": null
},
"convergence": {
"enable_early_stopping": true,
"patience": 15,
"min_improvement": 0.001
}
},
"canvas": {
"edges": [
{
"id": "edge_001",
"source": "model",
"target": "ext_001"
},
{
"id": "edge_002",
"source": "ext_001",
"target": "obj_001"
}
],
"layout_version": "2.0"
}
}