let domains author their own pass-2 estimator as formulas, not Python
Pass 2 previously only knew one estimator: a hardcoded physics model that matches dimensions literally named platform/actuator/energy_storage. Any domain outside that shape (e.g. archery) got all-zero estimates and failed every combo. Domains can now declare free variables and per-metric formulas as data instead; a safe AST-based evaluator (engine/formula.py, no eval()) resolves declared entity properties via dep(key, constraint_type) and generalizes the existing hand-nested mass-budget search into an N-variable recursive optimizer. Fully additive -- the legacy platform/actuator/ energy_storage path is untouched and still runs unchanged for every domain that declares no formulas. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import pytest
|
||||
from physcom.db.schema import init_db
|
||||
from physcom.db.repository import Repository
|
||||
from physcom.models.entity import Entity, Dependency
|
||||
from physcom.models.domain import Domain, DomainConstraint, MetricBound
|
||||
from physcom.models.domain import Domain, DomainConstraint, FreeVariable, MetricBound, MetricFormula
|
||||
from physcom.models.combination import Combination
|
||||
from physcom.snapshot import export_snapshot, import_snapshot
|
||||
|
||||
@@ -169,6 +169,46 @@ def test_import_with_combinations(seeded_repo, tmp_path):
|
||||
assert len(fresh_combos) == len(data["combinations"])
|
||||
|
||||
|
||||
def test_export_import_roundtrip_free_variables_and_formulas(repo, tmp_path):
|
||||
domain = Domain(
|
||||
name="archery_snapshot_test",
|
||||
metric_bounds=[MetricBound("drawback_force", weight=1.0, norm_min=0, norm_max=100)],
|
||||
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"),
|
||||
],
|
||||
)
|
||||
repo.add_domain(domain)
|
||||
|
||||
data = export_snapshot(repo)
|
||||
exported = next(d for d in data["domains"] if d["name"] == "archery_snapshot_test")
|
||||
assert exported["free_variables"] == [{
|
||||
"name": "draw_weight_chosen", "sort_order": 0,
|
||||
"floor_formula": 'dep("draw_weight", "range_min")',
|
||||
"ceiling_formula": 'dep("draw_weight", "range_max")',
|
||||
}]
|
||||
assert exported["metric_formulas"] == [
|
||||
{"metric_name": "drawback_force", "formula": "draw_weight_chosen * 2"},
|
||||
]
|
||||
|
||||
conn = init_db(tmp_path / "fresh.db")
|
||||
fresh = Repository(conn)
|
||||
import_snapshot(fresh, data, clear=True)
|
||||
|
||||
loaded = fresh.get_domain("archery_snapshot_test")
|
||||
assert len(loaded.free_variables) == 1
|
||||
assert loaded.free_variables[0].floor_formula == 'dep("draw_weight", "range_min")'
|
||||
assert len(loaded.metric_formulas) == 1
|
||||
assert loaded.metric_formulas[0].formula == "draw_weight_chosen * 2"
|
||||
|
||||
|
||||
def test_import_merge_skips_existing_domain(repo):
|
||||
"""Merge import skips domains that already exist."""
|
||||
domain = Domain(
|
||||
|
||||
Reference in New Issue
Block a user