"""End-to-end test that pass 2 can estimate a non-transport domain entirely from domain-authored formulas, without touching the platform/actuator/ energy_storage physics model in Pipeline._estimate_physics.""" import pytest from physcom.engine.constraint_resolver import ConstraintResolver from physcom.engine.scorer import Scorer from physcom.engine.pipeline import Pipeline from physcom.models.domain import Domain, FreeVariable, MetricBound, MetricFormula from physcom.models.entity import Dependency, Entity def _build_archery_domain(repo): repo.add_entity(Entity( name="Recurve", dimension="bow", dependencies=[ Dependency("physical", "draw_weight", "20", None, "range_min"), Dependency("physical", "draw_weight", "50", None, "range_max"), ], )) repo.add_entity(Entity( name="Carbon", dimension="arrow", dependencies=[ Dependency("physical", "arrow_mass", "0.02", None, "provides"), ], )) return repo.add_domain(Domain( name="archery_test", metric_bounds=[ MetricBound("drawback_force", weight=0.6, norm_min=0, norm_max=100), MetricBound("range", weight=0.4, norm_min=0, norm_max=300), ], free_variables=[ FreeVariable( name="draw_weight_chosen", floor_formula='dep("draw_weight", "range_min")', ceiling_formula='dep("draw_weight", "range_max")', sort_order=0, ), ], metric_formulas=[ MetricFormula(metric_name="drawback_force", formula="draw_weight_chosen * 2"), MetricFormula( metric_name="range", formula='draw_weight_chosen * 5 / dep("arrow_mass")', ), ], )) def test_formula_domain_scores_without_platform_actuator_shape(repo): domain = _build_archery_domain(repo) resolver = ConstraintResolver() scorer = Scorer(domain) pipeline = Pipeline(repo, resolver, scorer) result = pipeline.run( domain, ["bow", "arrow"], score_threshold=0.01, passes=[1, 2, 3, 5], ) assert result.total_generated == 1 assert result.pass1_failed == 0 assert result.pass2_estimated == 1 assert result.pass3_above_threshold == 1 combos = repo.list_combinations() assert len(combos) == 1 combo = combos[0] scores = { s["metric_name"]: s["raw_value"] for s in repo.get_combination_scores(combo.id, domain.id) } # Both metrics increase monotonically with draw_weight_chosen and nothing # trades off against it, so the optimizer should push to the declared # ceiling (50) -- confirms _search_free_variables is actually searching, # not just evaluating at the floor. assert scores["drawback_force"] == pytest.approx(100.0, rel=0.02) def test_formula_domain_zero_free_variables_direct_evaluation(repo): """A domain with metric_formulas but no free_variables should evaluate each formula once directly -- no search loop at all.""" repo.add_entity(Entity( name="Recurve", dimension="bow", dependencies=[Dependency("physical", "draw_weight", "30", None, "provides")], )) repo.add_entity(Entity(name="Carbon", dimension="arrow")) domain = repo.add_domain(Domain( name="archery_direct_test", metric_bounds=[MetricBound("drawback_force", weight=1.0, norm_min=0, norm_max=100)], metric_formulas=[ MetricFormula(metric_name="drawback_force", formula='dep("draw_weight") * 2'), ], )) resolver = ConstraintResolver() scorer = Scorer(domain) pipeline = Pipeline(repo, resolver, scorer) result = pipeline.run( domain, ["bow", "arrow"], score_threshold=0.01, passes=[1, 2, 3, 5], ) assert result.pass2_estimated == 1 combos = repo.list_combinations() scores = { s["metric_name"]: s["raw_value"] for s in repo.get_combination_scores(combos[0].id, domain.id) } assert scores["drawback_force"] == 60.0