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>
149 lines
5.0 KiB
Python
149 lines
5.0 KiB
Python
"""Tests for the database repository."""
|
|
|
|
from physcom.models.entity import Entity, Dependency
|
|
from physcom.models.domain import Domain, FreeVariable, MetricBound, MetricFormula
|
|
|
|
|
|
def test_ensure_dimension(repo):
|
|
dim_id = repo.ensure_dimension("platform", "Vehicle platforms")
|
|
assert dim_id > 0
|
|
# Idempotent
|
|
dim_id2 = repo.ensure_dimension("platform", "Vehicle platforms")
|
|
assert dim_id == dim_id2
|
|
|
|
|
|
def test_add_and_get_entity(repo):
|
|
entity = Entity(
|
|
name="TestBike",
|
|
dimension="platform",
|
|
description="A test bicycle",
|
|
dependencies=[
|
|
Dependency("environment", "ground_surface", "true", None, "requires"),
|
|
],
|
|
)
|
|
saved = repo.add_entity(entity)
|
|
assert saved.id is not None
|
|
|
|
loaded = repo.get_entity(saved.id)
|
|
assert loaded is not None
|
|
assert loaded.name == "TestBike"
|
|
assert loaded.dimension == "platform"
|
|
assert len(loaded.dependencies) == 1
|
|
assert loaded.dependencies[0].key == "ground_surface"
|
|
|
|
|
|
def test_list_entities_by_dimension(repo):
|
|
repo.add_entity(Entity(name="A", dimension="platform"))
|
|
repo.add_entity(Entity(name="B", dimension="platform"))
|
|
repo.add_entity(Entity(name="C", dimension="actuator"))
|
|
|
|
platforms = repo.list_entities(dimension="platform")
|
|
assert len(platforms) == 2
|
|
|
|
all_entities = repo.list_entities()
|
|
assert len(all_entities) == 3
|
|
|
|
|
|
def test_add_and_get_domain(repo):
|
|
domain = Domain(
|
|
name="test_domain",
|
|
description="A test domain",
|
|
metric_bounds=[
|
|
MetricBound("speed", weight=0.5, norm_min=0, norm_max=100),
|
|
MetricBound("safety", weight=0.5, norm_min=0, norm_max=1),
|
|
],
|
|
)
|
|
saved = repo.add_domain(domain)
|
|
assert saved.id is not None
|
|
|
|
loaded = repo.get_domain("test_domain")
|
|
assert loaded is not None
|
|
assert loaded.name == "test_domain"
|
|
assert len(loaded.metric_bounds) == 2
|
|
assert loaded.metric_bounds[0].metric_name == "speed"
|
|
|
|
|
|
def test_add_domain_with_free_variables_and_formulas(repo):
|
|
domain = Domain(
|
|
name="archery_test",
|
|
metric_bounds=[MetricBound("drawback_force", weight=1.0, norm_min=0, norm_max=500)],
|
|
free_variables=[
|
|
FreeVariable(
|
|
name="draw_weight",
|
|
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 * 1.5"),
|
|
],
|
|
)
|
|
saved = repo.add_domain(domain)
|
|
assert saved.id is not None
|
|
|
|
loaded = repo.get_domain("archery_test")
|
|
assert loaded is not None
|
|
assert len(loaded.free_variables) == 1
|
|
assert loaded.free_variables[0].name == "draw_weight"
|
|
assert loaded.free_variables[0].id is not None
|
|
assert len(loaded.metric_formulas) == 1
|
|
assert loaded.metric_formulas[0].formula == "draw_weight * 1.5"
|
|
|
|
|
|
def test_free_variable_and_formula_crud(repo):
|
|
domain = repo.add_domain(Domain(name="crud_test"))
|
|
|
|
fv = repo.add_free_variable(
|
|
domain.id,
|
|
FreeVariable(name="x", floor_formula="0", ceiling_formula="100", sort_order=0),
|
|
)
|
|
mf = repo.add_metric_formula(
|
|
domain.id, MetricFormula(metric_name="m", formula="x * 2")
|
|
)
|
|
|
|
repo.update_free_variable(
|
|
fv.id, FreeVariable(name="x", floor_formula="1", ceiling_formula="200", sort_order=0)
|
|
)
|
|
repo.update_metric_formula(mf.id, MetricFormula(metric_name="m", formula="x * 3"))
|
|
|
|
loaded = repo.get_domain_by_id(domain.id)
|
|
assert loaded.free_variables[0].floor_formula == "1"
|
|
assert loaded.free_variables[0].ceiling_formula == "200"
|
|
assert loaded.metric_formulas[0].formula == "x * 3"
|
|
|
|
repo.delete_free_variable(fv.id)
|
|
repo.delete_metric_formula(mf.id)
|
|
loaded = repo.get_domain_by_id(domain.id)
|
|
assert loaded.free_variables == []
|
|
assert loaded.metric_formulas == []
|
|
|
|
|
|
def test_combination_save_and_dedup(repo):
|
|
e1 = repo.add_entity(Entity(name="A", dimension="platform"))
|
|
e2 = repo.add_entity(Entity(name="B", dimension="actuator"))
|
|
|
|
from physcom.models.combination import Combination
|
|
combo = Combination(entities=[e1, e2])
|
|
saved = repo.save_combination(combo)
|
|
assert saved.id is not None
|
|
|
|
# Same entities, same hash → should not create duplicate
|
|
combo2 = Combination(entities=[e1, e2])
|
|
saved2 = repo.save_combination(combo2)
|
|
assert saved2.id == saved.id
|
|
|
|
|
|
def test_seed_loads(seeded_repo):
|
|
from physcom.seed.transport_example import PLATFORMS, ACTUATORS, ENERGY_STORAGES, ALL_DOMAINS
|
|
|
|
platforms = seeded_repo.list_entities(dimension="platform")
|
|
actuators = seeded_repo.list_entities(dimension="actuator")
|
|
energy_storages = seeded_repo.list_entities(dimension="energy_storage")
|
|
assert len(platforms) == len(PLATFORMS)
|
|
assert len(actuators) == len(ACTUATORS)
|
|
assert len(energy_storages) == len(ENERGY_STORAGES)
|
|
|
|
domains = seeded_repo.list_domains()
|
|
assert len(domains) == len(ALL_DOMAINS)
|