Skip to content

Commit c4f4c74

Browse files
committed
fix(math): preflight exact result growth
1 parent 1009d98 commit c4f4c74

13 files changed

Lines changed: 239 additions & 55 deletions

File tree

src/jacobian/math/finite_stochastic_processes/_models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ def require_same_space(self) -> Self:
5353
raise ValueError(
5454
"random variable and sigma algebra must share the same probability space"
5555
)
56+
for value in self.rv.values:
57+
require_bounded_rational(
58+
value,
59+
max_digits=256,
60+
label="random-variable value",
61+
)
5662
return self
5763

5864

src/jacobian/math/finite_stochastic_processes/values.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,6 @@ class FiniteRandomVariable(StrictModel):
6262
def require_valid_rv(self) -> Self:
6363
if len(self.values) != len(self.space.samples):
6464
raise ValueError("values must have one entry per sample")
65-
for value in self.values:
66-
require_bounded_rational(
67-
value,
68-
max_digits=256,
69-
label="random-variable value",
70-
)
7165
return self
7266

7367

src/jacobian/math/galois_theory/_models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ def require_reconstruction_certificate(self) -> Self:
162162

163163

164164
def _require_factor_residues(result: GaloisFactorResult) -> None:
165+
from sympy import GF, Poly, Symbol
166+
165167
prime = result.field_order
166168
if result.unit >= prime:
167169
raise ValueError("factorization unit must be a canonical nonzero residue")
@@ -172,6 +174,13 @@ def _require_factor_residues(result: GaloisFactorResult) -> None:
172174
raise ValueError("factor coefficients must be canonical field residues")
173175
if factor.coefficients[-1] != 1:
174176
raise ValueError("finite-field factors must be monic")
177+
polynomial = Poly(
178+
list(reversed(factor.coefficients)),
179+
Symbol("x"),
180+
domain=GF(prime),
181+
)
182+
if not polynomial.is_irreducible:
183+
raise ValueError("every finite-field factor must be irreducible")
175184

176185

177186
def _reconstruct_factorization(result: GaloisFactorResult) -> tuple[int, ...]:

src/jacobian/math/matrices/symbolic/_models.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,35 @@ def require_square(self) -> Self:
8989
return self
9090

9191

92+
class SymbolicDeterminantRequest(SquareSymbolicMatrixRequest):
93+
"""A square matrix whose exact determinant fits the public result type."""
94+
95+
@model_validator(mode="after")
96+
def require_representable_determinant(self) -> Self:
97+
# The input sparsity budget bounds backend work, but rational-function
98+
# expansion can still exceed the canonical result representation. Run
99+
# the bounded exact kernel at admission so an accepted request cannot
100+
# fail later while constructing its typed result.
101+
from jacobian.math.matrices.symbolic import symbolic_determinant
102+
103+
symbolic_determinant(self.matrix.entries, self.matrix.variables)
104+
return self
105+
106+
107+
class SymbolicCharacteristicPolynomialRequest(SquareSymbolicMatrixRequest):
108+
"""A square matrix whose characteristic polynomial fits the result type."""
109+
110+
@model_validator(mode="after")
111+
def require_representable_characteristic_polynomial(self) -> Self:
112+
from jacobian.math.matrices.symbolic import symbolic_characteristic_polynomial
113+
114+
symbolic_characteristic_polynomial(
115+
self.matrix.entries,
116+
self.matrix.variables,
117+
)
118+
return self
119+
120+
92121
class SymbolicDeterminantResult(StrictModel):
93122
"""The exact determinant in the matrix's rational-function field."""
94123

