feat: Add AtomizerField training data export and intelligent model discovery

Major additions:
- Training data export system for AtomizerField neural network training
- Bracket stiffness optimization study with 50+ training samples
- Intelligent NX model discovery (auto-detect solutions, expressions, mesh)
- Result extractors module for displacement, stress, frequency, mass
- User-generated NX journals for advanced workflows
- Archive structure for legacy scripts and test outputs
- Protocol documentation and dashboard launcher

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-26 12:01:50 -05:00
parent a0c008a593
commit 2b3573ec42
949 changed files with 1405144 additions and 470 deletions

View File

@@ -0,0 +1,386 @@
"""
Unit tests for TrainingDataExporter
Tests the training data export functionality for AtomizerField.
"""
import pytest
import json
import tempfile
import shutil
from pathlib import Path
from optimization_engine.training_data_exporter import TrainingDataExporter, create_exporter_from_config
class TestTrainingDataExporter:
"""Test suite for TrainingDataExporter class."""
@pytest.fixture
def temp_dir(self):
"""Create a temporary directory for tests."""
temp_path = Path(tempfile.mkdtemp())
yield temp_path
# Cleanup after test
if temp_path.exists():
shutil.rmtree(temp_path)
@pytest.fixture
def sample_exporter(self, temp_dir):
"""Create a sample exporter for testing."""
return TrainingDataExporter(
export_dir=temp_dir / "test_export",
study_name="test_study",
design_variable_names=["thickness", "width", "length"],
objective_names=["max_stress", "mass"],
constraint_names=["stress_constraint"],
metadata={"test_key": "test_value"}
)
@pytest.fixture
def sample_simulation_files(self, temp_dir):
"""Create sample .dat and .op2 files for testing."""
sim_dir = temp_dir / "sim_files"
sim_dir.mkdir(parents=True, exist_ok=True)
dat_file = sim_dir / "test_model.dat"
op2_file = sim_dir / "test_model.op2"
# Create dummy files with some content
dat_file.write_text("$ Nastran input deck\nGRID,1,0,0.0,0.0,0.0\n")
op2_file.write_bytes(b"DUMMY OP2 BINARY DATA" * 100)
return {
"dat_file": dat_file,
"op2_file": op2_file
}
def test_exporter_initialization(self, temp_dir):
"""Test that exporter initializes correctly."""
export_dir = temp_dir / "test_export"
exporter = TrainingDataExporter(
export_dir=export_dir,
study_name="beam_study",
design_variable_names=["thickness", "width"],
objective_names=["stress", "mass"]
)
assert exporter.export_dir == export_dir
assert exporter.study_name == "beam_study"
assert exporter.design_variable_names == ["thickness", "width"]
assert exporter.objective_names == ["stress", "mass"]
assert exporter.trial_count == 0
assert len(exporter.exported_trials) == 0
assert export_dir.exists()
def test_readme_creation(self, sample_exporter):
"""Test that README.md is created."""
readme_path = sample_exporter.export_dir / "README.md"
assert readme_path.exists()
content = readme_path.read_text()
assert "test_study" in content
assert "AtomizerField Training Data" in content
assert "thickness" in content
assert "max_stress" in content
def test_export_trial_success(self, sample_exporter, sample_simulation_files):
"""Test successful trial export."""
design_vars = {"thickness": 3.5, "width": 50.0, "length": 200.0}
results = {
"objectives": {"max_stress": 245.3, "mass": 1.25},
"constraints": {"stress_constraint": -54.7},
"max_displacement": 1.23
}
success = sample_exporter.export_trial(
trial_number=1,
design_variables=design_vars,
results=results,
simulation_files=sample_simulation_files
)
assert success
assert sample_exporter.trial_count == 1
assert len(sample_exporter.exported_trials) == 1
# Check directory structure
trial_dir = sample_exporter.export_dir / "trial_0001"
assert trial_dir.exists()
assert (trial_dir / "input").exists()
assert (trial_dir / "output").exists()
assert (trial_dir / "input" / "model.bdf").exists()
assert (trial_dir / "output" / "model.op2").exists()
assert (trial_dir / "metadata.json").exists()
def test_export_trial_metadata(self, sample_exporter, sample_simulation_files):
"""Test that metadata.json is created correctly."""
design_vars = {"thickness": 3.5, "width": 50.0, "length": 200.0}
results = {
"objectives": {"max_stress": 245.3, "mass": 1.25},
"constraints": {"stress_constraint": -54.7},
"max_displacement": 1.23
}
sample_exporter.export_trial(
trial_number=42,
design_variables=design_vars,
results=results,
simulation_files=sample_simulation_files
)
metadata_path = sample_exporter.export_dir / "trial_0042" / "metadata.json"
assert metadata_path.exists()
with open(metadata_path, 'r') as f:
metadata = json.load(f)
assert metadata["trial_number"] == 42
assert metadata["atomizer_study"] == "test_study"
assert metadata["design_parameters"] == design_vars
assert metadata["results"]["objectives"] == {"max_stress": 245.3, "mass": 1.25}
assert metadata["results"]["constraints"] == {"stress_constraint": -54.7}
assert metadata["results"]["max_displacement"] == 1.23
assert "timestamp" in metadata
def test_export_trial_missing_dat_file(self, sample_exporter, temp_dir):
"""Test export fails gracefully when .dat file is missing."""
design_vars = {"thickness": 3.5}
results = {"objectives": {"max_stress": 245.3}}
# Create only .op2 file
sim_dir = temp_dir / "sim_files"
sim_dir.mkdir(parents=True, exist_ok=True)
op2_file = sim_dir / "test_model.op2"
op2_file.write_bytes(b"DUMMY OP2 DATA")
simulation_files = {
"dat_file": sim_dir / "nonexistent.dat", # Does not exist
"op2_file": op2_file
}
success = sample_exporter.export_trial(
trial_number=1,
design_variables=design_vars,
results=results,
simulation_files=simulation_files
)
assert not success
assert sample_exporter.trial_count == 0
def test_export_trial_missing_op2_file(self, sample_exporter, temp_dir):
"""Test export fails gracefully when .op2 file is missing."""
design_vars = {"thickness": 3.5}
results = {"objectives": {"max_stress": 245.3}}
# Create only .dat file
sim_dir = temp_dir / "sim_files"
sim_dir.mkdir(parents=True, exist_ok=True)
dat_file = sim_dir / "test_model.dat"
dat_file.write_text("$ Nastran input")
simulation_files = {
"dat_file": dat_file,
"op2_file": sim_dir / "nonexistent.op2" # Does not exist
}
success = sample_exporter.export_trial(
trial_number=1,
design_variables=design_vars,
results=results,
simulation_files=simulation_files
)
assert not success
assert sample_exporter.trial_count == 0
def test_export_multiple_trials(self, sample_exporter, sample_simulation_files):
"""Test exporting multiple trials."""
for i in range(1, 6):
design_vars = {"thickness": 2.0 + i * 0.5, "width": 40.0 + i * 5, "length": 180.0 + i * 10}
results = {
"objectives": {"max_stress": 200.0 + i * 10, "mass": 1.0 + i * 0.1}
}
success = sample_exporter.export_trial(
trial_number=i,
design_variables=design_vars,
results=results,
simulation_files=sample_simulation_files
)
assert success
assert sample_exporter.trial_count == 5
assert len(sample_exporter.exported_trials) == 5
# Verify all trial directories exist
for i in range(1, 6):
trial_dir = sample_exporter.export_dir / f"trial_{i:04d}"
assert trial_dir.exists()
assert (trial_dir / "metadata.json").exists()
def test_finalize(self, sample_exporter, sample_simulation_files):
"""Test finalize creates study_summary.json."""
# Export a few trials
for i in range(1, 4):
design_vars = {"thickness": 2.0 + i * 0.5, "width": 50.0, "length": 200.0}
results = {"objectives": {"max_stress": 200.0 + i * 10, "mass": 1.2}}
sample_exporter.export_trial(i, design_vars, results, sample_simulation_files)
# Finalize
sample_exporter.finalize()
# Check study_summary.json
summary_path = sample_exporter.export_dir / "study_summary.json"
assert summary_path.exists()
with open(summary_path, 'r') as f:
summary = json.load(f)
assert summary["study_name"] == "test_study"
assert summary["total_trials"] == 3
assert summary["design_variables"] == ["thickness", "width", "length"]
assert summary["objectives"] == ["max_stress", "mass"]
assert summary["constraints"] == ["stress_constraint"]
assert "export_timestamp" in summary
assert summary["metadata"] == {"test_key": "test_value"}
def test_create_exporter_from_config_disabled(self):
"""Test that None is returned when export is disabled."""
config = {
"training_data_export": {
"enabled": False,
"export_dir": "/some/path"
}
}
exporter = create_exporter_from_config(config)
assert exporter is None
def test_create_exporter_from_config_missing_export_dir(self):
"""Test that None is returned when export_dir is missing."""
config = {
"training_data_export": {
"enabled": True
}
}
exporter = create_exporter_from_config(config)
assert exporter is None
def test_create_exporter_from_config_success(self, temp_dir):
"""Test successful exporter creation from config."""
config = {
"study_name": "beam_optimization",
"training_data_export": {
"enabled": True,
"export_dir": str(temp_dir / "export")
},
"design_variables": [
{"name": "thickness"},
{"parameter": "width"},
{"other_key": "value"} # Should use "var_2"
],
"objectives": [
{"name": "max_stress"},
{"name": "mass"}
],
"constraints": [
{"name": "stress_limit"}
],
"version": "1.0",
"optimization": {
"algorithm": "NSGA-II",
"n_trials": 100
}
}
exporter = create_exporter_from_config(config)
assert exporter is not None
assert exporter.study_name == "beam_optimization"
assert exporter.design_variable_names == ["thickness", "width", "var_2"]
assert exporter.objective_names == ["max_stress", "mass"]
assert exporter.constraint_names == ["stress_limit"]
assert exporter.study_metadata["atomizer_version"] == "1.0"
assert exporter.study_metadata["optimization_algorithm"] == "NSGA-II"
assert exporter.study_metadata["n_trials"] == 100
def test_create_exporter_from_config_defaults(self, temp_dir):
"""Test exporter creation with minimal config."""
config = {
"training_data_export": {
"enabled": True,
"export_dir": str(temp_dir / "export")
}
}
exporter = create_exporter_from_config(config)
assert exporter is not None
assert exporter.study_name == "unnamed_study"
assert exporter.design_variable_names == []
assert exporter.objective_names == []
assert exporter.constraint_names == []
def test_file_content_preservation(self, sample_exporter, temp_dir):
"""Test that file contents are preserved during copy."""
# Create files with specific content
sim_dir = temp_dir / "sim_files"
sim_dir.mkdir(parents=True, exist_ok=True)
dat_content = "$ Nastran Input Deck\nGRID,1,0,0.0,0.0,0.0\nGRID,2,0,1.0,0.0,0.0\n"
op2_content = b"\x00\x01\x02\x03NASTRAN_BINARY_DATA\xFF\xFE\xFD\xFC" * 50
dat_file = sim_dir / "model.dat"
op2_file = sim_dir / "model.op2"
dat_file.write_text(dat_content)
op2_file.write_bytes(op2_content)
simulation_files = {"dat_file": dat_file, "op2_file": op2_file}
# Export trial
sample_exporter.export_trial(
trial_number=1,
design_variables={"thickness": 3.0},
results={"objectives": {"stress": 200.0}},
simulation_files=simulation_files
)
# Verify copied files have same content
exported_dat = sample_exporter.export_dir / "trial_0001" / "input" / "model.bdf"
exported_op2 = sample_exporter.export_dir / "trial_0001" / "output" / "model.op2"
assert exported_dat.read_text() == dat_content
assert exported_op2.read_bytes() == op2_content
def test_trial_numbering_format(self, sample_exporter, sample_simulation_files):
"""Test that trial directories use correct numbering format."""
# Test various trial numbers
trial_numbers = [1, 9, 10, 99, 100, 999, 1000]
for trial_num in trial_numbers:
sample_exporter.export_trial(
trial_number=trial_num,
design_variables={"thickness": 3.0},
results={"objectives": {"stress": 200.0}},
simulation_files=sample_simulation_files
)
expected_dir = sample_exporter.export_dir / f"trial_{trial_num:04d}"
assert expected_dir.exists()
# Verify specific formatting
assert (sample_exporter.export_dir / "trial_0001").exists()
assert (sample_exporter.export_dir / "trial_0009").exists()
assert (sample_exporter.export_dir / "trial_0010").exists()
assert (sample_exporter.export_dir / "trial_0099").exists()
assert (sample_exporter.export_dir / "trial_0100").exists()
assert (sample_exporter.export_dir / "trial_0999").exists()
assert (sample_exporter.export_dir / "trial_1000").exists()
if __name__ == "__main__":
pytest.main([__file__, "-v"])