Add Flask web UI, Docker Compose, core engine + tests
- physcom core: CLI, 5-pass pipeline, SQLite repo, 37 tests - physcom_web: Flask app with HTMX for entity/domain/pipeline/results CRUD - Docker Compose: web + cli services sharing a named volume for the DB - Clean up local settings to use wildcard permissions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
158
tests/conftest.py
Normal file
158
tests/conftest.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""Shared fixtures for tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
|
||||
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, MetricBound
|
||||
from physcom.models.combination import Combination
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def repo(tmp_path):
|
||||
"""Fresh in-memory repository for each test."""
|
||||
db_path = tmp_path / "test.db"
|
||||
conn = init_db(db_path)
|
||||
return Repository(conn)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def seeded_repo(repo):
|
||||
"""Repository pre-loaded with transport seed data."""
|
||||
from physcom.seed.transport_example import load_transport_seed
|
||||
load_transport_seed(repo)
|
||||
return repo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def walking():
|
||||
return Entity(
|
||||
name="Walking",
|
||||
dimension="platform",
|
||||
description="Bipedal locomotion",
|
||||
dependencies=[
|
||||
Dependency("environment", "ground_surface", "true", None, "requires"),
|
||||
Dependency("environment", "gravity", "true", None, "requires"),
|
||||
Dependency("physical", "mass_kg", "150", "kg", "range_max"),
|
||||
Dependency("force", "force_required_watts", "75", "watts", "range_min"),
|
||||
Dependency("environment", "medium", "ground", None, "requires"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bicycle():
|
||||
return Entity(
|
||||
name="Bicycle",
|
||||
dimension="platform",
|
||||
description="Two-wheeled human-scale vehicle",
|
||||
dependencies=[
|
||||
Dependency("environment", "ground_surface", "true", None, "requires"),
|
||||
Dependency("environment", "gravity", "true", None, "requires"),
|
||||
Dependency("physical", "mass_kg", "30", "kg", "range_max"),
|
||||
Dependency("force", "force_required_watts", "75", "watts", "range_min"),
|
||||
Dependency("environment", "medium", "ground", None, "requires"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spaceship():
|
||||
return Entity(
|
||||
name="Spaceship",
|
||||
dimension="platform",
|
||||
description="Vehicle designed for space travel",
|
||||
dependencies=[
|
||||
Dependency("environment", "atmosphere", "vacuum_or_thin", None, "requires"),
|
||||
Dependency("physical", "mass_kg", "5000", "kg", "range_min"),
|
||||
Dependency("force", "force_required_watts", "1000000", "watts", "range_min"),
|
||||
Dependency("environment", "medium", "space", None, "requires"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def solar_sail():
|
||||
return Entity(
|
||||
name="Solar Sail",
|
||||
dimension="power_source",
|
||||
description="Propulsion via radiation pressure",
|
||||
dependencies=[
|
||||
Dependency("environment", "atmosphere", "vacuum_or_thin", None, "requires"),
|
||||
Dependency("force", "force_output_watts", "1", "watts", "provides"),
|
||||
Dependency("environment", "medium", "space", None, "requires"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def human_pedalling():
|
||||
return Entity(
|
||||
name="Human Pedalling",
|
||||
dimension="power_source",
|
||||
description="Human-powered via pedalling",
|
||||
dependencies=[
|
||||
Dependency("force", "force_output_watts", "75", "watts", "provides"),
|
||||
Dependency("physical", "mass_kg", "0", "kg", "range_min"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def nuclear_reactor():
|
||||
return Entity(
|
||||
name="Modular Nuclear Reactor",
|
||||
dimension="power_source",
|
||||
description="Small modular nuclear fission reactor",
|
||||
dependencies=[
|
||||
Dependency("force", "force_output_watts", "50000000", "watts", "provides"),
|
||||
Dependency("physical", "mass_kg", "2000", "kg", "range_min"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hydrogen_engine():
|
||||
return Entity(
|
||||
name="Hydrogen Combustion Engine",
|
||||
dimension="power_source",
|
||||
description="Hydrogen fuel cell",
|
||||
dependencies=[
|
||||
Dependency("force", "force_output_watts", "80000", "watts", "provides"),
|
||||
Dependency("physical", "mass_kg", "30", "kg", "range_min"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ice_engine():
|
||||
return Entity(
|
||||
name="Internal Combustion Engine",
|
||||
dimension="power_source",
|
||||
description="Gas-powered engine",
|
||||
dependencies=[
|
||||
Dependency("force", "force_output_watts", "100000", "watts", "provides"),
|
||||
Dependency("environment", "atmosphere", "standard", None, "requires"),
|
||||
Dependency("physical", "mass_kg", "50", "kg", "range_min"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def urban_domain():
|
||||
return Domain(
|
||||
name="urban_commuting",
|
||||
description="Daily city travel",
|
||||
metric_bounds=[
|
||||
MetricBound("speed", weight=0.25, norm_min=5, norm_max=120),
|
||||
MetricBound("cost_efficiency", weight=0.25, norm_min=0.01, norm_max=2.0),
|
||||
MetricBound("safety", weight=0.25, norm_min=0.0, norm_max=1.0),
|
||||
MetricBound("availability", weight=0.15, norm_min=0.0, norm_max=1.0),
|
||||
MetricBound("range_fuel", weight=0.10, norm_min=5, norm_max=500),
|
||||
],
|
||||
)
|
||||
26
tests/test_combinator.py
Normal file
26
tests/test_combinator.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Tests for the Cartesian product combinator."""
|
||||
|
||||
import pytest
|
||||
|
||||
from physcom.engine.combinator import generate_combinations
|
||||
from physcom.models.entity import Entity
|
||||
|
||||
|
||||
def test_generates_cartesian_product(seeded_repo):
|
||||
combos = generate_combinations(seeded_repo, ["platform", "power_source"])
|
||||
# 9 platforms x 9 power sources = 81
|
||||
assert len(combos) == 81
|
||||
|
||||
|
||||
def test_each_combo_has_one_per_dimension(seeded_repo):
|
||||
combos = generate_combinations(seeded_repo, ["platform", "power_source"])
|
||||
for combo in combos:
|
||||
dims = [e.dimension for e in combo.entities]
|
||||
assert "platform" in dims
|
||||
assert "power_source" in dims
|
||||
assert len(combo.entities) == 2
|
||||
|
||||
|
||||
def test_missing_dimension_raises(seeded_repo):
|
||||
with pytest.raises(ValueError, match="No entities found"):
|
||||
generate_combinations(seeded_repo, ["platform", "nonexistent"])
|
||||
121
tests/test_constraint_resolver.py
Normal file
121
tests/test_constraint_resolver.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Tests for the constraint resolver engine."""
|
||||
|
||||
from physcom.engine.constraint_resolver import ConstraintResolver
|
||||
from physcom.models.combination import Combination
|
||||
from physcom.models.entity import Entity, Dependency
|
||||
|
||||
|
||||
def test_compatible_ground_combo(bicycle, human_pedalling):
|
||||
"""Bicycle + Human Pedalling should be valid."""
|
||||
resolver = ConstraintResolver()
|
||||
combo = Combination(entities=[bicycle, human_pedalling])
|
||||
result = resolver.resolve(combo)
|
||||
assert result.status != "blocked", f"Unexpected block: {result.violations}"
|
||||
|
||||
|
||||
def test_solar_sail_blocks_with_walking(walking, solar_sail):
|
||||
"""Walking (ground) + Solar Sail (space) should be blocked by medium mutex."""
|
||||
resolver = ConstraintResolver()
|
||||
combo = Combination(entities=[walking, solar_sail])
|
||||
result = resolver.resolve(combo)
|
||||
assert result.status == "blocked"
|
||||
assert any("mutually exclusive" in v for v in result.violations)
|
||||
|
||||
|
||||
def test_spaceship_compatible_with_solar_sail(spaceship, solar_sail):
|
||||
"""Spaceship + Solar Sail both need space/vacuum — should not conflict."""
|
||||
resolver = ConstraintResolver()
|
||||
combo = Combination(entities=[spaceship, solar_sail])
|
||||
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_reactor_blocks_with_bicycle(bicycle, nuclear_reactor):
|
||||
"""Nuclear reactor min_mass=2000kg vs bicycle max_mass=30kg → range incompatibility."""
|
||||
resolver = ConstraintResolver()
|
||||
combo = Combination(entities=[bicycle, nuclear_reactor])
|
||||
result = resolver.resolve(combo)
|
||||
assert result.status == "blocked"
|
||||
assert any("mass" in v.lower() for v in result.violations)
|
||||
|
||||
|
||||
def test_force_scale_mismatch_blocks():
|
||||
"""A platform needing 1MW and a power source providing 1W → force deficit block."""
|
||||
platform = Entity(
|
||||
name="HeavyPlatform", dimension="platform",
|
||||
dependencies=[
|
||||
Dependency("force", "force_required_watts", "1000000", "watts", "range_min"),
|
||||
],
|
||||
)
|
||||
power = Entity(
|
||||
name="TinyPower", dimension="power_source",
|
||||
dependencies=[
|
||||
Dependency("force", "force_output_watts", "1", "watts", "provides"),
|
||||
],
|
||||
)
|
||||
resolver = ConstraintResolver()
|
||||
combo = Combination(entities=[platform, power])
|
||||
result = resolver.resolve(combo)
|
||||
assert result.status == "blocked"
|
||||
assert any("force deficit" in v for v in result.violations)
|
||||
|
||||
|
||||
def test_force_under_powered_warning():
|
||||
"""Power source slightly below requirement → warning, not block."""
|
||||
platform = Entity(
|
||||
name="MedPlatform", dimension="platform",
|
||||
dependencies=[
|
||||
Dependency("force", "force_required_watts", "1000", "watts", "range_min"),
|
||||
],
|
||||
)
|
||||
power = Entity(
|
||||
name="WeakPower", dimension="power_source",
|
||||
dependencies=[
|
||||
Dependency("force", "force_output_watts", "500", "watts", "provides"),
|
||||
],
|
||||
)
|
||||
resolver = ConstraintResolver()
|
||||
combo = Combination(entities=[platform, power])
|
||||
result = resolver.resolve(combo)
|
||||
# Under-powered but within 100x → warning, not block
|
||||
assert result.status != "blocked"
|
||||
assert any("under-powered" in w for w in result.warnings)
|
||||
|
||||
|
||||
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="power_source",
|
||||
dependencies=[Dependency("environment", "oxygen", "true", None, "excludes")],
|
||||
)
|
||||
resolver = ConstraintResolver()
|
||||
combo = Combination(entities=[a, b])
|
||||
result = resolver.resolve(combo)
|
||||
assert result.status == "blocked"
|
||||
assert any("excludes" in v for v in result.violations)
|
||||
|
||||
|
||||
def test_ice_engine_blocks_with_spaceship(spaceship, ice_engine):
|
||||
"""ICE requires standard atmosphere, spaceship requires vacuum_or_thin → mutex."""
|
||||
resolver = ConstraintResolver()
|
||||
combo = Combination(entities=[spaceship, ice_engine])
|
||||
result = resolver.resolve(combo)
|
||||
assert result.status == "blocked"
|
||||
assert any("atmosphere" in v for v in result.violations)
|
||||
|
||||
|
||||
def test_hydrogen_bicycle_valid(bicycle, hydrogen_engine):
|
||||
"""Hydrogen bike — the README's example of a plausible novel concept."""
|
||||
resolver = ConstraintResolver()
|
||||
combo = Combination(entities=[bicycle, hydrogen_engine])
|
||||
result = resolver.resolve(combo)
|
||||
# Should pass constraints (mass range is compatible: h2 min 30kg, 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
|
||||
72
tests/test_pipeline.py
Normal file
72
tests/test_pipeline.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Tests for the multi-pass pipeline."""
|
||||
|
||||
from physcom.engine.constraint_resolver import ConstraintResolver
|
||||
from physcom.engine.scorer import Scorer
|
||||
from physcom.engine.pipeline import Pipeline
|
||||
from physcom.llm.providers.mock import MockLLMProvider
|
||||
|
||||
|
||||
def test_pass1_filters_impossible_combos(seeded_repo):
|
||||
"""Pass 1 should block known-impossible combinations (e.g., solar sail + walking)."""
|
||||
domain = seeded_repo.get_domain("urban_commuting")
|
||||
resolver = ConstraintResolver()
|
||||
scorer = Scorer(domain)
|
||||
pipeline = Pipeline(seeded_repo, resolver, scorer)
|
||||
|
||||
result = pipeline.run(domain, ["platform", "power_source"], passes=[1])
|
||||
|
||||
assert result.total_generated == 81
|
||||
assert result.pass1_blocked > 0
|
||||
assert result.pass1_valid + result.pass1_conditional + result.pass1_blocked == 81
|
||||
|
||||
|
||||
def test_pass123_produces_scored_results(seeded_repo):
|
||||
"""Passes 1-3 should produce a scored shortlist."""
|
||||
domain = seeded_repo.get_domain("urban_commuting")
|
||||
resolver = ConstraintResolver()
|
||||
scorer = Scorer(domain)
|
||||
pipeline = Pipeline(seeded_repo, resolver, scorer)
|
||||
|
||||
result = pipeline.run(
|
||||
domain, ["platform", "power_source"],
|
||||
score_threshold=0.01, passes=[1, 2, 3, 5],
|
||||
)
|
||||
|
||||
assert result.pass2_estimated > 0
|
||||
assert result.pass3_above_threshold > 0
|
||||
|
||||
|
||||
def test_pass4_with_mock_llm(seeded_repo):
|
||||
"""Pass 4 should annotate with LLM review."""
|
||||
domain = seeded_repo.get_domain("urban_commuting")
|
||||
resolver = ConstraintResolver()
|
||||
scorer = Scorer(domain)
|
||||
mock_llm = MockLLMProvider(default_estimates={
|
||||
"speed": 50.0, "cost_efficiency": 0.5, "safety": 0.6,
|
||||
"availability": 0.7, "range_fuel": 200.0,
|
||||
})
|
||||
pipeline = Pipeline(seeded_repo, resolver, scorer, llm=mock_llm)
|
||||
|
||||
result = pipeline.run(
|
||||
domain, ["platform", "power_source"],
|
||||
score_threshold=0.01, passes=[1, 2, 3, 4, 5],
|
||||
)
|
||||
|
||||
assert result.pass4_reviewed > 0
|
||||
|
||||
|
||||
def test_blocked_combos_not_scored(seeded_repo):
|
||||
"""Blocked combinations should not make it to scoring."""
|
||||
domain = seeded_repo.get_domain("urban_commuting")
|
||||
resolver = ConstraintResolver()
|
||||
scorer = Scorer(domain)
|
||||
pipeline = Pipeline(seeded_repo, resolver, scorer)
|
||||
|
||||
result = pipeline.run(
|
||||
domain, ["platform", "power_source"],
|
||||
score_threshold=0.0, passes=[1, 2, 3, 5],
|
||||
)
|
||||
|
||||
# Estimated count should be less than total (blocked ones filtered)
|
||||
assert result.pass2_estimated < result.total_generated
|
||||
assert result.pass2_estimated == result.pass1_valid + result.pass1_conditional
|
||||
88
tests/test_repository.py
Normal file
88
tests/test_repository.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""Tests for the database repository."""
|
||||
|
||||
from physcom.models.entity import Entity, Dependency
|
||||
from physcom.models.domain import Domain, MetricBound
|
||||
|
||||
|
||||
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="power_source"))
|
||||
|
||||
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_combination_save_and_dedup(repo):
|
||||
e1 = repo.add_entity(Entity(name="A", dimension="platform"))
|
||||
e2 = repo.add_entity(Entity(name="B", dimension="power_source"))
|
||||
|
||||
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):
|
||||
platforms = seeded_repo.list_entities(dimension="platform")
|
||||
power_sources = seeded_repo.list_entities(dimension="power_source")
|
||||
assert len(platforms) == 9
|
||||
assert len(power_sources) == 9
|
||||
|
||||
domains = seeded_repo.list_domains()
|
||||
assert len(domains) == 2
|
||||
90
tests/test_scorer.py
Normal file
90
tests/test_scorer.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Tests for the scoring engine."""
|
||||
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from physcom.engine.scorer import normalize, composite_score, Scorer
|
||||
from physcom.models.combination import Combination
|
||||
from physcom.models.entity import Entity
|
||||
|
||||
|
||||
class TestNormalize:
|
||||
def test_below_min_is_zero(self):
|
||||
assert normalize(3.0, 5.0, 120.0) == 0.0
|
||||
|
||||
def test_at_min_is_zero(self):
|
||||
assert normalize(5.0, 5.0, 120.0) == 0.0
|
||||
|
||||
def test_above_max_is_one(self):
|
||||
assert normalize(200.0, 5.0, 120.0) == 1.0
|
||||
|
||||
def test_at_max_is_one(self):
|
||||
assert normalize(120.0, 5.0, 120.0) == 1.0
|
||||
|
||||
def test_mid_range_is_between_zero_and_one(self):
|
||||
result = normalize(50.0, 5.0, 120.0)
|
||||
assert 0.0 < result < 1.0
|
||||
|
||||
def test_monotonically_increasing(self):
|
||||
"""Higher raw values should give higher normalized scores."""
|
||||
scores = [normalize(v, 5.0, 120.0) for v in [10, 30, 60, 90, 110]]
|
||||
for a, b in zip(scores, scores[1:]):
|
||||
assert a < b
|
||||
|
||||
def test_degenerate_bounds(self):
|
||||
"""norm_min >= norm_max should return 0."""
|
||||
assert normalize(50.0, 100.0, 50.0) == 0.0
|
||||
|
||||
def test_zero_min(self):
|
||||
"""norm_min=0 should work (log1p(0)=0)."""
|
||||
result = normalize(0.5, 0.0, 1.0)
|
||||
assert 0.0 < result < 1.0
|
||||
|
||||
|
||||
class TestCompositeScore:
|
||||
def test_all_ones(self):
|
||||
assert composite_score([1.0, 1.0, 1.0], [0.33, 0.33, 0.34]) == pytest.approx(1.0, abs=0.01)
|
||||
|
||||
def test_single_zero_kills(self):
|
||||
"""Any score of 0 should make composite 0."""
|
||||
assert composite_score([0.0, 1.0, 1.0], [0.33, 0.33, 0.34]) == 0.0
|
||||
|
||||
def test_empty(self):
|
||||
assert composite_score([], []) == 0.0
|
||||
|
||||
def test_low_score_dominates(self):
|
||||
"""A single low score should dramatically reduce the composite."""
|
||||
high = composite_score([0.9, 0.9, 0.9], [0.33, 0.33, 0.34])
|
||||
mixed = composite_score([0.9, 0.9, 0.1], [0.33, 0.33, 0.34])
|
||||
assert mixed < high * 0.5
|
||||
|
||||
def test_weighted_geometric_mean(self):
|
||||
"""Manual verification of weighted geometric mean."""
|
||||
# s = 0.5^0.5 * 0.8^0.5 = sqrt(0.5) * sqrt(0.8)
|
||||
expected = (0.5 ** 0.5) * (0.8 ** 0.5)
|
||||
assert composite_score([0.5, 0.8], [0.5, 0.5]) == pytest.approx(expected)
|
||||
|
||||
|
||||
class TestScorer:
|
||||
def test_scorer_produces_valid_result(self, urban_domain):
|
||||
scorer = Scorer(urban_domain)
|
||||
combo = Combination(entities=[
|
||||
Entity(name="Car", dimension="platform"),
|
||||
Entity(name="ICE", dimension="power_source"),
|
||||
])
|
||||
combo.id = 1
|
||||
raw = {"speed": 60.0, "cost_efficiency": 0.5, "safety": 0.7,
|
||||
"availability": 0.8, "range_fuel": 400}
|
||||
result = scorer.score_combination(combo, raw)
|
||||
assert 0.0 < result.composite_score <= 1.0
|
||||
assert len(result.scores) == 5
|
||||
|
||||
def test_scorer_zero_metric_kills_score(self, urban_domain):
|
||||
scorer = Scorer(urban_domain)
|
||||
combo = Combination(entities=[])
|
||||
combo.id = 1
|
||||
raw = {"speed": 60.0, "cost_efficiency": 0.0, "safety": 0.7,
|
||||
"availability": 0.8, "range_fuel": 400}
|
||||
result = scorer.score_combination(combo, raw)
|
||||
assert result.composite_score == 0.0
|
||||
Reference in New Issue
Block a user