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:
386
tests/test_training_data_exporter.py
Normal file
386
tests/test_training_data_exporter.py
Normal 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"])
|
||||
Reference in New Issue
Block a user