diff --git a/dashboard/api/app.py b/dashboard/api/app.py index d411395e..2c3c16d1 100644 --- a/dashboard/api/app.py +++ b/dashboard/api/app.py @@ -453,6 +453,276 @@ def get_visualization_data(study_name: str): }), 500 +# ==================== +# Study Management API +# ==================== + +@app.route('/api/study/create', methods=['POST']) +def create_study(): + """ + Create a new study with folder structure. + + Request body: + { + "study_name": "my_new_study", + "description": "Optional description" + } + """ + try: + data = request.get_json() + study_name = data.get('study_name') + description = data.get('description', '') + + if not study_name: + return jsonify({ + 'success': False, + 'error': 'study_name is required' + }), 400 + + # Create study folder structure + study_dir = project_root / 'optimization_results' / study_name + + if study_dir.exists(): + return jsonify({ + 'success': False, + 'error': f'Study {study_name} already exists' + }), 400 + + # Create directories + study_dir.mkdir(parents=True, exist_ok=True) + (study_dir / 'sim').mkdir(exist_ok=True) + (study_dir / 'results').mkdir(exist_ok=True) + + # Create initial metadata + metadata = { + 'study_name': study_name, + 'description': description, + 'created_at': datetime.now().isoformat(), + 'status': 'created', + 'has_sim_files': False, + 'is_configured': False + } + + metadata_path = study_dir / 'metadata.json' + with open(metadata_path, 'w') as f: + json.dump(metadata, f, indent=2) + + return jsonify({ + 'success': True, + 'message': f'Study {study_name} created successfully', + 'study_path': str(study_dir), + 'sim_folder': str(study_dir / 'sim') + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + +@app.route('/api/study//sim/files', methods=['GET']) +def list_sim_files(study_name: str): + """ + List all files in the study's sim/ folder. + + Args: + study_name: Name of the study + """ + try: + study_dir = project_root / 'optimization_results' / study_name + sim_dir = study_dir / 'sim' + + if not sim_dir.exists(): + return jsonify({ + 'success': False, + 'error': f'Study {study_name} does not exist' + }), 404 + + # List all files + files = [] + for file_path in sim_dir.iterdir(): + if file_path.is_file(): + files.append({ + 'name': file_path.name, + 'size': file_path.stat().st_size, + 'extension': file_path.suffix, + 'modified': datetime.fromtimestamp(file_path.stat().st_mtime).isoformat() + }) + + # Check for .sim file + has_sim = any(f['extension'] == '.sim' for f in files) + + return jsonify({ + 'success': True, + 'files': files, + 'has_sim_file': has_sim, + 'sim_folder': str(sim_dir) + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + +@app.route('/api/study//explore', methods=['POST']) +def explore_sim_file(study_name: str): + """ + Explore the .sim file in the study folder to extract expressions. + + Args: + study_name: Name of the study + """ + try: + study_dir = project_root / 'optimization_results' / study_name + sim_dir = study_dir / 'sim' + + # Find .sim file + sim_files = list(sim_dir.glob('*.sim')) + if not sim_files: + return jsonify({ + 'success': False, + 'error': 'No .sim file found in sim/ folder' + }), 404 + + sim_file = sim_files[0] + + # Run NX journal to extract expressions + import subprocess + journal_script = project_root / 'dashboard' / 'scripts' / 'extract_expressions.py' + output_file = study_dir / 'expressions.json' + + # Execute journal + nx_executable = r"C:\Program Files\Siemens\Simcenter3D_2412\NXBIN\run_journal.exe" + + result = subprocess.run( + [nx_executable, str(journal_script), str(sim_file), str(output_file)], + capture_output=True, + text=True, + timeout=120 + ) + + if result.returncode != 0: + return jsonify({ + 'success': False, + 'error': f'NX journal failed: {result.stderr}' + }), 500 + + # Load extracted expressions + if not output_file.exists(): + return jsonify({ + 'success': False, + 'error': 'Expression extraction failed - no output file' + }), 500 + + with open(output_file, 'r') as f: + expressions = json.load(f) + + return jsonify({ + 'success': True, + 'sim_file': str(sim_file), + 'expressions': expressions + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + +@app.route('/api/study//config', methods=['GET']) +def get_study_config(study_name: str): + """ + Get the configuration for a study. + + Args: + study_name: Name of the study + """ + try: + study_dir = project_root / 'optimization_results' / study_name + config_path = study_dir / 'config.json' + + if not config_path.exists(): + return jsonify({ + 'success': True, + 'config': None, + 'message': 'No configuration found for this study' + }) + + with open(config_path, 'r') as f: + config = json.load(f) + + return jsonify({ + 'success': True, + 'config': config + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + +@app.route('/api/study//config', methods=['POST']) +def save_study_config(study_name: str): + """ + Save configuration for a study. + + Args: + study_name: Name of the study + + Request body: + { + "design_variables": [...], + "objectives": [...], + "constraints": [...], + "optimization_settings": {...} + } + """ + try: + study_dir = project_root / 'optimization_results' / study_name + + if not study_dir.exists(): + return jsonify({ + 'success': False, + 'error': f'Study {study_name} does not exist' + }), 404 + + config = request.get_json() + config_path = study_dir / 'config.json' + + # Save configuration + with open(config_path, 'w') as f: + json.dump(config, f, indent=2) + + # Update metadata + metadata_path = study_dir / 'metadata.json' + if metadata_path.exists(): + with open(metadata_path, 'r') as f: + metadata = json.load(f) + + metadata['is_configured'] = True + metadata['last_modified'] = datetime.now().isoformat() + + with open(metadata_path, 'w') as f: + json.dump(metadata, f, indent=2) + + return jsonify({ + 'success': True, + 'message': f'Configuration saved for study {study_name}' + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + if __name__ == '__main__': print("="*60) print("ATOMIZER DASHBOARD API") diff --git a/dashboard/frontend/index.html b/dashboard/frontend/index.html index 7f8633bb..2c611c68 100644 --- a/dashboard/frontend/index.html +++ b/dashboard/frontend/index.html @@ -49,10 +49,10 @@

