Compare commits
4 Commits
b05412f807
...
27e78d3d56
| Author | SHA1 | Date | |
|---|---|---|---|
| 27e78d3d56 | |||
| cb6b130908 | |||
| f067497e08 | |||
| ba0b9a1fae |
@@ -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():
|
||||||
|
|||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
646
atomizer-dashboard/backend/api/routes/spec.py
Normal file
646
atomizer-dashboard/backend/api/routes/spec.py
Normal 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))
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
454
atomizer-dashboard/backend/api/services/interview_engine.py
Normal file
454
atomizer-dashboard/backend/api/services/interview_engine.py
Normal 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
|
||||||
@@ -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 {
|
||||||
|
|||||||
747
atomizer-dashboard/backend/api/services/spec_manager.py
Normal file
747
atomizer-dashboard/backend/api/services/spec_manager.py
Normal 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)
|
||||||
@@ -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 />}>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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%' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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%' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
```
|
|
||||||
@@ -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';
|
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
349
atomizer-dashboard/frontend/src/hooks/useClaudeCode.ts
Normal file
349
atomizer-dashboard/frontend/src/hooks/useClaudeCode.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
288
atomizer-dashboard/frontend/src/hooks/useSpecWebSocket.ts
Normal file
288
atomizer-dashboard/frontend/src/hooks/useSpecWebSocket.ts
Normal 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;
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
572
atomizer-dashboard/frontend/src/types/atomizer-spec.ts
Normal file
572
atomizer-dashboard/frontend/src/types/atomizer-spec.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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']
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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%"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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[] = [
|
||||||
|
|||||||
1175
mcp-server/atomizer-tools/src/tools/spec.ts
Normal file
1175
mcp-server/atomizer-tools/src/tools/spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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',
|
||||||
]
|
]
|
||||||
|
|||||||
844
optimization_engine/config/migrator.py
Normal file
844
optimization_engine/config/migrator.py
Normal 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)
|
||||||
@@ -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',
|
||||||
]
|
]
|
||||||
|
|||||||
800
optimization_engine/schemas/atomizer_spec_v2.json
Normal file
800
optimization_engine/schemas/atomizer_spec_v2.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
406
studies/M1_Mirror/m1_mirror_adaptive_V11/atomizer_spec.json
Normal file
406
studies/M1_Mirror/m1_mirror_adaptive_V11/atomizer_spec.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
454
studies/M1_Mirror/m1_mirror_adaptive_V12/atomizer_spec.json
Normal file
454
studies/M1_Mirror/m1_mirror_adaptive_V12/atomizer_spec.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
406
studies/M1_Mirror/m1_mirror_adaptive_V13/atomizer_spec.json
Normal file
406
studies/M1_Mirror/m1_mirror_adaptive_V13/atomizer_spec.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
406
studies/M1_Mirror/m1_mirror_adaptive_V14/atomizer_spec.json
Normal file
406
studies/M1_Mirror/m1_mirror_adaptive_V14/atomizer_spec.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
407
studies/M1_Mirror/m1_mirror_adaptive_V15/atomizer_spec.json
Normal file
407
studies/M1_Mirror/m1_mirror_adaptive_V15/atomizer_spec.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
435
studies/M1_Mirror/m1_mirror_cost_reduction/atomizer_spec.json
Normal file
435
studies/M1_Mirror/m1_mirror_cost_reduction/atomizer_spec.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
527
studies/M1_Mirror/m1_mirror_cost_reduction_V9/atomizer_spec.json
Normal file
527
studies/M1_Mirror/m1_mirror_cost_reduction_V9/atomizer_spec.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
259
studies/M1_Mirror/m1_mirror_flat_back_final/atomizer_spec.json
Normal file
259
studies/M1_Mirror/m1_mirror_flat_back_final/atomizer_spec.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
325
studies/M1_Mirror/m1_mirror_flatback_lateral/atomizer_spec.json
Normal file
325
studies/M1_Mirror/m1_mirror_flatback_lateral/atomizer_spec.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
461
studies/M1_Mirror/m1_mirror_surrogate_turbo/atomizer_spec.json
Normal file
461
studies/M1_Mirror/m1_mirror_surrogate_turbo/atomizer_spec.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
462
studies/M1_Mirror/m1_mirror_turbo_V1/atomizer_spec.json
Normal file
462
studies/M1_Mirror/m1_mirror_turbo_V1/atomizer_spec.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
462
studies/M1_Mirror/m1_mirror_turbo_V2/atomizer_spec.json
Normal file
462
studies/M1_Mirror/m1_mirror_turbo_V2/atomizer_spec.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
213
studies/Simple_Bracket/bracket_pareto_3obj/atomizer_spec.json
Normal file
213
studies/Simple_Bracket/bracket_pareto_3obj/atomizer_spec.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
287
studies/UAV_Arm/uav_arm_atomizerfield_test/atomizer_spec.json
Normal file
287
studies/UAV_Arm/uav_arm_atomizerfield_test/atomizer_spec.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
287
studies/UAV_Arm/uav_arm_optimization/atomizer_spec.json
Normal file
287
studies/UAV_Arm/uav_arm_optimization/atomizer_spec.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
287
studies/UAV_Arm/uav_arm_optimization_V2/atomizer_spec.json
Normal file
287
studies/UAV_Arm/uav_arm_optimization_V2/atomizer_spec.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
83
studies/support_mass_v1/atomizer_spec.json
Normal file
83
studies/support_mass_v1/atomizer_spec.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user