|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
| 5 | +from itertools import combinations, permutations |
5 | 6 | from typing import Literal, Self |
6 | 7 |
|
7 | 8 | from pydantic import Field, model_validator |
|
12 | 13 | MAX_SYMBOLIC_MATRIX_DIMENSION = 8 |
13 | 14 | MAX_SYMBOLIC_VARIABLES = 8 |
14 | 15 | MAX_SYMBOLIC_MATRIX_TERMS = 512 |
| 16 | +MAX_SYMBOLIC_RESULT_TERMS = 256 |
| 17 | +MAX_SYMBOLIC_RESULT_EXPONENT = 64 |
| 18 | +MAX_SYMBOLIC_RESULT_COEFFICIENT_DIGITS = 128 |
| 19 | + |
| 20 | + |
| 21 | +def _is_polynomial_entry(value: RationalFunction) -> bool: |
| 22 | + terms = value.denominator.terms |
| 23 | + return ( |
| 24 | + len(terms) == 1 |
| 25 | + and terms[0].coefficient.num == "1" |
| 26 | + and terms[0].coefficient.den == "1" |
| 27 | + and all(exponent == 0 for exponent in terms[0].exponents) |
| 28 | + ) |
| 29 | + |
| 30 | + |
| 31 | +def _principal_minor_term_bounds( |
| 32 | + entries: tuple[tuple[RationalFunction, ...], ...], |
| 33 | +) -> tuple[int, ...]: |
| 34 | + """Bound raw terms in each characteristic coefficient by Leibniz expansion.""" |
| 35 | + |
| 36 | + dimension = len(entries) |
| 37 | + bounds = [1] |
| 38 | + for size in range(1, dimension + 1): |
| 39 | + coefficient_terms = 0 |
| 40 | + for axes in combinations(range(dimension), size): |
| 41 | + for columns in permutations(axes): |
| 42 | + product_terms = 1 |
| 43 | + for row, column in zip(axes, columns, strict=True): |
| 44 | + product_terms *= len(entries[row][column].numerator.terms) |
| 45 | + coefficient_terms += product_terms |
| 46 | + bounds.append(coefficient_terms) |
| 47 | + return tuple(bounds) |
| 48 | + |
| 49 | + |
| 50 | +def _require_determinant_family_result_budget( |
| 51 | + matrix: SymbolicMatrix, |
| 52 | + *, |
| 53 | + characteristic_polynomial: bool, |
| 54 | +) -> None: |
| 55 | + dimension = len(matrix.entries) |
| 56 | + if dimension == 1: |
| 57 | + return |
| 58 | + values = tuple(value for row in matrix.entries for value in row) |
| 59 | + if any(not _is_polynomial_entry(value) for value in values): |
| 60 | + raise ValueError( |
| 61 | + "multi-dimensional determinant-family requests require polynomial entries" |
| 62 | + ) |
| 63 | + term_bounds = _principal_minor_term_bounds(matrix.entries) |
| 64 | + relevant_bounds = term_bounds[1:] if characteristic_polynomial else term_bounds[-1:] |
| 65 | + if any(bound > MAX_SYMBOLIC_RESULT_TERMS for bound in relevant_bounds): |
| 66 | + raise ValueError("determinant-family expansion exceeds the result term budget") |
| 67 | + maximum_exponent = max( |
| 68 | + ( |
| 69 | + exponent |
| 70 | + for value in values |
| 71 | + for term in value.numerator.terms |
| 72 | + for exponent in term.exponents |
| 73 | + ), |
| 74 | + default=0, |
| 75 | + ) |
| 76 | + if dimension * maximum_exponent > MAX_SYMBOLIC_RESULT_EXPONENT: |
| 77 | + raise ValueError( |
| 78 | + "determinant-family expansion exceeds the result exponent budget" |
| 79 | + ) |
| 80 | + coefficient_digits = max( |
| 81 | + ( |
| 82 | + len(component.lstrip("-")) |
| 83 | + for value in values |
| 84 | + for term in value.numerator.terms |
| 85 | + for component in (term.coefficient.num, term.coefficient.den) |
| 86 | + ), |
| 87 | + default=1, |
| 88 | + ) |
| 89 | + if any( |
| 90 | + bound * dimension * coefficient_digits + len(str(max(bound, 1))) |
| 91 | + > MAX_SYMBOLIC_RESULT_COEFFICIENT_DIGITS |
| 92 | + for bound in relevant_bounds |
| 93 | + ): |
| 94 | + raise ValueError( |
| 95 | + "determinant-family expansion exceeds the result coefficient budget" |
| 96 | + ) |
15 | 97 |
|
16 | 98 |
|
17 | 99 | class SymbolicMatrix(StrictModel): |
@@ -92,28 +174,41 @@ def require_square(self) -> Self: |
92 | 174 | class SymbolicDeterminantRequest(SquareSymbolicMatrixRequest): |
93 | 175 | """A square matrix whose exact determinant fits the public result type.""" |
94 | 176 |
|
| 177 | + matrix: SymbolicMatrix = Field( |
| 178 | + description=( |
| 179 | + "A square symbolic matrix. One-dimensional matrices may contain any " |
| 180 | + "accepted rational function; larger matrices require polynomial " |
| 181 | + "entries whose derived determinant expansion has at most 256 terms, " |
| 182 | + "exponent 64, and 128-digit coefficient components." |
| 183 | + ) |
| 184 | + ) |
| 185 | + |
95 | 186 | @model_validator(mode="after") |
96 | 187 | 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) |
| 188 | + _require_determinant_family_result_budget( |
| 189 | + self.matrix, |
| 190 | + characteristic_polynomial=False, |
| 191 | + ) |
104 | 192 | return self |
105 | 193 |
|
106 | 194 |
|
107 | 195 | class SymbolicCharacteristicPolynomialRequest(SquareSymbolicMatrixRequest): |
108 | 196 | """A square matrix whose characteristic polynomial fits the result type.""" |
109 | 197 |
|
| 198 | + matrix: SymbolicMatrix = Field( |
| 199 | + description=( |
| 200 | + "A square symbolic matrix. One-dimensional matrices may contain any " |
| 201 | + "accepted rational function; larger matrices require polynomial " |
| 202 | + "entries whose derived principal-minor expansions each have at most " |
| 203 | + "256 terms, exponent 64, and 128-digit coefficient components." |
| 204 | + ) |
| 205 | + ) |
| 206 | + |
110 | 207 | @model_validator(mode="after") |
111 | 208 | 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, |
| 209 | + _require_determinant_family_result_budget( |
| 210 | + self.matrix, |
| 211 | + characteristic_polynomial=True, |
117 | 212 | ) |
118 | 213 | return self |
119 | 214 |
|
|
0 commit comments