Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/jacobian/math/code_theory/_admission.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,14 @@
AdmissionDecision.KEEP,
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
),
OperationAdmission(
"code.dual_code.compute",
AdmissionDecision.KEEP,
"exact parity check matrix via null space computation over GF(p)",
),
OperationAdmission(
"code.syndrome.compute",
AdmissionDecision.KEEP,
"exact syndrome vector H*r^T mod p for received word decoding",
),
)
69 changes: 69 additions & 0 deletions src/jacobian/math/code_theory/_dual_operations.py
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)
Comment on lines +26 to +42

Copy link
Copy Markdown
Contributor

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() at src/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:

  1. Fractional entries are truncated by int(entry) % p (src/jacobian/math/code_theory/_dual_operations.py:39). For field_order=3, generator_matrix=((2, 1)) SymPy returns the vector (-1/2, 1); int(-1/2) is 0, giving the row (0, 1), and 2*0 + 1*1 = 1 != 0 mod 3, so G * H^T != 0.
  2. The rank over Q can exceed the rank over GF(p). For 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 returns parity_check_matrix=() with dual_dimension=0, while the true dual has dimension 1.

The correct approach is Gaussian elimination modulo p (the repository already has _matrix_rank_mod_prime in src/jacobian/math/code_theory/_models.py:34-69) or SymPy's Matrix.nullspace over a GF(p) domain / DomainMatrix with GF(p).

Prompt for agents
compute_dual_code in src/jacobian/math/code_theory/_dual_operations.py uses SymPy's rational nullspace (mat.nullspace()) and then reduces the entries mod p with int(entry) % p. This is mathematically incorrect: (a) nullspace basis vectors generally contain Rationals, and int() truncates them, producing rows that are not orthogonal to the generator matrix; (b) the rank over Q may be larger than the rank over GF(p), so the rational nullspace can be empty or too small, understating dual_dimension and returning an incomplete/empty parity check matrix. Compute the kernel over GF(p) instead — e.g. Gaussian elimination mod p (the domain already has _matrix_rank_mod_prime in _models.py that can be extended to return a kernel basis) or a SymPy DomainMatrix over GF(p). Add a defining-invariant test asserting G * H^T == 0 mod p and dual_dimension == n - rank_p(G), including a case where the mod-p rank drops (e.g. p=3, G=((1,2),(2,1))) and a case producing fractional rational basis vectors (e.g. p=3, G=((2,1),)).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 (k = len(rows) at src/jacobian/math/code_theory/_dual_operations.py:21) instead of the number of independent rows, so a matrix with redundant rows reports a larger code than it actually is.
Impact: Callers receive an inflated code dimension for generator matrices whose rows are linearly dependent.

Dimension must be the rank over GF(p)

The request validator (src/jacobian/math/code_theory/_models.py:138-155) does not require the generator rows to be independent, so e.g. field_order=2, generator_matrix=((1,1),(1,1)) yields code_dimension=2 while the code has dimension 1. The domain already provides _matrix_rank_mod_prime (src/jacobian/math/code_theory/_models.py:34-69); either use it for code_dimension or reject dependent rows in the validator.

Prompt for agents
compute_dual_code reports code_dimension as the number of generator rows, but the mathematical dimension of the code is the rank of the generator matrix over GF(p). DualCodeRequest does not require the rows to be independent, so dependent rows (e.g. p=2, ((1,1),(1,1))) produce a wrong dimension. Fix by computing the rank mod p (see _matrix_rank_mod_prime in src/jacobian/math/code_theory/_models.py) for code_dimension, or by validating in DualCodeRequest that rows are independent over GF(p).
Open in Devin Review

Was 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),
)
74 changes: 74 additions & 0 deletions src/jacobian/math/code_theory/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 (Field(min_length=1) at src/jacobian/math/code_theory/_models.py:136 and :172-173), unlike every other operation in this domain, so arbitrarily large exact computations are accepted.
Impact: A single request can trigger unbounded exact linear algebra work in the server.

Boundedness is a stated proof obligation

AGENTS.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: LinearCodeRequest and CoveringRadiusRequest use max_length=8 rows plus width and enumeration bounds (src/jacobian/math/code_theory/_models.py:76, :100-118, :22-31). DualCodeRequest.generator_matrix, SyndromeRequest.parity_check_matrix, and SyndromeRequest.received_word have only min_length=1, so row count and row width are unbounded, and the row-width check for the generator matrix is duplicated without the 256-entry cap of _validate_prime_field_matrix.

Open in Devin Review

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 src/jacobian/math/code_theory/_models.py:186-190), so out-of-range or negative entries are accepted as if they were canonical.
Impact: Callers can pass a matrix that is not over the declared field and still receive a syndrome presented as exact.

Request must encode the advertised mathematical domain

DualCodeRequest validates entries with not 0 <= entry < self.field_order (src/jacobian/math/code_theory/_models.py:149-154), and _validate_prime_field_matrix does the same for the other code-theory operations (src/jacobian/math/code_theory/_models.py:29-30). SyndromeRequest.require_valid_request only validates row lengths and the received word, leaving parity_check_matrix entries unconstrained; compute_syndrome then multiplies them directly (src/jacobian/math/code_theory/_dual_operations.py:63), reducing only the final sum mod p, so a non-canonical input silently yields a result labeled exact over GF(p). AGENTS.md requires the request to encode the advertised mathematical domain, not only the JSON shape.

