Standard CRX over-approximates when Hasse diagram is non-linear (24% of RAGSAK packages). Cluster-then-infer groups sequences by (first, last, length), infers per-cluster, picks largest cluster's grammar. Results on RAGSAK: Avg max disjunction: 2.8 → 1.7 (39% tighter) Packages improved: 6/10 Tradeoff: cluster granularity (too coarse = over-approximation, too fine = no generalization). Current: (first, last, length_bucket). Exports crx_refined() and crx_with_confidence() from bex package. 20 new tests. All 199 tests pass.
141 lines
4.7 KiB
Python
141 lines
4.7 KiB
Python
"""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
|
|
|
|
|
|
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)
|
|
# Both start with 'a', end with 'c', length 3 (short)
|
|
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 = [
|
|
['a', 'b', 'a'], # short
|
|
['a', 'b', 'c', 'a'], # short (different last)
|
|
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'a'], # med (8, same first/last)
|
|
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'a'], # long (9, same first/last)
|
|
]
|
|
result = _cluster_by_structure(seqs)
|
|
keys = set(result.keys())
|
|
# 'a'→'a' short, 'a'→'a' med, 'a'→'a' long = 3 clusters for same first/last
|
|
# 'a'→'a' short has 1 seq, med has 1, long has 1
|
|
assert ('a', 'a', 'short') in keys
|
|
assert ('a', 'a', 'med') in keys
|
|
assert ('a', 'a', 'long') in keys
|
|
|
|
|
|
class TestCrxRefined:
|
|
def test_empty(self):
|
|
assert crx_refined([]) == 'ε'
|
|
|
|
def test_single_sequence(self):
|
|
result = crx_refined([['a', 'b', 'c']])
|
|
assert result is not None
|
|
assert 'a' in result
|
|
|
|
def test_identical_sequences(self):
|
|
seqs = [['a', 'b', 'c']] * 5
|
|
result = crx_refined(seqs)
|
|
assert 'a' in result
|
|
assert 'b' in result
|
|
|
|
def test_linear_pattern(self):
|
|
seqs = [['a', 'b', 'c'], ['a', 'b', 'c'], ['a', 'b', 'c']]
|
|
result = crx_refined(seqs)
|
|
# All same → should be tight
|
|
assert result == CRX().infer(seqs)
|
|
|
|
def test_branching_pattern(self):
|
|
# Paper counterexample
|
|
seqs = [['a', 'b', 'c'], ['a', 'd', 'e'], ['a', 'b', 'e']]
|
|
result = crx_refined(seqs)
|
|
refined_g = crx_refined(seqs)
|
|
# Refined should produce SOMETHING (not crash)
|
|
assert refined_g is not None
|
|
assert 'a' in refined_g
|
|
|
|
def test_falls_back_to_standard(self):
|
|
# Very diverse sequences — no cluster large enough
|
|
seqs = [['a'], ['b'], ['c'], ['d']]
|
|
result = crx_refined(seqs, min_cluster=2)
|
|
# Should fall back to standard CRX
|
|
assert result == CRX().infer(seqs)
|
|
|
|
|
|
class TestCrxWithConfidence:
|
|
def test_empty(self):
|
|
result = crx_with_confidence([])
|
|
assert result['grammar'] == 'ε'
|
|
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):
|
|
# Many sequences sharing first/last but with different internals
|
|
seqs = [
|
|
['a', 'b', 'c', 'z'],
|
|
['a', 'x', 'y', 'z'],
|
|
['a', 'p', 'q', 'z'],
|
|
['a', 'r', 's', 'z'],
|
|
]
|
|
result = crx_with_confidence(seqs)
|
|
# All share first/last → one cluster, but internals differ
|
|
# Confidence depends on how many pairs the cluster grammar captures
|
|
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
|
|
assert crx_refined(seqs) == 'a'
|
|
|
|
def test_two_symbols(self):
|
|
seqs = [['a', 'b']] * 5
|
|
assert crx_refined(seqs) == 'a.b'
|
|
|
|
def test_disjunction(self):
|
|
seqs = [['a', 'b'], ['a', 'c']]
|
|
result = crx_refined(seqs)
|
|
# Should have a disjunction somewhere
|
|
assert '+' in result
|