diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ff837866..2d186066 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -47,7 +47,65 @@ "Bash(cmd.exe /c:*)", "Bash(powershell.exe -Command:*)", "Bash(where:*)", - "Bash(type %USERPROFILE%.claude*)" + "Bash(type %USERPROFILE%.claude*)", + "Bash(conda create:*)", + "Bash(cmd /c \"conda create -n atomizer python=3.10 -y\")", + "Bash(cmd /c \"where conda\")", + "Bash(cmd /c \"dir /b C:\\Users\\antoi\\anaconda3\\Scripts\\conda.exe 2>nul || dir /b C:\\Users\\antoi\\miniconda3\\Scripts\\conda.exe 2>nul || dir /b C:\\ProgramData\\anaconda3\\Scripts\\conda.exe 2>nul || dir /b C:\\ProgramData\\miniconda3\\Scripts\\conda.exe 2>nul || echo NOT_FOUND\")", + "Bash(cmd /c \"if exist C:\\Users\\antoi\\anaconda3\\Scripts\\conda.exe (echo FOUND: anaconda3) else if exist C:\\Users\\antoi\\miniconda3\\Scripts\\conda.exe (echo FOUND: miniconda3) else if exist C:\\ProgramData\\anaconda3\\Scripts\\conda.exe (echo FOUND: ProgramData\\anaconda3) else (echo NOT_FOUND)\")", + "Bash(powershell:*)", + "Bash(C:Usersantoianaconda3Scriptsconda.exe create -n atomizer python=3.10 -y)", + "Bash(cmd /c \"C:\\Users\\antoi\\anaconda3\\Scripts\\conda.exe create -n atomizer python=3.10 -y\")", + "Bash(cmd /c \"set SPLM_LICENSE_SERVER=28000@dalidou;28000@100.80.199.40 && \"\"C:\\Program Files\\Siemens\\DesigncenterNX2512\\NXBIN\\run_journal.exe\"\" \"\"C:\\Users\\antoi\\Atomizer\\optimization_engine\\solve_simulation.py\"\" -args \"\"C:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_adaptive_V15\\2_iterations\\iter2\\ASSY_M1_assyfem1_sim1.sim\"\" \"\"Solution 1\"\" 2>&1\")", + "Bash(cmd /c \"set SPLM_LICENSE_SERVER=28000@dalidou;28000@100.80.199.40 && \"C:Program FilesSiemensDesigncenterNX2512NXBINrun_journal.exe\" \"C:UsersantoiAtomizernx_journalsextract_part_mass_material.py\" -args \"C:UsersantoiAtomizerstudiesm1_mirror_cost_reduction1_setupmodelM1_Blank.prt\" \"C:UsersantoiAtomizerstudiesm1_mirror_cost_reduction1_setupmodel\" 2>&1\")", + "Bash(npm run dev:*)", + "Bash(cmd /c \"cd /d C:\\Users\\antoi\\Atomizer\\atomizer-dashboard\\frontend && npm run dev\")", + "Bash(cmd /c \"cd /d C:\\Users\\antoi\\Atomizer\\atomizer-dashboard\\frontend && dir package.json && npm --version\")", + "Bash(cmd /c \"set SPLM_LICENSE_SERVER=28000@dalidou;28000@100.80.199.40 && \"\"C:\\Program Files\\Siemens\\DesigncenterNX2512\\NXBIN\\run_journal.exe\"\" \"\"C:\\Users\\antoi\\Atomizer\\nx_journals\\extract_part_mass_material.py\"\" -args \"\"C:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_cost_reduction\\1_setup\\model\\M1_Blank.prt\"\" \"\"C:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_cost_reduction\\1_setup\\model\"\" 2>&1\")", + "Bash(cmd /c \"set SPLM_LICENSE_SERVER=28000@dalidou;28000@100.80.199.40 && \"\"C:\\Program Files\\Siemens\\DesigncenterNX2512\\NXBIN\\run_journal.exe\"\" \"\"C:\\Users\\antoi\\Atomizer\\nx_journals\\extract_expressions.py\"\" -args \"\"C:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_cost_reduction\\1_setup\\model\\M1_Blank.prt\"\" \"\"C:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_cost_reduction\\1_setup\\model\"\" 2>&1\")", + "Bash(cmd /c \"set SPLM_LICENSE_SERVER=28000@dalidou;28000@100.80.199.40 && \"\"C:\\Program Files\\Siemens\\DesigncenterNX2512\\NXBIN\\run_journal.exe\"\" \"\"C:\\Users\\antoi\\Atomizer\\nx_journals\\extract_expressions.py\"\" -args \"\"C:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_cost_reduction\\1_setup\\model\\M1_Blank.prt\"\" \"\"C:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_cost_reduction\\1_setup\\model\"\"\")", + "Bash(cmd /c:*)", + "Bash(taskkill /F /FI \"WINDOWTITLE eq *uvicorn*\")", + "Bash(python -m uvicorn:*)", + "Bash(conda run:*)", + "Bash(/c/Users/antoi/miniconda3/envs/atomizer/python.exe -m uvicorn:*)", + "Bash(/c/Users/antoi/anaconda3/envs/atomizer/python.exe -m uvicorn:*)", + "Bash(/c/Users/antoi/anaconda3/envs/atomizer/python.exe:*)", + "Bash(tasklist:*)", + "Bash(wmic process where \"ProcessId=147068\" delete)", + "Bash(cmd.exe //c \"taskkill /F /PID 147068\")", + "Bash(pip show:*)", + "Bash(python3:*)", + "Bash(python extract_all_mirror_data.py:*)", + "Bash(C:Usersantoiminiconda3envsatomizerpython.exe extract_all_mirror_data.py)", + "Bash(/c/Users/antoi/miniconda3/envs/atomizer/python.exe:*)", + "Bash(grep:*)", + "Bash(python -c:*)", + "Bash(C:Usersantoianaconda3envsatomizerpython.exe -c \"\nimport pandas as pd\ndf = pd.read_csv(r''c:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_all_trials_export.csv'')\n\n# Check which columns have data\nprint(''=== Column data availability ==='')\nfor col in df.columns:\n non_null = df[col].notna().sum()\n print(f''{col}: {non_null}/{len(df)} ({100*non_null/len(df):.1f}%)'')\n\nprint(''\\n=== Studies in dataset ==='')\nprint(df[''study''].value_counts())\n\")", + "Bash(cmd /c \"C:\\Users\\antoi\\anaconda3\\envs\\atomizer\\python.exe -c \"\"import pandas as pd; df = pd.read_csv(r''c:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_all_trials_export.csv''); print(''Rows:'', len(df)); print(df.columns.tolist())\"\"\")", + "Bash(robocopy:*)", + "Bash(xcopy:*)", + "Bash(ls:*)", + "Bash(dir \"c:\\Users\\antoi\\Atomizer\\studies\\*.png\")", + "Bash(powershell -Command \"Get-Process | Where-Object { $_Modules.FileName -like ''*study.db*'' } | Select-Object Id, ProcessName\")", + "Bash(powershell -Command:*)", + "Bash(C:/Users/antoi/miniconda3/envs/atomizer/python.exe -m uvicorn:*)", + "Bash(dir /s /b \"C:\\Users\\antoi\\*conda*\")", + "Bash(conda run -n atomizer python:*)", + "Bash(C:/ProgramData/anaconda3/condabin/conda.bat run -n atomizer python -c \"\nimport sqlite3\n\ndb_path = ''studies/M1_Mirror/m1_mirror_cost_reduction_V6/3_results/study.db''\nconn = sqlite3.connect(db_path)\ncursor = conn.cursor()\n\n# Get counts\ncursor.execute(''SELECT COUNT(*) FROM trials'')\ntotal = cursor.fetchone()[0]\n\ncursor.execute(\"\"SELECT COUNT(*) FROM trials WHERE state = ''COMPLETE''\"\")\ncomplete = cursor.fetchone()[0]\n\nprint(f''=== V6 Study Status ==='')\nprint(f''Total trials: {total}'')\nprint(f''Completed: {complete}'')\nprint(f''Failed/Pruned: {total - complete}'')\nprint(f''Progress: {complete}/200 ({100*complete/200:.1f}%)'')\n\n# Get objectives stats\nobjs = [''rel_filtered_rms_40_vs_20'', ''rel_filtered_rms_60_vs_20'', ''mfg_90_optician_workload'', ''mass_kg'']\nprint(f''\\n=== Objectives Stats ==='')\nfor obj in objs:\n cursor.execute(f\"\"SELECT MIN({obj}), MAX({obj}), AVG({obj}) FROM trials WHERE state = ''COMPLETE'' AND {obj} IS NOT NULL\"\")\n result = cursor.fetchone()\n if result and result[0] is not None:\n print(f''{obj}: min={result[0]:.4f}, max={result[1]:.4f}, mean={result[2]:.4f}'')\n\n# Design variables stats \ndvs = [''whiffle_min'', ''whiffle_outer_to_vertical'', ''whiffle_triangle_closeness'', ''blank_backface_angle'', ''Pocket_Radius'']\nprint(f''\\n=== Design Variables Explored ==='')\nfor dv in dvs:\n try:\n cursor.execute(f\"\"SELECT MIN({dv}), MAX({dv}), AVG({dv}) FROM trials WHERE state = ''COMPLETE''\"\")\n result = cursor.fetchone()\n if result and result[0] is not None:\n print(f''{dv}: min={result[0]:.3f}, max={result[1]:.3f}, mean={result[2]:.3f}'')\n except Exception as e:\n print(f''{dv}: error - {e}'')\n\nconn.close()\n\")", + "Bash(/c/Users/antoi/anaconda3/python.exe:*)", + "Bash(C:UsersantoiAtomizertemp_extract.bat)", + "Bash(dir /b \"C:\\Users\\antoi\\Atomizer\\knowledge_base\\lac\")", + "Bash(pip install:*)", + "Bash(dir \"C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_cost_reduction_V7\\3_results\")", + "Bash(call \"%USERPROFILE%\\anaconda3\\Scripts\\activate.bat\" atomizer)", + "Bash(cmd /c \"cd /d c:\\Users\\antoi\\Atomizer && call %USERPROFILE%\\anaconda3\\Scripts\\activate.bat atomizer && python -c \"\"import sys; sys.path.insert(0, ''.''); from optimization_engine.extractors import ZernikeExtractor; print(''OK''); import inspect; print(inspect.signature(ZernikeExtractor.extract_relative))\"\"\")", + "Bash(cmd /c \"cd /d c:\\Users\\antoi\\Atomizer && c:\\Users\\antoi\\anaconda3\\envs\\atomizer\\python.exe -c \"\"import sys; sys.path.insert(0, ''.''); from optimization_engine.extractors import ZernikeExtractor; print(''Import OK''); import inspect; sig = inspect.signature(ZernikeExtractor.extract_relative); print(''Signature:'', sig)\"\"\")", + "Bash(c:Usersantoianaconda3envsatomizerpython.exe c:UsersantoiAtomizertoolstest_zernike_import.py)", + "Bash(dir \"C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_cost_reduction_V7\\3_results\\best_design_archive\")", + "Bash(dir \"C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_cost_reduction_V7\\3_results\\best_design_archive\\20251220_010128\")", + "Bash(dir /s /b \"C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_cost_reduction_V8\")", + "Bash(c:/Users/antoi/anaconda3/envs/atomizer/python.exe:*)" ], "deny": [], "ask": [] diff --git a/CLAUDE.md b/CLAUDE.md index ab60c764..e95fc9c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,7 @@ On **EVERY new Claude session**, perform these initialization steps: 1. Read `.claude/ATOMIZER_CONTEXT.md` for unified context (if not already loaded via this file) 2. This file (CLAUDE.md) provides system instructions 3. Use `.claude/skills/00_BOOTSTRAP.md` for task routing -4. Check `knowledge_base/lac/` for relevant prior learnings (see LAC section below) +4. **MANDATORY: Read `knowledge_base/lac/session_insights/failure.jsonl`** - Contains critical lessons from past sessions. These are hard-won insights about what NOT to do. ### Step 2: Detect Study Context If working directory is inside a study (`studies/*/`): @@ -27,16 +27,26 @@ If working directory is inside a study (`studies/*/`): 3. Summarize study state to user in first response ### Step 3: Route by User Intent + +**CRITICAL: Actually READ the protocol file before executing the task. Don't work from memory.** + | User Keywords | Load Protocol | Subagent Type | |---------------|---------------|---------------| -| "create", "new", "set up" | OP_01, SYS_12 | general-purpose | -| "run", "start", "trials" | OP_02, SYS_15 | - (direct execution) | +| "create", "new", "set up" | **READ** OP_01 first, then execute | general-purpose | +| "run", "start", "trials" | **READ** OP_02 first | - (direct execution) | | "status", "progress" | OP_03 | - (DB query) | | "results", "analyze", "Pareto" | OP_04 | - (analysis) | | "neural", "surrogate", "turbo" | SYS_14, SYS_15 | general-purpose | | "NX", "model", "expression" | MCP siemens-docs | general-purpose | | "error", "fix", "debug" | OP_06 | Explore | +**Protocol Loading Rule**: When a task matches a protocol (e.g., "create study" → OP_01), you MUST: +1. Read the protocol file (`docs/protocols/operations/OP_01_CREATE_STUDY.md`) +2. Extract the checklist/required outputs +3. Add ALL items to TodoWrite +4. Execute each item +5. Mark complete ONLY when all checklist items are done + ### Step 4: Proactive Actions - If optimization is running: Report progress automatically - If no study context: Offer to create one or list available studies diff --git a/atomizer-dashboard/backend/api/routes/terminal.py b/atomizer-dashboard/backend/api/routes/terminal.py index ee9a7577..7dc719b8 100644 --- a/atomizer-dashboard/backend/api/routes/terminal.py +++ b/atomizer-dashboard/backend/api/routes/terminal.py @@ -20,9 +20,43 @@ router = APIRouter() _terminal_sessions: dict = {} # Path to Atomizer root (for loading prompts) -ATOMIZER_ROOT = os.path.dirname(os.path.dirname(os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -))) +# Go up 5 levels: terminal.py -> routes -> api -> backend -> atomizer-dashboard -> Atomizer +_file_path = os.path.abspath(__file__) +ATOMIZER_ROOT = os.path.normpath(os.path.dirname(os.path.dirname(os.path.dirname( + os.path.dirname(os.path.dirname(_file_path)) +)))) +STUDIES_DIR = os.path.join(ATOMIZER_ROOT, "studies") +# Debug: print the resolved path at module load +print(f"[Terminal] ATOMIZER_ROOT resolved to: {ATOMIZER_ROOT}") + + +def resolve_study_path(study_id: str) -> str: + """Find study folder by scanning topic directories. + + Returns relative path from ATOMIZER_ROOT (e.g., 'studies/M1_Mirror/m1_mirror_adaptive_V14'). + """ + # First check direct path (flat structure) + direct_path = os.path.join(STUDIES_DIR, study_id) + if os.path.isdir(direct_path): + setup_dir = os.path.join(direct_path, "1_setup") + config_file = os.path.join(direct_path, "optimization_config.json") + if os.path.exists(setup_dir) or os.path.exists(config_file): + return f"studies/{study_id}" + + # Scan topic folders for nested structure + if os.path.isdir(STUDIES_DIR): + for topic_name in os.listdir(STUDIES_DIR): + topic_path = os.path.join(STUDIES_DIR, topic_name) + if os.path.isdir(topic_path) and not topic_name.startswith('.'): + study_path = os.path.join(topic_path, study_id) + if os.path.isdir(study_path): + setup_dir = os.path.join(study_path, "1_setup") + config_file = os.path.join(study_path, "optimization_config.json") + if os.path.exists(setup_dir) or os.path.exists(config_file): + return f"studies/{topic_name}/{study_id}" + + # Fallback to flat path + return f"studies/{study_id}" def get_session_prompt(study_name: str = None) -> str: @@ -54,19 +88,21 @@ def get_session_prompt(study_name: str = None) -> str: ] if study_name: + # Resolve actual study path (handles nested folder structure) + study_path = resolve_study_path(study_name) prompt_lines.extend([ f"## Current Study: `{study_name}`", "", - f"**Directory**: `studies/{study_name}/`", + f"**Directory**: `{study_path}/`", "", "Key files:", - f"- `studies/{study_name}/1_setup/optimization_config.json` - Configuration", - f"- `studies/{study_name}/2_results/study.db` - Optuna database", - f"- `studies/{study_name}/README.md` - Study documentation", + f"- `{study_path}/1_setup/optimization_config.json` - Configuration", + f"- `{study_path}/3_results/study.db` - Optuna database", + f"- `{study_path}/README.md` - Study documentation", "", "Quick status check:", "```bash", - f"python -c \"import optuna; s=optuna.load_study('{study_name}', 'sqlite:///studies/{study_name}/2_results/study.db'); print(f'Trials: {{len(s.trials)}}, Best: {{s.best_value}}')\"", + f"python -c \"import optuna; s=optuna.load_study('{study_name}', 'sqlite:///{study_path}/3_results/study.db'); print(f'Trials: {{len(s.trials)}}, Best: {{s.best_value}}')\"", "```", "", ]) @@ -99,8 +135,10 @@ def get_session_prompt(study_name: str = None) -> str: try: from winpty import PtyProcess HAS_WINPTY = True -except ImportError: + print("[Terminal] winpty is available") +except ImportError as e: HAS_WINPTY = False + print(f"[Terminal] winpty not available: {e}") class TerminalSession: @@ -120,6 +158,15 @@ class TerminalSession: self.websocket = websocket self._running = True + # Validate working directory exists + if not os.path.isdir(self.working_dir): + await self.websocket.send_json({ + "type": "error", + "message": f"Working directory does not exist: {self.working_dir}" + }) + self._running = False + return + try: if self._use_winpty: # Use winpty for proper PTY on Windows @@ -143,7 +190,8 @@ class TerminalSession: ) elif sys.platform == "win32": # Fallback: Windows without winpty - use subprocess - # Run claude with --dangerously-skip-permissions for non-interactive mode + import shutil + claude_cmd = shutil.which("claude") or "claude" self.process = subprocess.Popen( ["cmd.exe", "/k", claude_cmd], stdin=subprocess.PIPE, @@ -157,6 +205,8 @@ class TerminalSession: else: # On Unix, use pty import pty + import shutil + claude_cmd = shutil.which("claude") or "claude" master_fd, slave_fd = pty.openpty() self.process = subprocess.Popen( [claude_cmd], @@ -467,6 +517,9 @@ async def get_context(study_id: str = None): """ prompt = get_session_prompt(study_id) + # Resolve study path for nested folder structure + study_path = resolve_study_path(study_id) if study_id else None + return { "study_id": study_id, "prompt": prompt, @@ -476,8 +529,8 @@ async def get_context(study_id: str = None): ".claude/skills/02_CONTEXT_LOADER.md", ], "study_files": [ - f"studies/{study_id}/1_setup/optimization_config.json", - f"studies/{study_id}/2_results/study.db", - f"studies/{study_id}/README.md", - ] if study_id else [] + f"{study_path}/1_setup/optimization_config.json", + f"{study_path}/3_results/study.db", + f"{study_path}/README.md", + ] if study_path else [] } diff --git a/atomizer-dashboard/frontend/src/components/ClaudeTerminal.tsx b/atomizer-dashboard/frontend/src/components/ClaudeTerminal.tsx index f02eb720..16ea03c1 100644 --- a/atomizer-dashboard/frontend/src/components/ClaudeTerminal.tsx +++ b/atomizer-dashboard/frontend/src/components/ClaudeTerminal.tsx @@ -161,15 +161,12 @@ export const ClaudeTerminal: React.FC = ({ setIsConnecting(true); setError(null); - // Always use Atomizer root as working directory so Claude has access to: - // - CLAUDE.md (system instructions) - // - .claude/skills/ (skill definitions) + // Let backend determine the working directory (ATOMIZER_ROOT) // Pass study_id as parameter so we can inform Claude about the context - const workingDir = 'C:/Users/Antoine/Atomizer'; - const studyParam = selectedStudy?.id ? `&study_id=${selectedStudy.id}` : ''; + const studyParam = selectedStudy?.id ? `?study_id=${selectedStudy.id}` : ''; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const ws = new WebSocket(`${protocol}//${window.location.host}/api/terminal/claude?working_dir=${workingDir}${studyParam}`); + const ws = new WebSocket(`${protocol}//${window.location.host}/api/terminal/claude${studyParam}`); ws.onopen = () => { setIsConnected(true); diff --git a/atomizer-dashboard/frontend/src/components/ConvergencePlot.tsx b/atomizer-dashboard/frontend/src/components/ConvergencePlot.tsx index ef08f765..64190a8a 100644 --- a/atomizer-dashboard/frontend/src/components/ConvergencePlot.tsx +++ b/atomizer-dashboard/frontend/src/components/ConvergencePlot.tsx @@ -20,8 +20,13 @@ interface Trial { trial_number: number; values: number[]; state?: string; + constraint_satisfied?: boolean; + user_attrs?: Record; } +// Penalty threshold - objectives above this are considered failed/penalty trials +const PENALTY_THRESHOLD = 100000; + interface ConvergencePlotProps { trials: Trial[]; objectiveIndex?: number; @@ -38,9 +43,22 @@ export function ConvergencePlot({ const convergenceData = useMemo(() => { if (!trials || trials.length === 0) return []; - // Sort by trial number + // Sort by trial number, filtering out failed/penalty trials const sortedTrials = [...trials] - .filter(t => t.values && t.values.length > objectiveIndex && t.state !== 'FAIL') + .filter(t => { + // Must have valid values + if (!t.values || t.values.length <= objectiveIndex) return false; + // Filter out failed state + if (t.state === 'FAIL') return false; + // Filter out penalty values (e.g., 1000000 = solver failure) + const val = t.values[objectiveIndex]; + if (val >= PENALTY_THRESHOLD) return false; + // Filter out constraint violations + if (t.constraint_satisfied === false) return false; + // Filter out pruned trials + if (t.user_attrs?.pruned === true || t.user_attrs?.fail_reason) return false; + return true; + }) .sort((a, b) => a.trial_number - b.trial_number); if (sortedTrials.length === 0) return []; diff --git a/atomizer-dashboard/frontend/src/components/GlobalClaudeTerminal.tsx b/atomizer-dashboard/frontend/src/components/GlobalClaudeTerminal.tsx index b4718d84..7b2cab6f 100644 --- a/atomizer-dashboard/frontend/src/components/GlobalClaudeTerminal.tsx +++ b/atomizer-dashboard/frontend/src/components/GlobalClaudeTerminal.tsx @@ -33,12 +33,14 @@ export const GlobalClaudeTerminal: React.FC = () => { ); } - // Terminal panel + // Terminal panel - responsive sizing + // On mobile portrait: full width with small margins + // On tablet/desktop: fixed size panel return (
= ({ content, className = '' }) => { +export const MarkdownRenderer: React.FC = ({ content, className = '', studyId }) => { + // Helper to resolve image URLs - converts relative paths to API endpoints + const resolveImageSrc = (src: string | undefined): string => { + if (!src) return ''; + + // If it's already an absolute URL or data URL, return as-is + if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('data:')) { + return src; + } + + // If we have a studyId, route through the API + if (studyId) { + // Remove leading ./ or / from the path + const cleanPath = src.replace(/^\.?\//, ''); + return `/api/optimization/studies/${studyId}/image/${cleanPath}`; + } + + // Fallback: return original src + return src; + }; + return (
= ({ content, cla hr: () => (
), - // Images + // Images - resolve relative paths through API img: ({ src, alt }) => ( {alt} { + // Hide broken images + (e.target as HTMLImageElement).style.display = 'none'; + }} /> ), }} diff --git a/atomizer-dashboard/frontend/src/components/layout/MainLayout.tsx b/atomizer-dashboard/frontend/src/components/layout/MainLayout.tsx index c56c3bd3..13dad21a 100644 --- a/atomizer-dashboard/frontend/src/components/layout/MainLayout.tsx +++ b/atomizer-dashboard/frontend/src/components/layout/MainLayout.tsx @@ -5,8 +5,8 @@ export const MainLayout = () => { return (
-
-
+
+
diff --git a/atomizer-dashboard/frontend/src/components/plotly/PlotlyConvergencePlot.tsx b/atomizer-dashboard/frontend/src/components/plotly/PlotlyConvergencePlot.tsx index de9ef3ce..a9120bb1 100644 --- a/atomizer-dashboard/frontend/src/components/plotly/PlotlyConvergencePlot.tsx +++ b/atomizer-dashboard/frontend/src/components/plotly/PlotlyConvergencePlot.tsx @@ -19,8 +19,12 @@ interface Trial { params: Record; user_attrs?: Record; 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; @@ -58,6 +62,15 @@ export function PlotlyConvergencePlot({ 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}
${objectiveName}: ${val.toFixed(4)}
Source: ${source}`; diff --git a/atomizer-dashboard/frontend/src/context/StudyContext.tsx b/atomizer-dashboard/frontend/src/context/StudyContext.tsx index 9dd158b8..49098303 100644 --- a/atomizer-dashboard/frontend/src/context/StudyContext.tsx +++ b/atomizer-dashboard/frontend/src/context/StudyContext.tsx @@ -58,7 +58,9 @@ export const StudyProvider: React.FC<{ children: ReactNode }> = ({ children }) = useEffect(() => { const init = async () => { try { + console.log('[StudyContext] Fetching studies...'); const response = await apiClient.getStudies(); + console.log('[StudyContext] Got studies:', response.studies.length, response.studies); setStudies(response.studies); // Restore last selected study from localStorage @@ -70,8 +72,9 @@ export const StudyProvider: React.FC<{ children: ReactNode }> = ({ children }) = } } } catch (error) { - console.error('Failed to initialize studies:', error); + console.error('[StudyContext] Failed to initialize studies:', error); } finally { + console.log('[StudyContext] Initialization complete, isLoading=false'); setIsLoading(false); setIsInitialized(true); // Mark as initialized AFTER localStorage restoration } diff --git a/atomizer-dashboard/frontend/src/pages/Analysis.tsx b/atomizer-dashboard/frontend/src/pages/Analysis.tsx index 4a317b54..d5293847 100644 --- a/atomizer-dashboard/frontend/src/pages/Analysis.tsx +++ b/atomizer-dashboard/frontend/src/pages/Analysis.tsx @@ -227,7 +227,7 @@ export default function Analysis() { const isMultiObjective = (metadata?.objectives?.length || 0) > 1; return ( -
+
{/* Header */}
diff --git a/atomizer-dashboard/frontend/src/pages/Dashboard.tsx b/atomizer-dashboard/frontend/src/pages/Dashboard.tsx index 7f6ea849..7fdeb76c 100644 --- a/atomizer-dashboard/frontend/src/pages/Dashboard.tsx +++ b/atomizer-dashboard/frontend/src/pages/Dashboard.tsx @@ -375,7 +375,7 @@ export default function Dashboard() { }; return ( -
+
{/* Alerts */}
{alerts.map(alert => ( @@ -436,13 +436,21 @@ export default function Dashboard() { )} diff --git a/atomizer-dashboard/frontend/src/pages/Home.tsx b/atomizer-dashboard/frontend/src/pages/Home.tsx index 97a67b97..d1f63864 100644 --- a/atomizer-dashboard/frontend/src/pages/Home.tsx +++ b/atomizer-dashboard/frontend/src/pages/Home.tsx @@ -10,11 +10,16 @@ import { FileText, ChevronDown, ChevronUp, + ChevronRight, Target, Activity, BarChart3, TrendingUp, - ArrowRight + ArrowRight, + Folder, + FolderOpen, + Maximize2, + X } from 'lucide-react'; import { useStudy } from '../context/StudyContext'; import { Study } from '../types'; @@ -28,8 +33,64 @@ const Home: React.FC = () => { const [readmeLoading, setReadmeLoading] = useState(false); const [sortField, setSortField] = useState<'name' | 'status' | 'trials' | 'bestValue'>('trials'); const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc'); + const [expandedTopics, setExpandedTopics] = useState>(new Set()); + const [isFullscreen, setIsFullscreen] = useState(false); const navigate = useNavigate(); + // Group studies by topic, sorted by most recent first + const studiesByTopic = useMemo(() => { + const grouped: Record = {}; + studies.forEach(study => { + const topic = study.topic || 'Other'; + if (!grouped[topic]) grouped[topic] = []; + grouped[topic].push(study); + }); + + // Sort studies within each topic by last_modified (most recent first) + Object.keys(grouped).forEach(topic => { + grouped[topic].sort((a, b) => { + const aTime = a.last_modified ? new Date(a.last_modified).getTime() : 0; + const bTime = b.last_modified ? new Date(b.last_modified).getTime() : 0; + return bTime - aTime; // Descending (most recent first) + }); + }); + + // Get most recent study time for each topic (for topic sorting) + const topicMostRecent: Record = {}; + Object.keys(grouped).forEach(topic => { + const mostRecent = grouped[topic][0]?.last_modified; + topicMostRecent[topic] = mostRecent ? new Date(mostRecent).getTime() : 0; + }); + + // Sort topics by most recent study (most recent first), 'Other' always last + const sortedTopics = Object.keys(grouped).sort((a, b) => { + if (a === 'Other') return 1; + if (b === 'Other') return -1; + return topicMostRecent[b] - topicMostRecent[a]; // Descending (most recent first) + }); + + const result: Record = {}; + sortedTopics.forEach(topic => { + result[topic] = grouped[topic]; + }); + return result; + }, [studies]); + + // Topics start collapsed by default - no initialization needed + // Users can expand topics by clicking on them + + const toggleTopic = (topic: string) => { + setExpandedTopics(prev => { + const next = new Set(prev); + if (next.has(topic)) { + next.delete(topic); + } else { + next.add(topic); + } + return next; + }); + }; + // Load README when a study is selected for preview useEffect(() => { if (selectedPreview) { @@ -235,113 +296,105 @@ const Home: React.FC = () => {

Create a new study to get started

) : ( -
- - - - - - - - - - - {sortedStudies.map((study) => { - const completionPercent = study.progress.total > 0 - ? Math.round((study.progress.current / study.progress.total) * 100) - : 0; +
+ {Object.entries(studiesByTopic).map(([topic, topicStudies]) => { + const isExpanded = expandedTopics.has(topic); + const topicTrials = topicStudies.reduce((sum, s) => sum + s.progress.current, 0); + const runningCount = topicStudies.filter(s => s.status === 'running').length; - return ( -
setSelectedPreview(study)} - className={`border-b border-dark-700 hover:bg-dark-750 transition-colors cursor-pointer ${ - selectedPreview?.id === study.id ? 'bg-primary-900/20' : '' - }`} - > - - - - - - ); - })} - -
handleSort('name')} - > -
- Study Name - {sortField === 'name' && ( - sortDir === 'asc' ? : - )} -
-
handleSort('status')} - > -
- Status - {sortField === 'status' && ( - sortDir === 'asc' ? : - )} -
-
handleSort('trials')} - > -
- Progress - {sortField === 'trials' && ( - sortDir === 'asc' ? : - )} -
-
handleSort('bestValue')} - > -
- Best - {sortField === 'bestValue' && ( - sortDir === 'asc' ? : - )} -
-
-
- - {study.name || study.id} - - {study.name && ( - {study.id} - )} -
-
- - {getStatusIcon(study.status)} - {study.status} + return ( +
+ {/* Topic Header */} +
-
-
-
= 100 ? 'bg-green-500' : - completionPercent >= 50 ? 'bg-primary-500' : - 'bg-yellow-500' - }`} - style={{ width: `${Math.min(completionPercent, 100)}%` }} - /> + )} +
+
+ {topicTrials.toLocaleString()} trials + {isExpanded ? ( + + ) : ( + + )} +
+ + + {/* Topic Studies */} + {isExpanded && ( +
+ {topicStudies.map((study) => { + const completionPercent = study.progress.total > 0 + ? Math.round((study.progress.current / study.progress.total) * 100) + : 0; + + return ( +
setSelectedPreview(study)} + className={`px-4 py-3 pl-12 flex items-center gap-4 border-t border-dark-700 hover:bg-dark-700 transition-colors cursor-pointer ${ + selectedPreview?.id === study.id ? 'bg-primary-900/20' : '' + }`} + > + {/* Study Name */} +
+ + {study.name || study.id} + + {study.name && ( + {study.id} + )} +
+ + {/* Status */} + + {getStatusIcon(study.status)} + {study.status} + + + {/* Progress */} +
+
+
= 100 ? 'bg-green-500' : + completionPercent >= 50 ? 'bg-primary-500' : + 'bg-yellow-500' + }`} + style={{ width: `${Math.min(completionPercent, 100)}%` }} + /> +
+ + {study.progress.current}/{study.progress.total} + +
+ + {/* Best Value */} + + {study.best_value !== null ? study.best_value.toExponential(2) : 'N/A'} +
- - {study.progress.current}/{study.progress.total} - -
-
- - {study.best_value !== null ? study.best_value.toExponential(3) : 'N/A'} - -
+ ); + })} +
+ )} +
+ ); + })}
)}
@@ -375,21 +428,30 @@ const Home: React.FC = () => {
{/* Study Quick Stats */} -
-
- {getStatusIcon(selectedPreview.status)} - {selectedPreview.status} -
-
- - {selectedPreview.progress.current} / {selectedPreview.progress.total} trials -
- {selectedPreview.best_value !== null && ( -
- - Best: {selectedPreview.best_value.toExponential(4)} +
+
+
+ {getStatusIcon(selectedPreview.status)} + {selectedPreview.status}
- )} +
+ + {selectedPreview.progress.current} / {selectedPreview.progress.total} trials +
+ {selectedPreview.best_value !== null && ( +
+ + Best: {selectedPreview.best_value.toExponential(4)} +
+ )} +
+
{/* README Content */} @@ -400,7 +462,7 @@ const Home: React.FC = () => { Loading documentation...
) : ( - + )}
@@ -416,6 +478,59 @@ const Home: React.FC = () => {
+ + {/* Fullscreen README Modal */} + {isFullscreen && selectedPreview && ( +
+
+ {/* Modal Header */} +
+
+
+ +
+
+

+ {selectedPreview.name || selectedPreview.id} +

+

Study Documentation

+
+
+
+ + +
+
+ + {/* Modal Content */} +
+
+ {readmeLoading ? ( +
+ + Loading documentation... +
+ ) : ( + + )} +
+
+
+
+ )}
); }; diff --git a/atomizer-dashboard/frontend/src/pages/Results.tsx b/atomizer-dashboard/frontend/src/pages/Results.tsx index 219f571d..3874e232 100644 --- a/atomizer-dashboard/frontend/src/pages/Results.tsx +++ b/atomizer-dashboard/frontend/src/pages/Results.tsx @@ -295,7 +295,7 @@ export default function Results() { const visibleParams = showAllParams ? paramEntries : paramEntries.slice(0, 6); return ( -
+
{/* Header */}
diff --git a/atomizer-dashboard/frontend/src/pages/Setup.tsx b/atomizer-dashboard/frontend/src/pages/Setup.tsx index ea279be4..7ebfe68f 100644 --- a/atomizer-dashboard/frontend/src/pages/Setup.tsx +++ b/atomizer-dashboard/frontend/src/pages/Setup.tsx @@ -249,7 +249,7 @@ export default function Setup() { }, 1) || 0; return ( -
+
{/* Header */}
diff --git a/atomizer-dashboard/frontend/src/types/index.ts b/atomizer-dashboard/frontend/src/types/index.ts index c55ee758..928d0848 100644 --- a/atomizer-dashboard/frontend/src/types/index.ts +++ b/atomizer-dashboard/frontend/src/types/index.ts @@ -2,6 +2,7 @@ export interface Study { id: string; name: string; + topic: string | null; // Topic folder name for grouping (e.g., 'M1_Mirror', 'Simple_Bracket') status: 'not_started' | 'running' | 'paused' | 'completed'; progress: { current: number; diff --git a/atomizer-dashboard/frontend/vite.config.ts b/atomizer-dashboard/frontend/vite.config.ts index 248ad643..40c581d6 100644 --- a/atomizer-dashboard/frontend/vite.config.ts +++ b/atomizer-dashboard/frontend/vite.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ strictPort: false, // Allow fallback to next available port proxy: { '/api': { - target: 'http://127.0.0.1:8000', // Use 127.0.0.1 instead of localhost + target: 'http://127.0.0.1:8000', // Backend port changeOrigin: true, secure: false, ws: true, diff --git a/config.py b/config.py index feb802f1..5c1375a6 100644 --- a/config.py +++ b/config.py @@ -14,8 +14,8 @@ import os # NX Installation Directory # Change this to update NX version across entire Atomizer codebase -NX_VERSION = "2506" -NX_INSTALLATION_DIR = Path(f"C:/Program Files/Siemens/NX{NX_VERSION}") +NX_VERSION = "2512" +NX_INSTALLATION_DIR = Path("C:/Program Files/Siemens/DesigncenterNX2512") # Derived NX Paths (automatically updated when NX_VERSION changes) NX_BIN_DIR = NX_INSTALLATION_DIR / "NXBIN" diff --git a/launch_dashboard.py b/launch_dashboard.py index ba28e614..91cf5524 100644 --- a/launch_dashboard.py +++ b/launch_dashboard.py @@ -27,9 +27,9 @@ class Colors: def print_banner(): print(f""" -{Colors.BLUE}{Colors.BOLD}╔═══════════════════════════════════════════╗ -║ ATOMIZER DASHBOARD LAUNCHER ║ -╚═══════════════════════════════════════════╝{Colors.END} +{Colors.BLUE}{Colors.BOLD}============================================ + ATOMIZER DASHBOARD LAUNCHER +============================================{Colors.END} """) def main(): @@ -50,11 +50,12 @@ def main(): processes = [] try: - # Start backend + # Start backend - use conda run to ensure atomizer environment print(f"{Colors.YELLOW}Starting backend server (FastAPI on port 8000)...{Colors.END}") backend_proc = subprocess.Popen( - ["python", "-m", "uvicorn", "api.main:app", "--reload", "--port", "8000"], + ["conda", "run", "-n", "atomizer", "python", "-m", "uvicorn", "api.main:app", "--port", "8000"], cwd=str(backend_dir), + shell=True, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if sys.platform == "win32" else 0 ) processes.append(("Backend", backend_proc))