508 lines
18 KiB
JavaScript
508 lines
18 KiB
JavaScript
|
|
// 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;
|
|||
|
|
}
|