Files
physicalCombinatorics/tests/test_constraint_resolver.py

163 lines
6.8 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"