src/jacobian/math/matrices/symbolic/_operations.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
symbolic_rank,
1212
)
1313
from jacobian.math.matrices.symbolic._models import (
14-
SquareSymbolicMatrixRequest,
14+
SymbolicCharacteristicPolynomialRequest,
1515
SymbolicCharacteristicPolynomialResult,
16+
SymbolicDeterminantRequest,
1617
SymbolicDeterminantResult,
1718
SymbolicEigenvaluesResult,
1819
SymbolicMatrixRequest,
@@ -21,7 +22,7 @@
2122

2223

2324
def compute_symbolic_determinant(
24-
request: SquareSymbolicMatrixRequest,
25+
request: SymbolicDeterminantRequest,
2526
) -> SymbolicDeterminantResult:
2627
determinant = symbolic_determinant(
2728
request.matrix.entries,
@@ -41,7 +42,7 @@ def compute_symbolic_rank(
4142

4243

4344
def compute_symbolic_characteristic_polynomial(
44-
request: SquareSymbolicMatrixRequest,
45+
request: SymbolicCharacteristicPolynomialRequest,
4546
) -> SymbolicCharacteristicPolynomialResult:
4647
degree, coeffs = symbolic_characteristic_polynomial(
4748
request.matrix.entries,
@@ -54,7 +55,7 @@ def compute_symbolic_characteristic_polynomial(
5455

5556

5657
def compute_symbolic_eigenvalues(
57-
request: SquareSymbolicMatrixRequest,
58+
request: SymbolicCharacteristicPolynomialRequest,
5859
) -> SymbolicEigenvaluesResult:
5960
entries = request.matrix.entries
6061
variables = request.matrix.variables

src/jacobian/math/matrices/symbolic/_tools.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
from jacobian.catalog._examples import example
88
from jacobian.catalog.models import MathTool, OperationExample
99
from jacobian.math.matrices.symbolic._models import (
10-
SquareSymbolicMatrixRequest,
10+
SymbolicCharacteristicPolynomialRequest,
1111
SymbolicCharacteristicPolynomialResult,
12+
SymbolicDeterminantRequest,
1213
SymbolicDeterminantResult,
1314
SymbolicEigenvaluesResult,
1415
SymbolicMatrixRequest,
@@ -97,7 +98,7 @@ def _generic_two_by_two() -> dict[str, Any]:
9798
"matrix.symbolic.determinant.compute",
9899
"Compute an exact symbolic matrix determinant (det) over QQ(t_1, ..., t_n)",
99100
"Compute the determinant of a square matrix whose entries are rational functions in declared algebraically independent variables, using SymPy's exact fraction-free Bareiss algorithm.",
100-
SquareSymbolicMatrixRequest,
101+
SymbolicDeterminantRequest,
101102
SymbolicDeterminantResult,
102103
compute_symbolic_determinant,
103104
"matrix",
@@ -142,7 +143,7 @@ def _generic_two_by_two() -> dict[str, Any]:
142143
"matrix.symbolic.characteristic_polynomial.compute",
143144
"Compute an exact symbolic characteristic polynomial",
144145
"Compute the dense monic coefficients of det(lambda I - A) for a square symbolic matrix whose entries are rational functions in declared algebraically independent variables.",
145-
SquareSymbolicMatrixRequest,
146+
SymbolicCharacteristicPolynomialRequest,
146147
SymbolicCharacteristicPolynomialResult,
147148
compute_symbolic_characteristic_polynomial,
148149
"matrix",
@@ -165,7 +166,7 @@ def _generic_two_by_two() -> dict[str, Any]:
165166
"matrix.symbolic.eigenvalues.compute",
166167
"Compute exact symbolic eigenvalues",
167168
"Compute the exact eigenvalues with algebraic multiplicities of a square symbolic matrix using SymPy's eigenvals. Entries may be rational functions in declared algebraically independent variables; eigenvalues are returned as canonical SymPy expression strings.",
168-
SquareSymbolicMatrixRequest,
169+
SymbolicCharacteristicPolynomialRequest,
169170
SymbolicEigenvaluesResult,
170171
compute_symbolic_eigenvalues,
171172
"matrix",
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Shared exact kernels for polynomial interpolation contracts and operations."""
2+
3+
from __future__ import annotations
4+
5+
from fractions import Fraction
6+
7+
from jacobian._exact import CanonicalRational
8+
9+
10+
def divided_difference_coefficients(
11+
nodes: tuple[CanonicalRational, ...],
12+
values: tuple[CanonicalRational, ...],
13+
) -> tuple[Fraction, ...]:
14+
"""Return the exact Newton coefficients for pairwise-distinct nodes."""
15+
16+
node_values = tuple(node.as_fraction() for node in nodes)
17+
row = [value.as_fraction() for value in values]
18+
coefficients = [row[0]]
19+
for width in range(1, len(node_values)):
20+
row = [
21+
(row[index + 1] - row[index])
22+
/ (node_values[index + width] - node_values[index])
23+
for index in range(len(node_values) - width)
24+
]
25+
coefficients.append(row[0])
26+
return tuple(coefficients)
27+
28+
29+
def evaluate_newton_form(
30+
nodes: tuple[CanonicalRational, ...],
31+
coefficients: tuple[CanonicalRational, ...],
32+
point: CanonicalRational,
33+
) -> Fraction:
34+
"""Evaluate one Newton form exactly with nested multiplication."""
35+
36+
node_values = tuple(node.as_fraction() for node in nodes)
37+
coefficient_values = tuple(value.as_fraction() for value in coefficients)
38+
point_value = point.as_fraction()
39+
result = coefficient_values[-1]
40+
for index in range(len(coefficient_values) - 2, -1, -1):
41+
result = coefficient_values[index] + (point_value - node_values[index]) * result
42+
return result
43+
44+
45+
__all__ = ["divided_difference_coefficients", "evaluate_newton_form"]

src/jacobian/math/polynomial_interpolation_ops/_models.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,16 @@
66

77
from pydantic import Field, model_validator
88

9-
from jacobian._exact import CanonicalRational, require_bounded_rational
9+
from jacobian._exact import (
10+
MAX_CANONICAL_RATIONAL_DIGITS,
11+
CanonicalRational,
12+
require_bounded_rational,
13+
)
1014
from jacobian._models import StrictModel
15+
from jacobian.math.polynomial_interpolation_ops._kernel import (
16+
divided_difference_coefficients,
17+
evaluate_newton_form,
18+
)
1119

1220
MAX_POINTS = 32
1321
_MAX_RATIONAL_DIGITS = 256
@@ -46,6 +54,12 @@ def require_well_formed_samples(self) -> Self:
4654
_require_distinct(self.nodes)
4755
_require_bounded(self.nodes, "interpolation node")
4856
_require_bounded(self.values, "interpolation value")
57+
for coefficient in divided_difference_coefficients(self.nodes, self.values):
58+
require_bounded_rational(
59+
CanonicalRational.from_fraction(coefficient),
60+
max_digits=MAX_CANONICAL_RATIONAL_DIGITS,
61+
label="derived Newton coefficient",
62+
)
4963
return self
5064

5165

@@ -75,7 +89,6 @@ def require_basis_shape(self) -> Self:
7589
raise ValueError("Newton nodes and coefficients must have the same length")
7690
_require_distinct(self.nodes)
7791
_require_bounded(self.nodes, "Newton node")
78-
_require_bounded(self.coefficients, "Newton coefficient")
7992
return self
8093

8194

@@ -90,6 +103,17 @@ def require_bounded_point(self) -> Self:
90103
max_digits=_MAX_RATIONAL_DIGITS,
91104
label="evaluation point",
92105
)
106+
require_bounded_rational(
107+
CanonicalRational.from_fraction(
108+
evaluate_newton_form(
109+
self.newton_form.nodes,
110+
self.newton_form.coefficients,
111+
self.evaluation_point,
112+
)
113+
),
114+
max_digits=MAX_CANONICAL_RATIONAL_DIGITS,
115+
label="derived Newton evaluation",
116+
)
93117
return self
94118

95119

src/jacobian/math/polynomial_interpolation_ops/_operations.py

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
from fractions import Fraction
66

77
from jacobian._exact import CanonicalRational
8+
from jacobian.math.polynomial_interpolation_ops._kernel import (
9+
divided_difference_coefficients,
10+
evaluate_newton_form,
11+
)
812
from jacobian.math.polynomial_interpolation_ops._models import (
913
DividedDifferencesRequest,
1014
DividedDifferencesResult,
@@ -15,39 +19,22 @@
1519
)
1620

1721

18-
def _divided_difference_coefficients(
19-
nodes: tuple[CanonicalRational, ...],
20-
values: tuple[CanonicalRational, ...],
21-
) -> tuple[Fraction, ...]:
22-
node_values = tuple(node.as_fraction() for node in nodes)
23-
row = [value.as_fraction() for value in values]
24-
coefficients = [row[0]]
25-
for width in range(1, len(node_values)):
26-
row = [
27-
(row[index + 1] - row[index])
28-
/ (node_values[index + width] - node_values[index])
29-
for index in range(len(node_values) - width)
30-
]
31-
coefficients.append(row[0])
32-
return tuple(coefficients)
33-
34-
3522
def _canonical(values: tuple[Fraction, ...]) -> tuple[CanonicalRational, ...]:
3623
return tuple(CanonicalRational.from_fraction(value) for value in values)
3724

3825

3926
def compute_divided_differences(
4027
request: DividedDifferencesRequest,
4128
) -> DividedDifferencesResult:
42-
coefficients = _divided_difference_coefficients(
29+
coefficients = divided_difference_coefficients(
4330
request.samples.nodes,
4431
request.samples.values,
4532
)
4633
return DividedDifferencesResult(coefficients=_canonical(coefficients))
4734

4835

4936
def compute_newton_form(request: NewtonFormRequest) -> NewtonForm:
50-
coefficients = _divided_difference_coefficients(
37+
coefficients = divided_difference_coefficients(
5138
request.samples.nodes,
5239
request.samples.values,
5340
)
@@ -58,15 +45,15 @@ def compute_newton_form(request: NewtonFormRequest) -> NewtonForm:
5845

5946

6047
def compute_newton_evaluate(request: NewtonEvaluateRequest) -> NewtonEvaluateResult:
61-
nodes = tuple(node.as_fraction() for node in request.newton_form.nodes)
62-
coefficients = tuple(
63-
coefficient.as_fraction() for coefficient in request.newton_form.coefficients
48+
return NewtonEvaluateResult(
49+
result=CanonicalRational.from_fraction(
50+
evaluate_newton_form(
51+
request.newton_form.nodes,
52+
request.newton_form.coefficients,
53+
request.evaluation_point,
54+
)
55+
)
6456
)
65-
point = request.evaluation_point.as_fraction()
66-
result = coefficients[-1]
67-
for index in range(len(coefficients) - 2, -1, -1):
68-
result = coefficients[index] + (point - nodes[index]) * result
69-
return NewtonEvaluateResult(result=CanonicalRational.from_fraction(result))
7057

7158

7259
__all__ = [

tests/math/finite_stochastic_processes/test_finite_stochastic_processes.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,20 @@ def test_discrete_sigma(self) -> None:
123123
# E[X | {H}] = 1, E[X | {T}] = 0
124124
assert result.values == (_q(1), _q(0))
125125

126+
def test_exact_result_may_grow_beyond_input_digit_bound(self) -> None:
127+
left = 10**255 + 19
128+
right = 10**255 + 21
129+
rv = FiniteRandomVariable(
130+
space=_coin_space(), values=(_q(1, left), _q(1, right))
131+
)
132+
sigma = FiniteSigmaAlgebra(space=_coin_space(), blocks=(("H", "T"),))
133+
134+
result = compute_conditional_expectation(
135+
ConditionalExpectationRequest(rv=rv, sigma=sigma)
136+
)
137+
138+
assert len(result.values[0].den) > 256
139+
126140

127141
# ---------------------------------------------------------------------------
128142
# Filtration

0 commit comments

Comments
 (0)