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>
This commit is contained in:
@@ -35,7 +35,9 @@ def road_vehicle():
|
||||
description="Generic wheeled road vehicle",
|
||||
dependencies=[
|
||||
Dependency("environment", "ground_surface", "true", None, "requires"),
|
||||
Dependency("environment", "ground_surface", "true", None, "provides"),
|
||||
Dependency("environment", "gravity", "true", None, "requires"),
|
||||
Dependency("environment", "gravity", "true", None, "provides"),
|
||||
Dependency("physical", "mass", "36000", "kg", "range_max"),
|
||||
Dependency("physical", "mass", "50", "kg", "range_min"),
|
||||
Dependency("environment", "medium", "ground", None, "requires"),
|
||||
@@ -51,7 +53,9 @@ def bicycle():
|
||||
description="Two-wheeled human-scale vehicle",
|
||||
dependencies=[
|
||||
Dependency("environment", "ground_surface", "true", None, "requires"),
|
||||
Dependency("environment", "ground_surface", "true", None, "provides"),
|
||||
Dependency("environment", "gravity", "true", None, "requires"),
|
||||
Dependency("environment", "gravity", "true", None, "provides"),
|
||||
Dependency("physical", "mass", "30", "kg", "range_max"),
|
||||
Dependency("environment", "medium", "ground", None, "requires"),
|
||||
],
|
||||
|
||||
@@ -160,3 +160,231 @@ def test_domain_constraint_allows_matching_medium(bicycle, human_pedalling, food
|
||||
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", "m²", "range_max"),
|
||||
Dependency("physical", "footprint", "0.5", "m²", "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", "m²", "range_max"),
|
||||
Dependency("physical", "footprint", "10", "m²", "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", "m²", "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", "m²", "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)
|
||||
|
||||
32
tests/test_llm_parsing.py
Normal file
32
tests/test_llm_parsing.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""Tests for shared LLM response-parsing logic."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from physcom.llm.parsing import parse_metric_json, parse_verdict
|
||||
from physcom.models.domain import MetricBound
|
||||
|
||||
|
||||
def _bounds():
|
||||
return [
|
||||
MetricBound("power_density", weight=0.5, norm_min=1, norm_max=2000, unit="W/kg"),
|
||||
MetricBound("safety", weight=0.5, norm_min=0.0, norm_max=1.0, unit="0-1"),
|
||||
]
|
||||
|
||||
|
||||
def test_parse_metric_json_strips_fences():
|
||||
text = '```json\n{"power_density": 500.0, "safety": 0.7}\n```'
|
||||
result = parse_metric_json(text, _bounds())
|
||||
assert result == {"power_density": 500.0, "safety": 0.7}
|
||||
|
||||
|
||||
def test_parse_metric_json_falls_back_to_range_midpoint_on_invalid():
|
||||
result = parse_metric_json("not json", _bounds())
|
||||
assert result == {"power_density": 1000.5, "safety": 0.5}
|
||||
|
||||
|
||||
def test_parse_verdict_plausible():
|
||||
assert parse_verdict("blah blah\nVERDICT: PLAUSIBLE") is True
|
||||
|
||||
|
||||
def test_parse_verdict_implausible():
|
||||
assert parse_verdict("blah blah\nVERDICT: IMPLAUSIBLE") is False
|
||||
@@ -1,36 +1,10 @@
|
||||
"""Tests for the Ollama provider's parsing logic and registry wiring."""
|
||||
"""Tests for the Ollama provider's registry wiring."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from physcom.llm.providers.ollama import OllamaLLMProvider
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def provider():
|
||||
return OllamaLLMProvider()
|
||||
|
||||
|
||||
def test_parse_json_strips_fences(provider):
|
||||
text = '```json\n{"power_density": 500.0, "safety": 0.7}\n```'
|
||||
result = provider._parse_json(text, ["power_density", "safety"])
|
||||
assert result == {"power_density": 500.0, "safety": 0.7}
|
||||
|
||||
|
||||
def test_parse_json_falls_back_on_invalid(provider):
|
||||
result = provider._parse_json("not json", ["power_density", "safety"])
|
||||
assert result == {"power_density": 0.5, "safety": 0.5}
|
||||
|
||||
|
||||
def test_parse_verdict_plausible(provider):
|
||||
assert provider._parse_verdict("blah blah\nVERDICT: PLAUSIBLE") is True
|
||||
|
||||
|
||||
def test_parse_verdict_implausible(provider):
|
||||
assert provider._parse_verdict("blah blah\nVERDICT: IMPLAUSIBLE") is False
|
||||
|
||||
|
||||
def test_registry_builds_ollama_provider(monkeypatch):
|
||||
from physcom.llm.registry import build_llm_provider
|
||||
|
||||
|
||||
Reference in New Issue
Block a user