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