fix(canvas): Multiple fixes for drag-drop, undo/redo, and code generation
Drag-drop fixes: - Fix Objective default data: use nested 'source' object with extractor_id/output_name - Fix Constraint default data: use 'type' field (not constraint_type), 'threshold' (not limit) Undo/Redo fixes: - Remove dependency on isDirty flag (which is always false due to auto-save) - Record snapshots based on actual spec changes via deep comparison Code generation improvements: - Update system prompt to support multiple extractor types: * OP2-based extractors for FEA results (stress, displacement, frequency) * Expression-based extractors for NX model values (dimensions, volumes) * Computed extractors for derived values (no FEA needed) - Claude will now choose appropriate signature based on user's description
This commit is contained in:
@@ -62,7 +62,23 @@
|
||||
"Bash(xargs -I{} git ls-tree -r -l HEAD {})",
|
||||
"Bash(sort:*)",
|
||||
"Bash(C:Usersantoianaconda3envsatomizerpython.exe introspect_model.py)",
|
||||
"Bash(xargs:*)"
|
||||
"Bash(xargs:*)",
|
||||
"Bash(ping:*)",
|
||||
"Bash(C:Usersantoianaconda3envsatomizerpython.exe -c \"import requests; r = requests.post\\(''http://127.0.0.1:8001/api/claude/sessions'', json={''mode'': ''user''}\\); print\\(f''Status: {r.status_code}''\\); print\\(f''Response: {r.text}''\\)\")",
|
||||
"Bash(start \"Atomizer Backend\" cmd /k C:UsersantoiAtomizerrestart_backend.bat)",
|
||||
"Bash(start \"Test Backend\" cmd /c \"cd /d C:\\\\Users\\\\antoi\\\\Atomizer\\\\atomizer-dashboard\\\\backend && C:\\\\Users\\\\antoi\\\\anaconda3\\\\Scripts\\\\activate.bat atomizer && python -m uvicorn api.main:app --port 8002\")",
|
||||
"Bash(C:Usersantoianaconda3envsatomizerpython.exe C:UsersantoiAtomizertest_backend.py)",
|
||||
"Bash(start \"Backend 8002\" C:UsersantoiAtomizerstart_backend_8002.bat)",
|
||||
"Bash(C:Usersantoianaconda3envsatomizerpython.exe -c \"from api.main import app; print\\(''Import OK''\\)\")",
|
||||
"Bash(find:*)",
|
||||
"Bash(npx tailwindcss:*)",
|
||||
"Bash(C:Usersantoianaconda3envsatomizerpython.exe -c \"from pathlib import Path; p = Path\\(''C:/Users/antoi/Atomizer/studies''\\) / ''M1_Mirror/m1_mirror_cost_reduction_lateral''; print\\(''exists:'', p.exists\\(\\), ''path:'', p\\)\")",
|
||||
"Bash(C:Usersantoianaconda3envsatomizerpython.exe -c \"import sys, json; d=json.load\\(sys.stdin\\); print\\(''Study:'', d.get\\(''meta'',{}\\).get\\(''study_name'',''N/A''\\)\\); print\\(''Design Variables:''\\); [print\\(f'' - {dv[\"\"name\"\"]} \\({dv[\"\"expression_name\"\"]}\\)''\\) for dv in d.get\\(''design_variables'',[]\\)]\")",
|
||||
"Bash(C:Usersantoianaconda3envsatomizerpython.exe -m py_compile:*)",
|
||||
"Skill(ralph-loop:ralph-loop)",
|
||||
"Skill(ralph-loop:ralph-loop:*)",
|
||||
"mcp__Claude_in_Chrome__computer",
|
||||
"mcp__Claude_in_Chrome__navigate"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
1
CUsersantoiAtomizeropenapi_dump.json
Normal file
1
CUsersantoiAtomizeropenapi_dump.json
Normal file
File diff suppressed because one or more lines are too long
@@ -83,23 +83,49 @@ async def generate_extractor_code(request: ExtractorGenerationRequest):
|
||||
# Build focused system prompt for extractor generation
|
||||
system_prompt = """You are generating a Python custom extractor function for Atomizer FEA optimization.
|
||||
|
||||
The function MUST:
|
||||
1. Have signature: def extract(op2_path: str, fem_path: str, params: dict, subcase_id: int = 1) -> dict
|
||||
2. Return a dict with extracted values (e.g., {"max_stress": 150.5, "mass": 2.3})
|
||||
3. Use pyNastran.op2.op2.OP2 for reading OP2 results
|
||||
4. Handle missing data gracefully with try/except blocks
|
||||
IMPORTANT: Choose the appropriate function signature based on what data is needed:
|
||||
|
||||
Available imports (already available, just use them):
|
||||
- from pyNastran.op2.op2 import OP2
|
||||
- import numpy as np
|
||||
- from pathlib import Path
|
||||
## Option 1: FEA Results (OP2) - Use for stresses, displacements, frequencies, forces
|
||||
```python
|
||||
def extract(op2_path: str, fem_path: str, params: dict, subcase_id: int = 1) -> dict:
|
||||
from pyNastran.op2.op2 import OP2
|
||||
op2 = OP2()
|
||||
op2.read_op2(op2_path)
|
||||
# Access: op2.displacements[subcase_id], op2.cquad4_stress[subcase_id], etc.
|
||||
return {"max_stress": value}
|
||||
```
|
||||
|
||||
Common patterns:
|
||||
- Displacement: op2.displacements[subcase_id].data[0, :, 1:4] (x,y,z components)
|
||||
## Option 2: Expression/Computed Values (no FEA needed) - Use for dimensions, volumes, derived values
|
||||
```python
|
||||
def extract(trial_dir: str, config: dict, context: dict) -> dict:
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# Read mass properties (if available from model introspection)
|
||||
mass_file = Path(trial_dir) / "mass_properties.json"
|
||||
if mass_file.exists():
|
||||
with open(mass_file) as f:
|
||||
props = json.load(f)
|
||||
mass = props.get("mass_kg", 0)
|
||||
|
||||
# Or use config values directly (e.g., expression values)
|
||||
length_mm = config.get("length_expression", 100)
|
||||
|
||||
# context has results from other extractors
|
||||
other_value = context.get("other_extractor_output", 0)
|
||||
|
||||
return {"computed_value": length_mm * 2}
|
||||
```
|
||||
|
||||
Available imports: pyNastran.op2.op2.OP2, numpy, pathlib.Path, json
|
||||
|
||||
Common OP2 patterns:
|
||||
- Displacement: op2.displacements[subcase_id].data[0, :, 1:4] (x,y,z)
|
||||
- Stress: op2.cquad4_stress[subcase_id] or op2.ctria3_stress[subcase_id]
|
||||
- Eigenvalues: op2.eigenvalues[subcase_id]
|
||||
- Mass: op2.grid_point_weight (if available)
|
||||
|
||||
Return ONLY the complete Python code wrapped in ```python ... ```. No explanations outside the code block."""
|
||||
Return ONLY the complete Python code wrapped in ```python ... ```. No explanations."""
|
||||
|
||||
# Build user prompt with context
|
||||
user_prompt = f"Generate a custom extractor that: {request.prompt}"
|
||||
|
||||
Binary file not shown.
@@ -104,18 +104,24 @@ function getDefaultNodeData(type: AddableNodeType, position: { x: number; y: num
|
||||
name: `objective_${timestamp}`,
|
||||
direction: 'minimize',
|
||||
weight: 1.0,
|
||||
source_extractor_id: null,
|
||||
source_output: null,
|
||||
// Source is required - use placeholder that user must configure
|
||||
source: {
|
||||
extractor_id: 'ext_001', // Placeholder - user needs to configure
|
||||
output_name: 'value',
|
||||
},
|
||||
canvas_position: position,
|
||||
};
|
||||
case 'constraint':
|
||||
return {
|
||||
name: `constraint_${timestamp}`,
|
||||
constraint_type: 'hard', // Must be 'hard' or 'soft'
|
||||
type: 'hard', // Must be 'hard' or 'soft' (field is 'type' not 'constraint_type')
|
||||
operator: '<=',
|
||||
limit: 1.0,
|
||||
source_extractor_id: null,
|
||||
source_output: null,
|
||||
threshold: 1.0, // Field is 'threshold' not 'limit'
|
||||
// Source is required
|
||||
source: {
|
||||
extractor_id: 'ext_001', // Placeholder - user needs to configure
|
||||
output_name: 'value',
|
||||
},
|
||||
enabled: true,
|
||||
canvas_position: position,
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useUndoRedo, UndoRedoResult } from './useUndoRedo';
|
||||
import { useSpecStore, useSpec, useSpecIsDirty } from './useSpecStore';
|
||||
import { useSpecStore, useSpec } from './useSpecStore';
|
||||
import { AtomizerSpec } from '../types/atomizer-spec';
|
||||
|
||||
const STORAGE_KEY_PREFIX = 'atomizer-spec-history-';
|
||||
@@ -28,7 +28,6 @@ export interface SpecUndoRedoResult extends UndoRedoResult<AtomizerSpec | null>
|
||||
|
||||
export function useSpecUndoRedo(): SpecUndoRedoResult {
|
||||
const spec = useSpec();
|
||||
const isDirty = useSpecIsDirty();
|
||||
const studyId = useSpecStore((state) => state.studyId);
|
||||
const lastSpecRef = useRef<AtomizerSpec | null>(null);
|
||||
|
||||
@@ -56,13 +55,21 @@ export function useSpecUndoRedo(): SpecUndoRedoResult {
|
||||
},
|
||||
});
|
||||
|
||||
// Record snapshot when spec changes (and is dirty)
|
||||
// Record snapshot when spec changes
|
||||
// Note: We removed the isDirty check because with auto-save, isDirty is always false
|
||||
// after the API call completes. Instead, we compare the spec directly.
|
||||
useEffect(() => {
|
||||
if (spec && isDirty && spec !== lastSpecRef.current) {
|
||||
lastSpecRef.current = spec;
|
||||
undoRedo.recordSnapshot();
|
||||
if (spec && spec !== lastSpecRef.current) {
|
||||
// Deep compare to avoid recording duplicate snapshots
|
||||
const specStr = JSON.stringify(spec);
|
||||
const lastStr = lastSpecRef.current ? JSON.stringify(lastSpecRef.current) : '';
|
||||
|
||||
if (specStr !== lastStr) {
|
||||
lastSpecRef.current = spec;
|
||||
undoRedo.recordSnapshot();
|
||||
}
|
||||
}
|
||||
}, [spec, isDirty, undoRedo]);
|
||||
}, [spec, undoRedo]);
|
||||
|
||||
// Clear history when study changes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -19,14 +19,14 @@ Atomizer is a structural optimization platform that enables engineers to optimiz
|
||||
|
||||
### Architecture Quality Score: **8.5/10**
|
||||
|
||||
| Aspect | Score | Notes |
|
||||
|--------|-------|-------|
|
||||
| Data Integrity | 9/10 | Single source of truth, hash-based conflict detection |
|
||||
| Type Safety | 9/10 | Pydantic models throughout backend |
|
||||
| Extensibility | 8/10 | Custom extractors, algorithms supported |
|
||||
| Performance | 8/10 | Optimistic updates, WebSocket streaming |
|
||||
| Maintainability | 8/10 | Clear separation of concerns |
|
||||
| Documentation | 7/10 | Good inline docs, needs more high-level guides |
|
||||
| Aspect | Score | Notes |
|
||||
| --------------- | ----- | ----------------------------------------------------- |
|
||||
| Data Integrity | 9/10 | Single source of truth, hash-based conflict detection |
|
||||
| Type Safety | 9/10 | Pydantic models throughout backend |
|
||||
| Extensibility | 8/10 | Custom extractors, algorithms supported |
|
||||
| Performance | 8/10 | Optimistic updates, WebSocket streaming |
|
||||
| Maintainability | 8/10 | Clear separation of concerns |
|
||||
| Documentation | 7/10 | Good inline docs, needs more high-level guides |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"part_file": "ASSY_M1.prt",
|
||||
"part_path": "C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_cost_reduction_lateral\\1_setup\\model\\ASSY_M1.prt",
|
||||
"success": true,
|
||||
"error": null,
|
||||
"expressions": {
|
||||
"user": [
|
||||
{
|
||||
"name": "p7_CircularPattern_pattern_Circular_Dir_count",
|
||||
"value": 3.0,
|
||||
"rhs": "3",
|
||||
"units": null,
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p8_CircularPattern_pattern_Circular_Dir_offset_angle",
|
||||
"value": 120.0,
|
||||
"rhs": "120",
|
||||
"units": "Degrees",
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p66_x",
|
||||
"value": 0.0,
|
||||
"rhs": "0.00000000000",
|
||||
"units": "MilliMeter",
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p68_z",
|
||||
"value": 0.0,
|
||||
"rhs": "0.00000000000",
|
||||
"units": "MilliMeter",
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p67_y",
|
||||
"value": 0.0,
|
||||
"rhs": "0.00000000000",
|
||||
"units": "MilliMeter",
|
||||
"type": "Number"
|
||||
}
|
||||
],
|
||||
"internal": [
|
||||
{
|
||||
"name": "p14",
|
||||
"value": 0.0,
|
||||
"rhs": "0",
|
||||
"units": "Degrees",
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p64",
|
||||
"value": 120.0,
|
||||
"rhs": "120",
|
||||
"units": "Degrees",
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p9",
|
||||
"value": 10.0,
|
||||
"rhs": "10",
|
||||
"units": "MilliMeter",
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p10",
|
||||
"value": 240.0,
|
||||
"rhs": "240",
|
||||
"units": "Degrees",
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p11",
|
||||
"value": 1.0,
|
||||
"rhs": "1",
|
||||
"units": null,
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p12",
|
||||
"value": 10.0,
|
||||
"rhs": "10",
|
||||
"units": "MilliMeter",
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p13",
|
||||
"value": 0.0,
|
||||
"rhs": "0",
|
||||
"units": "MilliMeter",
|
||||
"type": "Number"
|
||||
}
|
||||
],
|
||||
"total_count": 12,
|
||||
"user_count": 5
|
||||
},
|
||||
"mass_properties": {
|
||||
"mass_kg": 0.0,
|
||||
"mass_g": 0.0,
|
||||
"volume_mm3": 0.0,
|
||||
"surface_area_mm2": 0.0,
|
||||
"center_of_gravity_mm": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"num_bodies": 0,
|
||||
"success": false
|
||||
},
|
||||
"materials": {
|
||||
"assigned": [],
|
||||
"available": [],
|
||||
"library": [],
|
||||
"pmm_error": "'NXOpen.Part' object has no attribute 'PhysicalMaterialManager'"
|
||||
},
|
||||
"bodies": {
|
||||
"solid_bodies": [],
|
||||
"sheet_bodies": [],
|
||||
"counts": {
|
||||
"solid": 0,
|
||||
"sheet": 0,
|
||||
"total": 0
|
||||
}
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"title": "NX_Arrangement",
|
||||
"type": "5",
|
||||
"value": "Arrangement 1"
|
||||
},
|
||||
{
|
||||
"title": "NX_ComponentGroup",
|
||||
"type": "5",
|
||||
"value": "AllComponents"
|
||||
},
|
||||
{
|
||||
"title": "NX_ReferenceSet",
|
||||
"type": "5",
|
||||
"value": "Empty"
|
||||
},
|
||||
{
|
||||
"title": "NX_MaterialMissingAssignments",
|
||||
"type": "5",
|
||||
"value": "TRUE"
|
||||
},
|
||||
{
|
||||
"title": "NX_MaterialMultipleAssigned",
|
||||
"type": "5",
|
||||
"value": "FALSE"
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"features": {
|
||||
"total_count": 0,
|
||||
"by_type": {},
|
||||
"first_10": []
|
||||
},
|
||||
"datums": {
|
||||
"planes": [],
|
||||
"csys": [],
|
||||
"axes": []
|
||||
},
|
||||
"units": {
|
||||
"base_units": {
|
||||
"Length": "MilliMeter",
|
||||
"Mass": "Kilogram",
|
||||
"Time": "Second",
|
||||
"Temperature": "Kelvin",
|
||||
"Angle": "Radian",
|
||||
"Area": "SquareMilliMeter",
|
||||
"Volume": "CubicMilliMeter",
|
||||
"Force": "MilliNewton",
|
||||
"Pressure": "PressureMilliNewtonPerSquareMilliMeter"
|
||||
},
|
||||
"system": "Metric (mm)"
|
||||
},
|
||||
"linked_parts": {
|
||||
"loaded_parts": [
|
||||
{
|
||||
"name": "M1_Blank",
|
||||
"path": "C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_cost_reduction_lateral\\1_setup\\model\\M1_Blank.prt",
|
||||
"leaf_name": "M1_Blank"
|
||||
},
|
||||
{
|
||||
"name": "ASSY_M1",
|
||||
"path": "C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_cost_reduction_lateral\\1_setup\\model\\ASSY_M1.prt",
|
||||
"leaf_name": "ASSY_M1"
|
||||
}
|
||||
],
|
||||
"fem_parts": [],
|
||||
"sim_parts": [],
|
||||
"idealized_parts": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"part_file": "ASSY_M1.prt",
|
||||
"part_path": "C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_cost_reduction_lateral\\1_setup\\model\\ASSY_M1.prt",
|
||||
"success": true,
|
||||
"error": null,
|
||||
"expressions": {
|
||||
"user": [
|
||||
{
|
||||
"name": "p7_CircularPattern_pattern_Circular_Dir_count",
|
||||
"value": 3.0,
|
||||
"rhs": "3",
|
||||
"units": null,
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p8_CircularPattern_pattern_Circular_Dir_offset_angle",
|
||||
"value": 120.0,
|
||||
"rhs": "120",
|
||||
"units": "Degrees",
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p66_x",
|
||||
"value": 0.0,
|
||||
"rhs": "0.00000000000",
|
||||
"units": "MilliMeter",
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p68_z",
|
||||
"value": 0.0,
|
||||
"rhs": "0.00000000000",
|
||||
"units": "MilliMeter",
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p67_y",
|
||||
"value": 0.0,
|
||||
"rhs": "0.00000000000",
|
||||
"units": "MilliMeter",
|
||||
"type": "Number"
|
||||
}
|
||||
],
|
||||
"internal": [
|
||||
{
|
||||
"name": "p14",
|
||||
"value": 0.0,
|
||||
"rhs": "0",
|
||||
"units": "Degrees",
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p64",
|
||||
"value": 120.0,
|
||||
"rhs": "120",
|
||||
"units": "Degrees",
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p9",
|
||||
"value": 10.0,
|
||||
"rhs": "10",
|
||||
"units": "MilliMeter",
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p10",
|
||||
"value": 240.0,
|
||||
"rhs": "240",
|
||||
"units": "Degrees",
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p11",
|
||||
"value": 1.0,
|
||||
"rhs": "1",
|
||||
"units": null,
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p12",
|
||||
"value": 10.0,
|
||||
"rhs": "10",
|
||||
"units": "MilliMeter",
|
||||
"type": "Number"
|
||||
},
|
||||
{
|
||||
"name": "p13",
|
||||
"value": 0.0,
|
||||
"rhs": "0",
|
||||
"units": "MilliMeter",
|
||||
"type": "Number"
|
||||
}
|
||||
],
|
||||
"total_count": 12,
|
||||
"user_count": 5
|
||||
},
|
||||
"mass_properties": {
|
||||
"mass_kg": 0.0,
|
||||
"mass_g": 0.0,
|
||||
"volume_mm3": 0.0,
|
||||
"surface_area_mm2": 0.0,
|
||||
"center_of_gravity_mm": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"num_bodies": 0,
|
||||
"success": false
|
||||
},
|
||||
"materials": {
|
||||
"assigned": [],
|
||||
"available": [],
|
||||
"library": [],
|
||||
"pmm_error": "'NXOpen.Part' object has no attribute 'PhysicalMaterialManager'"
|
||||
},
|
||||
"bodies": {
|
||||
"solid_bodies": [],
|
||||
"sheet_bodies": [],
|
||||
"counts": {
|
||||
"solid": 0,
|
||||
"sheet": 0,
|
||||
"total": 0
|
||||
}
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"title": "NX_Arrangement",
|
||||
"type": "5",
|
||||
"value": "Arrangement 1"
|
||||
},
|
||||
{
|
||||
"title": "NX_ComponentGroup",
|
||||
"type": "5",
|
||||
"value": "AllComponents"
|
||||
},
|
||||
{
|
||||
"title": "NX_ReferenceSet",
|
||||
"type": "5",
|
||||
"value": "Empty"
|
||||
},
|
||||
{
|
||||
"title": "NX_MaterialMissingAssignments",
|
||||
"type": "5",
|
||||
"value": "TRUE"
|
||||
},
|
||||
{
|
||||
"title": "NX_MaterialMultipleAssigned",
|
||||
"type": "5",
|
||||
"value": "FALSE"
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"features": {
|
||||
"total_count": 0,
|
||||
"by_type": {},
|
||||
"first_10": []
|
||||
},
|
||||
"datums": {
|
||||
"planes": [],
|
||||
"csys": [],
|
||||
"axes": []
|
||||
},
|
||||
"units": {
|
||||
"base_units": {
|
||||
"Length": "MilliMeter",
|
||||
"Mass": "Kilogram",
|
||||
"Time": "Second",
|
||||
"Temperature": "Kelvin",
|
||||
"Angle": "Radian",
|
||||
"Area": "SquareMilliMeter",
|
||||
"Volume": "CubicMilliMeter",
|
||||
"Force": "MilliNewton",
|
||||
"Pressure": "PressureMilliNewtonPerSquareMilliMeter"
|
||||
},
|
||||
"system": "Metric (mm)"
|
||||
},
|
||||
"linked_parts": {
|
||||
"loaded_parts": [
|
||||
{
|
||||
"name": "M1_Blank",
|
||||
"path": "C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_cost_reduction_lateral\\1_setup\\model\\M1_Blank.prt",
|
||||
"leaf_name": "M1_Blank"
|
||||
},
|
||||
{
|
||||
"name": "ASSY_M1",
|
||||
"path": "C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_cost_reduction_lateral\\1_setup\\model\\ASSY_M1.prt",
|
||||
"leaf_name": "ASSY_M1"
|
||||
}
|
||||
],
|
||||
"fem_parts": [],
|
||||
"sim_parts": [],
|
||||
"idealized_parts": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"blank_backface_angle": 4.0,
|
||||
"lateral_inner_angle": 31.93,
|
||||
"whiffle_p1": 390.0,
|
||||
"whiffle_p2": 135.0,
|
||||
"whiffle_p3": 80.0,
|
||||
"mirror_face_thickness": 15.0,
|
||||
"blank_mass": 66.7514321518352,
|
||||
"lateral_second_row_angle": 10.0,
|
||||
"p1049_x": 0.0,
|
||||
"blank_backface_max_radius": 582.0,
|
||||
"p1051_z": 0.0,
|
||||
"rib_thickness_lataral": 10.0,
|
||||
"hole_count": 10.0,
|
||||
"Pocket_Radius": 10.05,
|
||||
"lateral_closeness": 7.89,
|
||||
"offset_whiffle": 35.0,
|
||||
"inner_circular_rib_dia": 537.86,
|
||||
"whiffle_min": 56.65,
|
||||
"Pattern_p5610": 60.0,
|
||||
"whiffle_triangle_closeness": 69.24,
|
||||
"beam_face_thickness": 20.0,
|
||||
"offset_lateral_support_contact": 5.0,
|
||||
"Pattern_p2656": 360.0,
|
||||
"rib_thickness_lateral_truss": 12.06,
|
||||
"whiffle_max": 8.0,
|
||||
"outer_post_distance": 551.7504884773748,
|
||||
"Pattern_p2653": 3.0,
|
||||
"Pattern_p2654": 120.0,
|
||||
"Pattern_p2883": 3.0,
|
||||
"Pattern_p2884": 120.0,
|
||||
"Pattern_p2886": 360.0,
|
||||
"ribs_circular_thk": 6.81,
|
||||
"support_cone_angle": 0.0,
|
||||
"rib_thickness": 8.07,
|
||||
"rib_lin_1": 60.0,
|
||||
"rib_lin_2": 80.0,
|
||||
"rib_lin_4": 80.0,
|
||||
"in_between_u": 0.5,
|
||||
"beam_half_core_thickness": 20.0,
|
||||
"lateral_middle_pivot": 21.07,
|
||||
"Pattern_p5609": 6.0,
|
||||
"lateral_inner_u": 0.3,
|
||||
"center_thickness": 85.0,
|
||||
"rib_pocket_bottom_radius": 10.0,
|
||||
"lateral_outer_pivot": 8.615999999999998,
|
||||
"Pattern_p5612": 360.0,
|
||||
"Pattern_p3829": 3.0,
|
||||
"Pattern_p3830": 120.0,
|
||||
"Pattern_p3832": 360.0,
|
||||
"p1050_y": 0.0,
|
||||
"holes_diameter": 400.0,
|
||||
"lateral_outer_angle": 10.77,
|
||||
"vertical_support_diameter_seat_offset": 10.0,
|
||||
"lateral_outer_u": 0.8,
|
||||
"vertical_support_seat_depth": 20.0,
|
||||
"lateral_inner_pivot": 9.578999999999997
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"$schema": "Atomizer M1 Mirror Cost Reduction - Lateral Supports Optimization",
|
||||
"study_name": "m1_mirror_cost_reduction_lateral",
|
||||
"study_tag": "CMA-ES-100",
|
||||
"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.",
|
||||
"business_context": {
|
||||
"purpose": "Optimize lateral support geometry using new U-joint parameterization on cost reduction model",
|
||||
"benefit": "Improved lateral support performance with cleaner parameterization",
|
||||
"goal": "Minimize WFE at 40/60 deg and MFG at 90 deg"
|
||||
},
|
||||
"optimization": {
|
||||
"algorithm": "CMA-ES",
|
||||
"n_trials": 100,
|
||||
"n_startup_trials": 0,
|
||||
"sigma0": 0.3,
|
||||
"notes": "CMA-ES is optimal for 5D continuous optimization - fast convergence, robust"
|
||||
},
|
||||
"extraction_method": {
|
||||
"type": "zernike_opd",
|
||||
"class": "ZernikeOPDExtractor",
|
||||
"method": "extract_relative",
|
||||
"inner_radius": 135.75,
|
||||
"description": "OPD-based Zernike with ANNULAR aperture (271.5mm central hole excluded)"
|
||||
},
|
||||
"design_variables": [
|
||||
{
|
||||
"name": "lateral_inner_u",
|
||||
"expression_name": "lateral_inner_u",
|
||||
"min": 0.2,
|
||||
"max": 0.95,
|
||||
"baseline": 0.3,
|
||||
"units": "unitless",
|
||||
"enabled": true,
|
||||
"notes": "U-joint ratio for inner lateral support (replaces lateral_inner_pivot)"
|
||||
},
|
||||
{
|
||||
"name": "lateral_outer_u",
|
||||
"expression_name": "lateral_outer_u",
|
||||
"min": 0.2,
|
||||
"max": 0.95,
|
||||
"baseline": 0.8,
|
||||
"units": "unitless",
|
||||
"enabled": true,
|
||||
"notes": "U-joint ratio for outer lateral support (replaces lateral_outer_pivot)"
|
||||
},
|
||||
{
|
||||
"name": "lateral_middle_pivot",
|
||||
"expression_name": "lateral_middle_pivot",
|
||||
"min": 15.0,
|
||||
"max": 27.0,
|
||||
"baseline": 21.07,
|
||||
"units": "mm",
|
||||
"enabled": true,
|
||||
"notes": "Middle pivot position on lateral support"
|
||||
},
|
||||
{
|
||||
"name": "lateral_inner_angle",
|
||||
"expression_name": "lateral_inner_angle",
|
||||
"min": 25.0,
|
||||
"max": 35.0,
|
||||
"baseline": 31.93,
|
||||
"units": "degrees",
|
||||
"enabled": true,
|
||||
"notes": "Inner lateral support angle"
|
||||
},
|
||||
{
|
||||
"name": "lateral_outer_angle",
|
||||
"expression_name": "lateral_outer_angle",
|
||||
"min": 8.0,
|
||||
"max": 17.0,
|
||||
"baseline": 10.77,
|
||||
"units": "degrees",
|
||||
"enabled": true,
|
||||
"notes": "Outer lateral support angle"
|
||||
}
|
||||
],
|
||||
"fixed_parameters": [],
|
||||
"constraints": [
|
||||
{
|
||||
"name": "blank_mass_max",
|
||||
"type": "hard",
|
||||
"expression": "mass_kg <= 120.0",
|
||||
"description": "Maximum blank mass constraint (still enforced even though mass not optimized)",
|
||||
"penalty_weight": 1000.0
|
||||
}
|
||||
],
|
||||
"objectives": [
|
||||
{
|
||||
"name": "wfe_40_20",
|
||||
"description": "Filtered RMS WFE at 40 deg relative to 20 deg (annular)",
|
||||
"direction": "minimize",
|
||||
"weight": 6.0,
|
||||
"target": 4.0,
|
||||
"units": "nm"
|
||||
},
|
||||
{
|
||||
"name": "wfe_60_20",
|
||||
"description": "Filtered RMS WFE at 60 deg relative to 20 deg (annular)",
|
||||
"direction": "minimize",
|
||||
"weight": 5.0,
|
||||
"target": 10.0,
|
||||
"units": "nm"
|
||||
},
|
||||
{
|
||||
"name": "mfg_90",
|
||||
"description": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered, annular)",
|
||||
"direction": "minimize",
|
||||
"weight": 3.0,
|
||||
"target": 20.0,
|
||||
"units": "nm"
|
||||
}
|
||||
],
|
||||
"weighted_sum_formula": "6*wfe_40_20 + 5*wfe_60_20 + 3*mfg_90",
|
||||
"zernike_settings": {
|
||||
"n_modes": 50,
|
||||
"filter_low_orders": 4,
|
||||
"displacement_unit": "mm",
|
||||
"subcases": ["1", "2", "3", "4"],
|
||||
"subcase_labels": {"1": "90deg", "2": "20deg", "3": "40deg", "4": "60deg"},
|
||||
"reference_subcase": "2",
|
||||
"method": "opd",
|
||||
"inner_radius": 135.75
|
||||
},
|
||||
"nx_settings": {
|
||||
"nx_install_path": "C:\\Program Files\\Siemens\\DesigncenterNX2512",
|
||||
"sim_file": "ASSY_M1_assyfem1_sim1.sim",
|
||||
"solution_name": "Solution 1",
|
||||
"op2_pattern": "*-solution_1.op2",
|
||||
"simulation_timeout_s": 600
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"study_name": "m1_mirror_cost_reduction_lateral",
|
||||
"algorithm": "CMA-ES",
|
||||
"extraction_method": "ZernikeOPD_Annular",
|
||||
"inner_radius_mm": 135.75,
|
||||
"objectives_note": "Mass NOT in objective - WFE only",
|
||||
"total_trials": 169,
|
||||
"feasible_trials": 167,
|
||||
"best_trial": {
|
||||
"number": 163,
|
||||
"weighted_sum": 181.1220151783071,
|
||||
"objectives": {
|
||||
"wfe_40_20": 5.901179945313834,
|
||||
"wfe_60_20": 13.198682506114679,
|
||||
"mfg_90": 26.573840991950224,
|
||||
"mass_kg": 96.75011491846891
|
||||
},
|
||||
"params": {
|
||||
"lateral_inner_u": 0.32248417341983515,
|
||||
"lateral_outer_u": 0.9038210727913156,
|
||||
"lateral_middle_pivot": 21.25398896032501,
|
||||
"lateral_inner_angle": 30.182447933329243,
|
||||
"lateral_outer_angle": 15.08932828662093
|
||||
},
|
||||
"iter_folder": "C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_cost_reduction_lateral\\2_iterations\\iter164"
|
||||
},
|
||||
"timestamp": "2026-01-14T17:59:38.649254"
|
||||
}
|
||||
Binary file not shown.
136
studies/M1_Mirror/m1_mirror_cost_reduction_lateral/README.md
Normal file
136
studies/M1_Mirror/m1_mirror_cost_reduction_lateral/README.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# M1 Mirror Cost Reduction - Lateral Supports Optimization
|
||||
|
||||
> See [../README.md](../README.md) for project overview and optical specifications.
|
||||
|
||||
## Study Overview
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Study Name** | m1_mirror_cost_reduction_lateral |
|
||||
| **Algorithm** | CMA-ES |
|
||||
| **Status** | Ready to run |
|
||||
| **Created** | 2026-01-13 |
|
||||
| **Trials** | 100 planned |
|
||||
| **Focus** | Lateral support geometry only |
|
||||
|
||||
## Purpose
|
||||
|
||||
Optimize **lateral support parameters only** for the COST REDUCTION model using the new U-joint parameterization:
|
||||
- `lateral_inner_u` and `lateral_outer_u` replace the old `lateral_inner_pivot` and `lateral_outer_pivot`
|
||||
- All other parameters (whiffle, ribs, thickness) are **fixed at baseline values**
|
||||
- **Mass is NOT an objective** - only WFE and MFG are optimized
|
||||
|
||||
## Design Variables (5)
|
||||
|
||||
| Variable | Min | Max | Baseline | Units | Notes |
|
||||
|----------|-----|-----|----------|-------|-------|
|
||||
| `lateral_inner_u` | 0.2 | 0.95 | TBD | unitless | U-joint ratio for inner lateral (NEW) |
|
||||
| `lateral_outer_u` | 0.2 | 0.95 | TBD | unitless | U-joint ratio for outer lateral (NEW) |
|
||||
| `lateral_middle_pivot` | 15.0 | 27.0 | TBD | mm | Middle pivot position |
|
||||
| `lateral_inner_angle` | 25.0 | 35.0 | TBD | degrees | Inner lateral angle |
|
||||
| `lateral_outer_angle` | 8.0 | 17.0 | TBD | degrees | Outer lateral angle |
|
||||
|
||||
**Note:** Baselines marked TBD will be updated after model introspection.
|
||||
|
||||
## Objectives
|
||||
|
||||
| Objective | Weight | Target | Description |
|
||||
|-----------|--------|--------|-------------|
|
||||
| `wfe_40_20` | 6.0 | 4.0 nm | Filtered RMS WFE at 40 deg relative to 20 deg |
|
||||
| `wfe_60_20` | 5.0 | 10.0 nm | Filtered RMS WFE at 60 deg relative to 20 deg |
|
||||
| `mfg_90` | 3.0 | 20.0 nm | Manufacturing deformation at 90 deg polishing |
|
||||
|
||||
**Weighted Sum Formula:** `6*wfe_40_20 + 5*wfe_60_20 + 3*mfg_90`
|
||||
|
||||
**Note:** Mass is **NOT** in the objective function. A hard constraint (mass <= 120 kg) is still enforced.
|
||||
|
||||
## Fixed Parameters (Baked in Model)
|
||||
|
||||
All non-lateral parameters are fixed at the model's current values. These are **not pushed** during optimization - the model already contains the correct values.
|
||||
|
||||
## Why CMA-ES?
|
||||
|
||||
| Factor | This Problem | Why CMA-ES |
|
||||
|--------|--------------|------------|
|
||||
| Dimensions | 5 variables | CMA-ES optimal for 5-50D |
|
||||
| Variable type | All continuous | CMA-ES designed for continuous |
|
||||
| Landscape | Smooth (physics-based) | CMA-ES exploits gradient structure |
|
||||
| Correlation | Lateral params likely correlated | CMA-ES learns correlations automatically |
|
||||
| Convergence | 100 trials budget | CMA-ES converges 2-3x faster than TPE |
|
||||
|
||||
### CMA-ES Settings
|
||||
- `sigma0`: 0.3 (30% of range for initial exploration)
|
||||
- `restart_strategy`: IPOP (restarts with larger population if stuck)
|
||||
- `seed`: 42
|
||||
|
||||
## Model Files
|
||||
|
||||
Place the following files in `1_setup/model/`:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `ASSY_M1_assyfem1_sim1.sim` | Simulation file |
|
||||
| `*.fem` | FEM mesh files |
|
||||
| `*.prt` | Geometry parts |
|
||||
| `*_i.prt` | Idealized part (critical for mesh updates) |
|
||||
|
||||
## Extraction Method
|
||||
|
||||
- **Type:** ZernikeOPDExtractor with ANNULAR aperture
|
||||
- **Inner radius:** 135.75 mm (271.5 mm central hole excluded)
|
||||
- **Zernike modes:** 50
|
||||
- **Filter orders:** J1-J4 removed for WFE, J1-J3 for MFG
|
||||
- **Subcases:** 90 deg (1), 20 deg (2), 40 deg (3), 60 deg (4)
|
||||
- **Reference:** 20 deg (subcase 2)
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Single test trial
|
||||
python run_optimization.py --test
|
||||
|
||||
# Full optimization (100 trials) - auto-launches dashboard
|
||||
python run_optimization.py --start
|
||||
|
||||
# Custom trial count
|
||||
python run_optimization.py --start --trials 50
|
||||
|
||||
# Resume interrupted run
|
||||
python run_optimization.py --start --resume
|
||||
|
||||
# Without dashboard
|
||||
python run_optimization.py --start --no-dashboard
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
m1_mirror_cost_reduction_lateral/
|
||||
|-- 1_setup/
|
||||
| |-- model/ # NX model files (user to add)
|
||||
| `-- optimization_config.json # Study configuration
|
||||
|-- 2_iterations/ # FEA iteration folders
|
||||
|-- 3_results/ # Results database & summaries
|
||||
| |-- study.db # Optuna SQLite database
|
||||
| |-- optimization.log # Run log
|
||||
| `-- optimization_summary.json # Final results
|
||||
|-- run_optimization.py # Main optimization script
|
||||
|-- README.md # This file
|
||||
`-- STUDY_REPORT.md # Results template
|
||||
```
|
||||
|
||||
## Setup Checklist
|
||||
|
||||
- [ ] Copy model files to `1_setup/model/`
|
||||
- [ ] Run introspection to get baseline values
|
||||
- [ ] Update `optimization_config.json` with correct baselines
|
||||
- [ ] Run `--test` to verify setup
|
||||
- [ ] Run full optimization
|
||||
|
||||
## Results
|
||||
|
||||
*Study not yet run. Results will be updated after optimization completes.*
|
||||
|
||||
## References
|
||||
|
||||
- Sister study: [m1_mirror_flatback_lateral](../m1_mirror_flatback_lateral/) (same approach, flat back model)
|
||||
@@ -0,0 +1,126 @@
|
||||
# Study Report: m1_mirror_cost_reduction_lateral
|
||||
|
||||
## Executive Summary
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| **Study Name** | m1_mirror_cost_reduction_lateral |
|
||||
| **Algorithm** | CMA-ES |
|
||||
| **Trials Completed** | _pending_ |
|
||||
| **Best Weighted Sum** | _pending_ |
|
||||
| **Constraint Satisfaction** | _pending_ |
|
||||
|
||||
## Optimization Focus
|
||||
|
||||
This study optimizes **lateral support parameters only** for the COST REDUCTION model using the new U-joint parameterization:
|
||||
- `lateral_inner_u`, `lateral_outer_u` (NEW - replace pivot params)
|
||||
- `lateral_middle_pivot`, `lateral_inner_angle`, `lateral_outer_angle`
|
||||
|
||||
**Key Difference**: Mass is NOT an objective - only WFE and MFG are optimized.
|
||||
|
||||
## Optimization Progress
|
||||
|
||||
_To be filled after optimization run_
|
||||
|
||||
### Convergence Plot
|
||||
|
||||
_Insert convergence plot here_
|
||||
|
||||
### Parameter Evolution
|
||||
|
||||
_Insert parameter evolution plots here_
|
||||
|
||||
## Best Designs Found
|
||||
|
||||
### Top 5 Designs
|
||||
|
||||
| Rank | Trial | WS | WFE 40/20 | WFE 60/20 | MFG 90 | Mass |
|
||||
|------|-------|-------|-----------|-----------|--------|------|
|
||||
| 1 | _pending_ | _pending_ | _pending_ | _pending_ | _pending_ | _pending_ |
|
||||
| 2 | _pending_ | _pending_ | _pending_ | _pending_ | _pending_ | _pending_ |
|
||||
| 3 | _pending_ | _pending_ | _pending_ | _pending_ | _pending_ | _pending_ |
|
||||
| 4 | _pending_ | _pending_ | _pending_ | _pending_ | _pending_ | _pending_ |
|
||||
| 5 | _pending_ | _pending_ | _pending_ | _pending_ | _pending_ | _pending_ |
|
||||
|
||||
### Best Design Parameters
|
||||
|
||||
| Parameter | Baseline | Best | Change |
|
||||
|-----------|----------|------|--------|
|
||||
| lateral_inner_u | _TBD_ | _pending_ | _pending_ |
|
||||
| lateral_outer_u | _TBD_ | _pending_ | _pending_ |
|
||||
| lateral_middle_pivot | _TBD_ | _pending_ | _pending_ |
|
||||
| lateral_inner_angle | _TBD_ | _pending_ | _pending_ |
|
||||
| lateral_outer_angle | _TBD_ | _pending_ | _pending_ |
|
||||
|
||||
## Parameter Sensitivity
|
||||
|
||||
_To be filled after analysis_
|
||||
|
||||
### Most Influential Parameters
|
||||
|
||||
1. _pending_
|
||||
2. _pending_
|
||||
3. _pending_
|
||||
|
||||
### Parameter Correlations
|
||||
|
||||
_Insert correlation analysis_
|
||||
|
||||
## Comparison to Baseline
|
||||
|
||||
| Metric | Baseline | Best | Improvement |
|
||||
|--------|----------|------|-------------|
|
||||
| Weighted Sum | _pending_ | _pending_ | _pending_ |
|
||||
| WFE 40/20 | _pending_ | _pending_ | _pending_ |
|
||||
| WFE 60/20 | _pending_ | _pending_ | _pending_ |
|
||||
| MFG 90 | _pending_ | _pending_ | _pending_ |
|
||||
|
||||
## Comparison to Flat Back Lateral Study
|
||||
|
||||
| Metric | Flat Back | Cost Reduction | Difference |
|
||||
|--------|-----------|----------------|------------|
|
||||
| Best WS | _pending_ | _pending_ | _pending_ |
|
||||
| Best WFE 40/20 | _pending_ | _pending_ | _pending_ |
|
||||
| Best WFE 60/20 | _pending_ | _pending_ | _pending_ |
|
||||
|
||||
## Key Learnings
|
||||
|
||||
_To be filled after analysis_
|
||||
|
||||
1. _pending_
|
||||
2. _pending_
|
||||
3. _pending_
|
||||
|
||||
## Recommendations
|
||||
|
||||
_To be filled after analysis_
|
||||
|
||||
### For Next Study
|
||||
|
||||
- [ ] _pending_
|
||||
|
||||
### For Production
|
||||
|
||||
- [ ] _pending_
|
||||
|
||||
## Appendix
|
||||
|
||||
### Run Configuration
|
||||
|
||||
```json
|
||||
Algorithm: CMA-ES
|
||||
Trials: 100
|
||||
sigma0: 0.3
|
||||
restart_strategy: IPOP
|
||||
```
|
||||
|
||||
### Files Generated
|
||||
|
||||
- `3_results/study.db` - Optuna database
|
||||
- `3_results/optimization.log` - Run log
|
||||
- `3_results/optimization_summary.json` - Final results
|
||||
- `3_results/best_design_archive/` - Archived best designs
|
||||
|
||||
---
|
||||
|
||||
*Report generated: _pending_*
|
||||
@@ -2,9 +2,9 @@
|
||||
"meta": {
|
||||
"version": "2.0",
|
||||
"created": "2026-01-17T15:35:12.024432Z",
|
||||
"modified": "2026-01-20T19:01:36.016065Z",
|
||||
"modified": "2026-01-20T20:05:28.197219Z",
|
||||
"created_by": "migration",
|
||||
"modified_by": "canvas",
|
||||
"modified_by": "test",
|
||||
"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": [
|
||||
@@ -151,6 +151,38 @@
|
||||
"x": 50,
|
||||
"y": 580
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "variable_1768938898079",
|
||||
"expression_name": "expr_1768938898079",
|
||||
"type": "continuous",
|
||||
"bounds": {
|
||||
"min": 0,
|
||||
"max": 1
|
||||
},
|
||||
"baseline": 0.5,
|
||||
"enabled": true,
|
||||
"canvas_position": {
|
||||
"x": -185.06035488622524,
|
||||
"y": 91.62521000204346
|
||||
},
|
||||
"id": "dv_008"
|
||||
},
|
||||
{
|
||||
"name": "test_dv",
|
||||
"expression_name": "test_expr",
|
||||
"type": "continuous",
|
||||
"bounds": {
|
||||
"min": 0,
|
||||
"max": 1
|
||||
},
|
||||
"baseline": 0.5,
|
||||
"enabled": true,
|
||||
"id": "dv_009",
|
||||
"canvas_position": {
|
||||
"x": 50,
|
||||
"y": 680
|
||||
}
|
||||
}
|
||||
],
|
||||
"extractors": [
|
||||
@@ -228,11 +260,11 @@
|
||||
"name": "extract_volume",
|
||||
"module": null,
|
||||
"signature": null,
|
||||
"source_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 * 1000000\n \n return {\n 'volume_m3': volume_m3,\n 'volume_liters': volume_liters\n }\n"
|
||||
"source_code": "\"\"\"Extract modal mass matrix from Nastran OP2 file\"\"\"\n\nfrom pyNastran.op2.op2 import OP2\nimport numpy as np\n\ndef extract(op2_path: str, fem_path: str, params: dict, subcase_id: int = 1) -> dict:\n \"\"\"\n Extract modal mass matrix from Nastran modal analysis results.\n \n The modal mass matrix (generalized mass) is typically diagonal for\n mass-normalized modes, with each diagonal entry representing the\n effective mass participation for that mode.\n \n Returns:\n dict with keys:\n - modal_mass_1, modal_mass_2, modal_mass_3: First three modal masses\n - total_modal_mass: Sum of all modal masses\n - modal_mass_matrix: Full diagonal modal mass array (as list)\n \"\"\"\n op2 = OP2()\n op2.read_op2(op2_path)\n \n # Initialize outputs\n modal_mass_1 = 0.0\n modal_mass_2 = 0.0\n modal_mass_3 = 0.0\n total_modal_mass = 0.0\n modal_mass_matrix = []\n \n try:\n # Method 1: Check for modal participation factors / effective mass\n # This is stored in op2.modal_contribution if available\n if hasattr(op2, 'modal_contribution') and op2.modal_contribution:\n mc = op2.modal_contribution\n if subcase_id in mc:\n modal_data = mc[subcase_id]\n if hasattr(modal_data, 'effective_mass'):\n eff_mass = modal_data.effective_mass\n modal_mass_matrix = eff_mass.tolist() if hasattr(eff_mass, 'tolist') else list(eff_mass)\n \n # Method 2: Check eigenvalues for generalized mass\n # pyNastran stores generalized mass in eigenvalues object\n if not modal_mass_matrix and subcase_id in op2.eigenvalues:\n eig = op2.eigenvalues[subcase_id]\n \n # Generalized mass is typically stored as 'generalized_mass' attribute\n if hasattr(eig, 'generalized_mass'):\n gen_mass = eig.generalized_mass\n modal_mass_matrix = gen_mass.tolist() if hasattr(gen_mass, 'tolist') else list(gen_mass)\n \n # Alternative: mass-normalized modes have unit modal mass\n # Check the mode_cycle attribute for mass normalization info\n elif hasattr(eig, 'mass'):\n mass = eig.mass\n modal_mass_matrix = mass.tolist() if hasattr(mass, 'tolist') else list(mass)\n \n # Method 3: For mass-normalized eigenvectors, modal mass = 1.0\n # Check if eigenvectors exist and compute modal mass from them\n if not modal_mass_matrix and subcase_id in op2.eigenvectors:\n eigvec = op2.eigenvectors[subcase_id]\n n_modes = eigvec.data.shape[1] if len(eigvec.data.shape) > 1 else 1\n # For mass-normalized modes, modal mass is unity\n modal_mass_matrix = [1.0] * n_modes\n \n # Extract individual modal masses\n if modal_mass_matrix:\n if len(modal_mass_matrix) >= 1:\n modal_mass_1 = float(modal_mass_matrix[0])\n if len(modal_mass_matrix) >= 2:\n modal_mass_2 = float(modal_mass_matrix[1])\n if len(modal_mass_matrix) >= 3:\n modal_mass_3 = float(modal_mass_matrix[2])\n total_modal_mass = float(sum(modal_mass_matrix))\n \n except Exception as e:\n # Log error but return zeros gracefully\n print(f\"Warning: Could not extract modal mass matrix: {e}\")\n \n return {\n 'modal_mass_1': modal_mass_1,\n 'modal_mass_2': modal_mass_2,\n 'modal_mass_3': modal_mass_3,\n 'total_modal_mass': total_modal_mass,\n 'modal_mass_matrix': modal_mass_matrix,\n }"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "a1768934465995fsfdadd",
|
||||
"name": "extractor_1768938758443",
|
||||
"type": "custom_function",
|
||||
"builtin": false,
|
||||
"enabled": true,
|
||||
@@ -247,31 +279,31 @@
|
||||
}
|
||||
],
|
||||
"canvas_position": {
|
||||
"x": 661.4703818070815,
|
||||
"y": 655.713625352519
|
||||
},
|
||||
"id": "ext_004"
|
||||
},
|
||||
{
|
||||
"name": "extractor_1768934622682",
|
||||
"type": "custom_function",
|
||||
"builtin": false,
|
||||
"enabled": true,
|
||||
"function": {
|
||||
"name": "extract",
|
||||
"source_code": "def extract(op2_path: str, config: dict = None) -> dict:\n \"\"\"\n Custom extractor function.\n \n Args:\n op2_path: Path to the OP2 results file\n config: Optional configuration dict\n \n Returns:\n Dictionary with extracted values\n \"\"\"\n # TODO: Implement extraction logic\n return {'value': 0.0}\n"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "value",
|
||||
"metric": "custom"
|
||||
}
|
||||
],
|
||||
"canvas_position": {
|
||||
"x": 588.8370255010856,
|
||||
"y": 516.8654070156841
|
||||
"x": 522.740988960073,
|
||||
"y": 560.0208026883463
|
||||
},
|
||||
"id": "ext_005"
|
||||
},
|
||||
{
|
||||
"name": "extractor_1768938897219",
|
||||
"type": "custom_function",
|
||||
"builtin": false,
|
||||
"enabled": true,
|
||||
"function": {
|
||||
"name": "extract",
|
||||
"source_code": "def extract(op2_path: str, config: dict = None) -> dict:\n \"\"\"\n Custom extractor function.\n \n Args:\n op2_path: Path to the OP2 results file\n config: Optional configuration dict\n \n Returns:\n Dictionary with extracted values\n \"\"\"\n # TODO: Implement extraction logic\n return {'value': 0.0}\n"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "value",
|
||||
"metric": "custom"
|
||||
}
|
||||
],
|
||||
"canvas_position": {
|
||||
"x": -197.5451097726711,
|
||||
"y": 262.2501934501369
|
||||
},
|
||||
"id": "ext_004"
|
||||
}
|
||||
],
|
||||
"objectives": [
|
||||
@@ -418,10 +450,6 @@
|
||||
"source": "ext_001",
|
||||
"target": "obj_003"
|
||||
},
|
||||
{
|
||||
"source": "ext_002",
|
||||
"target": "con_001"
|
||||
},
|
||||
{
|
||||
"source": "obj_001",
|
||||
"target": "optimization"
|
||||
@@ -437,6 +465,10 @@
|
||||
{
|
||||
"source": "con_001",
|
||||
"target": "optimization"
|
||||
},
|
||||
{
|
||||
"source": "ext_002",
|
||||
"target": "con_001"
|
||||
}
|
||||
],
|
||||
"layout_version": "2.0"
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Introspect M1_Blank.prt to get current expression values."""
|
||||
import sys
|
||||
import json
|
||||
sys.path.insert(0, 'c:/Users/antoi/Atomizer')
|
||||
|
||||
from optimization_engine.extractors.introspect_part import introspect_part, get_expressions_dict
|
||||
|
||||
MODEL_PATH = 'c:/Users/antoi/Atomizer/studies/M1_Mirror/m1_mirror_cost_reduction_lateral/1_setup/model/M1_Blank.prt'
|
||||
OUTPUT_DIR = 'c:/Users/antoi/Atomizer/studies/M1_Mirror/m1_mirror_cost_reduction_lateral/1_setup/model'
|
||||
|
||||
# Expressions we care about
|
||||
LATERAL_VARS = [
|
||||
'lateral_inner_u',
|
||||
'lateral_outer_u',
|
||||
'lateral_middle_pivot',
|
||||
'lateral_inner_angle',
|
||||
'lateral_outer_angle',
|
||||
'lateral_closeness',
|
||||
]
|
||||
|
||||
print("Running NX introspection on M1_Blank.prt (cost reduction model)...")
|
||||
result = introspect_part(MODEL_PATH, OUTPUT_DIR, verbose=True)
|
||||
|
||||
if result.get('success'):
|
||||
exprs = get_expressions_dict(result)
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("DESIGN VARIABLES (lateral) - current values in model:")
|
||||
print("=" * 60)
|
||||
for name in LATERAL_VARS:
|
||||
if name in exprs:
|
||||
print(f" {name}: {exprs[name]}")
|
||||
else:
|
||||
print(f" {name}: NOT FOUND")
|
||||
|
||||
# Save all expressions to JSON for easy reference
|
||||
output_json = 'c:/Users/antoi/Atomizer/studies/M1_Mirror/m1_mirror_cost_reduction_lateral/1_setup/model_expressions.json'
|
||||
with open(output_json, 'w') as f:
|
||||
json.dump(exprs, f, indent=2)
|
||||
print(f"\nAll expressions saved to: {output_json}")
|
||||
else:
|
||||
print(f"Introspection failed: {result.get('error', 'Unknown error')}")
|
||||
@@ -0,0 +1,695 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
M1 Mirror Cost Reduction Lateral - Lateral Supports Optimization (CMA-ES)
|
||||
==========================================================================
|
||||
|
||||
Lateral support optimization for COST REDUCTION model with new U-joint expressions:
|
||||
- lateral_inner_u (replaces lateral_inner_pivot)
|
||||
- lateral_outer_u (replaces lateral_outer_pivot)
|
||||
- lateral_middle_pivot (unchanged)
|
||||
- lateral_inner_angle (unchanged)
|
||||
- lateral_outer_angle (unchanged)
|
||||
|
||||
Key Features:
|
||||
1. CMA-ES sampler - ideal for 5D continuous optimization
|
||||
2. ANNULAR APERTURE - excludes 271.5mm central hole from Zernike fitting
|
||||
3. Uses ZernikeOPDExtractor.extract_relative() with inner_radius=135.75mm
|
||||
4. Weighted sum: 6*wfe_40_20 + 5*wfe_60_20 + 3*mfg_90 (NO mass objective)
|
||||
5. Hard constraint: mass <= 120 kg (still enforced)
|
||||
|
||||
Usage:
|
||||
python run_optimization.py --start
|
||||
python run_optimization.py --start --trials 100
|
||||
python run_optimization.py --start --trials 100 --resume
|
||||
python run_optimization.py --test # Single trial test
|
||||
|
||||
Author: Atomizer
|
||||
Created: 2026-01-13
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
LICENSE_SERVER = "28000@dalidou;28000@100.80.199.40"
|
||||
os.environ['SPLM_LICENSE_SERVER'] = LICENSE_SERVER
|
||||
print(f"[LICENSE] SPLM_LICENSE_SERVER set to: {LICENSE_SERVER}")
|
||||
|
||||
# Add Atomizer root to path (study is at studies/M1_Mirror/study_name/)
|
||||
STUDY_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(STUDY_DIR)))
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
import json
|
||||
import time
|
||||
import argparse
|
||||
import logging
|
||||
import shutil
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Any
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Dashboard Auto-Launch
|
||||
# ============================================================================
|
||||
|
||||
def launch_dashboard():
|
||||
"""Launch the Atomizer dashboard in background."""
|
||||
dashboard_dir = Path(PROJECT_ROOT) / "atomizer-dashboard"
|
||||
start_script = dashboard_dir / "start-dashboard.bat"
|
||||
|
||||
if not start_script.exists():
|
||||
print(f"[DASHBOARD] Warning: start-dashboard.bat not found at {start_script}")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Launch dashboard in background (detached process)
|
||||
subprocess.Popen(
|
||||
["cmd", "/c", "start", "/min", str(start_script)],
|
||||
cwd=str(dashboard_dir),
|
||||
shell=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
print("[DASHBOARD] Launched in background")
|
||||
print("[DASHBOARD] Frontend: http://localhost:5173")
|
||||
print("[DASHBOARD] Backend: http://localhost:8000")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[DASHBOARD] Failed to launch: {e}")
|
||||
return False
|
||||
|
||||
import optuna
|
||||
from optuna.samplers import CmaEsSampler
|
||||
|
||||
# Atomizer imports
|
||||
from optimization_engine.nx.solver import NXSolver
|
||||
from optimization_engine.extractors import ZernikeOPDExtractor # Supports annular apertures
|
||||
|
||||
# ============================================================================
|
||||
# Paths
|
||||
# ============================================================================
|
||||
|
||||
STUDY_DIR = Path(__file__).parent
|
||||
SETUP_DIR = STUDY_DIR / "1_setup"
|
||||
MODEL_DIR = SETUP_DIR / "model"
|
||||
ITERATIONS_DIR = STUDY_DIR / "2_iterations"
|
||||
RESULTS_DIR = STUDY_DIR / "3_results"
|
||||
CONFIG_PATH = SETUP_DIR / "optimization_config.json"
|
||||
|
||||
# Ensure directories exist
|
||||
ITERATIONS_DIR.mkdir(exist_ok=True)
|
||||
RESULTS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# Logging
|
||||
LOG_FILE = RESULTS_DIR / "optimization.log"
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s | %(levelname)-8s | %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout),
|
||||
logging.FileHandler(LOG_FILE, mode='a')
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
with open(CONFIG_PATH) as f:
|
||||
CONFIG = json.load(f)
|
||||
|
||||
STUDY_NAME = CONFIG["study_name"]
|
||||
|
||||
# Objective weights from config (NO MASS!)
|
||||
OBJ_WEIGHTS = {
|
||||
'wfe_40_20': 6.0,
|
||||
'wfe_60_20': 5.0,
|
||||
'mfg_90': 3.0
|
||||
}
|
||||
|
||||
# Hard constraint: blank_mass <= 120kg (still enforced even though not optimizing)
|
||||
MAX_BLANK_MASS_KG = 120.0
|
||||
CONSTRAINT_PENALTY = 1e6
|
||||
|
||||
# ANNULAR APERTURE: 271.5mm central hole diameter -> 135.75mm radius
|
||||
INNER_RADIUS_MM = CONFIG.get('extraction_method', {}).get('inner_radius', 135.75)
|
||||
|
||||
|
||||
def compute_weighted_sum(objectives: Dict[str, float]) -> float:
|
||||
"""Compute weighted sum of objectives (NO MASS!)."""
|
||||
return (OBJ_WEIGHTS['wfe_40_20'] * objectives.get('wfe_40_20', 1000.0) +
|
||||
OBJ_WEIGHTS['wfe_60_20'] * objectives.get('wfe_60_20', 1000.0) +
|
||||
OBJ_WEIGHTS['mfg_90'] * objectives.get('mfg_90', 1000.0))
|
||||
|
||||
|
||||
def check_mass_constraint(mass_kg: float) -> tuple:
|
||||
"""Check if mass constraint is satisfied."""
|
||||
if mass_kg <= MAX_BLANK_MASS_KG:
|
||||
return True, 0.0
|
||||
else:
|
||||
return False, mass_kg - MAX_BLANK_MASS_KG
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# FEA Runner with Annular Zernike Extraction
|
||||
# ============================================================================
|
||||
|
||||
class FEARunner:
|
||||
"""Runs FEA simulations with annular aperture Zernike extraction."""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.config = config
|
||||
self.nx_solver = None
|
||||
self.master_model_dir = MODEL_DIR
|
||||
|
||||
# Get fixed parameter values to apply on every run
|
||||
self.fixed_params = {}
|
||||
for fp in config.get('fixed_parameters', []):
|
||||
self.fixed_params[fp['name']] = fp['value']
|
||||
|
||||
def setup(self):
|
||||
"""Setup NX solver (assumes NX is already running)."""
|
||||
study_name = self.config.get('study_name', 'm1_mirror_flatback_lateral')
|
||||
|
||||
nx_settings = self.config.get('nx_settings', {})
|
||||
nx_install_dir = nx_settings.get('nx_install_path', 'C:\\Program Files\\Siemens\\DesigncenterNX2512')
|
||||
version_match = re.search(r'NX(\d+)|DesigncenterNX(\d+)', nx_install_dir)
|
||||
nastran_version = (version_match.group(1) or version_match.group(2)) if version_match else "2512"
|
||||
|
||||
self.nx_solver = NXSolver(
|
||||
master_model_dir=str(self.master_model_dir),
|
||||
nx_install_dir=nx_install_dir,
|
||||
nastran_version=nastran_version,
|
||||
timeout=nx_settings.get('simulation_timeout_s', 600),
|
||||
use_iteration_folders=True,
|
||||
study_name=study_name
|
||||
)
|
||||
logger.info(f"[NX] Solver ready (Nastran {nastran_version})")
|
||||
|
||||
def run_fea(self, params: Dict[str, float], trial_num: int) -> Optional[Dict]:
|
||||
"""Run FEA and extract objectives using ZernikeOPDExtractor with annular aperture."""
|
||||
if self.nx_solver is None:
|
||||
self.setup()
|
||||
|
||||
logger.info(f" [FEA {trial_num}] Running simulation...")
|
||||
|
||||
# Build expressions: start with fixed params, then add optimization params
|
||||
expressions = {}
|
||||
|
||||
# Fixed parameters
|
||||
for name, value in self.fixed_params.items():
|
||||
expressions[name] = value
|
||||
|
||||
# Optimization variables (the ones we're actually varying)
|
||||
for var in self.config['design_variables']:
|
||||
if var.get('enabled', True) and var['name'] in params:
|
||||
expressions[var['expression_name']] = params[var['name']]
|
||||
|
||||
iter_folder = self.nx_solver.create_iteration_folder(
|
||||
iterations_base_dir=ITERATIONS_DIR,
|
||||
iteration_number=trial_num,
|
||||
expression_updates=expressions
|
||||
)
|
||||
|
||||
try:
|
||||
nx_settings = self.config.get('nx_settings', {})
|
||||
sim_file = iter_folder / nx_settings.get('sim_file', 'ASSY_M1_assyfem1_sim1.sim')
|
||||
|
||||
t_start = time.time()
|
||||
|
||||
result = self.nx_solver.run_simulation(
|
||||
sim_file=sim_file,
|
||||
working_dir=iter_folder,
|
||||
expression_updates=expressions,
|
||||
solution_name=nx_settings.get('solution_name', 'Solution 1'),
|
||||
cleanup=False
|
||||
)
|
||||
|
||||
solve_time = time.time() - t_start
|
||||
|
||||
if not result['success']:
|
||||
logger.error(f" [FEA {trial_num}] Solve failed: {result.get('error')}")
|
||||
return None
|
||||
|
||||
logger.info(f" [FEA {trial_num}] Solved in {solve_time:.1f}s")
|
||||
|
||||
# Extract objectives using ZernikeOPDExtractor with ANNULAR APERTURE
|
||||
op2_path = Path(result['op2_file'])
|
||||
objectives, lateral_diag = self._extract_objectives_annular(op2_path, iter_folder)
|
||||
|
||||
if objectives is None:
|
||||
return None
|
||||
|
||||
# Check constraint
|
||||
mass_kg = objectives['mass_kg']
|
||||
is_feasible, violation = check_mass_constraint(mass_kg)
|
||||
|
||||
if is_feasible:
|
||||
weighted_sum = compute_weighted_sum(objectives)
|
||||
constraint_status = "OK"
|
||||
else:
|
||||
weighted_sum = compute_weighted_sum(objectives) + CONSTRAINT_PENALTY * violation
|
||||
constraint_status = f"VIOLATED (+{violation:.1f}kg)"
|
||||
|
||||
logger.info(f" [FEA {trial_num}] 40-20: {objectives['wfe_40_20']:.2f} nm (Annular OPD)")
|
||||
logger.info(f" [FEA {trial_num}] 60-20: {objectives['wfe_60_20']:.2f} nm (Annular OPD)")
|
||||
logger.info(f" [FEA {trial_num}] Mfg: {objectives['mfg_90']:.2f} nm (Annular OPD)")
|
||||
logger.info(f" [FEA {trial_num}] Mass: {objectives['mass_kg']:.3f} kg [Constraint: {constraint_status}]")
|
||||
logger.info(f" [FEA {trial_num}] Weighted Sum: {weighted_sum:.2f}")
|
||||
|
||||
return {
|
||||
'trial_num': trial_num,
|
||||
'params': params,
|
||||
'objectives': objectives,
|
||||
'weighted_sum': weighted_sum,
|
||||
'is_feasible': is_feasible,
|
||||
'constraint_violation': violation,
|
||||
'source': 'FEA_ZernikeOPD_Annular',
|
||||
'solve_time': solve_time,
|
||||
'iter_folder': str(iter_folder),
|
||||
'lateral_diagnostics': lateral_diag
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f" [FEA {trial_num}] Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def _extract_objectives_annular(self, op2_path: Path, iter_folder: Path) -> tuple:
|
||||
"""
|
||||
Extract objectives using ZernikeOPDExtractor with ANNULAR APERTURE.
|
||||
|
||||
The central hole (271.5mm diameter, inner_radius=135.75mm) is EXCLUDED
|
||||
from Zernike fitting and RMS calculations.
|
||||
"""
|
||||
try:
|
||||
zernike_settings = self.config.get('zernike_settings', {})
|
||||
|
||||
# Create ZernikeOPDExtractor with ANNULAR APERTURE
|
||||
extractor = ZernikeOPDExtractor(
|
||||
op2_path,
|
||||
figure_path=None, # Uses BDF geometry
|
||||
bdf_path=None, # Auto-detected
|
||||
displacement_unit=zernike_settings.get('displacement_unit', 'mm'),
|
||||
n_modes=zernike_settings.get('n_modes', 50),
|
||||
filter_orders=zernike_settings.get('filter_low_orders', 4),
|
||||
inner_radius=INNER_RADIUS_MM # ANNULAR APERTURE!
|
||||
)
|
||||
|
||||
ref = zernike_settings.get('reference_subcase', '2')
|
||||
|
||||
# Extract RELATIVE metrics with annular masking
|
||||
rel_40 = extractor.extract_relative("3", ref) # 40 deg vs 20 deg
|
||||
rel_60 = extractor.extract_relative("4", ref) # 60 deg vs 20 deg
|
||||
rel_90 = extractor.extract_relative("1", ref) # 90 deg vs 20 deg (for MFG)
|
||||
|
||||
# Log annular info
|
||||
if 'obscuration_ratio' in rel_40:
|
||||
logger.info(f" [Annular] Inner R={INNER_RADIUS_MM:.1f}mm, Obscuration={rel_40['obscuration_ratio']*100:.1f}%")
|
||||
logger.info(f" [Annular] Using {rel_40.get('n_annular_nodes', '?')} nodes (excl. central hole)")
|
||||
|
||||
# Extract mass from temp file
|
||||
mass_kg = 0.0
|
||||
mass_file = iter_folder / "_temp_mass.txt"
|
||||
if mass_file.exists():
|
||||
try:
|
||||
with open(mass_file, 'r') as f:
|
||||
mass_kg = float(f.read().strip())
|
||||
except Exception as mass_err:
|
||||
logger.warning(f" Could not read mass file: {mass_err}")
|
||||
|
||||
# Also check _temp_part_properties.json
|
||||
if mass_kg == 0:
|
||||
props_file = iter_folder / "_temp_part_properties.json"
|
||||
if props_file.exists():
|
||||
try:
|
||||
with open(props_file, 'r') as f:
|
||||
props = json.load(f)
|
||||
mass_kg = props.get('mass_kg', 0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
lateral_diag = {
|
||||
'max_um': rel_40.get('max_lateral_displacement_um', 0),
|
||||
'rms_um': rel_40.get('rms_lateral_displacement_um', 0),
|
||||
}
|
||||
|
||||
objectives = {
|
||||
'wfe_40_20': rel_40['relative_filtered_rms_nm'],
|
||||
'wfe_60_20': rel_60['relative_filtered_rms_nm'],
|
||||
'mfg_90': rel_90['relative_rms_filter_j1to3'],
|
||||
'mass_kg': mass_kg
|
||||
}
|
||||
|
||||
return objectives, lateral_diag
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Annular Zernike extraction failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None, {}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CMA-ES Optimizer
|
||||
# ============================================================================
|
||||
|
||||
class CMAESOptimizer:
|
||||
"""CMA-ES optimizer for lateral support parameters."""
|
||||
|
||||
def __init__(self, config: Dict[str, Any], resume: bool = False):
|
||||
self.config = config
|
||||
self.resume = resume
|
||||
self.fea_runner = FEARunner(config)
|
||||
|
||||
# Load design variable bounds (only enabled variables)
|
||||
self.design_vars = {
|
||||
v['name']: {'min': v['min'], 'max': v['max'], 'baseline': v.get('baseline')}
|
||||
for v in config['design_variables']
|
||||
if v.get('enabled', True)
|
||||
}
|
||||
|
||||
# CMA-ES settings
|
||||
opt_settings = config.get('optimization', {})
|
||||
self.sigma0 = opt_settings.get('sigma0', 0.3)
|
||||
self.seed = opt_settings.get('seed', 42)
|
||||
|
||||
# Study
|
||||
self.study_name = config.get('study_name', 'm1_mirror_flatback_lateral')
|
||||
self.db_path = RESULTS_DIR / "study.db"
|
||||
|
||||
# Track best
|
||||
self.best_weighted_sum = float('inf')
|
||||
self.best_trial_info = None
|
||||
|
||||
# Track FEA count
|
||||
self._count_existing_iterations()
|
||||
|
||||
def _count_existing_iterations(self):
|
||||
"""Count existing iteration folders."""
|
||||
self.fea_count = 0
|
||||
if ITERATIONS_DIR.exists():
|
||||
for d in ITERATIONS_DIR.iterdir():
|
||||
if d.is_dir() and d.name.startswith('iter'):
|
||||
try:
|
||||
num = int(d.name.replace('iter', ''))
|
||||
self.fea_count = max(self.fea_count, num)
|
||||
except ValueError:
|
||||
pass
|
||||
logger.info(f"Existing FEA iterations: {self.fea_count}")
|
||||
|
||||
def create_study(self) -> optuna.Study:
|
||||
"""Create or load Optuna study with CMA-ES sampler."""
|
||||
# Get baseline values for x0 (starting point)
|
||||
x0 = {}
|
||||
for name, bounds in self.design_vars.items():
|
||||
x0[name] = bounds['baseline']
|
||||
|
||||
sampler = CmaEsSampler(
|
||||
x0=x0,
|
||||
sigma0=self.sigma0,
|
||||
seed=self.seed,
|
||||
restart_strategy='ipop'
|
||||
)
|
||||
|
||||
storage = f"sqlite:///{self.db_path}"
|
||||
|
||||
if self.resume:
|
||||
try:
|
||||
study = optuna.load_study(
|
||||
study_name=self.study_name,
|
||||
storage=storage,
|
||||
sampler=sampler
|
||||
)
|
||||
logger.info(f"Resumed study with {len(study.trials)} existing trials")
|
||||
|
||||
# Find current best
|
||||
completed = [t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE]
|
||||
feasible = [t for t in completed if t.user_attrs.get('is_feasible', False)]
|
||||
if feasible:
|
||||
best = min(feasible, key=lambda t: t.value if t.value else float('inf'))
|
||||
if best.value:
|
||||
self.best_weighted_sum = best.value
|
||||
logger.info(f"Current best (feasible): {self.best_weighted_sum:.2f}")
|
||||
|
||||
return study
|
||||
except KeyError:
|
||||
logger.info("No existing study found, creating new one")
|
||||
|
||||
study = optuna.create_study(
|
||||
study_name=self.study_name,
|
||||
storage=storage,
|
||||
sampler=sampler,
|
||||
direction="minimize",
|
||||
load_if_exists=True
|
||||
)
|
||||
|
||||
# Enqueue baseline as first trial
|
||||
if len(study.trials) == 0:
|
||||
logger.info("Enqueueing baseline as trial 0...")
|
||||
study.enqueue_trial(x0)
|
||||
|
||||
return study
|
||||
|
||||
def objective(self, trial: optuna.Trial) -> float:
|
||||
"""CMA-ES objective function."""
|
||||
# Sample parameters
|
||||
params = {}
|
||||
for name, bounds in self.design_vars.items():
|
||||
params[name] = trial.suggest_float(name, bounds['min'], bounds['max'])
|
||||
|
||||
# Increment FEA counter
|
||||
self.fea_count += 1
|
||||
iter_num = self.fea_count
|
||||
|
||||
logger.info(f"Trial {trial.number} -> iter{iter_num}")
|
||||
for name, value in params.items():
|
||||
logger.info(f" {name} = {value:.3f}")
|
||||
|
||||
# Run FEA
|
||||
result = self.fea_runner.run_fea(params, iter_num)
|
||||
|
||||
if result is None:
|
||||
trial.set_user_attr('source', 'FEA_FAILED')
|
||||
trial.set_user_attr('iter_num', iter_num)
|
||||
trial.set_user_attr('is_feasible', False)
|
||||
return 1e6
|
||||
|
||||
# Store metadata
|
||||
trial.set_user_attr('source', 'FEA_ZernikeOPD_Annular')
|
||||
trial.set_user_attr('iter_num', iter_num)
|
||||
trial.set_user_attr('iter_folder', result['iter_folder'])
|
||||
trial.set_user_attr('wfe_40_20', result['objectives']['wfe_40_20'])
|
||||
trial.set_user_attr('wfe_60_20', result['objectives']['wfe_60_20'])
|
||||
trial.set_user_attr('mfg_90', result['objectives']['mfg_90'])
|
||||
trial.set_user_attr('mass_kg', result['objectives']['mass_kg'])
|
||||
trial.set_user_attr('solve_time', result['solve_time'])
|
||||
trial.set_user_attr('is_feasible', result['is_feasible'])
|
||||
|
||||
weighted_sum = result['weighted_sum']
|
||||
|
||||
# Check if new best
|
||||
if result['is_feasible'] and weighted_sum < self.best_weighted_sum:
|
||||
logger.info(f" NEW BEST! {weighted_sum:.2f} (was {self.best_weighted_sum:.2f})")
|
||||
self.best_weighted_sum = weighted_sum
|
||||
self.best_trial_info = {
|
||||
'trial_number': trial.number,
|
||||
'iter_num': iter_num,
|
||||
'iter_folder': result['iter_folder'],
|
||||
'weighted_sum': weighted_sum,
|
||||
'objectives': result['objectives'],
|
||||
'params': params
|
||||
}
|
||||
self._archive_best_design()
|
||||
|
||||
return weighted_sum
|
||||
|
||||
def _archive_best_design(self):
|
||||
"""Archive current best design."""
|
||||
if self.best_trial_info is None:
|
||||
return
|
||||
|
||||
try:
|
||||
archive_dir = RESULTS_DIR / "best_design_archive"
|
||||
archive_dir.mkdir(exist_ok=True)
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
dest_dir = archive_dir / timestamp
|
||||
|
||||
src_dir = Path(self.best_trial_info['iter_folder'])
|
||||
if src_dir.exists():
|
||||
shutil.copytree(src_dir, dest_dir)
|
||||
|
||||
info = {
|
||||
'study_name': self.study_name,
|
||||
'trial_number': self.best_trial_info['trial_number'],
|
||||
'iteration_folder': f"iter{self.best_trial_info['iter_num']}",
|
||||
'weighted_sum': self.best_trial_info['weighted_sum'],
|
||||
'objectives': self.best_trial_info['objectives'],
|
||||
'params': self.best_trial_info['params'],
|
||||
'extraction_method': 'ZernikeOPD_Annular (271.5mm central hole excluded)',
|
||||
'inner_radius_mm': INNER_RADIUS_MM,
|
||||
'archived_at': datetime.now().isoformat()
|
||||
}
|
||||
with open(dest_dir / '_archive_info.json', 'w') as f:
|
||||
json.dump(info, f, indent=2)
|
||||
|
||||
logger.info(f" Archived to: {dest_dir.name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not archive best design: {e}")
|
||||
|
||||
def run(self, n_trials: int):
|
||||
"""Run CMA-ES optimization."""
|
||||
study = self.create_study()
|
||||
|
||||
logger.info("=" * 70)
|
||||
logger.info("M1 MIRROR FLAT BACK - LATERAL SUPPORTS OPTIMIZATION (CMA-ES)")
|
||||
logger.info("=" * 70)
|
||||
logger.info("*** ANNULAR APERTURE: 271.5mm central hole EXCLUDED ***")
|
||||
logger.info(f"*** Inner radius: {INNER_RADIUS_MM} mm ***")
|
||||
logger.info(f"Study: {self.study_name}")
|
||||
logger.info("*** OBJECTIVES: WFE only (mass NOT in objective) ***")
|
||||
logger.info(f"Total trials in DB: {len(study.trials)}")
|
||||
logger.info(f"New FEA trials to run: {n_trials}")
|
||||
logger.info(f"Active Design Variables: {len(self.design_vars)}")
|
||||
for name, bounds in self.design_vars.items():
|
||||
baseline = bounds.get('baseline', 'N/A')
|
||||
logger.info(f" - {name}: [{bounds['min']}, {bounds['max']}] (baseline: {baseline})")
|
||||
logger.info(f"CONSTRAINT: blank_mass <= {MAX_BLANK_MASS_KG} kg")
|
||||
logger.info(f"CMA-ES sigma0: {self.sigma0}")
|
||||
logger.info("=" * 70)
|
||||
|
||||
try:
|
||||
study.optimize(
|
||||
self.objective,
|
||||
n_trials=n_trials,
|
||||
show_progress_bar=True,
|
||||
gc_after_trial=True
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Optimization interrupted by user")
|
||||
|
||||
self._report_results(study)
|
||||
return study
|
||||
|
||||
def _report_results(self, study: optuna.Study):
|
||||
"""Report optimization results."""
|
||||
logger.info("\n" + "=" * 70)
|
||||
logger.info("OPTIMIZATION RESULTS (Annular Aperture)")
|
||||
logger.info("=" * 70)
|
||||
|
||||
completed = [t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE and t.value < 1e5]
|
||||
feasible = [t for t in completed if t.user_attrs.get('is_feasible', False)]
|
||||
|
||||
logger.info(f"\nTotal completed: {len(completed)}")
|
||||
logger.info(f"Feasible (mass <= {MAX_BLANK_MASS_KG}kg): {len(feasible)}")
|
||||
|
||||
if not feasible:
|
||||
logger.warning("No feasible trials found!")
|
||||
return
|
||||
|
||||
sorted_trials = sorted(feasible, key=lambda t: t.value)
|
||||
|
||||
print(f"\n{'Trial':>6} | {'WS':>10} | {'40vs20':>10} | {'60vs20':>10} | {'MFG':>10} | {'Mass':>10} | Iter")
|
||||
print("-" * 85)
|
||||
|
||||
for t in sorted_trials[:15]:
|
||||
obj_40 = t.user_attrs.get('wfe_40_20', 0)
|
||||
obj_60 = t.user_attrs.get('wfe_60_20', 0)
|
||||
obj_mfg = t.user_attrs.get('mfg_90', 0)
|
||||
obj_mass = t.user_attrs.get('mass_kg', 0)
|
||||
iter_num = t.user_attrs.get('iter_num', '?')
|
||||
print(f"{t.number:>6} | {t.value:>10.2f} | {obj_40:>10.2f} | {obj_60:>10.2f} | {obj_mfg:>10.2f} | {obj_mass:>10.3f} | iter{iter_num}")
|
||||
|
||||
best = sorted_trials[0]
|
||||
logger.info(f"\nBEST FEASIBLE TRIAL: #{best.number}")
|
||||
logger.info(f" Weighted Sum: {best.value:.2f}")
|
||||
logger.info(f" 40-20: {best.user_attrs.get('wfe_40_20', 0):.2f} nm")
|
||||
logger.info(f" 60-20: {best.user_attrs.get('wfe_60_20', 0):.2f} nm")
|
||||
logger.info(f" MFG: {best.user_attrs.get('mfg_90', 0):.2f} nm")
|
||||
logger.info(f" Mass: {best.user_attrs.get('mass_kg', 0):.3f} kg")
|
||||
logger.info(f"\n Best Lateral Parameters:")
|
||||
for k, v in best.params.items():
|
||||
logger.info(f" {k}: {v:.3f}")
|
||||
|
||||
# Save summary
|
||||
results_summary = {
|
||||
'study_name': self.study_name,
|
||||
'algorithm': 'CMA-ES',
|
||||
'extraction_method': 'ZernikeOPD_Annular',
|
||||
'inner_radius_mm': INNER_RADIUS_MM,
|
||||
'objectives_note': 'Mass NOT in objective - WFE only',
|
||||
'total_trials': len(study.trials),
|
||||
'feasible_trials': len(feasible),
|
||||
'best_trial': {
|
||||
'number': best.number,
|
||||
'weighted_sum': best.value,
|
||||
'objectives': {
|
||||
'wfe_40_20': best.user_attrs.get('wfe_40_20'),
|
||||
'wfe_60_20': best.user_attrs.get('wfe_60_20'),
|
||||
'mfg_90': best.user_attrs.get('mfg_90'),
|
||||
'mass_kg': best.user_attrs.get('mass_kg')
|
||||
},
|
||||
'params': dict(best.params),
|
||||
'iter_folder': best.user_attrs.get('iter_folder')
|
||||
},
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
with open(RESULTS_DIR / 'optimization_summary.json', 'w') as f:
|
||||
json.dump(results_summary, f, indent=2)
|
||||
|
||||
logger.info(f"\nResults saved to: {RESULTS_DIR / 'optimization_summary.json'}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="M1 Mirror Cost Reduction Lateral - Lateral Supports Optimization")
|
||||
parser.add_argument("--start", action="store_true", help="Start optimization")
|
||||
parser.add_argument("--trials", type=int, default=100, help="Number of FEA trials")
|
||||
parser.add_argument("--resume", action="store_true", help="Resume interrupted run")
|
||||
parser.add_argument("--test", action="store_true", help="Run single test trial")
|
||||
parser.add_argument("--no-dashboard", action="store_true", help="Don't auto-launch dashboard")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.start and not args.test:
|
||||
parser.print_help()
|
||||
print("\nUse --start to begin optimization or --test for single trial")
|
||||
print("\n*** Optimizing LATERAL SUPPORT parameters only ***")
|
||||
print("*** Design Variables: lateral_inner_u, lateral_outer_u, lateral_middle_pivot, ***")
|
||||
print("*** lateral_inner_angle, lateral_outer_angle ***")
|
||||
print("*** Objectives: WFE only (mass NOT included) ***")
|
||||
print("*** Using ANNULAR APERTURE - central hole excluded from Zernike fitting ***")
|
||||
return
|
||||
|
||||
if not CONFIG_PATH.exists():
|
||||
print(f"Error: Config not found at {CONFIG_PATH}")
|
||||
sys.exit(1)
|
||||
|
||||
# Auto-launch dashboard (unless disabled)
|
||||
if not args.no_dashboard:
|
||||
launch_dashboard()
|
||||
time.sleep(2) # Give dashboard time to start
|
||||
|
||||
with open(CONFIG_PATH) as f:
|
||||
config = json.load(f)
|
||||
|
||||
optimizer = CMAESOptimizer(config, resume=args.resume)
|
||||
|
||||
n_trials = 1 if args.test else args.trials
|
||||
optimizer.run(n_trials=n_trials)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -4,25 +4,25 @@
|
||||
"extraction_method": "ZernikeOPD_Annular",
|
||||
"inner_radius_mm": 135.75,
|
||||
"objectives_note": "Mass NOT in objective - WFE only",
|
||||
"total_trials": 1,
|
||||
"feasible_trials": 1,
|
||||
"total_trials": 101,
|
||||
"feasible_trials": 100,
|
||||
"best_trial": {
|
||||
"number": 0,
|
||||
"weighted_sum": 341.40717511411987,
|
||||
"number": 76,
|
||||
"weighted_sum": 220.12317796085603,
|
||||
"objectives": {
|
||||
"wfe_40_20": 9.738648075724171,
|
||||
"wfe_60_20": 24.138392317227122,
|
||||
"mfg_90": 54.09444169121308,
|
||||
"mass_kg": 102.89579477048632
|
||||
"wfe_40_20": 7.033921022459454,
|
||||
"wfe_60_20": 16.109562572565014,
|
||||
"mfg_90": 32.457279654424745,
|
||||
"mass_kg": 102.89579477048622
|
||||
},
|
||||
"params": {
|
||||
"lateral_inner_u": 0.4,
|
||||
"lateral_outer_u": 0.4,
|
||||
"lateral_middle_pivot": 22.42,
|
||||
"lateral_inner_angle": 31.96,
|
||||
"lateral_outer_angle": 9.08
|
||||
"lateral_inner_u": 0.40304412850085514,
|
||||
"lateral_outer_u": 0.9043062289622721,
|
||||
"lateral_middle_pivot": 25.869245488671304,
|
||||
"lateral_inner_angle": 32.008659765295675,
|
||||
"lateral_outer_angle": 13.952742709877848
|
||||
},
|
||||
"iter_folder": "C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_flatback_lateral\\2_iterations\\iter1"
|
||||
"iter_folder": "C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_flatback_lateral\\2_iterations\\iter77"
|
||||
},
|
||||
"timestamp": "2026-01-13T11:01:22.360549"
|
||||
"timestamp": "2026-01-13T18:41:14.992549"
|
||||
}
|
||||
Binary file not shown.
@@ -2,9 +2,9 @@
|
||||
"meta": {
|
||||
"version": "2.0",
|
||||
"created": "2026-01-17T15:35:12.034330Z",
|
||||
"modified": "2026-01-17T15:35:12.034330Z",
|
||||
"modified": "2026-01-20T18:24:29.805432Z",
|
||||
"created_by": "migration",
|
||||
"modified_by": "migration",
|
||||
"modified_by": "canvas",
|
||||
"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": [
|
||||
@@ -104,10 +104,10 @@
|
||||
"expression_name": "lateral_outer_angle",
|
||||
"type": "continuous",
|
||||
"bounds": {
|
||||
"min": 8.0,
|
||||
"max": 17.0
|
||||
"min": 8,
|
||||
"max": 17
|
||||
},
|
||||
"baseline": 9.08,
|
||||
"baseline": 10,
|
||||
"units": "degrees",
|
||||
"enabled": true,
|
||||
"description": "Outer lateral support angle",
|
||||
|
||||
BIN
studies/new_study/1_model/Beam_sim1.sim
Normal file
BIN
studies/new_study/1_model/Beam_sim1.sim
Normal file
Binary file not shown.
BIN
studies/new_study/1_model/Bracket_sim1.sim
Normal file
BIN
studies/new_study/1_model/Bracket_sim1.sim
Normal file
Binary file not shown.
Reference in New Issue
Block a user