"""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