Skip to content

Commit 266c17e

Browse files
committed
feat(math): add Latin square operations (#1887)
Add latin_squares_ops domain with 3 operations: Latin square verification, orthogonality check, and transpose. Uses exact combinatorial checks over bounded n x n matrices. Closes #1887
1 parent 2e54684 commit 266c17e

10 files changed

Lines changed: 354 additions & 3 deletions

File tree

src/jacobian/catalog/builtins.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,12 @@
170170
ADMISSIONS as IMPARTIAL_GAMES_ADMISSIONS,
171171
)
172172
from jacobian.math.impartial_games._tools import TOOLS as IMPARTIAL_GAMES_TOOLS
173+
from jacobian.math.latin_squares_ops._admission import (
174+
ADMISSIONS as LATIN_SQUARES_OPS_ADMISSIONS,
175+
)
176+
from jacobian.math.latin_squares_ops._tools import (
177+
TOOLS as LATIN_SQUARES_OPS_TOOLS,
178+
)
173179
from jacobian.math.lattices._admission import ADMISSIONS as LATTICES_ADMISSIONS
174180
from jacobian.math.lattices._tools import TOOLS as LATTICES_TOOLS
175181
from jacobian.math.logic._admission import ADMISSIONS as LOGIC_ADMISSIONS
@@ -307,6 +313,7 @@
307313
*SYMBOLIC_MATRIX_TOOLS,
308314
*RATIONAL_LINEAR_TOOLS,
309315
*LATTICES_TOOLS,
316+
*LATIN_SQUARES_OPS_TOOLS,
310317
*FORMAL_POWER_SERIES_TOOLS,
311318
*POLYNOMIAL_TOOLS,
312319
*MULTIVARIATE_POLYNOMIAL_TOOLS,
@@ -391,6 +398,7 @@
391398
*GROUP_ADMISSIONS,
392399
*IMPARTIAL_GAMES_ADMISSIONS,
393400
*LATTICES_ADMISSIONS,
401+
*LATIN_SQUARES_OPS_ADMISSIONS,
394402
*LOGIC_ADMISSIONS,
395403
*MARKOV_CHAIN_ADMISSIONS,
396404
*MATRICES_ADMISSIONS,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Latin square operations."""
2+
3+
__all__: list[str] = []
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Owner-local admission decisions for built-in math operations."""
2+
3+
from __future__ import annotations
4+
5+
from jacobian.catalog.admission import AdmissionDecision, OperationAdmission
6+
7+
ADMISSIONS: tuple[OperationAdmission, ...] = (
8+
OperationAdmission(
9+
"latin_square.check",
10+
AdmissionDecision.KEEP,
11+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
12+
),
13+
OperationAdmission(
14+
"latin_square.orthogonality.check",
15+
AdmissionDecision.KEEP,
16+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
17+
),
18+
OperationAdmission(
19+
"latin_square.transpose.compute",
20+
AdmissionDecision.KEEP,
21+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
22+
),
23+
)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Typed wire contracts for Latin square operations."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Self
6+
7+
from pydantic import Field, model_validator
8+
9+
from jacobian._models import StrictModel
10+
11+
MAX_N = 32
12+
13+
14+
class LatinSquare(StrictModel):
15+
"""A Latin square as an n x n matrix of symbols 0..n-1."""
16+
17+
order: int = Field(ge=1, le=MAX_N)
18+
cells: tuple[tuple[int, ...], ...] = Field(min_length=1, max_length=MAX_N)
19+
20+
@model_validator(mode="after")
21+
def require_valid(self) -> Self:
22+
if len(self.cells) != self.order:
23+
raise ValueError("cells must be order x order")
24+
if any(len(row) != self.order for row in self.cells):
25+
raise ValueError("cells must be a square matrix")
26+
if any(not 0 <= v < self.order for row in self.cells for v in row):
27+
raise ValueError("cell values must be in 0..order-1")
28+
return self
29+
30+
31+
class LatinSquareRequest(StrictModel):
32+
square: LatinSquare
33+
34+
35+
class OrthogonalityRequest(StrictModel):
36+
square_a: LatinSquare
37+
square_b: LatinSquare
38+
39+
@model_validator(mode="after")
40+
def require_valid(self) -> Self:
41+
if self.square_a.order != self.square_b.order:
42+
raise ValueError("squares must have the same order")
43+
return self
44+
45+
46+
# Results
47+
48+
49+
class LatinSquareCheckResult(StrictModel):
50+
is_latin: bool
51+
method: str = "ROW_COLUMN_SYMBOL_UNIQUENESS"
52+
53+
54+
class OrthogonalityResult(StrictModel):
55+
is_orthogonal: bool
56+
pair_count: int = Field(ge=0)
57+
method: str = "ORDERED_PAIR_UNIQUENESS"
58+
59+
60+
class LatinSquareTransposeResult(StrictModel):
61+
transposed: tuple[tuple[int, ...], ...]
62+
method: str = "MATRIX_TRANSPOSE"
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Domain functions for Latin square operations."""
2+
3+
from __future__ import annotations
4+
5+
from jacobian.math.latin_squares_ops._models import (
6+
LatinSquareCheckResult,
7+
LatinSquareRequest,
8+
LatinSquareTransposeResult,
9+
OrthogonalityRequest,
10+
OrthogonalityResult,
11+
)
12+
13+
14+
def compute_latin_square_check(request: LatinSquareRequest) -> LatinSquareCheckResult:
15+
"""Check if a matrix is a Latin square."""
16+
n = request.square.order
17+
cells = request.square.cells
18+
for i in range(n):
19+
if len(set(cells[i])) != n:
20+
return LatinSquareCheckResult(is_latin=False)
21+
col = set()
22+
for j in range(n):
23+
col.add(cells[j][i])
24+
if len(col) != n:
25+
return LatinSquareCheckResult(is_latin=False)
26+
return LatinSquareCheckResult(is_latin=True)
27+
28+
29+
def compute_orthogonality(request: OrthogonalityRequest) -> OrthogonalityResult:
30+
"""Check if two Latin squares of the same order are orthogonal."""
31+
n = request.square_a.order
32+
pairs: set[tuple[int, int]] = set()
33+
for i in range(n):
34+
for j in range(n):
35+
pair = (request.square_a.cells[i][j], request.square_b.cells[i][j])
36+
if pair in pairs:
37+
return OrthogonalityResult(is_orthogonal=False, pair_count=len(pairs))
38+
pairs.add(pair)
39+
return OrthogonalityResult(is_orthogonal=True, pair_count=len(pairs))
40+
41+
42+
def compute_latin_square_transpose(
43+
request: LatinSquareRequest,
44+
) -> LatinSquareTransposeResult:
45+
"""Transpose a Latin square (swap rows and columns)."""
46+
n = request.square.order
47+
cells = request.square.cells
48+
transposed = tuple(
49+
tuple(cells[j][i] for j in range(n)) for i in range(n)
50+
)
51+
return LatinSquareTransposeResult(transposed=transposed)
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""Latin square operation declarations."""
2+
3+
from collections.abc import Callable
4+
from typing import Any
5+
6+
from jacobian._models import StrictModel
7+
from jacobian.catalog._examples import example
8+
from jacobian.catalog.models import MathTool, OperationExample
9+
from jacobian.math.latin_squares_ops._models import (
10+
LatinSquareCheckResult,
11+
LatinSquareRequest,
12+
LatinSquareTransposeResult,
13+
OrthogonalityRequest,
14+
OrthogonalityResult,
15+
)
16+
from jacobian.math.latin_squares_ops._operations import (
17+
compute_latin_square_check,
18+
compute_latin_square_transpose,
19+
compute_orthogonality,
20+
)
21+
22+
23+
def _op[RequestT: StrictModel, ResultT: StrictModel](
24+
operation_id: str,
25+
title: str,
26+
description: str,
27+
request_model: type[RequestT],
28+
result_model: type[ResultT],
29+
operation: Callable[[RequestT], ResultT],
30+
*tags: str,
31+
examples: tuple[OperationExample, ...] = (),
32+
version: str = "1",
33+
) -> MathTool[RequestT, ResultT]:
34+
return MathTool(
35+
operation_id=operation_id,
36+
version=version,
37+
title=title,
38+
description=description,
39+
request_type=request_model,
40+
result_type=result_model,
41+
run=operation,
42+
tags=tags,
43+
examples=examples,
44+
)
45+
46+
47+
TOOLS: tuple[MathTool[Any, Any], ...] = (
48+
_op(
49+
"latin_square.check",
50+
"Check if a matrix is a Latin square",
51+
"Verify that each row and column contains every symbol 0..n-1 "
52+
"exactly once.",
53+
LatinSquareRequest,
54+
LatinSquareCheckResult,
55+
compute_latin_square_check,
56+
"latin-square",
57+
"verification",
58+
"exact",
59+
examples=(
60+
example(
61+
"z2_latin_square",
62+
"Check the 2x2 Latin square [[0,1],[1,0]].",
63+
{
64+
"square": {
65+
"order": 2,
66+
"cells": [[0, 1], [1, 0]],
67+
},
68+
},
69+
),
70+
),
71+
),
72+
_op(
73+
"latin_square.orthogonality.check",
74+
"Check orthogonality of two Latin squares",
75+
"Check whether two Latin squares of the same order are orthogonal, "
76+
"i.e., all ordered pairs of entries are distinct.",
77+
OrthogonalityRequest,
78+
OrthogonalityResult,
79+
compute_orthogonality,
80+
"latin-square",
81+
"orthogonality",
82+
"exact",
83+
examples=(
84+
example(
85+
"orthogonal_z2",
86+
"Check orthogonality of [[0,1],[1,0]] and [[0,1],[1,0]].",
87+
{
88+
"square_a": {
89+
"order": 2,
90+
"cells": [[0, 1], [1, 0]],
91+
},
92+
"square_b": {
93+
"order": 2,
94+
"cells": [[0, 1], [1, 0]],
95+
},
96+
},
97+
),
98+
),
99+
),
100+
_op(
101+
"latin_square.transpose.compute",
102+
"Transpose a Latin square",
103+
"Swap rows and columns of a Latin square.",
104+
LatinSquareRequest,
105+
LatinSquareTransposeResult,
106+
compute_latin_square_transpose,
107+
"latin-square",
108+
"transpose",
109+
"exact",
110+
examples=(
111+
example(
112+
"transpose_z2",
113+
"Transpose [[0,1],[1,0]].",
114+
{
115+
"square": {
116+
"order": 2,
117+
"cells": [[0, 1], [1, 0]],
118+
},
119+
},
120+
),
121+
),
122+
),
123+
)
124+
125+
__all__ = ["TOOLS"]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"catalog_version": "1",
3+
"domain": "latin_squares_ops",
4+
"operations": {
5+
"latin_square.check": {
6+
"input_schema": "sha256:59ca806a02fcdf709c1d356e67e767fe50d825270a4231ef7b23c3722b915107",
7+
"output_schema": "sha256:d15c5c9d2c54f60418b60ea8015e174603b627b803c244d946b48276d70a703d"
8+
},
9+
"latin_square.orthogonality.check": {
10+
"input_schema": "sha256:595c651ee68bc30d9a7f7aada49dcfa6d8f94e9af2ae05fe91eec877d0f490c1",
11+
"output_schema": "sha256:82729ec18eee98eae447f61a0099751476a72fcd056b1a2f2aa5bc8fba6fd186"
12+
},
13+
"latin_square.transpose.compute": {
14+
"input_schema": "sha256:59ca806a02fcdf709c1d356e67e767fe50d825270a4231ef7b23c3722b915107",
15+
"output_schema": "sha256:f3e5e3a172846ac5c250efe7fc525ccf15457ba605a0987f3ccaf2ed0235d5a4"
16+
}
17+
}
18+
}

