Skip to content

Commit 1238225

Browse files
committed
feat(matrix): extend exact determinant to order 64
1 parent 73c6d17 commit 1238225

7 files changed

Lines changed: 175 additions & 19 deletions

File tree

src/jacobian/contracts/matrix_operations.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from jacobian.contracts.results import ContractModel
2020

2121
MAX_INPUT_SCALAR_DIGITS = 256
22+
MAX_DETERMINANT_MATRIX_DIMENSION = 64
2223

2324

2425
def _check_integer_digits(
@@ -74,23 +75,44 @@ def require_square(self) -> Self:
7475
return self
7576

7677

77-
class MatrixDeterminantRequest(ContractModel):
78-
"""One bounded square matrix whose exact determinant is requested."""
78+
class DeterminantRationalMatrix(ContractModel):
79+
"""One determinant-owned rational matrix bounded independently to order 64."""
7980

80-
matrix: RationalMatrix
81+
matrix_schema_version: Literal["1"] = "1"
82+
domain: Literal["QQ"] = "QQ"
83+
entries: tuple[tuple[CanonicalRational, ...], ...] = Field(
84+
min_length=1, max_length=MAX_DETERMINANT_MATRIX_DIMENSION
85+
)
8186

8287
@model_validator(mode="after")
83-
def require_square(self) -> Self:
84-
if len(self.matrix.entries) != len(self.matrix.entries[0]):
85-
raise ValueError("determinant computation requires a square matrix")
88+
def require_rectangular_nonempty_rows(self) -> Self:
89+
column_count = len(self.entries[0])
90+
if not 1 <= column_count <= MAX_DETERMINANT_MATRIX_DIMENSION:
91+
raise ValueError(
92+
"determinant matrix rows must contain between 1 and 64 entries"
93+
)
94+
if any(len(row) != column_count for row in self.entries):
95+
raise ValueError("determinant matrix rows must all have the same length")
8696
require_matrix_scalar_digits(
87-
self.matrix.entries,
97+
self.entries,
8898
maximum=MAX_INPUT_SCALAR_DIGITS,
8999
label="determinant input",
90100
)
91101
return self
92102

93103

104+
class MatrixDeterminantRequest(ContractModel):
105+
"""One square rational matrix of order at most 64."""
106+
107+
matrix: DeterminantRationalMatrix
108+
109+
@model_validator(mode="after")
110+
def require_square(self) -> Self:
111+
if len(self.matrix.entries) != len(self.matrix.entries[0]):
112+
raise ValueError("determinant computation requires a square matrix")
113+
return self
114+
115+
94116
class MatrixRankRequest(ContractModel):
95117
"""One bounded rectangular matrix whose exact rank is requested."""
96118

src/jacobian/domains/matrix_lattice/capabilities.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def implementation(request: RequestT) -> ResultT:
114114
matrix_operation(
115115
"matrix.determinant.compute",
116116
"Compute an exact rational matrix determinant",
117-
"Compute the determinant of one square matrix over QQ with SymPy's exact Bareiss algorithm.",
117+
"Compute the determinant of one square matrix over QQ through order 64 with SymPy's exact Bareiss algorithm.",
118118
MatrixDeterminantRequest,
119119
MatrixDeterminantResult,
120120
compute_determinant,
@@ -141,7 +141,7 @@ def implementation(request: RequestT) -> ResultT:
141141
},
142142
),
143143
),
144-
version="2",
144+
version="3",
145145
),
146146
matrix_operation(
147147
"matrix.rank.compute",

src/jacobian/domains/matrix_lattice/conversions.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from jacobian.canonical import format_canonical_integer, parse_canonical_integer
88
from jacobian.contracts.exact import CanonicalRational
99
from jacobian.contracts.matrices import IntegerMatrix, RationalMatrix
10+
from jacobian.contracts.matrix_operations import DeterminantRationalMatrix
1011

1112
__all__ = [
1213
"integer_matrix_from_sympy",
@@ -30,7 +31,9 @@ def rational_from_sympy(value: Any) -> CanonicalRational:
3031
)
3132

3233

33-
def rational_matrix_to_sympy(matrix: RationalMatrix) -> Any:
34+
def rational_matrix_to_sympy(
35+
matrix: RationalMatrix | DeterminantRationalMatrix,
36+
) -> Any:
3437
import sympy
3538

3639
return sympy.Matrix(

src/jacobian/math/matrices/_sympy.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@
88
from sympy.matrices.matrixbase import MatrixBase
99

1010

11-
def exact_matrix(value: MatrixBase) -> MatrixBase:
11+
def exact_matrix(
12+
value: MatrixBase, *, maximum_dimension: int = 32
13+
) -> MatrixBase:
1214
if not isinstance(value, MatrixBase):
1315
raise TypeError("matrix must be a SymPy MatrixBase")
14-
if not 1 <= value.rows <= 32 or not 1 <= value.cols <= 32:
15-
raise ValueError("matrix dimensions must be between 1 and 32")
16+
if (
17+
not 1 <= value.rows <= maximum_dimension
18+
or not 1 <= value.cols <= maximum_dimension
19+
):
20+
raise ValueError(
21+
f"matrix dimensions must be between 1 and {maximum_dimension}"
22+
)
1623
if any(not entry.is_number or entry.is_finite is not True for entry in value):
1724
raise ValueError("matrix entries must be finite exact numbers")
1825
if any(entry.has(sympy.Float) for entry in value):
@@ -53,7 +60,7 @@ def characteristic_polynomial(matrix: MatrixBase, variable: str) -> Any:
5360

5461

5562
def determinant(matrix: MatrixBase) -> Any:
56-
source = exact_matrix(matrix)
63+
source = exact_matrix(matrix, maximum_dimension=64)
5764
if source.rows != source.cols:
5865
raise ValueError("determinant requires a square matrix")
5966
return source.det(method="bareiss")

src/jacobian_checkers/exact_domain_operations.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
_MAX_RESIDUE_ASSIGNMENTS = 4_096
2828
_MAX_RESIDUE_MODULUS = 1_000_000
2929
_MAX_MATRIX_DIMENSION = 32
30+
_MAX_DETERMINANT_MATRIX_DIMENSION = 64
3031
_MAX_MATRIX_INPUT_DIGITS = 256
3132
_MAX_MATRIX_OUTPUT_DIGITS = 32_768
3233

@@ -100,6 +101,18 @@ def _q(value: object) -> fmpq:
100101
return fmpq(numerator, denominator)
101102

102103

104+
def _bounded_q(value: object, *, maximum_digits: int) -> fmpq:
105+
if not isinstance(value, dict) or set(value) != {"num", "den"}:
106+
raise ValueError("bounded rational is malformed")
107+
for component in (value["num"], value["den"]):
108+
if (
109+
not isinstance(component, str)
110+
or len(component.lstrip("-")) > maximum_digits
111+
):
112+
raise ValueError("bounded rational exceeds the checker digit limit")
113+
return _q(value)
114+
115+
103116
def _polynomial(value: object) -> fmpq_poly:
104117
if (
105118
not isinstance(value, dict)
@@ -213,15 +226,20 @@ def _integer_matrix(value: object) -> fmpz_mat:
213226
return fmpz_mat([[_integer(item) for item in row] for row in entries])
214227

215228

216-
def _bounded_rational_matrix(value: object, *, maximum_digits: int) -> fmpq_mat:
229+
def _bounded_rational_matrix(
230+
value: object,
231+
*,
232+
maximum_digits: int,
233+
maximum_dimension: int = _MAX_MATRIX_DIMENSION,
234+
) -> fmpq_mat:
217235
if not isinstance(value, dict):
218236
raise ValueError("rational matrix is malformed")
219237
entries = value.get("entries")
220238
if (
221239
not isinstance(entries, list)
222-
or not 1 <= len(entries) <= _MAX_MATRIX_DIMENSION
240+
or not 1 <= len(entries) <= maximum_dimension
223241
or not isinstance(entries[0], list)
224-
or not 1 <= len(entries[0]) <= _MAX_MATRIX_DIMENSION
242+
or not 1 <= len(entries[0]) <= maximum_dimension
225243
):
226244
raise ValueError("rational matrix exceeds the checker dimension bound")
227245
for row in entries:
@@ -1008,10 +1026,19 @@ def _matrix_determinant(source: dict[str, Any], result: dict[str, Any]) -> bool:
10081026
"FRACTION_FREE_BAREISS"
10091027
):
10101028
return False
1011-
matrix = _matrix_source(source)
1029+
if set(source) != {"matrix"}:
1030+
return False
1031+
matrix = _bounded_rational_matrix(
1032+
source["matrix"],
1033+
maximum_digits=_MAX_MATRIX_INPUT_DIGITS,
1034+
maximum_dimension=_MAX_DETERMINANT_MATRIX_DIMENSION,
1035+
)
10121036
if matrix.nrows() != matrix.ncols():
10131037
return False
1014-
return bool(_q(result["determinant"]) == matrix.det())
1038+
declared = _bounded_q(
1039+
result["determinant"], maximum_digits=_MAX_MATRIX_OUTPUT_DIGITS
1040+
)
1041+
return bool(declared == matrix.det())
10151042

10161043

10171044
def check_matrix_determinant(request: dict[str, Any]) -> dict[str, Any]:
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Iterator
4+
from pathlib import Path
5+
6+
import pytest
7+
from tests.support.exact_domain import open_exact_domain_services
8+
from tests.support.services import DomainTestServices
9+
10+
from jacobian.contracts.capabilities import CapabilityRequest
11+
from jacobian.contracts.results import ExecutionStatus
12+
from jacobian.domains.matrix_lattice import build_matrix_bundle
13+
14+
15+
def _q(value: int) -> dict[str, str]:
16+
return {"num": str(value), "den": "1"}
17+
18+
19+
def _matrix(entries: list[list[int]]) -> dict[str, object]:
20+
return {"entries": [[_q(value) for value in row] for row in entries]}
21+
22+
23+
def _truncated_legendre_matrix(prime: int) -> dict[str, object]:
24+
order = (prime - 5) // 2
25+
26+
def entry(row: int, column: int) -> int:
27+
residue = (row - column) % prime
28+
if residue == 0:
29+
character = 0
30+
else:
31+
character = (
32+
1 if pow(residue, (prime - 1) // 2, prime) == 1 else -1
33+
)
34+
return 1 + character
35+
36+
return _matrix(
37+
[[entry(row, column) for column in range(order)] for row in range(order)]
38+
)
39+
40+
41+
@pytest.fixture
42+
def matrix_services(tmp_path: Path) -> Iterator[DomainTestServices]:
43+
with open_exact_domain_services(
44+
tmp_path / "state", build_matrix_bundle()
45+
) as services:
46+
yield services
47+
48+
49+
def test_order_33_determinant_computes_and_verifies(
50+
matrix_services: DomainTestServices,
51+
) -> None:
52+
payload = {"matrix": _truncated_legendre_matrix(71)}
53+
computed = matrix_services.core.capabilities.invoke(
54+
CapabilityRequest(capability_id="matrix.determinant.compute", input=payload)
55+
)
56+
verified = matrix_services.core.capabilities.invoke(
57+
CapabilityRequest(
58+
capability_id="matrix.determinant.verify",
59+
input={"input": payload, "candidate": computed.output["result"]},
60+
)
61+
)
62+
assert computed.execution.status is ExecutionStatus.COMPLETED
63+
assert computed.output["result"]["determinant"] == _q(529)
64+
assert verified.execution.status is ExecutionStatus.COMPLETED
65+
assert verified.output["status"] == "VERIFIED"
66+
assert verified.verification_record_uri is not None
67+
68+
69+
def test_determinant_rejects_order_above_64(
70+
matrix_services: DomainTestServices,
71+
) -> None:
72+
matrix = _matrix(
73+
[
74+
[1 if row == column else 0 for column in range(65)]
75+
for row in range(65)
76+
]
77+
)
78+
result = matrix_services.core.capabilities.invoke(
79+
CapabilityRequest(
80+
capability_id="matrix.determinant.compute",
81+
input={"matrix": matrix},
82+
)
83+
)
84+
assert result.execution.status is ExecutionStatus.ERROR
85+
assert result.artifact_uris == ()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from __future__ import annotations
2+
3+
from jacobian.contracts.matrix_operations import MatrixDeterminantRequest
4+
from jacobian.schema_registry import model_schema
5+
6+
7+
def test_determinant_schema_publishes_64_by_64_input_bound() -> None:
8+
schema = model_schema(MatrixDeterminantRequest)
9+
matrix = schema["$defs"]["DeterminantRationalMatrix"]
10+
assert matrix["properties"]["entries"]["maxItems"] == 64
11+
row = matrix["properties"]["entries"]["items"]
12+
assert row["maxItems"] == 64

0 commit comments

Comments
 (0)