2026-07-12 01:50:40 +02:00
|
|
|
"""Tests for CRX Refined (cluster-then-infer)."""
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from bex.crx_refined import crx_refined, crx_with_confidence, _cluster_by_structure
|
|
|
|
|
from bex.crx import CRX
|
2026-07-12 23:46:25 +02:00
|
|
|
from bex.grammar import (
|
|
|
|
|
Symbol, Concat, Alt, Plus, Optional, Star, Epsilon, Empty,
|
|
|
|
|
match, alphabet,
|
|
|
|
|
)
|
2026-07-12 01:50:40 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestClusterByStructure:
|
|
|
|
|
def test_empty(self):
|
|
|
|
|
assert _cluster_by_structure([]) == {}
|
|
|
|
|
|
|
|
|
|
def test_single_sequence(self):
|
|
|
|
|
result = _cluster_by_structure([['a', 'b', 'c']])
|
|
|
|
|
assert len(result) == 1
|
|
|
|
|
assert ('a', 'c', 'short') in result
|
|
|
|
|
|
|
|
|
|
def test_same_start_end_same_length(self):
|
|
|
|
|
seqs = [['a', 'b', 'c'], ['a', 'd', 'c']]
|
|
|
|
|
result = _cluster_by_structure(seqs)
|
|
|
|
|
assert len(result) == 1
|
|
|
|
|
|
|
|
|
|
def test_different_start_end(self):
|
|
|
|
|
seqs = [['a', 'b', 'c'], ['x', 'y', 'z']]
|
|
|
|
|
result = _cluster_by_structure(seqs)
|
|
|
|
|
assert len(result) == 2
|
|
|
|
|
|
|
|
|
|
def test_length_buckets(self):
|
|
|
|
|
seqs = [
|
2026-07-12 23:46:25 +02:00
|
|
|
['a', 'b', 'a'],
|
|
|
|
|
['a', 'b', 'c', 'a'],
|
|
|
|
|
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'a'],
|
|
|
|
|
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'a'],
|
2026-07-12 01:50:40 +02:00
|
|
|
]
|
|
|
|
|
result = _cluster_by_structure(seqs)
|
|
|
|
|
keys = set(result.keys())
|
|
|
|
|
assert ('a', 'a', 'short') in keys
|
|
|
|
|
assert ('a', 'a', 'med') in keys
|
|
|
|
|
assert ('a', 'a', 'long') in keys
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCrxRefined:
|
|
|
|
|
def test_empty(self):
|
2026-07-12 23:46:25 +02:00
|
|
|
assert isinstance(crx_refined([]), Epsilon)
|
2026-07-12 01:50:40 +02:00
|
|
|
|
|
|
|
|
def test_single_sequence(self):
|
|
|
|
|
result = crx_refined([['a', 'b', 'c']])
|
|
|
|
|
assert result is not None
|
2026-07-12 23:46:25 +02:00
|
|
|
assert match(result, ['a', 'b', 'c'])
|
2026-07-12 01:50:40 +02:00
|
|
|
|
|
|
|
|
def test_identical_sequences(self):
|
|
|
|
|
seqs = [['a', 'b', 'c']] * 5
|
|
|
|
|
result = crx_refined(seqs)
|
2026-07-12 23:46:25 +02:00
|
|
|
alpha = alphabet(result)
|
|
|
|
|
assert 'a' in alpha
|
|
|
|
|
assert 'b' in alpha
|
2026-07-12 01:50:40 +02:00
|
|
|
|
|
|
|
|
def test_linear_pattern(self):
|
|
|
|
|
seqs = [['a', 'b', 'c'], ['a', 'b', 'c'], ['a', 'b', 'c']]
|
|
|
|
|
result = crx_refined(seqs)
|
|
|
|
|
assert result == CRX().infer(seqs)
|
|
|
|
|
|
|
|
|
|
def test_branching_pattern(self):
|
|
|
|
|
seqs = [['a', 'b', 'c'], ['a', 'd', 'e'], ['a', 'b', 'e']]
|
|
|
|
|
result = crx_refined(seqs)
|
2026-07-12 23:46:25 +02:00
|
|
|
assert result is not None
|
|
|
|
|
alpha = alphabet(result)
|
|
|
|
|
assert 'a' in alpha
|
2026-07-12 01:50:40 +02:00
|
|
|
|
|
|
|
|
def test_falls_back_to_standard(self):
|
|
|
|
|
seqs = [['a'], ['b'], ['c'], ['d']]
|
|
|
|
|
result = crx_refined(seqs, min_cluster=2)
|
|
|
|
|
assert result == CRX().infer(seqs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCrxWithConfidence:
|
|
|
|
|
def test_empty(self):
|
|
|
|
|
result = crx_with_confidence([])
|
2026-07-12 23:46:25 +02:00
|
|
|
assert isinstance(result['grammar'], Epsilon)
|
2026-07-12 01:50:40 +02:00
|
|
|
assert result['confidence'] == 1.0
|
|
|
|
|
assert result['n_clusters'] == 0
|
|
|
|
|
|
|
|
|
|
def test_returns_all_fields(self):
|
|
|
|
|
seqs = [['a', 'b', 'c'], ['a', 'b', 'c']]
|
|
|
|
|
result = crx_with_confidence(seqs)
|
|
|
|
|
assert 'grammar' in result
|
|
|
|
|
assert 'confidence' in result
|
|
|
|
|
assert 'n_clusters' in result
|
|
|
|
|
assert 'largest_cluster' in result
|
|
|
|
|
assert 'n_sequences' in result
|
|
|
|
|
|
|
|
|
|
def test_confidence_range(self):
|
|
|
|
|
seqs = [['a', 'b', 'c'], ['a', 'd', 'e']]
|
|
|
|
|
result = crx_with_confidence(seqs)
|
|
|
|
|
assert 0.0 <= result['confidence'] <= 1.0
|
|
|
|
|
|
|
|
|
|
def test_confident_when_tight(self):
|
|
|
|
|
seqs = [['a', 'b', 'c']] * 10
|
|
|
|
|
result = crx_with_confidence(seqs)
|
|
|
|
|
assert result['confidence'] >= 0.9
|
|
|
|
|
|
|
|
|
|
def test_less_confident_when_diverse(self):
|
|
|
|
|
seqs = [
|
|
|
|
|
['a', 'b', 'c', 'z'],
|
|
|
|
|
['a', 'x', 'y', 'z'],
|
|
|
|
|
['a', 'p', 'q', 'z'],
|
|
|
|
|
['a', 'r', 's', 'z'],
|
|
|
|
|
]
|
|
|
|
|
result = crx_with_confidence(seqs)
|
|
|
|
|
assert result['n_clusters'] >= 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestComparisonWithStandard:
|
|
|
|
|
"""Compare refined vs standard CRX on various inputs."""
|
|
|
|
|
|
|
|
|
|
def test_linear_same_result(self):
|
|
|
|
|
seqs = [['a', 'b', 'c']] * 5
|
|
|
|
|
assert crx_refined(seqs) == CRX().infer(seqs)
|
|
|
|
|
|
|
|
|
|
def test_single_symbol(self):
|
|
|
|
|
seqs = [['a']] * 5
|
2026-07-12 23:46:25 +02:00
|
|
|
result = crx_refined(seqs)
|
|
|
|
|
assert isinstance(result, Symbol) and result.value == 'a'
|
2026-07-12 01:50:40 +02:00
|
|
|
|
|
|
|
|
def test_two_symbols(self):
|
|
|
|
|
seqs = [['a', 'b']] * 5
|
2026-07-12 23:46:25 +02:00
|
|
|
result = crx_refined(seqs)
|
|
|
|
|
assert isinstance(result, Concat) and len(result.parts) == 2
|
2026-07-12 01:50:40 +02:00
|
|
|
|
|
|
|
|
def test_disjunction(self):
|
|
|
|
|
seqs = [['a', 'b'], ['a', 'c']]
|
|
|
|
|
result = crx_refined(seqs)
|
2026-07-12 23:46:25 +02:00
|
|
|
assert isinstance(result, Alt) or (isinstance(result, Concat) and any(isinstance(p, Alt) for p in result.parts))
|