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