Skip to content

Commit 7c21055

Browse files
committed
feat(math): add polynomial vector calculus operations (#1865)
Add polynomial_field domain with 5 operations: gradient, Laplacian, directional derivative, divergence, and curl. Uses SymPy for exact symbolic differentiation. Closes #1865
1 parent 2e54684 commit 7c21055

10 files changed

Lines changed: 476 additions & 3 deletions

File tree

src/jacobian/catalog/builtins.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,12 @@
218218
from jacobian.math.optimization._tools import TOOLS as OPTIMIZATION_TOOLS
219219
from jacobian.math.petri_nets._admission import ADMISSIONS as PETRI_NETS_ADMISSIONS
220220
from jacobian.math.petri_nets._tools import TOOLS as PETRI_NET_TOOLS
221+
from jacobian.math.polynomial_vector_calc._admission import (
222+
ADMISSIONS as POLYNOMIAL_VECTOR_CALC_ADMISSIONS,
223+
)
224+
from jacobian.math.polynomial_vector_calc._tools import (
225+
TOOLS as POLYNOMIAL_VECTOR_CALC_TOOLS,
226+
)
221227
from jacobian.math.polynomials._admission import ADMISSIONS as POLYNOMIALS_ADMISSIONS
222228
from jacobian.math.polynomials._tools import TOOLS as POLYNOMIAL_TOOLS
223229
from jacobian.math.polynomials.maps._admission import (
@@ -309,6 +315,7 @@
309315
*LATTICES_TOOLS,
310316
*FORMAL_POWER_SERIES_TOOLS,
311317
*POLYNOMIAL_TOOLS,
318+
*POLYNOMIAL_VECTOR_CALC_TOOLS,
312319
*MULTIVARIATE_POLYNOMIAL_TOOLS,
313320
*ANALYSIS_TOOLS,
314321
*PROBABILITY_TOOLS,
@@ -406,6 +413,7 @@
406413
*OPTIMIZATION_ADMISSIONS,
407414
*PETRI_NETS_ADMISSIONS,
408415
*POLYNOMIALS_ADMISSIONS,
416+
*POLYNOMIAL_VECTOR_CALC_ADMISSIONS,
409417
*POLYNOMIALS_MAPS_ADMISSIONS,
410418
*POLYNOMIALS_MULTIVARIATE_ADMISSIONS,
411419
*POLYNOMIALS_REAL_ALGEBRA_ADMISSIONS,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Polynomial vector calculus operations."""
2+
3+
__all__: list[str] = []
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
"polynomial_field.scalar.gradient.compute",
10+
AdmissionDecision.KEEP,
11+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
12+
),
13+
OperationAdmission(
14+
"polynomial_field.scalar.laplacian.compute",
15+
AdmissionDecision.KEEP,
16+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
17+
),
18+
OperationAdmission(
19+
"polynomial_field.scalar.directional_derivative.compute",
20+
AdmissionDecision.KEEP,
21+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
22+
),
23+
OperationAdmission(
24+
"polynomial_field.vector.divergence.compute",
25+
AdmissionDecision.KEEP,
26+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
27+
),
28+
OperationAdmission(
29+
"polynomial_field.vector.curl.compute",
30+
AdmissionDecision.KEEP,
31+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
32+
),
33+
)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Typed wire contracts for polynomial vector calculus 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_VARS = 8
12+
MAX_POLYS = 8
13+
14+
15+
class ScalarFieldRequest(StrictModel):
16+
"""A multivariate polynomial scalar field."""
17+
18+
variables: tuple[str, ...] = Field(min_length=1, max_length=MAX_VARS)
19+
polynomial: str = Field(min_length=1, max_length=4096)
20+
21+
@model_validator(mode="after")
22+
def require_valid(self) -> Self:
23+
if any(not v.isidentifier() for v in self.variables):
24+
raise ValueError("variable names must be valid identifiers")
25+
return self
26+
27+
28+
class VectorFieldRequest(StrictModel):
29+
"""A multivariate polynomial vector field."""
30+
31+
variables: tuple[str, ...] = Field(min_length=1, max_length=MAX_VARS)
32+
components: tuple[str, ...] = Field(
33+
min_length=1, max_length=MAX_POLYS
34+
)
35+
36+
@model_validator(mode="after")
37+
def require_valid(self) -> Self:
38+
if any(not v.isidentifier() for v in self.variables):
39+
raise ValueError("variable names must be valid identifiers")
40+
return self
41+
42+
43+
class DirectionalDerivativeRequest(StrictModel):
44+
"""Directional derivative of a scalar field along a direction vector."""
45+
46+
variables: tuple[str, ...] = Field(min_length=1, max_length=MAX_VARS)
47+
polynomial: str = Field(min_length=1, max_length=4096)
48+
direction: tuple[str, ...] = Field(
49+
min_length=1, max_length=MAX_POLYS
50+
)
51+
52+
@model_validator(mode="after")
53+
def require_valid(self) -> Self:
54+
if len(self.direction) != len(self.variables):
55+
raise ValueError("direction vector length must match variables length")
56+
return self
57+
58+
59+
# Results
60+
61+
62+
class ScalarResult(StrictModel):
63+
"""A scalar polynomial result."""
64+
65+
result: str
66+
variables: tuple[str, ...] = ()
67+
method: str = "SYMPY_GRADIENT"
68+
69+
70+
class VectorResult(StrictModel):
71+
"""A vector polynomial result."""
72+
73+
components: tuple[str, ...]
74+
variables: tuple[str, ...] = ()
75+
method: str = "SYMPY_GRADIENT"
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Domain functions for polynomial vector calculus operations."""
2+
3+
from __future__ import annotations
4+
5+
import sympy
6+
7+
from jacobian.math.polynomial_vector_calc._models import (
8+
DirectionalDerivativeRequest,
9+
ScalarFieldRequest,
10+
ScalarResult,
11+
VectorFieldRequest,
12+
VectorResult,
13+
)
14+
15+
16+
def _parse_poly(expr_str: str, variables: tuple[str, ...]) -> sympy.Expr:
17+
"""Parse a polynomial expression string with given variable names."""
18+
var_symbols = sympy.symbols(variables)
19+
if len(variables) == 1:
20+
var_symbols = (var_symbols,)
21+
return sympy.sympify(expr_str, locals=dict(zip(variables, var_symbols, strict=True)))
22+
23+
24+
def compute_gradient(request: ScalarFieldRequest) -> VectorResult:
25+
var_symbols = sympy.symbols(variables := request.variables)
26+
if len(variables) == 1:
27+
var_symbols = (var_symbols,)
28+
poly = _parse_poly(request.polynomial, request.variables)
29+
grads = [sympy.diff(poly, v) for v in var_symbols]
30+
return VectorResult(
31+
components=tuple(str(g) for g in grads),
32+
variables=request.variables,
33+
method="SYMPY_GRADIENT",
34+
)
35+
36+
37+
def compute_divergence(request: VectorFieldRequest) -> ScalarResult:
38+
var_symbols = sympy.symbols(request.variables)
39+
if len(request.variables) == 1:
40+
var_symbols = (var_symbols,)
41+
polys = [_parse_poly(c, request.variables) for c in request.components]
42+
div = sum(
43+
sympy.diff(p, v) for p, v in zip(polys, var_symbols, strict=True)
44+
)
45+
return ScalarResult(
46+
result=str(sympy.expand(div)),
47+
variables=request.variables,
48+
method="SYMPY_DIVERGENCE",
49+
)
50+
51+
52+
def compute_curl(request: VectorFieldRequest) -> VectorResult:
53+
"""Curl in 3D: (d_z F_y - d_y F_z, d_x F_z - d_z F_x, d_y F_x - d_x F_y)."""
54+
if len(request.variables) != 3:
55+
raise ValueError("curl is defined for 3D vector fields")
56+
x, y, z = sympy.symbols(request.variables)
57+
fx, fy, fz = (sympy.sympify(c) for c in [
58+
_parse_poly(request.components[0], request.variables),
59+
_parse_poly(request.components[1], request.variables),
60+
_parse_poly(request.components[2], request.variables),
61+
])
62+
curl_x = sympy.diff(fz, y) - sympy.diff(fy, z)
63+
curl_y = sympy.diff(fx, z) - sympy.diff(fz, x)
64+
curl_z = sympy.diff(fy, x) - sympy.diff(fx, y)
65+
return VectorResult(
66+
components=(str(sympy.expand(curl_x)), str(sympy.expand(curl_y)), str(sympy.expand(curl_z))),
67+
variables=request.variables,
68+
method="SYMPY_CURL",
69+
)
70+
71+
72+
def compute_laplacian(request: ScalarFieldRequest) -> ScalarResult:
73+
var_symbols = sympy.symbols(request.variables)
74+
if len(request.variables) == 1:
75+
var_symbols = (var_symbols,)
76+
poly = _parse_poly(request.polynomial, request.variables)
77+
laplacian = sum(sympy.diff(poly, v, 2) for v in var_symbols)
78+
return ScalarResult(
79+
result=str(sympy.expand(laplacian)),
80+
variables=request.variables,
81+
method="SYMPY_LAPLACIAN",
82+
)
83+
84+
85+
def compute_directional_derivative(
86+
request: DirectionalDerivativeRequest,
87+
) -> ScalarResult:
88+
var_symbols = sympy.symbols(request.variables)
89+
if len(request.variables) == 1:
90+
var_symbols = (var_symbols,)
91+
poly = _parse_poly(request.polynomial, request.variables)
92+
grad = [sympy.diff(poly, v) for v in var_symbols]
93+
direction = [sympy.sympify(d) for d in request.direction]
94+
result = sum(g * d for g, d in zip(grad, direction, strict=True))
95+
return ScalarResult(
96+
result=str(sympy.expand(result)),
97+
variables=request.variables,
98+
method="SYMPY_DIRECTIONAL_DERIVATIVE",
99+
)

0 commit comments

Comments
 (0)