-
Notifications
You must be signed in to change notification settings - Fork 9
feat(math): add dual code and syndrome operations using SymPy (#1851) #2026
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| """Dual code and syndrome operations for coding theory.""" | ||
|
|
||
| from jacobian.math.code_theory._models import ( | ||
| DualCodeRequest, | ||
| DualCodeResult, | ||
| SyndromeRequest, | ||
| SyndromeResult, | ||
| ) | ||
|
|
||
|
|
||
| def compute_dual_code(request: DualCodeRequest) -> DualCodeResult: | ||
| """Compute the dual code (parity check matrix) from a generator matrix. | ||
|
|
||
| Uses SymPy's null space computation over GF(p) to find the parity | ||
| check matrix H such that G * H^T = 0. | ||
| """ | ||
| from sympy import Matrix | ||
|
|
||
| p = request.field_order | ||
| rows = request.generator_matrix | ||
| k = len(rows) | ||
| n = len(rows[0]) | ||
|
|
||
| mat = Matrix(rows) | ||
|
|
||
| # Compute null space over GF(p) | ||
| null_space = mat.nullspace() | ||
|
|
||
| # Convert null space vectors to rows of H | ||
| if not null_space: | ||
| # Null space is trivial - shouldn't happen for k < n | ||
| parity_check: tuple[tuple[int, ...], ...] = () | ||
| else: | ||
| # Convert each null space vector to a tuple of residues mod p | ||
| parity_rows = [] | ||
| for vec in null_space: | ||
| row = [] | ||
| for entry in vec: | ||
| val = int(entry) % p | ||
| row.append(val) | ||
| parity_rows.append(tuple(row)) | ||
| parity_check = tuple(parity_rows) | ||
|
|
||
| return DualCodeResult( | ||
| field_order=p, | ||
| parity_check_matrix=parity_check, | ||
| code_dimension=k, | ||
| code_length=n, | ||
| dual_dimension=len(parity_check), | ||
| ) | ||
|
Comment on lines
+44
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Reported code dimension is wrong when generator rows are dependent The reported dimension of the code is taken as the number of supplied generator rows ( Dimension must be the rank over GF(p)The request validator ( Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
|
|
||
| def compute_syndrome(request: SyndromeRequest) -> SyndromeResult: | ||
| """Compute the syndrome H * r^T mod p for a received word.""" | ||
| p = request.field_order | ||
| h = request.parity_check_matrix | ||
| r = request.received_word | ||
| num_rows = len(h) | ||
| num_cols = len(r) | ||
|
|
||
| syndrome = [] | ||
| for i in range(num_rows): | ||
| s = sum(h[i][j] * r[j] for j in range(num_cols)) % p | ||
| syndrome.append(s) | ||
|
|
||
| return SyndromeResult( | ||
| field_order=p, | ||
| syndrome=tuple(syndrome), | ||
| ) | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -122,3 +122,77 @@ def require_bounded_syndrome_graph(self) -> Self: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class CoveringRadiusResult(StrictModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| covering_radius: int = Field(ge=0, le=256) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: Literal["SYNDROME_BFS"] = "SYNDROME_BFS" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Dual code operations | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class DualCodeRequest(StrictModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Compute the dual code (parity check matrix) from a generator matrix.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| field_order: int = Field(ge=2, le=251) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| generator_matrix: tuple[tuple[int, ...], ...] = Field(min_length=1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @model_validator(mode="after") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def require_valid_prime_field(self) -> Self: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from sympy import isprime | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not isprime(self.field_order): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError("field_order must be prime") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width = len(self.generator_matrix[0]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if width == 0: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError("generator rows must be nonempty") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if any(len(row) != width for row in self.generator_matrix): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError("generator rows must have equal length") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if any( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| not 0 <= entry < self.field_order | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for row in self.generator_matrix | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for entry in row | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError("entries must be canonical field residues") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return self | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class DualCodeResult(StrictModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """The dual code: parity check matrix (rows span the null space).""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| field_order: int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parity_check_matrix: tuple[tuple[int, ...], ...] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code_dimension: int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code_length: int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dual_dimension: int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class SyndromeRequest(StrictModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Compute the syndrome of a received word under a parity check matrix.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| field_order: int = Field(ge=2, le=251) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parity_check_matrix: tuple[tuple[int, ...], ...] = Field(min_length=1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| received_word: tuple[int, ...] = Field(min_length=1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+132
to
+173
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 New dual code and syndrome requests accept unbounded matrix sizes The new request models place no upper limit on how large the supplied matrices or word may be ( Boundedness is a stated proof obligationAGENTS.md requires each operation to separately bound accepted input, algorithmic work, and result ("Mathematical boundedness is a proof obligation"), and the sibling models enforce it: Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @model_validator(mode="after") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def require_valid_request(self) -> Self: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from sympy import isprime | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not isprime(self.field_order): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError("field_order must be prime") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cols = len(self.parity_check_matrix[0]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if any(len(row) != cols for row in self.parity_check_matrix): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError("parity check rows must have equal length") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(self.received_word) != cols: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError("received word length must match parity check columns") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for entry in self.received_word: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not 0 <= entry < self.field_order: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "received word entries must be canonical field residues" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return self | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+181
to
+191
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Syndrome request accepts parity check entries outside the field The syndrome request never checks that the parity check matrix entries are valid field values (only the received word is checked, at Request must encode the advertised mathematical domain
Suggested change
Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class SyndromeResult(StrictModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """The syndrome vector H * r^T mod p.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| field_order: int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| syndrome: tuple[int, ...] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| """Tests for dual code and syndrome operations.""" | ||
|
|
||
| from jacobian.math.code_theory._dual_operations import ( | ||
| compute_dual_code, | ||
| compute_syndrome, | ||
| ) | ||
| from jacobian.math.code_theory._models import DualCodeRequest, SyndromeRequest | ||
|
|
||
|
|
||
| def test_dual_hamming_7_4() -> None: | ||
| result = compute_dual_code( | ||
| DualCodeRequest( | ||
| field_order=2, | ||
| generator_matrix=( | ||
| (1, 0, 0, 0, 1, 1, 0), | ||
| (0, 1, 0, 0, 1, 0, 1), | ||
| (0, 0, 1, 0, 0, 1, 1), | ||
| (0, 0, 0, 1, 1, 1, 1), | ||
| ), | ||
| ) | ||
| ) | ||
| assert result.code_dimension == 4 | ||
| assert result.code_length == 7 | ||
| assert result.dual_dimension == 3 | ||
| assert len(result.parity_check_matrix) == 3 | ||
| assert len(result.parity_check_matrix[0]) == 7 | ||
|
|
||
|
|
||
| def test_dual_identity_matrix() -> None: | ||
| result = compute_dual_code( | ||
| DualCodeRequest( | ||
| field_order=2, | ||
| generator_matrix=((1, 0), (0, 1)), | ||
| ) | ||
| ) | ||
| assert result.code_dimension == 2 | ||
| assert result.code_length == 2 | ||
| assert result.dual_dimension == 0 | ||
|
Comment on lines
+10
to
+38
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Tests only cover cases where rational null space happens to agree with GF(p) The five added tests all use matrices whose rational null space happens to have integer entries and the same rank as over GF(p) (identity, and the [7,4] Hamming generator), so the fundamental mismatch between the rational null space and the GF(p) null space is invisible. AGENTS.md requires defining-invariant and adversarial tests; a test asserting Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
|
|
||
| def test_syndrome_zero() -> None: | ||
| result = compute_syndrome( | ||
| SyndromeRequest( | ||
| field_order=2, | ||
| parity_check_matrix=((1, 1, 0), (0, 1, 1)), | ||
| received_word=(0, 0, 0), | ||
| ) | ||
| ) | ||
| assert result.syndrome == (0, 0) | ||
|
|
||
|
|
||
| def test_syndrome_nonzero() -> None: | ||
| result = compute_syndrome( | ||
| SyndromeRequest( | ||
| field_order=2, | ||
| parity_check_matrix=((1, 1, 0), (0, 1, 1)), | ||
| received_word=(1, 0, 1), | ||
| ) | ||
| ) | ||
| assert result.syndrome == (1, 1) | ||
|
|
||
|
|
||
| def test_syndrome_mod_3() -> None: | ||
| result = compute_syndrome( | ||
| SyndromeRequest( | ||
| field_order=3, | ||
| parity_check_matrix=((1, 1), (0, 1)), | ||
| received_word=(2, 2), | ||
| ) | ||
| ) | ||
| # s = (1*2+1*2) mod 3 = 4 mod 3 = 1, (0*2+1*2) mod 3 = 2 | ||
| assert result.syndrome == (1, 2) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Dual code operation returns wrong parity check matrix for many inputs
The parity check rows are computed with ordinary fraction arithmetic and only afterwards reduced modulo the field size (
mat.nullspace()atsrc/jacobian/math/code_theory/_dual_operations.py:27), so the returned dual code is silently wrong whenever the fractions do not survive that reduction.Impact: Users asking for the dual code of a valid generator matrix can get rows that are not orthogonal to the code, or an empty answer where a real dual code exists.
Rational null space vs. null space over GF(p)
Matrix(rows).nullspace()computes a basis of the kernel over the rationals, not over GF(p). Two independent failure modes:int(entry) % p(src/jacobian/math/code_theory/_dual_operations.py:39). Forfield_order=3,generator_matrix=((2, 1))SymPy returns the vector(-1/2, 1);int(-1/2)is0, giving the row(0, 1), and2*0 + 1*1 = 1 != 0 mod 3, soG * H^T != 0.field_order=3,generator_matrix=((1, 2), (2, 1))the determinant is-3, nonzero over Q but zero mod 3, so the rational null space is empty and the operation returnsparity_check_matrix=()withdual_dimension=0, while the true dual has dimension 1.The correct approach is Gaussian elimination modulo p (the repository already has
_matrix_rank_mod_primeinsrc/jacobian/math/code_theory/_models.py:34-69) or SymPy'sMatrix.nullspaceover aGF(p)domain /DomainMatrixwithGF(p).Prompt for agents
Was this helpful? React with 👍 or 👎 to provide feedback.