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