Files
Atomizer/dashboard/frontend/study_config.js
Anto01 9ddc065d31 feat: Add comprehensive study management system to dashboard
Added full study configuration UI:
- Create studies with isolated folder structure (sim/, results/, config.json)
- File management: users drop .sim/.prt files into study's sim folder
- NX expression extraction: journal script to explore .sim file
- Configuration UI for design variables, objectives, and constraints
- Save/load study configurations through API
- Step-by-step workflow: create → add files → explore → configure → run

Backend API (app.py):
- POST /api/study/create - Create new study with folder structure
- GET /api/study/<name>/sim/files - List files in sim folder
- POST /api/study/<name>/explore - Extract expressions from .sim file
- GET/POST /api/study/<name>/config - Load/save study configuration

Frontend:
- New study configuration view with 5-step wizard
- Modal for creating new studies
- Expression explorer with clickable selection
- Dynamic forms for variables/objectives/constraints
- Professional styling with config cards

NX Integration:
- extract_expressions.py journal script
- Scans .sim and all loaded .prt files
- Identifies potential design variable candidates
- Exports expressions with values, formulas, units

Each study is self-contained with its own geometry files and config.
2025-11-15 14:00:00 -05:00

508 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = '<p class="empty">No files yet. Drop your .sim and .prt files in the folder.</p>';
} else {
const html = data.files.map(file => `
<div class="file-item">
<div>
<div class="file-name">${file.name}</div>
<div class="file-meta">${(file.size / 1024).toFixed(1)} KB</div>
</div>
</div>
`).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 = `
<h4>Expressions Found: ${metadata.total_expressions}
(${metadata.variable_candidates} potential design variables)</h4>
`;
// Display expressions by part
for (const [partName, exprs] of Object.entries(expressions)) {
if (exprs.length === 0) continue;
html += `<h5 style="margin-top: 1rem;">${partName}</h5>`;
exprs.forEach(expr => {
const isCandidate = expr.is_variable_candidate ? '✓' : '';
html += `
<div class="expression-item ${expr.is_variable_candidate ? 'selected' : ''}"
onclick="selectExpressionForVariable('${partName}', '${expr.name}')">
<div class="expression-name">${isCandidate} ${expr.name}</div>
<div class="expression-meta">Value: ${expr.value} ${expr.units} | Formula: ${expr.formula}</div>
</div>
`;
});
}
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 = '<p class="empty">No design variables yet</p>';
return;
}
const html = studyConfiguration.design_variables.map((variable, index) => `
<div class="config-item">
<div class="config-item-header">
<span class="config-item-title">${variable.name}</span>
<button class="config-item-remove" onclick="removeDesignVariable(${index})">×</button>
</div>
<div class="config-item-fields">
<div class="form-group">
<label>Name</label>
<input type="text" value="${variable.name}"
onchange="updateDesignVariable(${index}, 'name', this.value)">
</div>
<div class="form-group">
<label>Minimum</label>
<input type="number" value="${variable.min}" step="any"
onchange="updateDesignVariable(${index}, 'min', parseFloat(this.value))">
</div>
<div class="form-group">
<label>Maximum</label>
<input type="number" value="${variable.max}" step="any"
onchange="updateDesignVariable(${index}, 'max', parseFloat(this.value))">
</div>
<div class="form-group">
<label>Units</label>
<input type="text" value="${variable.units}"
onchange="updateDesignVariable(${index}, 'units', this.value)">
</div>
</div>
</div>
`).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 = '<p class="empty">No objectives yet</p>';
return;
}
const html = studyConfiguration.objectives.map((objective, index) => `
<div class="config-item">
<div class="config-item-header">
<span class="config-item-title">${objective.name}</span>
<button class="config-item-remove" onclick="removeObjective(${index})">×</button>
</div>
<div class="config-item-fields">
<div class="form-group">
<label>Name</label>
<input type="text" value="${objective.name}"
onchange="updateObjective(${index}, 'name', this.value)">
</div>
<div class="form-group">
<label>Extractor</label>
<select onchange="updateObjective(${index}, 'extractor', this.value)">
<option value="stress_extractor" ${objective.extractor === 'stress_extractor' ? 'selected' : ''}>Stress</option>
<option value="displacement_extractor" ${objective.extractor === 'displacement_extractor' ? 'selected' : ''}>Displacement</option>
</select>
</div>
<div class="form-group">
<label>Metric</label>
<input type="text" value="${objective.metric}"
onchange="updateObjective(${index}, 'metric', this.value)">
</div>
<div class="form-group">
<label>Direction</label>
<select onchange="updateObjective(${index}, 'direction', this.value)">
<option value="minimize" ${objective.direction === 'minimize' ? 'selected' : ''}>Minimize</option>
<option value="maximize" ${objective.direction === 'maximize' ? 'selected' : ''}>Maximize</option>
</select>
</div>
<div class="form-group">
<label>Weight</label>
<input type="number" value="${objective.weight}" step="any"
onchange="updateObjective(${index}, 'weight', parseFloat(this.value))">
</div>
<div class="form-group">
<label>Units</label>
<input type="text" value="${objective.units}"
onchange="updateObjective(${index}, 'units', this.value)">
</div>
</div>
</div>
`).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 = '<p class="empty">No constraints yet</p>';
return;
}
const html = studyConfiguration.constraints.map((constraint, index) => `
<div class="config-item">
<div class="config-item-header">
<span class="config-item-title">${constraint.name}</span>
<button class="config-item-remove" onclick="removeConstraint(${index})">×</button>
</div>
<div class="config-item-fields">
<div class="form-group">
<label>Name</label>
<input type="text" value="${constraint.name}"
onchange="updateConstraint(${index}, 'name', this.value)">
</div>
<div class="form-group">
<label>Extractor</label>
<select onchange="updateConstraint(${index}, 'extractor', this.value)">
<option value="stress_extractor" ${constraint.extractor === 'stress_extractor' ? 'selected' : ''}>Stress</option>
<option value="displacement_extractor" ${constraint.extractor === 'displacement_extractor' ? 'selected' : ''}>Displacement</option>
</select>
</div>
<div class="form-group">
<label>Metric</label>
<input type="text" value="${constraint.metric}"
onchange="updateConstraint(${index}, 'metric', this.value)">
</div>
<div class="form-group">
<label>Type</label>
<select onchange="updateConstraint(${index}, 'type', this.value)">
<option value="upper_bound" ${constraint.type === 'upper_bound' ? 'selected' : ''}>Upper Bound</option>
<option value="lower_bound" ${constraint.type === 'lower_bound' ? 'selected' : ''}>Lower Bound</option>
</select>
</div>
<div class="form-group">
<label>Limit</label>
<input type="number" value="${constraint.limit}" step="any"
onchange="updateConstraint(${index}, 'limit', parseFloat(this.value))">
</div>
<div class="form-group">
<label>Units</label>
<input type="text" value="${constraint.units}"
onchange="updateConstraint(${index}, 'units', this.value)">
</div>
</div>
</div>
`).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;
}