Skip to content

Commit 85328d1

Browse files
committed
feat(math): add quadratic forms domain with evaluate, discriminant, signature
Create the quadratic_forms domain with three operations: - quadratic_form.evaluate.compute: exact q(x) = x^T A x for an integral symmetric matrix and integer vector. - quadratic_form.discriminant.compute: exact det(A) via SymPy integer matrix computation. - quadratic_form.signature.compute: inertia (n_pos, n_neg, n_zero) via SymPy eigenvalue computation, with definiteness classification. Partially addresses #1841.
1 parent efce533 commit 85328d1

9 files changed

Lines changed: 385 additions & 3 deletions

File tree

src/jacobian/catalog/builtins.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,10 @@
238238
from jacobian.math.posets._tools import TOOLS as POSETS_TOOLS
239239
from jacobian.math.probability._admission import ADMISSIONS as PROBABILITY_ADMISSIONS
240240
from jacobian.math.probability._tools import TOOLS as PROBABILITY_TOOLS
241+
from jacobian.math.quadratic_forms._admission import (
242+
ADMISSIONS as QUADRATIC_FORMS_ADMISSIONS,
243+
)
244+
from jacobian.math.quadratic_forms._tools import TOOLS as QUADRATIC_FORMS_TOOLS
241245
from jacobian.math.recurrence_solving._admission import (
242246
ADMISSIONS as RECURRENCE_SOLVING_ADMISSIONS,
243247
)
@@ -274,6 +278,7 @@
274278
from jacobian.math.words._tools import TOOLS as WORDS_TOOLS
275279