tests/catalog/test_admission.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ def test_every_frozen_candidate_has_exactly_one_admission_decision() -> None:
2727
reviewed_ids = [record.operation_id for record in OPERATION_ADMISSIONS]
2828

2929
assert REVIEWED_BASE_REVISION == "61589543bbbff546edbc51d34a07887982fa4ad6"
30-
assert len(candidate_ids) == len(set(candidate_ids)) == 399
30+
assert len(candidate_ids) == len(set(candidate_ids)) == 402
3131
assert reviewed_ids == sorted(reviewed_ids)
3232
assert set(reviewed_ids) == set(candidate_ids)
3333
assert all(record.rationale.strip() for record in OPERATION_ADMISSIONS)
3434
assert Counter(record.decision for record in OPERATION_ADMISSIONS) == {
35-
AdmissionDecision.KEEP: 239,
35+
AdmissionDecision.KEEP: 242,
3636
AdmissionDecision.NATIVE_ONLY: 56,
3737
AdmissionDecision.DROP: 104,
3838
}
@@ -46,7 +46,7 @@ def test_public_catalog_contains_only_admitted_atomic_operations() -> None:
4646
}
4747

4848
assert {tool.operation_id for tool in BUILTIN_TOOLS} == expected
49-
assert len(BUILTIN_TOOLS) == 239
49+
assert len(BUILTIN_TOOLS) == 242
5050

5151

5252
def test_catalog_construction_fails_closed_on_duplicate_candidates() -> None:

tests/math/latin_squares_ops/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)