File size: 3,092 Bytes
1f71c7d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | """Tests for the multi-expert Kuramoto attractor."""
from __future__ import annotations
import numpy as np
import pytest
from ensemble import Expert, ExpertOscillator, ExpertKuramotoAttractor
QA_A = [
("what is cats", "cats are animals that purr"),
("what is dogs", "dogs are animals that bark"),
] * 3
QA_B = [
("what is cats", "cats are felines with whiskers"),
("what is lions", "lions are big felines"),
] * 3
@pytest.fixture
def two_experts():
a = Expert.from_qa_pairs(QA_A, domain="a", D=2000)
b = Expert.from_qa_pairs(QA_B, domain="b", D=2000)
return a, b
class TestOscillator:
def test_prepare_sets_state(self, two_experts):
a, _ = two_experts
osc = ExpertOscillator(a)
osc.prepare("what is cats")
assert -1.0 <= osc.relevance <= 1.0
assert 0.0 <= osc.natural_frequency <= 1.0
assert osc.candidate is not None
def test_signature_matches_expert(self, two_experts):
a, _ = two_experts
osc = ExpertOscillator(a)
assert osc.signature == a.signature_hv
def test_weight_settable(self, two_experts):
a, _ = two_experts
osc = ExpertOscillator(a, weight=2.5)
assert osc.weight == 2.5
class TestAttractor:
def test_single_expert_returns_candidate(self, two_experts):
a, _ = two_experts
osc = ExpertOscillator(a)
osc.prepare("what is cats")
attr = ExpertKuramotoAttractor()
result = attr.evolve([osc])
assert result.coherence == 1.0
assert result.n_with_candidates == 1
assert result.dominant_expert == osc.name
def test_two_experts_synthesize(self, two_experts):
a, b = two_experts
oscs = [ExpertOscillator(a), ExpertOscillator(b)]
for o in oscs:
o.prepare("what is cats")
attr = ExpertKuramotoAttractor(n_iterations=20)
result = attr.evolve(oscs)
assert result.n_with_candidates == 2
assert 0.0 <= result.coherence <= 1.0
assert len(result.oscillator_states) == 2
def test_empty_returns_gracefully(self):
attr = ExpertKuramotoAttractor()
# no oscillators -> returns a random HV, coherence 0
result = attr.evolve([])
assert result.n_experts == 0
assert result.coherence == 0.0
def test_no_candidate_experts_filtered(self, two_experts):
"""An expert with no candidate contributes nothing."""
a, _ = two_experts
osc = ExpertOscillator(a)
# don't prepare -> candidate is None
attr = ExpertKuramotoAttractor()
result = attr.evolve([osc])
assert result.n_with_candidates == 0
def test_weight_contributions_sum_to_one(self, two_experts):
a, b = two_experts
oscs = [ExpertOscillator(a), ExpertOscillator(b)]
for o in oscs:
o.prepare("what is cats")
attr = ExpertKuramotoAttractor(n_iterations=20)
result = attr.evolve(oscs)
total = sum(s.weight_contribution for s in result.oscillator_states)
assert abs(total - 1.0) < 1e-6
|