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.
This commit is contained in:
@@ -453,6 +453,276 @@ def get_visualization_data(study_name: str):
|
|||||||
}), 500
|
}), 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/<study_name>/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/<study_name>/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/<study_name>/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/<study_name>/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__':
|
if __name__ == '__main__':
|
||||||
print("="*60)
|
print("="*60)
|
||||||
print("ATOMIZER DASHBOARD API")
|
print("ATOMIZER DASHBOARD API")
|
||||||
|
|||||||
@@ -49,10 +49,10 @@
|
|||||||
<!-- Welcome Screen -->
|
<!-- Welcome Screen -->
|
||||||
<div id="welcomeScreen" class="welcome-screen">
|
<div id="welcomeScreen" class="welcome-screen">
|
||||||
<h2>Welcome to Atomizer</h2>
|
<h2>Welcome to Atomizer</h2>
|
||||||
<p>Select a study from the sidebar or start a new optimization</p>
|
<p>Select a study from the sidebar or create a new study</p>
|
||||||
<div class="quick-actions">
|
<div class="quick-actions">
|
||||||
<button class="btn btn-large btn-primary" onclick="showNewOptimizationModal()">
|
<button class="btn btn-large btn-primary" onclick="showCreateStudyModal()">
|
||||||
Start New Optimization
|
Create New Study
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-large btn-secondary" onclick="refreshStudies()">
|
<button class="btn btn-large btn-secondary" onclick="refreshStudies()">
|
||||||
View Existing Studies
|
View Existing Studies
|
||||||
@@ -60,6 +60,104 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Study Configuration View -->
|
||||||
|
<div id="studyConfig" class="study-config" style="display: none;">
|
||||||
|
<div class="study-header">
|
||||||
|
<div>
|
||||||
|
<h2 id="configStudyTitle">Configure Study</h2>
|
||||||
|
<p id="configStudyMeta" class="study-meta"></p>
|
||||||
|
</div>
|
||||||
|
<div class="study-actions">
|
||||||
|
<button class="btn btn-secondary" onclick="backToStudyList()">
|
||||||
|
← Back
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" onclick="saveStudyConfiguration()">
|
||||||
|
💾 Save Configuration
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Configuration Steps -->
|
||||||
|
<div class="config-steps">
|
||||||
|
<!-- Step 1: Sim Files -->
|
||||||
|
<div class="config-step">
|
||||||
|
<h3>1. Simulation Files</h3>
|
||||||
|
<div class="step-content">
|
||||||
|
<p class="step-description">Drop your .sim and .prt files in the sim folder, then explore the model</p>
|
||||||
|
<div class="file-info">
|
||||||
|
<p><strong>Sim Folder:</strong> <span id="simFolderPath"></span></p>
|
||||||
|
<button class="btn btn-secondary" onclick="openSimFolder()">Open Folder</button>
|
||||||
|
<button class="btn btn-primary" onclick="refreshSimFiles()">Refresh Files</button>
|
||||||
|
</div>
|
||||||
|
<div id="simFilesList" class="files-list"></div>
|
||||||
|
<button id="exploreBtn" class="btn btn-primary" onclick="exploreSimFile()" disabled>
|
||||||
|
🔍 Explore .sim File
|
||||||
|
</button>
|
||||||
|
<div id="expressionsList" class="expressions-list" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: Design Variables -->
|
||||||
|
<div class="config-step">
|
||||||
|
<h3>2. Design Variables</h3>
|
||||||
|
<div class="step-content">
|
||||||
|
<p class="step-description">Select expressions to use as design variables and set their bounds</p>
|
||||||
|
<div id="designVariablesConfig" class="variables-config">
|
||||||
|
<p class="empty">Explore .sim file first to see available expressions</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary" onclick="addDesignVariable()">+ Add Variable</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3: Objectives -->
|
||||||
|
<div class="config-step">
|
||||||
|
<h3>3. Objectives</h3>
|
||||||
|
<div class="step-content">
|
||||||
|
<p class="step-description">Define what you want to optimize (minimize or maximize)</p>
|
||||||
|
<div id="objectivesConfig" class="objectives-config">
|
||||||
|
<p class="empty">No objectives defined yet</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary" onclick="addObjective()">+ Add Objective</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 4: Constraints -->
|
||||||
|
<div class="config-step">
|
||||||
|
<h3>4. Constraints</h3>
|
||||||
|
<div class="step-content">
|
||||||
|
<p class="step-description">Set limits on simulation outputs</p>
|
||||||
|
<div id="constraintsConfig" class="constraints-config">
|
||||||
|
<p class="empty">No constraints defined yet</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary" onclick="addConstraint()">+ Add Constraint</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 5: Optimization Settings -->
|
||||||
|
<div class="config-step">
|
||||||
|
<h3>5. Optimization Settings</h3>
|
||||||
|
<div class="step-content">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Number of Trials</label>
|
||||||
|
<input type="number" id="nTrials" value="50" min="1">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Sampler</label>
|
||||||
|
<select id="samplerType">
|
||||||
|
<option value="TPE" selected>TPE (Tree-structured Parzen Estimator)</option>
|
||||||
|
<option value="CMAES">CMA-ES</option>
|
||||||
|
<option value="GP">Gaussian Process</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Startup Trials (random exploration)</label>
|
||||||
|
<input type="number" id="startupTrials" value="20" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Study Details View -->
|
<!-- Study Details View -->
|
||||||
<div id="studyDetails" class="study-details" style="display: none;">
|
<div id="studyDetails" class="study-details" style="display: none;">
|
||||||
<div class="study-header">
|
<div class="study-header">
|
||||||
@@ -179,6 +277,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Create Study Modal -->
|
||||||
|
<div id="createStudyModal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Create New Study</h2>
|
||||||
|
<button class="close-btn" onclick="closeCreateStudyModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Study Name</label>
|
||||||
|
<input type="text" id="createStudyName" placeholder="my_optimization_study" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Description (optional)</label>
|
||||||
|
<input type="text" id="createStudyDescription" placeholder="Brief description of this study" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="closeCreateStudyModal()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="createNewStudy()">Create Study</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
|
<script src="study_config.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
507
dashboard/frontend/study_config.js
Normal file
507
dashboard/frontend/study_config.js
Normal file
@@ -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 = '<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;
|
||||||
|
}
|
||||||
@@ -528,6 +528,157 @@ tbody tr:hover {
|
|||||||
color: var(--text-light);
|
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 */
|
/* Responsive */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.main-content {
|
.main-content {
|
||||||
@@ -541,4 +692,8 @@ tbody tr:hover {
|
|||||||
.charts-container {
|
.charts-container {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-item-fields {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
158
dashboard/scripts/extract_expressions.py
Normal file
158
dashboard/scripts/extract_expressions.py
Normal file
@@ -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 <sim_file_path> <output_json_path>
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 <sim_file_path> <output_json_path>")
|
||||||
|
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()
|
||||||
Reference in New Issue
Block a user