Files
physicalCombinatorics/tests/test_constraint_resolver.py
Andrew Simonson 434df718d7 close guardrail gaps and fix the scoring pipeline top to bottom
Constraint resolver: aggregate mass/footprint across a combo instead of
pairwise-only checks, treat medium/atmosphere as agreement not supply/demand,
reduce multi-provider checks by best/sum instead of AND-ing every provider,
fail closed on unrecognized mutex values, add a propulsion-viability
(thrust-to-weight) rule. Seed data updated to match (nuclear/solar-sail
footprint floors, water-medium exclusions, explicit ground/gravity providers).

Domain metric units were stored globally per metric name instead of
per-domain, silently corrupting cost_efficiency for every domain but the
first one seeded — fixed with a schema migration.

Stub estimator's cost_efficiency/safety/availability/reliability were a
backwards formula and flat constants; replaced with heuristics grounded in
each entity's thrust_profile/energy_form/infrastructure.

LLM estimate_physics() now receives each metric's unit and expected range
instead of a bare name, fixing wildly miscalibrated estimates traced back to
the prompt's own hardcoded example anchoring the model to the wrong order of
magnitude. Sharpened the safety-estimation and plausibility-review prompts.
Deduped provider parsing logic into llm/parsing.py.

Web pipeline form can now pick an LLM provider per run instead of only via
server env var.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 00:13:16 -05:00

391 lines
16 KiB
Python

