smk-playground/tests/test_similarity.py

71 lines
3.3 KiB
Python

"""Focused invariants for exact similarity."""
import pytest
from smk_playground.similarity.engine import SimilarityEngine, compare
PROFILE = {
"name": "test",
"required_features": ["centroid_l", "centroid_a", "centroid_b", "spread"],
"components": [
{"name": "centroid", "fields": ["centroid_l", "centroid_a", "centroid_b"],
"metric": "euclidean", "scale": 0.5, "weight": 0.8},
{"name": "spread", "fields": ["spread"], "metric": "absolute",
"scale": 1.0, "weight": 0.15},
{"name": "hue", "fields": ["hue"], "metric": "absolute",
"scale": 1.0, "weight": 0.05},
],
}
def row(entity_id, l, spread, hue=0.5, geometry=0.2):
return {"smk_entity_id": entity_id, "object_number": entity_id, "centroid_l": l,
"centroid_a": 0.0, "centroid_b": 0.0, "spread": spread, "hue": hue,
"geometry": geometry, "quality_flags": []}
def test_self_distance_and_contribution_sum():
item = row("a", 0.5, 0.2)
result = compare(item, item, PROFILE)
assert result["total_distance"] == 0.0
assert sum(part["contribution"] for part in result["components"].values()) == 0.0
def test_symmetry_and_optional_component_renormalization():
left, right = row("a", 0.5, 0.2, None), row("b", 0.6, 0.4, 0.8)
forward, backward = compare(left, right, PROFILE), compare(right, left, PROFILE)
assert forward["total_distance"] == pytest.approx(backward["total_distance"])
assert forward["active_components"] == ["centroid", "spread"]
assert forward["active_weight_sum"] == pytest.approx(0.95)
assert sum(part["contribution"] for part in forward["components"].values()) == pytest.approx(forward["total_distance"])
def test_required_feature_and_geometry_eligibility():
missing = row("a", 0.5, None)
assert compare(missing, row("b", 0.6, 0.2), PROFILE) is None
geometry_profile = {"name": "geometry", "required_features": ["geometry"],
"components": [{"name": "geometry", "fields": ["geometry"],
"metric": "absolute", "scale": 1.0, "weight": 1.0}]}
assert compare(row("a", 0.5, 0.2, geometry=None), row("b", 0.6, 0.2), geometry_profile) is None
def test_exact_topk_stable_tie_breaking_and_achromatic_eligibility():
engine = SimilarityEngine.__new__(SimilarityEngine)
engine.rows = [row("q", 0.5, 0.2, None), row("b", 0.6, 0.2), row("a", 0.4, 0.2), row("z", 0.9, 0.9)]
engine.by_id = {item["smk_entity_id"]: item for item in engine.rows}
engine.by_object_number = {item["object_number"]: [item] for item in engine.rows}
engine.titles = {}
engine.profiles = {"color-similarity-v1": PROFILE}
first = engine.search_row(engine.rows[0], "color", 2)
second = engine.search_row(engine.rows[0], "color", 2)
assert [item["smk_entity_id"] for item in first] == ["a", "b"]
assert first == second
def test_object_number_ambiguity_requires_entity_id():
engine = SimilarityEngine.__new__(SimilarityEngine)
engine.by_id = {"a": row("a", 0.1, 0.1), "b": row("b", 0.2, 0.2)}
engine.by_object_number = {"shared": [engine.by_id["a"], engine.by_id["b"]]}
with pytest.raises(ValueError, match="Ambiguous"):
engine.resolve(object_number="shared")
assert engine.resolve(smk_entity_id="b")["smk_entity_id"] == "b"