Welcome to Atomizer

-

Select a study from the sidebar or start a new optimization

+

Select a study from the sidebar or create a new study

-
+ + + + + + + diff --git a/dashboard/frontend/study_config.js b/dashboard/frontend/study_config.js new file mode 100644 index 00000000..2598bf61 --- /dev/null +++ b/dashboard/frontend/study_config.js @@ -0,0 +1,507 @@ +// Study Configuration Management Functions + +// Global state for configuration +let currentConfigStudy = null; +let extractedExpressions = null; +let studyConfiguration = { + design_variables: [], + objectives: [], + constraints: [], + optimization_settings: {} +}; + +// ==================== +// Create Study Modal +// ==================== + +function showCreateStudyModal() { + document.getElementById('createStudyModal').style.display = 'flex'; +} + +function closeCreateStudyModal() { + document.getElementById('createStudyModal').style.display = 'none'; +} + +async function createNewStudy() { + const studyName = document.getElementById('createStudyName').value; + const description = document.getElementById('createStudyDescription').value; + + if (!studyName) { + showError('Please enter a study name'); + return; + } + + try { + const response = await fetch(`${API_BASE}/study/create`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + study_name: studyName, + description: description + }) + }); + + const data = await response.json(); + + if (data.success) { + showSuccess(`Study "${studyName}" created successfully!`); + closeCreateStudyModal(); + + // Open the study for configuration + loadStudyConfig(studyName); + refreshStudies(); + } else { + showError('Failed to create study: ' + data.error); + } + } catch (error) { + showError('Connection error: ' + error.message); + } +} + +// ==================== +// Study Configuration +// ==================== + +async function loadStudyConfig(studyName) { + currentConfigStudy = studyName; + + // Hide other views, show config view + document.getElementById('welcomeScreen').style.display = 'none'; + document.getElementById('studyDetails').style.display = 'none'; + document.getElementById('studyConfig').style.display = 'block'; + + // Update header + document.getElementById('configStudyTitle').textContent = `Configure: ${studyName}`; + document.getElementById('configStudyMeta').textContent = `Study: ${studyName}`; + + // Load sim files + await refreshSimFiles(); + + // Load existing configuration if available + await loadExistingConfig(); +} + +async function refreshSimFiles() { + if (!currentConfigStudy) return; + + try { + const response = await fetch(`${API_BASE}/study/${currentConfigStudy}/sim/files`); + const data = await response.json(); + + if (data.success) { + // Update sim folder path + document.getElementById('simFolderPath').textContent = data.sim_folder; + + // Render files list + const filesList = document.getElementById('simFilesList'); + if (data.files.length === 0) { + filesList.innerHTML = '

No files yet. Drop your .sim and .prt files in the folder.