"""Tests for the constraint resolver engine."""
from physcom.engine.constraint_resolver import ConstraintResolver
from physcom.models.combination import Combination
from physcom.models.domain import DomainConstraint
from physcom.models.entity import Entity, Dependency
def test_compatible_ground_combo(bicycle, human_pedalling, food_calories):
"""Bicycle + Human Pedalling + Food/Calories should be valid."""
resolver = ConstraintResolver()
combo = Combination(entities=[bicycle, human_pedalling, food_calories])
result = resolver.resolve(combo)
assert result.status != "p1_fail", f"Unexpected block: {result.violations}"
def test_solar_sail_blocks_with_road_vehicle(road_vehicle, solar_sail, solar_radiation):
"""Road Vehicle (ground) + Solar Sail (space) should be blocked by medium mutex."""
resolver = ConstraintResolver()
combo = Combination(entities=[road_vehicle, solar_sail, solar_radiation])
result = resolver.resolve(combo)
assert result.status == "p1_fail"
assert any("mutually exclusive" in v for v in result.violations)
def test_spaceship_compatible_with_solar_sail(spaceship, solar_sail, solar_radiation):
"""Spaceship + Solar Sail both need space/vacuum — should not conflict."""
resolver = ConstraintResolver()
combo = Combination(entities=[spaceship, solar_sail, solar_radiation])
result = resolver.resolve(combo)
# Should not be blocked by atmosphere or medium
medium_blocks = [v for v in result.violations if "mutually exclusive" in v]
assert len(medium_blocks) == 0
def test_nuclear_drive_blocks_with_bicycle(bicycle, nuclear_thermal_drive, nuclear_fuel):
"""Nuclear drive min_mass=1500kg + fuel min_mass=500kg vs bicycle max_mass=30kg → range incompatibility."""
resolver = ConstraintResolver()
combo = Combination(entities=[bicycle, nuclear_thermal_drive, nuclear_fuel])
result = resolver.resolve(combo)
assert result.status == "p1_fail"
assert any("mass" in v.lower() for v in result.violations)
def test_requires_vs_excludes():
"""Direct requires/excludes contradiction."""
a = Entity(
name="A", dimension="platform",
dependencies=[Dependency("environment", "oxygen", "true", None, "requires")],
)
b = Entity(
name="B", dimension="actuator",
dependencies=[Dependency("environment", "oxygen", "true", None, "excludes")],
)
resolver = ConstraintResolver()
combo = Combination(entities=[a, b])
result = resolver.resolve(combo)
assert result.status == "p1_fail"
assert any("excludes" in v for v in result.violations)
def test_ice_engine_blocks_with_spaceship(spaceship, ice_engine, gasoline):
"""ICE requires standard atmosphere, spaceship requires vacuum_or_thin → mutex."""
resolver = ConstraintResolver()
combo = Combination(entities=[spaceship, ice_engine, gasoline])
result = resolver.resolve(combo)
assert result.status == "p1_fail"
assert any("atmosphere" in v for v in result.violations)
def test_hydrogen_bicycle_valid(bicycle, hydrogen_engine, hydrogen):
"""Hydrogen bike — the README's example of a plausible novel concept."""
resolver = ConstraintResolver()
combo = Combination(entities=[bicycle, hydrogen_engine, hydrogen])
result = resolver.resolve(combo)
# Should pass constraints (mass range is compatible: h2 engine min 25kg, bike max 30kg)
# This is actually a borderline case — let's just verify no hard physics blocks
range_blocks = [v for v in result.violations if "mutually exclusive" in v or "atmosphere" in v]
assert len(range_blocks) == 0
def test_energy_density_deficit_blocks():
"""A platform needing 7200000 J/kg paired with a 720000 J/kg battery → blocked."""
platform = Entity(
name="Spaceship", dimension="platform",
dependencies=[
Dependency("physical", "energy_density", "7200000", "J/kg", "range_min"),
],
)
storage = Entity(
name="Battery", dimension="energy_storage",
dependencies=[
Dependency("physical", "energy_density", "720000", "J/kg", "provides"),
],
)
resolver = ConstraintResolver()
combo = Combination(entities=[platform, storage])
result = resolver.resolve(combo)
assert result.status == "p1_fail"
assert any("deficit" in v for v in result.violations)
def test_energy_density_under_density_warning():
"""A platform needing 1440000 J/kg paired with a 720000 J/kg battery → conditional."""
platform = Entity(
name="Airplane", dimension="platform",
dependencies=[
Dependency("physical", "energy_density", "1440000", "J/kg", "range_min"),
],
)
storage = Entity(
name="Battery", dimension="energy_storage",
dependencies=[
Dependency("physical", "energy_density", "720000", "J/kg", "provides"),
],
)
resolver = ConstraintResolver()
combo = Combination(entities=[platform, storage])
result = resolver.resolve(combo)
assert result.status != "p1_fail"
assert any("under-provision" in w for w in result.warnings)
def test_energy_density_no_constraint_if_no_provider():
"""A platform with energy density requirement but no declared provider → no violation."""
platform = Entity(
name="Spaceship", dimension="platform",
dependencies=[
Dependency("physical", "energy_density", "7200000", "J/kg", "range_min"),
],
)
# Solar Sail-style: no energy_density declared
actuator = Entity(
name="Solar Sail", dimension="actuator",
dependencies=[
Dependency("force", "power_density", "0.01", "W/kg", "provides"),
],
)
resolver = ConstraintResolver()
combo = Combination(entities=[platform, actuator])
result = resolver.resolve(combo)
density_violations = [v for v in result.violations if "energy_density" in v]
assert len(density_violations) == 0
def test_domain_constraint_blocks_wrong_medium(spaceship, solar_sail, solar_radiation):
"""Spaceship (space medium) should be blocked in a ground-only domain."""
resolver = ConstraintResolver()
combo = Combination(entities=[spaceship, solar_sail, solar_radiation])
constraints = [DomainConstraint("medium", ["ground", "air"])]
result = resolver.check_domain_constraints(combo, constraints)
assert result.status == "p1_fail"
assert any("medium" in v for v in result.violations)
def test_domain_constraint_allows_matching_medium(bicycle, human_pedalling, food_calories):
"""Bicycle (ground medium) should pass a ground+air domain constraint."""
resolver = ConstraintResolver()
combo = Combination(entities=[bicycle, human_pedalling, food_calories])
constraints = [DomainConstraint("medium", ["ground", "air"])]
result = resolver.check_domain_constraints(combo, constraints)
assert result.status == "valid"
def _rotorcraft():
return Entity(
name="Rotorcraft", dimension="platform",
dependencies=[
Dependency("physical", "footprint", "20", "", "range_max"),
Dependency("physical", "footprint", "0.5", "", "range_min"),
Dependency("physical", "mass", "5000", "kg", "range_max"),
Dependency("physical", "mass", "1", "kg", "range_min"),
],
)
def _spaceship_with_footprint():
return Entity(
name="Spaceship", dimension="platform",
dependencies=[
Dependency("physical", "footprint", "500", "", "range_max"),
Dependency("physical", "footprint", "10", "", "range_min"),
Dependency("physical", "mass", "5000", "kg", "range_min"),
],
)
def _nuclear_thermal_drive_with_footprint():
return Entity(
name="Nuclear Thermal Drive", dimension="actuator",
dependencies=[
Dependency("physical", "footprint", "20", "", "range_min"),
Dependency("physical", "mass", "1500", "kg", "range_min"),
],
)
def _nuclear_fuel_with_footprint():
return Entity(
name="Nuclear Fuel", dimension="energy_storage",
dependencies=[
Dependency("physical", "footprint", "5", "", "range_min"),
Dependency("physical", "mass", "500", "kg", "range_min"),
],
)
def test_footprint_aggregation_blocks_reactor_on_rotorcraft():
"""P1: individual footprint floors each fit under the ceiling (20, 5 <= 20)
but their sum (25.5) doesn't — must block even though no single component
exceeds the ceiling on its own."""
resolver = ConstraintResolver()
combo = Combination(entities=[
_rotorcraft(), _nuclear_thermal_drive_with_footprint(), _nuclear_fuel_with_footprint(),
])
result = resolver.resolve(combo)
assert result.status == "p1_fail"
assert any("combined footprint" in v for v in result.violations)
def test_footprint_aggregation_still_passes_spaceship():
"""Same reactor + fuel, but a platform with enough footprint budget (500 m²)
must still pass — aggregation shouldn't over-block combos with real headroom."""
resolver = ConstraintResolver()
combo = Combination(entities=[
_spaceship_with_footprint(), _nuclear_thermal_drive_with_footprint(), _nuclear_fuel_with_footprint(),
])
result = resolver.resolve(combo)
assert result.status != "p1_fail"
assert not any("footprint" in v for v in result.violations)
def test_mass_aggregation_within_tolerance_warns_not_blocks():
"""Sum only slightly over the ceiling (65 vs 60, +8.3%) is a data-calibration
signal, not a hard physical impossibility — should warn, not block."""
platform = Entity(
name="Light Personal Vehicle", dimension="platform",
dependencies=[
Dependency("physical", "mass", "60", "kg", "range_max"),
Dependency("physical", "mass", "5", "kg", "range_min"),
],
)
actuator = Entity(
name="Piston Engine", dimension="actuator",
dependencies=[Dependency("physical", "mass", "45", "kg", "range_min")],
)
storage = Entity(
name="Compressed Natural Gas", dimension="energy_storage",
dependencies=[Dependency("physical", "mass", "15", "kg", "range_min")],
)
resolver = ConstraintResolver()
result = resolver.resolve(Combination(entities=[platform, actuator, storage]))
assert result.status == "conditional"
assert any("combined mass" in w for w in result.warnings)
def test_weak_secondary_provider_does_not_block_satisfied_requirement():
"""P3: a strong provider (nuclear fuel) already satisfies the requirement;
a weak secondary provider (solar panel) in the same combo must not
retroactively block it — a real backup power source shouldn't break a
vehicle that already has enough primary power."""
platform = Entity(
name="Spaceship", dimension="platform",
dependencies=[Dependency("physical", "energy_density", "7200000", "J/kg", "range_min")],
)
nuclear_fuel = Entity(
name="Nuclear Fuel", dimension="energy_storage",
dependencies=[Dependency("physical", "energy_density", "1800000000", "J/kg", "provides")],
)
solar_panel = Entity(
name="Solar Photovoltaic Panel", dimension="energy_storage",
dependencies=[Dependency("physical", "energy_density", "180000", "J/kg", "provides")],
)
resolver = ConstraintResolver()
result = resolver.resolve(Combination(entities=[platform, nuclear_fuel, solar_panel]))
assert result.status != "p1_fail"
assert not any("energy_density" in v for v in result.violations)
def test_unrecognized_mutex_value_fails_closed():
"""P4: a value not in any registered mutex set (e.g. a new 'medium' typed
into the admin UI) must conflict with a recognized value on the same key,
not silently pass."""
a = Entity(
name="A", dimension="platform",
dependencies=[Dependency("environment", "medium", "underground", None, "requires")],
)
b = Entity(
name="B", dimension="actuator",
dependencies=[Dependency("environment", "medium", "space", None, "requires")],
)
resolver = ConstraintResolver()
result = resolver.resolve(Combination(entities=[a, b]))
assert result.status == "p1_fail"
assert any("mutually exclusive" in v for v in result.violations)
def test_propulsion_viability_blocks_weak_actuator_regardless_of_scale():
"""G4: specific_thrust below min_effective_accel can never be fixed by
adding more actuator mass — must block unconditionally (Case 1)."""
platform = Entity(
name="Rotorcraft", dimension="platform",
dependencies=[
Dependency("physical", "mass", "5000", "kg", "range_max"),
Dependency("physical", "min_effective_accel", "10", "m/s²", "range_min"),
],
)
actuator = Entity(
name="Ion Drive", dimension="actuator",
dependencies=[
Dependency("physical", "mass", "8", "kg", "range_min"),
Dependency("force", "specific_thrust", "0.01", "N/kg", "provides"),
],
)
resolver = ConstraintResolver()
result = resolver.resolve(Combination(entities=[platform, actuator]))
assert result.status == "p1_fail"
assert any("regardless of scale" in v for v in result.violations)
def test_propulsion_viability_blocks_when_required_mass_exceeds_ceiling():
"""G4 Case 2: specific_thrust clears min_effective_accel, but the mass
needed to hit that thrust doesn't fit the vehicle's mass budget."""
platform = Entity(
name="Test Platform", dimension="platform",
dependencies=[
Dependency("physical", "mass", "50", "kg", "range_max"),
Dependency("physical", "mass", "10", "kg", "range_min"),
Dependency("physical", "min_effective_accel", "5", "m/s²", "range_min"),
],
)
actuator = Entity(
name="Weak Reaction Drive", dimension="actuator",
dependencies=[
Dependency("physical", "mass", "1", "kg", "range_min"),
Dependency("force", "specific_thrust", "6", "N/kg", "provides"),
],
)
storage = Entity(
name="Fuel", dimension="energy_storage",
dependencies=[Dependency("physical", "mass", "1", "kg", "range_min")],
)
resolver = ConstraintResolver()
result = resolver.resolve(Combination(entities=[platform, actuator, storage]))
assert result.status == "p1_fail"
assert any("would need >=" in v for v in result.violations)
def test_propulsion_viability_passes_with_enough_budget():
"""Same shape as above but with a generous mass ceiling — must pass."""
platform = Entity(
name="Test Platform", dimension="platform",
dependencies=[
Dependency("physical", "mass", "5000", "kg", "range_max"),
Dependency("physical", "mass", "10", "kg", "range_min"),
Dependency("physical", "min_effective_accel", "5", "m/s²", "range_min"),
],
)
actuator = Entity(
name="Weak Reaction Drive", dimension="actuator",
dependencies=[
Dependency("physical", "mass", "1", "kg", "range_min"),
Dependency("force", "specific_thrust", "6", "N/kg", "provides"),
],
)
storage = Entity(
name="Fuel", dimension="energy_storage",
dependencies=[Dependency("physical", "mass", "1", "kg", "range_min")],
)
resolver = ConstraintResolver()
result = resolver.resolve(Combination(entities=[platform, actuator, storage]))
assert not any("specific thrust" in v or "would need" in v for v in result.violations)
def test_propulsion_viability_skips_when_undeclared(bicycle, human_pedalling, food_calories):
"""Platforms/actuators that never declare min_effective_accel or
specific_thrust (most of the catalog, for now) must be unaffected."""
resolver = ConstraintResolver()
result = resolver.resolve(Combination(entities=[bicycle, human_pedalling, food_calories]))
assert not any("specific thrust" in v or "would need" in v for v in result.violations)
def test_agreement_key_reaches_valid_status(bicycle, human_pedalling, food_calories):
"""P2: medium/atmosphere are agreement keys, not supply/demand — a combo
with no other issues should reach 'valid', not get stuck at 'conditional'
forever because nothing 'provides' medium=ground."""
resolver = ConstraintResolver()
result = resolver.resolve(Combination(entities=[bicycle, human_pedalling, food_calories]))
assert result.status == "valid"
assert not any("medium" in w for w in result.warnings)