276280
_BUILTIN_CANDIDATES: MathTools = (
281+
*QUADRATIC_FORMS_TOOLS,
277282
*BOOLEAN_TOOLS,
278283
*GROUP_TOOLS,
279284
*GRAPH_COLORING_OPS_TOOLS,
@@ -411,6 +416,7 @@
411416
*POLYNOMIALS_REAL_ALGEBRA_ADMISSIONS,
412417
*POSETS_ADMISSIONS,
413418
*PROBABILITY_ADMISSIONS,
419+
*QUADRATIC_FORMS_ADMISSIONS,
414420
*RECURRENCE_SOLVING_ADMISSIONS,
415421
*REGULAR_LANGUAGES_ADMISSIONS,
416422
*ROOT_ISOLATION_ADMISSIONS,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Quadratic form operations."""
2+
3+
__all__: list[str] = []
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Owner-local admission decisions for built-in math operations."""
2+
3+
from jacobian.catalog.admission import AdmissionDecision, OperationAdmission
4+
5+
ADMISSIONS: tuple[OperationAdmission, ...] = (
6+
OperationAdmission(
7+
"quadratic_form.evaluate.compute",
8+
AdmissionDecision.KEEP,
9+
"exact integer evaluation q(x) = x^T A x for an integral quadratic form",
10+
),
11+
OperationAdmission(
12+
"quadratic_form.discriminant.compute",
13+
AdmissionDecision.KEEP,
14+
"exact determinant of the symmetric matrix via SymPy",
15+
),
16+
OperationAdmission(
17+
"quadratic_form.signature.compute",
18+
AdmissionDecision.KEEP,
19+
"exact inertia and definiteness classification via SymPy eigenvalues",
20+
),
21+
)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Typed wire contracts for quadratic form 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_DIM = 10
12+
13+
14+
class SymmetricMatrix(StrictModel):
15+
"""A symmetric integer matrix representing a quadratic form."""
16+
17+
matrix: tuple[tuple[int, ...], ...] = Field(min_length=1)
18+
19+
@model_validator(mode="after")
20+
def require_symmetric_square(self) -> Self:
21+
n = len(self.matrix)
22+
if n > MAX_DIM:
23+
raise ValueError(f"dimension must not exceed {MAX_DIM}")
24+
for row in self.matrix:
25+
if len(row) != n:
26+
raise ValueError("matrix must be square")
27+
for i in range(n):
28+
for j in range(n):
29+
if self.matrix[i][j] != self.matrix[j][i]:
30+
raise ValueError("matrix must be symmetric")
31+
return self
32+
33+
34+
class EvaluationRequest(StrictModel):
35+
"""Evaluate q(x) = x^T A x for an integer vector x."""
36+
37+
form: SymmetricMatrix
38+
vector: tuple[int, ...] = Field(min_length=1)
39+
40+
@model_validator(mode="after")
41+
def require_matching_dimension(self) -> Self:
42+
if len(self.vector) != len(self.form.matrix):
43+
raise ValueError("vector dimension must match form dimension")
44+
return self
45+
46+
47+
class DiscriminantRequest(StrictModel):
48+
"""Compute the discriminant of a quadratic form."""
49+
50+
form: SymmetricMatrix
51+
52+
53+
class SignatureRequest(StrictModel):
54+
"""Compute the signature (n_pos, n_neg, n_zero) of a quadratic form."""
55+
56+
form: SymmetricMatrix
57+
58+
59+
class EvaluationResult(StrictModel):
60+
"""The value q(x) = x^T A x."""
61+
62+
value: int
63+
dimension: int
64+
65+
66+
class DiscriminantResult(StrictModel):
67+
"""The discriminant det(A) of a quadratic form."""
68+
69+
discriminant: int
70+
dimension: int
71+
72+
73+
class SignatureResult(StrictModel):
74+
"""The inertia (positive, negative, zero eigenvalue counts) of a form."""
75+
76+
n_positive: int
77+
n_negative: int
78+
n_zero: int
79+
is_positive_definite: bool
80+
is_negative_definite: bool
81+
is_indefinite: bool
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Exact quadratic form operations using SymPy for linear algebra."""
2+
3+
from jacobian.math.quadratic_forms._models import (
4+
DiscriminantRequest,
5+
DiscriminantResult,
6+
EvaluationRequest,
7+
EvaluationResult,
8+
SignatureRequest,
9+
SignatureResult,
10+
)
11+
12+
13+
def evaluate_form(request: EvaluationRequest) -> EvaluationResult:
14+
"""Evaluate q(x) = x^T A x for an integer vector x."""
15+
a = request.form.matrix
16+
x = request.vector
17+
n = len(a)
18+
value = 0
19+
for i in range(n):
20+
for j in range(n):
21+
value += a[i][j] * x[i] * x[j]
22+
return EvaluationResult(value=value, dimension=n)
23+
24+
25+
def compute_discriminant(request: DiscriminantRequest) -> DiscriminantResult:
26+
"""Compute det(A) for the symmetric matrix A."""
27+
from sympy import Matrix
28+
29+
a = request.form.matrix
30+
n = len(a)
31+
m = Matrix(a)
32+
det = int(m.det())
33+
return DiscriminantResult(discriminant=det, dimension=n)
34+
35+
36+
def compute_signature(request: SignatureRequest) -> SignatureResult:
37+
"""Compute the signature (inertia) of a quadratic form using SymPy eigenvalues."""
38+
from sympy import Matrix
39+
40+
a = request.form.matrix
41+
n = len(a)
42+
m = Matrix(a)
43+
44+
# Compute eigenvalues
45+
eigenvals = m.eigenvals()
46+
47+
n_positive = 0
48+
n_negative = 0
49+
n_zero = 0
50+
51+
for eigenval, mult in eigenvals.items():
52+
eigenval_int = int(eigenval)
53+
if eigenval_int > 0:
54+
n_positive += mult
55+
elif eigenval_int < 0:
56+
n_negative += mult
57+
else:
58+
n_zero += mult
59+
60+
return SignatureResult(
61+
n_positive=n_positive,
62+
n_negative=n_negative,
63+
n_zero=n_zero,
64+
is_positive_definite=n_positive == n and n_zero == 0 and n_negative == 0,
65+
is_negative_definite=n_negative == n and n_zero == 0 and n_positive == 0,
66+
is_indefinite=n_positive > 0 and n_negative > 0,
67+
)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"""Quadratic form 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.quadratic_forms._models import (
10+
DiscriminantRequest,
11+
DiscriminantResult,
12+
EvaluationRequest,
13+
EvaluationResult,
14+
SignatureRequest,
15+
SignatureResult,
16+
)
17+
from jacobian.math.quadratic_forms._operations import (
18+
compute_discriminant,
19+
compute_signature,
20+
evaluate_form,
21+
)
22+
23+
24+
def _op[RequestT: StrictModel, ResultT: StrictModel](
25+
operation_id: str,
26+
title: str,
27+
description: str,
28+
request_model: type[RequestT],
29+
result_model: type[ResultT],
30+
operation: Callable[[RequestT], ResultT],
31+
*tags: str,
32+
examples: tuple[OperationExample, ...] = (),
33+
) -> MathTool[RequestT, ResultT]:
34+
return MathTool(
35+
operation_id=operation_id,
36+
version="1",
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+
_FORM_2D = {"matrix": [[1, 0], [0, 1]]}
48+
49+
TOOLS: tuple[MathTool[Any, Any], ...] = (
50+
_op(
51+
"quadratic_form.evaluate.compute",
52+
"Evaluate a quadratic form q(x) = x^T A x",
53+
"Compute the exact integer value q(x) = x^T A x for an "
54+
"integral quadratic form (symmetric matrix) and integer vector.",
55+
EvaluationRequest,
56+
EvaluationResult,
57+
evaluate_form,
58+
"algebra",
59+
"quadratic-form",
60+
"exact",
61+
examples=(
62+
example(
63+
"identity_2d_at_3_4",
64+
"Evaluate x^T I x at (3, 4); "
65+
"the matrix must be symmetric and the vector length must match.",
66+
{"form": _FORM_2D, "vector": [3, 4]},
67+
),
68+
),
69+
),
70+
_op(
71+
"quadratic_form.discriminant.compute",
72+
"Compute the discriminant det(A) of a quadratic form",
73+
"Compute the exact determinant of the symmetric matrix "
74+
"representing the quadratic form, using SymPy for exact "
75+
"integer matrix computation.",
76+
DiscriminantRequest,
77+
DiscriminantResult,
78+
compute_discriminant,
79+
"algebra",
80+
"quadratic-form",
81+
"exact",
82+
examples=(
83+
example(
84+
"identity_2d_discriminant",
85+
"Compute det(I_2) = 1; the matrix must be symmetric.",
86+
{"form": _FORM_2D},
87+
),
88+
),
89+
),
90+
_op(
91+
"quadratic_form.signature.compute",
92+
"Compute the signature/inertia of a quadratic form",
93+
"Compute the inertia (n_positive, n_negative, n_zero) of a "
94+
"quadratic form using SymPy eigenvalue computation, with "
95+
"definiteness classification.",
96+
SignatureRequest,
97+
SignatureResult,
98+
compute_signature,
99+
"algebra",
100+
"quadratic-form",
101+
"exact",
102+
examples=(
103+
example(
104+
"identity_2d_signature",
105+
"Compute the signature of I_2; the matrix must be symmetric.",
106+
{"form": _FORM_2D},
107+
),
108+
),
109+
),
110+
)
111+
112+
__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": "quadratic_forms",
4+
"operations": {
5+
"quadratic_form.discriminant.compute": {
6+
"input_schema": "sha256:9b4d46e182bc38cf57216b1848dfadc7a24c455b6296e75d7354c5891ea1b3ef",
7+
"output_schema": "sha256:532affab5b9e21899414e52ee6eab97cb705d22d0bb36b4c2f3458e68b00b600"
8+
},
9+
"quadratic_form.evaluate.compute": {
10+
"input_schema": "sha256:de7a0719f61ae1937853880ce14c23f857593dd8ff28ecdab463a93d87c0d830",
11+
"output_schema": "sha256:75dbab9324cd95ed8e266b5e1d5cedcdfdb2a4529b6114ee04798fa52dd48f82"
12+
},
13+
"quadratic_form.signature.compute": {
14+
"input_schema": "sha256:e028e2197eda2d9e65c3848aece7a71f2dc2338caac38cb0227ea4da4cdf6698",
15+
"output_schema": "sha256:b455fedbc2b023b4755dd5f7d24cc8781a02e9649ab2f9efca912219f0ab719d"
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:

0 commit comments

Comments
 (0)