From 35346a9d69bb12335c0b5bc7d2aa96af0b80d125 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 12:04:12 +0000 Subject: [PATCH] correction: Drop redundant BCH decoding paths Remove code from the substitution and erasure decoder that cannot change a result. Every BCH correction was re-checked against the full residue. The check cannot fail. The generator is the product of the distinct minimal polynomials of the 8 consecutive roots. GF(32) values that clear those syndromes also clear the conjugate roots, so the generator divides the corrected word. The linear erasure solver already rejects inconsistent systems. Delete _corrections_reach_target. The linear solver ran after a failed BCH decode even with at most 8 erasures. In that case an erasure-only solution leaves the Forney syndromes zero, so BCH would already have found it. Call the solver only when the erasures exceed the BCH roots. The worksheet path now makes the same choice inline, so _error_corrections is gone. Its word-length and substitution-limit parameters were unused. The precomputed erasure-product tables were never built. Remove that plumbing from _locator_poly, _bch_syndrome_corrections, _repair_body and _FixedCorrector. Also drop the no-op "except TypeError: raise" clauses. Keep the direct small-locator solve. Replacing it with Berlekamp-Massey for every call returned identical results but made the bounded indel search about 15% slower, which could change truncation under the ten- second deadline. Validation: full pytest in normal and -O modes; differential_correction and differential_wallet --verify; verify_correction_constants; ruff check, ruff format --check and mypy. An offline comparison against the previous module found identical results for random BCH syndromes, damaged ms strings with up to 14 errors or erasures, and worksheet residues with up to 15 erasures. --- src/codex32/correction.py | 143 ++++++++++---------------------------- 1 file changed, 37 insertions(+), 106 deletions(-) diff --git a/src/codex32/correction.py b/src/codex32/correction.py index 4a1a5616..d65f3981 100644 --- a/src/codex32/correction.py +++ b/src/codex32/correction.py @@ -376,28 +376,15 @@ def _small_locator(sequence: list[int], maximum: int) -> list[int] | None: return [1, *coefficients] if len(coefficients) <= maximum else None -def _locator_poly( - syndromes: list[int], - erasure_poly: list[int], - maximum: int | None, - erasure_products: tuple[tuple[int, ...], ...] | None = None, -) -> list[int] | None: +def _locator_poly(syndromes: list[int], erasure_poly: list[int], maximum: int | None) -> list[int] | None: + """Locate substitutions from Forney syndromes, which hide the known erasures.""" degree = len(erasure_poly) - 1 - if degree == 2 and erasure_products is not None: - first, second = erasure_products - modified = [ - syndromes[index - 2] ^ first[syndromes[index]] ^ second[syndromes[index - 1]] - for index in range(2, len(syndromes)) - ] - else: - modified = [] - for output_index in range(degree, len(syndromes)): - value = syndromes[output_index - degree] - products = erasure_products - for index, coefficient in enumerate(erasure_poly[:-1]): - syndrome = syndromes[output_index - index] - value ^= _gf1024_mul(coefficient, syndrome) if products is None else products[index][syndrome] - modified.append(value) + modified = [] + for output_index in range(degree, len(syndromes)): + value = syndromes[output_index - degree] + for index, coefficient in enumerate(erasure_poly[:-1]): + value ^= _gf1024_mul(coefficient, syndromes[output_index - index]) + modified.append(value) if maximum is not None and maximum <= 2: return _small_locator(modified, maximum) coefficients, _adjustment = _synthesize_rec(list(reversed(modified))) @@ -425,13 +412,17 @@ def _bch_syndrome_corrections( word_length: int, max_substitutions: int | None, erasure_state: tuple[tuple[int, ...], list[int]] | None = None, - erasure_products: tuple[tuple[int, ...], ...] | None = None, ) -> list[tuple[int, int]] | None: + """Fill erasures and locate substitutions: locator, Chien search, then Forney. + + GF(32) values that clear these 8 syndromes also clear their conjugates, which are the + remaining roots of the generator, so every returned correction reaches the target. + """ word_roots = _word_roots(spec, word_length) erasure_roots, erasure_poly = ( _erasure_state(spec, word_length, tuple(erasure_indices)) if erasure_state is None else erasure_state ) - locator = _locator_poly(syndromes, erasure_poly, max_substitutions, erasure_products) + locator = _locator_poly(syndromes, erasure_poly, max_substitutions) if locator is None or max_substitutions is not None and len(locator) - 1 > max_substitutions: return None errors = [ @@ -465,45 +456,30 @@ def _repair_body( alignment: tuple[int, tuple[tuple[int, ...], ...]], erasure_indices: Sequence[int] | None, erasure_state: tuple[tuple[int, ...], list[int]] | None, - erasure_products: tuple[tuple[int, ...], ...] | None, packed_syndromes: int | None = None, ) -> tuple[list[int], list[tuple[int, int]]] | None: erasures = ( - tuple(index for index, value in enumerate(reversed(body)) if value < 0) + [index for index, value in enumerate(reversed(body)) if value < 0] if erasure_indices is None - else erasure_indices + else list(erasure_indices) ) - result = ( - _bch_syndrome_corrections( - spec, - list(erasures), - ( - _aligned_syndromes(alignment, body, len(spec.roots)) - if packed_syndromes is None - else [(packed_syndromes >> (10 * i)) & 1023 for i in range(len(spec.roots))] - ), - len(body), - max_substitutions, - erasure_state, - erasure_products, + if len(erasures) <= len(spec.roots): + syndromes = ( + _aligned_syndromes(alignment, body, len(spec.roots)) + if packed_syndromes is None + else [(packed_syndromes >> (10 * i)) & 1023 for i in range(len(spec.roots))] ) - if len(erasures) <= len(spec.roots) - else None - ) - if result is None and max_substitutions is not None: - return None - zeroed = [max(value, 0) for value in body] - residue: list[int] | None = None - if result is not None: - residue = _residue(spec, hrp, zeroed) - if not _corrections_reach_target(spec, residue, result): - result = None - if result is None and max_substitutions is None: - residue = _residue(spec, hrp, zeroed) if residue is None else residue - result = _linear_error_corrections(spec, list(erasures), residue) - if result is not None and not _corrections_reach_target(spec, residue, result): - result = None - return None if result is None else (zeroed, result) + result = _bch_syndrome_corrections( + spec, erasures, syndromes, len(body), max_substitutions, erasure_state + ) + elif max_substitutions is None: + # More erasures than BCH roots: solve the residue directly for erasures only. + result = _linear_error_corrections( + spec, erasures, _residue(spec, hrp, [max(value, 0) for value in body]) + ) + else: + result = None + return None if result is None else ([max(value, 0) for value in body], result) def _capture_volume(mutable_symbols: int, erasures: int, substitutions: int) -> int: @@ -520,7 +496,6 @@ class _FixedCorrector: __slots__ = ( "alignment", "erasure_indices", - "erasure_products", "erasure_state", "hrp", "max_substitutions", @@ -551,7 +526,6 @@ def __init__( self.max_substitutions = max_substitutions self.mutable_start = mutable_start self.erasure_indices: tuple[int, ...] | None = None - self.erasure_products: tuple[tuple[int, ...], ...] | None = None self.erasure_state: tuple[tuple[int, ...], list[int]] | None = None def correct( @@ -569,9 +543,6 @@ def correct( return None self.erasure_indices = tuple(erasure_indices) self.erasure_state = _erasure_state(self.spec, len(values), self.erasure_indices) - # Alignment locations change frequently. Building 2048 products per - # pair costs more than the handful of GF(1024) multiplies per call. - self.erasure_products = None repair = _repair_body( self.spec, self.hrp, @@ -580,7 +551,6 @@ def correct( self.alignment, erasure_indices, self.erasure_state if erasure_indices is not None else None, - self.erasure_products if erasure_indices is not None else None, packed_syndromes, ) if repair is None: @@ -674,45 +644,6 @@ def _linear_error_corrections( return None if solution is None else list(zip(erasure_indices, solution)) -def _error_corrections( - spec: _Spec, - erasure_indices: list[int], - residue: list[int], - word_length: int | None = None, - max_substitutions: int | None = None, -) -> list[tuple[int, int]] | None: - length = spec.period if word_length is None else word_length - bch = None - if len(erasure_indices) <= len(spec.roots): - bch = _bch_syndrome_corrections( - spec, - erasure_indices, - list(_syndromes(spec, residue, target=True)), - length, - max_substitutions, - ) - if bch is not None and _corrections_reach_target(spec, residue, bch): - return bch - if max_substitutions is not None and max_substitutions > 0: - return None - if len(erasure_indices) > len(spec.generator): - return None - linear = _linear_error_corrections(spec, erasure_indices, residue) - return linear if linear is not None and _corrections_reach_target(spec, residue, linear) else None - - -def _corrections_reach_target(spec: _Spec, residue: list[int], corrections: list[tuple[int, int]]) -> bool: - count = max((index for index, _addend in corrections), default=-1) + 1 - powers = _poly_powers(spec.generator, count) - corrected = list(residue) - for reverse_index, addend in corrections: - corrected = _poly_sum( - corrected, - [_gf32_multiply(addend, value) for value in powers[reverse_index]], - ) - return corrected == list(reversed(spec.target)) - - def _correct_fixed( damaged_text: str, *, @@ -725,8 +656,6 @@ def _correct_fixed( hrp = suspected_profile.value if isinstance(suspected_profile, Profile) else suspected_profile.lower() try: uppercase = _validate_single_case_ascii(damaged_text) - except TypeError: - raise except CodexError: return None prefix = f"{hrp}1" @@ -910,8 +839,6 @@ def correct_worksheet_residue( try: _validate_single_case_ascii(residue) values = list(reversed(_chars_to_u5(residue))) - except TypeError: - raise except CodexError as error: raise InvalidCorrectionInput(str(error)) from error if len(values) == len(_SHORT_SPEC.generator): @@ -931,7 +858,11 @@ def correct_worksheet_residue( raise InvalidCorrectionInput("erasure indices must be distinct") if any(index < 0 or index >= spec.period for index in indices): raise InvalidCorrectionInput(f"erasure indices must be between 0 and {spec.period - 1}") - result = _error_corrections(spec, indices, values) + if len(indices) <= len(spec.roots): + syndromes = list(_syndromes(spec, values, target=True)) + result = _bch_syndrome_corrections(spec, indices, syndromes, spec.period, None) + else: + result = _linear_error_corrections(spec, indices, values) if result is None: return None return tuple(WorksheetCorrection(index, CHARSET[addend]) for index, addend in sorted(result))