"""MDL scoring for iDRegEx (Algorithm 4, arXiv 1004.2372).""" import math from .grammar import ( Concat, Alt, Plus, Optional, Star, Symbol, Epsilon, Empty, alphabet, count_words, lang_size, model_cost, match as grammar_match, _COUNT_CAP, ) def data_cost(expr, sequences): """MDL data cost: Σ_i log₂(|L_i(r)|) where |L_i(r)| is the number of words of length len(seq_i) accepted by the grammar. Lower cost = more specific grammar that still covers the data. Exact computation is capped at max_len=50 to prevent combinatorial explosion. Longer sequences use an alphabet-size upper bound. """ MAX_EXACT = 50 n = 2 * model_cost(expr) + 1 runtime_n = min(max(n, max((len(s) for s in sequences), default=0)), MAX_EXACT) lang_sizes = [count_words(expr, l) for l in range(runtime_n + 1)] alpha_size = len(alphabet(expr)) total_cost = 0.0 for seq in sequences: length = len(seq) if length <= runtime_n: ls = lang_sizes[length] if ls > 0: total_cost += math.log2(ls) else: total_cost += length * math.log2(max(alpha_size, 1)) else: total_cost += length * math.log2(max(alpha_size, 1)) return total_cost def lang_size_score(expr, sequences): """Language Size: Σ |L(r)|_len(seq) — sum of words at each sequence length. From Bex et al. (arXiv:1004.2372), Section 4.3.1, adapted for our setting where candidates have different n values. Counts words at exactly the lengths present in the input sequences. Lower is better — the grammar that accepts the fewest words at the observed lengths wins. Generic grammars like `info+` accept many words at each length; specific grammars like `a.b.c.d.e+` accept exactly one. """ if not sequences: return lang_size(expr, 2 * model_cost(expr) + 1) total = 0 for seq in sequences: length = len(seq) total += count_words(expr, length) if total >= _COUNT_CAP: return _COUNT_CAP return total def mdl_score(expr, sequences): """MDL = model cost + data cost. (Fallback, Bex et al. Section 4.3.2.)""" model = model_cost(expr) data = data_cost(expr, sequences) return model + data _SCORERS = { 'langsize': lang_size_score, 'mdl': mdl_score, } def score_grammar(expr, sequences, method='langsize'): """Score a grammar using the specified method. Args: expr: Grammar AST node. sequences: List of sequences (each a list of strings). method: 'langsize' (default, Bex et al.) or 'mdl' (fallback). Returns: Numeric score (lower is better). """ fn = _SCORERS.get(method) if fn is None: raise ValueError(f"Unknown scoring method '{method}'. Choose from: {list(_SCORERS)}") return fn(expr, sequences) # For backward compatibility class MDLScorer: def score(self, expr, sequences): return mdl_score(expr, sequences)