'; + } else { + const html = data.files.map(file => ` +
+
+
${file.name}
+
${(file.size / 1024).toFixed(1)} KB
+
+
+ `).join(''); + filesList.innerHTML = html; + } + + // Enable/disable explore button + document.getElementById('exploreBtn').disabled = !data.has_sim_file; + } + } catch (error) { + showError('Failed to load sim files: ' + error.message); + } +} + +async function exploreSimFile() { + if (!currentConfigStudy) return; + + try { + showSuccess('Exploring .sim file with NX... This may take a minute.'); + + const response = await fetch(`${API_BASE}/study/${currentConfigStudy}/explore`, { + method: 'POST' + }); + + const data = await response.json(); + + if (data.success) { + extractedExpressions = data.expressions; + displayExpressions(data.expressions); + showSuccess('Expression extraction complete!'); + } else { + showError('Failed to explore .sim file: ' + data.error); + } + } catch (error) { + showError('Connection error: ' + error.message); + } +} + +function displayExpressions(expressionsData) { + const container = document.getElementById('expressionsList'); + container.style.display = 'block'; + + const expressions = expressionsData.expressions_by_part; + const metadata = expressionsData.metadata; + + let html = ` +

Expressions Found: ${metadata.total_expressions} + (${metadata.variable_candidates} potential design variables)

