"""Tests for StudyBlueprint and BlueprintBuilder.""" import pytest import json from optimization_engine.interview.study_blueprint import ( StudyBlueprint, DesignVariable, Objective, Constraint, BlueprintBuilder, ) from optimization_engine.interview.interview_state import InterviewState class TestDesignVariable: """Tests for DesignVariable dataclass.""" def test_creation(self): """Test creating design variable.""" dv = DesignVariable( parameter="thickness", current_value=5.0, min_value=1.0, max_value=10.0, units="mm" ) assert dv.parameter == "thickness" assert dv.min_value == 1.0 assert dv.max_value == 10.0 assert dv.current_value == 5.0 assert dv.units == "mm" def test_to_dict(self): """Test conversion to dict.""" dv = DesignVariable( parameter="thickness", current_value=5.0, min_value=1.0, max_value=10.0 ) d = dv.to_dict() assert d["parameter"] == "thickness" assert d["min_value"] == 1.0 assert d["max_value"] == 10.0 def test_to_config_format(self): """Test conversion to config format.""" dv = DesignVariable( parameter="thickness", current_value=5.0, min_value=1.0, max_value=10.0, units="mm" ) config = dv.to_config_format() assert config["expression_name"] == "thickness" assert config["bounds"] == [1.0, 10.0] class TestObjective: """Tests for Objective dataclass.""" def test_creation(self): """Test creating objective.""" obj = Objective( name="mass", goal="minimize", extractor="E4", weight=1.0 ) assert obj.name == "mass" assert obj.goal == "minimize" assert obj.extractor == "E4" assert obj.weight == 1.0 def test_to_dict(self): """Test conversion to dict.""" obj = Objective( name="displacement", goal="minimize", extractor="E1", extractor_params={"node_id": 123} ) d = obj.to_dict() assert d["name"] == "displacement" assert d["extractor"] == "E1" assert d["extractor_params"]["node_id"] == 123 def test_to_config_format(self): """Test conversion to config format.""" obj = Objective( name="mass", goal="minimize", extractor="E4", weight=0.5 ) config = obj.to_config_format() assert config["name"] == "mass" assert config["type"] == "minimize" assert config["weight"] == 0.5 class TestConstraint: """Tests for Constraint dataclass.""" def test_creation(self): """Test creating constraint.""" con = Constraint( name="max_stress", constraint_type="max", threshold=200.0, extractor="E3" ) assert con.name == "max_stress" assert con.constraint_type == "max" assert con.threshold == 200.0 def test_to_dict(self): """Test conversion to dict.""" con = Constraint( name="max_displacement", constraint_type="max", threshold=0.5, extractor="E1" ) d = con.to_dict() assert d["name"] == "max_displacement" assert d["threshold"] == 0.5 def test_to_config_format(self): """Test conversion to config format.""" con = Constraint( name="max_stress", constraint_type="max", threshold=200.0, extractor="E3", is_hard=True ) config = con.to_config_format() assert config["type"] == "max" assert config["threshold"] == 200.0 assert config["hard"] is True class TestStudyBlueprint: """Tests for StudyBlueprint dataclass.""" def test_creation(self): """Test creating blueprint.""" bp = StudyBlueprint( study_name="test_study", study_description="A test study", model_path="/path/model.prt", sim_path="/path/sim.sim", design_variables=[ DesignVariable(parameter="t", current_value=5, min_value=1, max_value=10) ], objectives=[ Objective(name="mass", goal="minimize", extractor="E4") ], constraints=[ Constraint(name="stress", constraint_type="max", threshold=200, extractor="E3") ], protocol="protocol_10_single", n_trials=100, sampler="TPE" ) assert bp.study_name == "test_study" assert len(bp.design_variables) == 1 assert len(bp.objectives) == 1 assert len(bp.constraints) == 1 def test_to_config_json(self): """Test conversion to optimization_config.json format.""" bp = StudyBlueprint( study_name="test", study_description="Test", model_path="/model.prt", sim_path="/sim.sim", design_variables=[ DesignVariable(parameter="thickness", current_value=5, min_value=1, max_value=10) ], objectives=[ Objective(name="mass", goal="minimize", extractor="E4") ], constraints=[], protocol="protocol_10_single", n_trials=50, sampler="TPE" ) config = bp.to_config_json() assert isinstance(config, dict) assert config["study_name"] == "test" assert "design_variables" in config assert "objectives" in config # Should be valid JSON json_str = json.dumps(config) assert len(json_str) > 0 def test_to_markdown(self): """Test conversion to markdown summary.""" bp = StudyBlueprint( study_name="bracket_v1", study_description="Bracket optimization", model_path="/model.prt", sim_path="/sim.sim", design_variables=[ DesignVariable(parameter="thickness", current_value=5, min_value=1, max_value=10, units="mm") ], objectives=[ Objective(name="mass", goal="minimize", extractor="E4") ], constraints=[ Constraint(name="stress", constraint_type="max", threshold=200, extractor="E3") ], protocol="protocol_10_single", n_trials=100, sampler="TPE" ) md = bp.to_markdown() assert "bracket_v1" in md assert "thickness" in md assert "mass" in md.lower() assert "stress" in md assert "200" in md assert "100" in md # n_trials assert "TPE" in md def test_validate_valid_blueprint(self): """Test validation passes for valid blueprint.""" bp = StudyBlueprint( study_name="test", study_description="Test", model_path="/model.prt", sim_path="/sim.sim", design_variables=[ DesignVariable(parameter="t", current_value=5, min_value=1, max_value=10) ], objectives=[ Objective(name="mass", goal="minimize", extractor="E4") ], constraints=[ Constraint(name="stress", constraint_type="max", threshold=200, extractor="E3") ], protocol="protocol_10_single", n_trials=100, sampler="TPE" ) errors = bp.validate() assert len(errors) == 0 def test_validate_missing_objectives(self): """Test validation catches missing objectives.""" bp = StudyBlueprint( study_name="test", study_description="Test", model_path="/model.prt", sim_path="/sim.sim", design_variables=[ DesignVariable(parameter="t", current_value=5, min_value=1, max_value=10) ], objectives=[], # No objectives constraints=[], protocol="protocol_10_single", n_trials=100, sampler="TPE" ) errors = bp.validate() assert any("objective" in e.lower() for e in errors) def test_validate_missing_design_variables(self): """Test validation catches missing design variables.""" bp = StudyBlueprint( study_name="test", study_description="Test", model_path="/model.prt", sim_path="/sim.sim", design_variables=[], # No design variables objectives=[ Objective(name="mass", goal="minimize", extractor="E4") ], constraints=[], protocol="protocol_10_single", n_trials=100, sampler="TPE" ) errors = bp.validate() assert any("design variable" in e.lower() for e in errors) def test_validate_invalid_bounds(self): """Test validation catches invalid bounds.""" bp = StudyBlueprint( study_name="test", study_description="Test", model_path="/model.prt", sim_path="/sim.sim", design_variables=[ DesignVariable(parameter="t", current_value=5, min_value=10, max_value=1) # min > max ], objectives=[ Objective(name="mass", goal="minimize", extractor="E4") ], constraints=[], protocol="protocol_10_single", n_trials=100, sampler="TPE" ) errors = bp.validate() assert any("bound" in e.lower() or "min" in e.lower() for e in errors) def test_to_dict_from_dict_roundtrip(self): """Test dict serialization roundtrip.""" bp = StudyBlueprint( study_name="test", study_description="Test", model_path="/model.prt", sim_path="/sim.sim", design_variables=[ DesignVariable(parameter="t", current_value=5, min_value=1, max_value=10) ], objectives=[ Objective(name="mass", goal="minimize", extractor="E4") ], constraints=[ Constraint(name="stress", constraint_type="max", threshold=200, extractor="E3") ], protocol="protocol_10_single", n_trials=100, sampler="TPE" ) d = bp.to_dict() bp2 = StudyBlueprint.from_dict(d) assert bp2.study_name == bp.study_name assert len(bp2.design_variables) == len(bp.design_variables) assert bp2.n_trials == bp.n_trials class TestBlueprintBuilder: """Tests for BlueprintBuilder.""" def test_from_interview_state_simple(self): """Test building blueprint from simple interview state.""" builder = BlueprintBuilder() state = InterviewState( study_name="bracket_v1", study_path="/path/to/study" ) state.answers = { "study_description": "Bracket mass optimization", "objectives": [{"goal": "minimize_mass"}], "constraints": [{"type": "stress", "threshold": 200}], "design_variables": [ {"name": "thickness", "min": 1, "max": 10, "current": 5} ], "n_trials": 100, } introspection = { "model_path": "/path/model.prt", "sim_path": "/path/sim.sim" } bp = builder.from_interview_state(state, introspection) assert bp.study_name == "bracket_v1" assert len(bp.design_variables) >= 1 assert len(bp.objectives) >= 1 def test_from_interview_state_multi_objective(self): """Test building blueprint for multi-objective optimization.""" builder = BlueprintBuilder() state = InterviewState(study_name="multi_obj") state.answers = { "study_description": "Multi-objective", "objectives": [ {"goal": "minimize_mass"}, {"goal": "minimize_displacement"} ], "constraints": [], "design_variables": [ {"name": "t", "min": 1, "max": 10} ], "n_trials": 200 } introspection = {} bp = builder.from_interview_state(state, introspection) # Blueprint creation succeeds assert bp is not None assert bp.study_name == "multi_obj" def test_auto_assign_extractors(self): """Test automatic extractor assignment.""" builder = BlueprintBuilder() state = InterviewState(study_name="test") state.answers = { "study_description": "Test", "objectives": [{"goal": "minimize_mass"}], # No extractor specified "constraints": [], "design_variables": [{"name": "t", "min": 1, "max": 10}], "n_trials": 50 } bp = builder.from_interview_state(state, {}) # Should auto-assign E4 for mass assert bp.objectives[0].extractor == "E4" def test_calculate_n_trials(self): """Test automatic trial count calculation.""" builder = BlueprintBuilder() # Few design variables = fewer trials state = InterviewState(study_name="test") state.answers = { "study_description": "Test", "objectives": [{"goal": "minimize_mass"}], "constraints": [], "design_variables": [ {"name": "t1", "min": 1, "max": 10}, {"name": "t2", "min": 1, "max": 10}, ], } state.complexity = "simple" bp = builder.from_interview_state(state, {}) assert bp.n_trials >= 50 # Minimum trials def test_select_sampler(self): """Test automatic sampler selection.""" builder = BlueprintBuilder() # Single objective = TPE state = InterviewState(study_name="test") state.answers = { "study_description": "Test", "objectives": [{"goal": "minimize_mass"}], "constraints": [], "design_variables": [{"name": "t", "min": 1, "max": 10}], } bp = builder.from_interview_state(state, {}) assert bp.sampler == "TPE" # Multi-objective case - sampler selection depends on implementation state.answers["objectives"] = [ {"goal": "minimize_mass"}, {"goal": "minimize_displacement"} ] bp = builder.from_interview_state(state, {}) # Just verify blueprint is created successfully assert bp.sampler is not None