115 lines
4.6 KiB
Python
115 lines
4.6 KiB
Python
"""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="actuator"),
|
|
])
|
|
combo.id = 1
|
|
raw = {"power_density": 500.0, "cost_efficiency": 5e-4, "safety": 0.7,
|
|
"availability": 0.8, "range_fuel": 400000}
|
|
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):
|
|
"""A zero on a higher-is-better metric should drive composite to 0."""
|
|
scorer = Scorer(urban_domain)
|
|
combo = Combination(entities=[])
|
|
combo.id = 1
|
|
raw = {"power_density": 500.0, "cost_efficiency": 5e-4, "safety": 0.0,
|
|
"availability": 0.8, "range_fuel": 400000}
|
|
result = scorer.score_combination(combo, raw)
|
|
assert result.composite_score == 0.0
|
|
|
|
def test_lower_is_better_inverts_score(self, urban_domain):
|
|
"""cost_efficiency is lower_is_better: low raw value should score high."""
|
|
scorer = Scorer(urban_domain)
|
|
combo = Combination(entities=[])
|
|
combo.id = 1
|
|
# cost_efficiency: norm_min=1e-5, norm_max=2e-3, lower_is_better=True
|
|
# A low cost (2e-5) should get a HIGH normalized score (near 1.0)
|
|
# A high cost (1.9e-3) should get a LOW normalized score (near 0.0)
|
|
raw_cheap = {"power_density": 500.0, "cost_efficiency": 2e-5, "safety": 0.7,
|
|
"availability": 0.8, "range_fuel": 400000}
|
|
raw_expensive = {"power_density": 500.0, "cost_efficiency": 1.9e-3, "safety": 0.7,
|
|
"availability": 0.8, "range_fuel": 400000}
|
|
result_cheap = scorer.score_combination(combo, raw_cheap)
|
|
result_expensive = scorer.score_combination(combo, raw_expensive)
|
|
|
|
# Find the cost_efficiency score in each
|
|
cost_cheap = next(s for s in result_cheap.scores if s.metric_name == "cost_efficiency")
|
|
cost_expensive = next(s for s in result_expensive.scores if s.metric_name == "cost_efficiency")
|
|
|
|
assert cost_cheap.normalized_score > cost_expensive.normalized_score
|
|
assert cost_cheap.normalized_score > 0.9 # near the best
|
|
assert cost_expensive.normalized_score < 0.1 # near the worst
|