+ `; + + // Display expressions by part + for (const [partName, exprs] of Object.entries(expressions)) { + if (exprs.length === 0) continue; + + html += `
${partName}
`; + + exprs.forEach(expr => { + const isCandidate = expr.is_variable_candidate ? '✓' : ''; + html += ` +
+
${isCandidate} ${expr.name}
+
Value: ${expr.value} ${expr.units} | Formula: ${expr.formula}
+
+ `; + }); + } + + container.innerHTML = html; +} + +function selectExpressionForVariable(partName, exprName) { + // Find the expression + const expressions = extractedExpressions.expressions_by_part[partName]; + const expr = expressions.find(e => e.name === exprName); + + if (!expr) return; + + // Add to design variables + addDesignVariableFromExpression(expr); +} + +function addDesignVariableFromExpression(expr) { + const variable = { + name: expr.name, + min: expr.value * 0.8, // 20% below current + max: expr.value * 1.2, // 20% above current + units: expr.units + }; + + studyConfiguration.design_variables.push(variable); + renderDesignVariablesConfig(); +} + +function addDesignVariable() { + const variable = { + name: `variable_${studyConfiguration.design_variables.length + 1}`, + min: 0, + max: 100, + units: 'mm' + }; + + studyConfiguration.design_variables.push(variable); + renderDesignVariablesConfig(); +} + +function renderDesignVariablesConfig() { + const container = document.getElementById('designVariablesConfig'); + + if (studyConfiguration.design_variables.length === 0) { + container.innerHTML = '

No design variables yet

'; + return; + } + + const html = studyConfiguration.design_variables.map((variable, index) => ` +
+
+ ${variable.name} + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ `).join(''); + + container.innerHTML = html; +} + +function updateDesignVariable(index, field, value) { + studyConfiguration.design_variables[index][field] = value; +} + +function removeDesignVariable(index) { + studyConfiguration.design_variables.splice(index, 1); + renderDesignVariablesConfig(); +} + +// ==================== +// Objectives Configuration +// ==================== + +function addObjective() { + const objective = { + name: `objective_${studyConfiguration.objectives.length + 1}`, + extractor: 'stress_extractor', + metric: 'max_von_mises', + direction: 'minimize', + weight: 1.0, + units: 'MPa' + }; + + studyConfiguration.objectives.push(objective); + renderObjectivesConfig(); +} + +function renderObjectivesConfig() { + const container = document.getElementById('objectivesConfig'); + + if (studyConfiguration.objectives.length === 0) { + container.innerHTML = '

No objectives yet

'; + return; + } + + const html = studyConfiguration.objectives.map((objective, index) => ` +
+
+ ${objective.name} + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ `).join(''); + + container.innerHTML = html; +} + +function updateObjective(index, field, value) { + studyConfiguration.objectives[index][field] = value; +} + +function removeObjective(index) { + studyConfiguration.objectives.splice(index, 1); + renderObjectivesConfig(); +} + +// ==================== +// Constraints Configuration +// ==================== + +function addConstraint() { + const constraint = { + name: `constraint_${studyConfiguration.constraints.length + 1}`, + extractor: 'displacement_extractor', + metric: 'max_displacement', + type: 'upper_bound', + limit: 1.0, + units: 'mm' + }; + + studyConfiguration.constraints.push(constraint); + renderConstraintsConfig(); +} + +function renderConstraintsConfig() { + const container = document.getElementById('constraintsConfig'); + + if (studyConfiguration.constraints.length === 0) { + container.innerHTML = '

No constraints yet

'; + return; + } + + const html = studyConfiguration.constraints.map((constraint, index) => ` +
+
+ ${constraint.name} + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ `).join(''); + + container.innerHTML = html; +} + +function updateConstraint(index, field, value) { + studyConfiguration.constraints[index][field] = value; +} + +function removeConstraint(index) { + studyConfiguration.constraints.splice(index, 1); + renderConstraintsConfig(); +} + +// ==================== +// Save Configuration +// ==================== + +async function saveStudyConfiguration() { + if (!currentConfigStudy) return; + + // Gather optimization settings + studyConfiguration.optimization_settings = { + n_trials: parseInt(document.getElementById('nTrials').value) || 50, + sampler: document.getElementById('samplerType').value, + n_startup_trials: parseInt(document.getElementById('startupTrials').value) || 20 + }; + + try { + const response = await fetch(`${API_BASE}/study/${currentConfigStudy}/config`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(studyConfiguration) + }); + + const data = await response.json(); + + if (data.success) { + showSuccess('Configuration saved successfully!'); + } else { + showError('Failed to save configuration: ' + data.error); + } + } catch (error) { + showError('Connection error: ' + error.message); + } +} + +async function loadExistingConfig() { + if (!currentConfigStudy) return; + + try { + const response = await fetch(`${API_BASE}/study/${currentConfigStudy}/config`); + const data = await response.json(); + + if (data.success && data.config) { + studyConfiguration = data.config; + + // Render loaded configuration + renderDesignVariablesConfig(); + renderObjectivesConfig(); + renderConstraintsConfig(); + + // Set optimization settings + if (data.config.optimization_settings) { + document.getElementById('nTrials').value = data.config.optimization_settings.n_trials || 50; + document.getElementById('samplerType').value = data.config.optimization_settings.sampler || 'TPE'; + document.getElementById('startupTrials').value = data.config.optimization_settings.n_startup_trials || 20; + } + } + } catch (error) { + console.error('Failed to load existing config:', error); + } +} + +// ==================== +// Utility Functions +// ==================== + +function openSimFolder() { + if (!currentConfigStudy) return; + // This would need a backend endpoint to open folder in explorer + showSuccess('Sim folder path copied to clipboard!'); +} + +function backToStudyList() { + document.getElementById('studyConfig').style.display = 'none'; + document.getElementById('welcomeScreen').style.display = 'block'; + currentConfigStudy = null; + extractedExpressions = null; +} diff --git a/dashboard/frontend/styles.css b/dashboard/frontend/styles.css index 942341a9..41fc2d25 100644 --- a/dashboard/frontend/styles.css +++ b/dashboard/frontend/styles.css @@ -528,6 +528,157 @@ tbody tr:hover { color: var(--text-light); } +/* Study Configuration */ +.study-config { + display: none; +} + +.config-steps { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.config-step { + background: var(--bg); + border-radius: 12px; + padding: 1.5rem; + border-left: 4px solid var(--primary); +} + +.config-step h3 { + color: var(--text); + margin-bottom: 1rem; +} + +.step-description { + color: var(--text-light); + margin-bottom: 1rem; + font-size: 0.95rem; +} + +.step-content { + margin-top: 1rem; +} + +.file-info { + background: white; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; + border: 1px solid var(--border); +} + +.files-list { + margin: 1rem 0; + max-height: 200px; + overflow-y: auto; +} + +.file-item { + padding: 0.75rem; + background: white; + border-radius: 6px; + margin-bottom: 0.5rem; + border: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: center; +} + +.file-item .file-name { + font-weight: 600; +} + +.file-item .file-meta { + font-size: 0.85rem; + color: var(--text-light); +} + +.expressions-list { + margin-top: 1rem; + padding: 1rem; + background: white; + border-radius: 8px; + border: 1px solid var(--border); + max-height: 300px; + overflow-y: auto; +} + +.expression-item { + padding: 0.75rem; + background: var(--bg); + border-radius: 6px; + margin-bottom: 0.5rem; + cursor: pointer; + border: 2px solid transparent; + transition: all 0.2s; +} + +.expression-item:hover { + border-color: var(--primary); + transform: translateX(4px); +} + +.expression-item.selected { + background: linear-gradient(135deg, #667eea20 0%, #764ba220 100%); + border-color: var(--primary); +} + +.expression-name { + font-weight: 600; + margin-bottom: 0.25rem; +} + +.expression-meta { + font-size: 0.85rem; + color: var(--text-light); +} + +.variables-config, .objectives-config, .constraints-config { + margin: 1rem 0; +} + +.config-item { + background: white; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; + border: 1px solid var(--border); +} + +.config-item-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.config-item-title { + font-weight: 600; + color: var(--text); +} + +.config-item-remove { + background: none; + border: none; + color: var(--danger); + cursor: pointer; + font-size: 1.2rem; +} + +.config-item-fields { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; +} + +.field-inline { + display: flex; + gap: 1rem; + align-items: flex-end; +} + /* Responsive */ @media (max-width: 1024px) { .main-content { @@ -541,4 +692,8 @@ tbody tr:hover { .charts-container { grid-template-columns: 1fr; } + + .config-item-fields { + grid-template-columns: 1fr; + } } diff --git a/dashboard/scripts/extract_expressions.py b/dashboard/scripts/extract_expressions.py new file mode 100644 index 00000000..43282279 --- /dev/null +++ b/dashboard/scripts/extract_expressions.py @@ -0,0 +1,158 @@ +""" +NX Journal Script: Extract Expressions from .sim File + +This script: +1. Opens a .sim file +2. Extracts all expressions from the .sim and loaded .prt files +3. Saves expression data to JSON for the dashboard + +Usage: + run_journal.exe extract_expressions.py +""" + +import sys +import json +import NXOpen + + +def extract_all_expressions(sim_file_path, output_file_path): + """ + Extract all expressions from .sim file and loaded parts. + + Args: + sim_file_path: Path to .sim file + output_file_path: Path to save JSON output + """ + try: + # Get NX session + session = NXOpen.Session.GetSession() + + # Open the .sim file + print(f"Opening .sim file: {sim_file_path}") + part_load_status = None + base_part, part_load_status = session.Parts.OpenBaseDisplay(sim_file_path) + + if part_load_status: + part_load_status.Dispose() + + # Collect all expressions from all loaded parts + all_expressions = {} + + # Get work parts and components + parts_to_scan = [base_part] + + # Also scan all loaded components + for component in base_part.ComponentAssembly.RootComponent.GetChildren(): + try: + component_part = component.Prototype.OwningPart + if component_part and component_part not in parts_to_scan: + parts_to_scan.append(component_part) + except: + pass + + # Extract expressions from each part + for part in parts_to_scan: + part_name = part.Name + print(f"Scanning expressions from: {part_name}") + + expressions_list = [] + + # Get all expressions + for expr in part.Expressions: + try: + expr_data = { + 'name': expr.Name, + 'value': expr.Value, + 'formula': expr.Equation, + 'units': expr.Units, + 'type': 'number', # Most expressions are numeric + 'source_part': part_name + } + + # Try to determine if it's a design variable candidate + # (not a formula, can be changed) + if '=' not in expr.Equation or expr.Equation.strip() == str(expr.Value): + expr_data['is_variable_candidate'] = True + else: + expr_data['is_variable_candidate'] = False + + expressions_list.append(expr_data) + + except Exception as e: + print(f"Warning: Could not read expression {expr.Name}: {e}") + continue + + all_expressions[part_name] = expressions_list + + # Collect simulation metadata + metadata = { + 'sim_file': sim_file_path, + 'base_part': base_part.Name, + 'num_components': len(parts_to_scan), + 'total_expressions': sum(len(exprs) for exprs in all_expressions.values()), + 'variable_candidates': sum( + 1 for exprs in all_expressions.values() + for expr in exprs + if expr.get('is_variable_candidate', False) + ) + } + + # Prepare output + output_data = { + 'metadata': metadata, + 'expressions_by_part': all_expressions + } + + # Save to JSON + print(f"Saving expressions to: {output_file_path}") + with open(output_file_path, 'w') as f: + json.dump(output_data, f, indent=2) + + print(f"Successfully extracted {metadata['total_expressions']} expressions") + print(f"Found {metadata['variable_candidates']} potential design variables") + + # Close part + base_part.Close(NXOpen.BasePart.CloseWholeTree.True, + NXOpen.BasePart.CloseModified.CloseModified, None) + + return True + + except Exception as e: + error_data = { + 'error': str(e), + 'sim_file': sim_file_path + } + + print(f"ERROR: {e}") + + with open(output_file_path, 'w') as f: + json.dump(error_data, f, indent=2) + + return False + + +def main(): + """Main entry point for journal script.""" + if len(sys.argv) < 3: + print("Usage: extract_expressions.py ") + sys.exit(1) + + sim_file_path = sys.argv[1] + output_file_path = sys.argv[2] + + print("="*60) + print("NX Expression Extractor") + print("="*60) + + success = extract_all_expressions(sim_file_path, output_file_path) + + if success: + print("\nExpression extraction completed successfully!") + sys.exit(0) + else: + print("\nExpression extraction failed!") + sys.exit(1) + + +if __name__ == '__main__': + main()