Suggested change
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
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 any(
not 0 <= entry < self.field_order
for row in self.parity_check_matrix
for entry in row
):
raise ValueError(
"parity check entries must be canonical field residues"
)
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
Open in Devin Review

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, ...]
62 changes: 62 additions & 0 deletions src/jacobian/math/code_theory/_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@
from jacobian._models import StrictModel
from jacobian.catalog._examples import example
from jacobian.catalog.models import MathTool, OperationExample
from jacobian.math.code_theory._dual_operations import (
compute_dual_code,
compute_syndrome,
)
from jacobian.math.code_theory._models import (
CoveringRadiusRequest,
CoveringRadiusResult,
DualCodeRequest,
DualCodeResult,
LinearCodeRequest,
MinimumDistanceResult,
SyndromeRequest,
SyndromeResult,
WeightDistributionResult,
)
from jacobian.math.code_theory._operations import (
Expand Down Expand Up @@ -99,6 +107,60 @@ def ct_operation[RequestT: StrictModel, ResultT: StrictModel](
),
),
),
ct_operation(
"code.dual_code.compute",
"Compute the dual code",
"Compute the parity check matrix (dual code) from a "
"generator matrix over a prime field GF(p), using exact null "
"space computation.",
DualCodeRequest,
DualCodeResult,
compute_dual_code,
"coding-theory",
"dual-code",
examples=(
example(
"hamming_7_4_generator",
"Compute the dual code of a [7,4] Hamming generator; "
"field_order must be prime and entries must be canonical.",
{
"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],
],
},
),
),
),
ct_operation(
"code.syndrome.compute",
"Compute the syndrome of a received word",
"Compute the syndrome vector H * r^T mod p for a "
"received word r under a parity check matrix H over GF(p).",
SyndromeRequest,
SyndromeResult,
compute_syndrome,
"coding-theory",
"syndrome",
examples=(
example(
"syndrome_of_correctable_error",
"Compute the syndrome of a received word; "
"field_order must be prime and word length must match columns.",
{
"field_order": 2,
"parity_check_matrix": [
[1, 1, 0],
[0, 1, 1],
],
"received_word": [1, 0, 1],
},
),
),
),
)

TOOLS = CODE_THEORY_OPERATIONS
Expand Down
8 changes: 8 additions & 0 deletions tests/catalog/operation_schema_snapshots/code_theory.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@
"input_schema": "sha256:9e533b134e5eb5811e8d8ec5dac191d471be05be818fb4aa8c566d2b889a8063",
"output_schema": "sha256:f8613705759bf28c2f022ce5fae00c230dce5a002a9d8539553eb0b3442b4aa4"
},
"code.dual_code.compute": {
"input_schema": "sha256:dbaecd7fa1f57a37cc77d3daf15f72436455c6093a6e8340e1fa1713c5d9dfcb",
"output_schema": "sha256:fc853ac42627d09b1c002407e57eb561b0352912eb3e7975f940b820a1886239"
},
"code.minimum_distance.compute": {
"input_schema": "sha256:fa50e6e300b9d68003f291057aee5e36dbd44d3f99372e52b8f82db34ba93a45",
"output_schema": "sha256:6276aa53796076a23a9e497c2f329a5f04383bcc4a056aa70ec100d2e50b568b"
},
"code.syndrome.compute": {
"input_schema": "sha256:8352edab16c1f6b419b1d9cedb0066d8b7d1f3fa3d83e8b5520fb6686b3f7748",
"output_schema": "sha256:b1e180cc1c0fb6040b61a3735a0ba0e040fa199e12d6dfb0b50e96c547f7ffd7"
},
"code.weight_distribution.compute": {
"input_schema": "sha256:fa50e6e300b9d68003f291057aee5e36dbd44d3f99372e52b8f82db34ba93a45",
"output_schema": "sha256:7ea8455e470ebb43fb2bf03b209082c139737397b54c72ff7825fee7ef0b0874"
Expand Down
6 changes: 3 additions & 3 deletions tests/catalog/test_admission.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ def test_every_frozen_candidate_has_exactly_one_admission_decision() -> None:
reviewed_ids = [record.operation_id for record in OPERATION_ADMISSIONS]

assert REVIEWED_BASE_REVISION == "61589543bbbff546edbc51d34a07887982fa4ad6"
assert len(candidate_ids) == len(set(candidate_ids)) == 399
assert len(candidate_ids) == len(set(candidate_ids)) == 401
assert reviewed_ids == sorted(reviewed_ids)
assert set(reviewed_ids) == set(candidate_ids)
assert all(record.rationale.strip() for record in OPERATION_ADMISSIONS)
assert Counter(record.decision for record in OPERATION_ADMISSIONS) == {
AdmissionDecision.KEEP: 239,
AdmissionDecision.KEEP: 241,
AdmissionDecision.NATIVE_ONLY: 56,
AdmissionDecision.DROP: 104,
}
Expand All @@ -46,7 +46,7 @@ def test_public_catalog_contains_only_admitted_atomic_operations() -> None:
}

assert {tool.operation_id for tool in BUILTIN_TOOLS} == expected
assert len(BUILTIN_TOOLS) == 239
assert len(BUILTIN_TOOLS) == 241


def test_catalog_construction_fails_closed_on_duplicate_candidates() -> None:
Expand Down
72 changes: 72 additions & 0 deletions tests/math/code_theory/test_dual_syndrome.py
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 G * H^T == 0 mod p for cases like p=3, G=((2,1),) or p=3, G=((1,2),(2,1)) would fail today.

Open in Devin Review

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)