libsymtorch 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
symtorch/main.py
ADDED
|
@@ -0,0 +1,1363 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""main.py — SymPy-to-Torch conversion and numerical integration helpers.
|
|
3
|
+
|
|
4
|
+
Standalone library.
|
|
5
|
+
|
|
6
|
+
Two-stage design for performance:
|
|
7
|
+
1. SymTorch().torchify() — expensive SymPy work done ONCE
|
|
8
|
+
(change-of-vars + ``lambdify``)
|
|
9
|
+
2. TorchExpr.torchquad_integrate() — cheap torch-only integration,
|
|
10
|
+
called MANY times
|
|
11
|
+
|
|
12
|
+
USAGE
|
|
13
|
+
=====
|
|
14
|
+
import symtorch
|
|
15
|
+
|
|
16
|
+
# Step 1: build once (slow: SymPy subs + lambdify, ~200–500 ms)
|
|
17
|
+
lt = symtorch.SymTorch()
|
|
18
|
+
texpr = lt.torchify(
|
|
19
|
+
sp.Integral(integrand, (y_A, -sp.oo, sp.oo), (y_B, -sp.oo, sp.oo))
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Step 2: integrate many times (fast: pure torch, ~3–8 ms each)
|
|
23
|
+
re, im = texpr.torchquad_integrate(N=121)
|
|
24
|
+
|
|
25
|
+
# Batched integration over a parameter grid using Simpson rule
|
|
26
|
+
re_b, im_b = texpr.torch_integrate_batched_simpson(
|
|
27
|
+
params_values=params_grid,
|
|
28
|
+
N=21,
|
|
29
|
+
chunk_size_params=256,
|
|
30
|
+
chunk_size_points=10_000,
|
|
31
|
+
)
|
|
32
|
+
"""
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
import sys
|
|
35
|
+
from typing import List, Callable, Any
|
|
36
|
+
|
|
37
|
+
import numpy as np
|
|
38
|
+
import torch
|
|
39
|
+
from scipy import special as scipy_special
|
|
40
|
+
from sympy import (
|
|
41
|
+
Eq,
|
|
42
|
+
Function,
|
|
43
|
+
Integral,
|
|
44
|
+
Integer,
|
|
45
|
+
Mul,
|
|
46
|
+
Symbol,
|
|
47
|
+
atan,
|
|
48
|
+
atanh,
|
|
49
|
+
cos,
|
|
50
|
+
cosh,
|
|
51
|
+
diff,
|
|
52
|
+
lambdify,
|
|
53
|
+
oo,
|
|
54
|
+
pi,
|
|
55
|
+
sinh,
|
|
56
|
+
tan,
|
|
57
|
+
tanh,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
from loguru import logger
|
|
61
|
+
|
|
62
|
+
def setup_logging(enable: bool = False, level: str = "INFO"):
|
|
63
|
+
"""
|
|
64
|
+
By default, we remove all handlers (silence).
|
|
65
|
+
If enable=True, we add a standard output handler.
|
|
66
|
+
------
|
|
67
|
+
Example:
|
|
68
|
+
# Disable logging (default)
|
|
69
|
+
setup_logging()
|
|
70
|
+
# Enable logging to stderr at INFO level
|
|
71
|
+
setup_logging(enable=True, level="INFO")
|
|
72
|
+
# Enable logging to stderr at DEBUG level
|
|
73
|
+
setup_logging(enable=True, level="DEBUG")
|
|
74
|
+
"""
|
|
75
|
+
logger.remove()
|
|
76
|
+
if enable:
|
|
77
|
+
logger.add(sys.stderr, level=level)
|
|
78
|
+
|
|
79
|
+
setup_logging(enable=False)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class TorchExpr:
|
|
84
|
+
"""Pre-built torch function together with its finite integration domain.
|
|
85
|
+
|
|
86
|
+
This is the object returned by ``SymTorch.torchify`` when integration
|
|
87
|
+
limits are supplied (directly or via a SymPy ``Integral``). It stores
|
|
88
|
+
both the numerical callable and all metadata needed by the integration
|
|
89
|
+
helpers in this module.
|
|
90
|
+
|
|
91
|
+
Attributes
|
|
92
|
+
----------
|
|
93
|
+
func : Callable
|
|
94
|
+
The torch-compatible function produced by ``lambdify``.
|
|
95
|
+
Its signature is::
|
|
96
|
+
|
|
97
|
+
func(new_var_0, ..., new_var_{dim-1}, param_0, ..., param_{n_params-1})
|
|
98
|
+
|
|
99
|
+
where the ``new_var_*`` correspond to the transformed integration
|
|
100
|
+
variables after any change-of-variables and the ``param_*`` are any
|
|
101
|
+
additional symbolic parameters.
|
|
102
|
+
domain : list[list[float]]
|
|
103
|
+
Finite box for the integration domain. Each entry is ``[a, b]``
|
|
104
|
+
for one dimension after change-of-variables. This is what the
|
|
105
|
+
integrators use as their integration range.
|
|
106
|
+
dim : int
|
|
107
|
+
Number of integration variables (i.e. the dimensionality of the
|
|
108
|
+
domain).
|
|
109
|
+
n_params : int
|
|
110
|
+
Number of additional symbolic parameters expected by ``func``.
|
|
111
|
+
sympy_expr : Any
|
|
112
|
+
The SymPy expression *after* all symbolic substitutions and
|
|
113
|
+
Jacobians have been applied. Kept for debugging / inspection.
|
|
114
|
+
variables : list[sympy.Symbol] | None
|
|
115
|
+
Integration variables used by the numerical object after any
|
|
116
|
+
change-of-variables has been applied. For infinite limits these
|
|
117
|
+
are the transformed symbols such as ``t_x`` rather than the
|
|
118
|
+
original symbolic variable ``x``.
|
|
119
|
+
"""
|
|
120
|
+
func: Callable # f(new_var_0, …, new_var_n, param_0, …, param_m)
|
|
121
|
+
domain: List[List[float]] # finite box for torchquad
|
|
122
|
+
dim: int # number of integration variables
|
|
123
|
+
n_params: int # number of extra parameters
|
|
124
|
+
sympy_expr: Any # sympy expression reduced if
|
|
125
|
+
variables: List[Symbol] = None # sympy variables after change of variables
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _rule_simpson(a: float, b: float, N: int, *, device=None, dtype=None):
|
|
129
|
+
if N < 3 or (N % 2) == 0:
|
|
130
|
+
raise ValueError(f"Simpson rule requires odd N>=3; got N={N}")
|
|
131
|
+
coords = torch.linspace(a, b, N, device=device, dtype=dtype)
|
|
132
|
+
dx = (b - a) / (N - 1)
|
|
133
|
+
w = torch.ones(N, device=device, dtype=dtype)
|
|
134
|
+
w[1:-1:2] = 4
|
|
135
|
+
w[2:-1:2] = 2
|
|
136
|
+
weights = w * (dx / 3.0)
|
|
137
|
+
return coords, weights
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _rule_trapezoid(a: float, b: float, N: int, *, device=None, dtype=None):
|
|
141
|
+
if N < 2:
|
|
142
|
+
raise ValueError(f"Trapezoid rule requires N>=2; got N={N}")
|
|
143
|
+
coords = torch.linspace(a, b, N, device=device, dtype=dtype)
|
|
144
|
+
dx = (b - a) / (N - 1)
|
|
145
|
+
w = torch.ones(N, device=device, dtype=dtype)
|
|
146
|
+
w[0] = 0.5
|
|
147
|
+
w[-1] = 0.5
|
|
148
|
+
weights = w * dx
|
|
149
|
+
return coords, weights
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _rule_gauss_legendre(a: float, b: float, N: int, *, device=None, dtype=None):
|
|
153
|
+
if N < 1:
|
|
154
|
+
raise ValueError(f"Gauss-Legendre rule requires N>=1; got N={N}")
|
|
155
|
+
nodes, weights = np.polynomial.legendre.leggauss(N)
|
|
156
|
+
nodes_t = torch.as_tensor(nodes, device=device, dtype=dtype)
|
|
157
|
+
weights_t = torch.as_tensor(weights, device=device, dtype=dtype)
|
|
158
|
+
# Affine map from [-1, 1] to [a, b]
|
|
159
|
+
coords = 0.5 * (b - a) * nodes_t + 0.5 * (b + a)
|
|
160
|
+
weights = 0.5 * (b - a) * weights_t
|
|
161
|
+
return coords, weights
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _resolve_quadrature_rule(method):
|
|
165
|
+
if callable(method):
|
|
166
|
+
return method
|
|
167
|
+
|
|
168
|
+
if method is None:
|
|
169
|
+
method = "simpson"
|
|
170
|
+
|
|
171
|
+
method_name = str(method).strip().lower()
|
|
172
|
+
if method_name == "simpson":
|
|
173
|
+
return TorchExpr._rule_simpson
|
|
174
|
+
if method_name == "trapezoid":
|
|
175
|
+
return TorchExpr._rule_trapezoid
|
|
176
|
+
if method_name in {"gauss-legendre", "gauss_legendre", "legendre"}:
|
|
177
|
+
return TorchExpr._rule_gauss_legendre
|
|
178
|
+
|
|
179
|
+
raise ValueError(
|
|
180
|
+
"Unknown integration method. Expected one of {'simpson', 'trapezoid', 'gauss-legendre'} or a callable. "
|
|
181
|
+
f"Got: {method}"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def _is_scalar_like(x) -> bool:
|
|
186
|
+
"""Return ``True`` for scalar-like values.
|
|
187
|
+
|
|
188
|
+
This is intentionally permissive: besides builtin numeric types,
|
|
189
|
+
it treats SymPy/mpmath numeric scalars as scalar-like if they can
|
|
190
|
+
be coerced to ``float`` or ``complex``.
|
|
191
|
+
"""
|
|
192
|
+
if torch.is_tensor(x):
|
|
193
|
+
return x.ndim == 0
|
|
194
|
+
if isinstance(x, (int, float, complex, np.number)):
|
|
195
|
+
return True
|
|
196
|
+
try:
|
|
197
|
+
float(x)
|
|
198
|
+
return True
|
|
199
|
+
except Exception:
|
|
200
|
+
pass
|
|
201
|
+
try:
|
|
202
|
+
complex(x)
|
|
203
|
+
return True
|
|
204
|
+
except Exception:
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def _normalize_params_values(params_values, n_params: int, *, device, dtype):
|
|
209
|
+
"""Normalize parameter inputs into a 2D tensor and batch shape.
|
|
210
|
+
|
|
211
|
+
Accepted input formats are:
|
|
212
|
+
|
|
213
|
+
- ``None`` when ``n_params == 0``,
|
|
214
|
+
- a tensor of shape ``(..., n_params)``, or
|
|
215
|
+
- a list/tuple of length ``n_params`` whose elements are scalars or
|
|
216
|
+
tensors with the same shape.
|
|
217
|
+
"""
|
|
218
|
+
if params_values is None:
|
|
219
|
+
if n_params != 0:
|
|
220
|
+
raise ValueError(f"Expected {n_params} params; got None")
|
|
221
|
+
return torch.empty((1, 0), device=device, dtype=dtype), tuple()
|
|
222
|
+
|
|
223
|
+
if torch.is_tensor(params_values):
|
|
224
|
+
params_tensor = params_values
|
|
225
|
+
if params_tensor.shape[-1] != n_params:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"params_values last dimension must be n_params={n_params}; got shape={tuple(params_tensor.shape)}"
|
|
228
|
+
)
|
|
229
|
+
batch_shape = tuple(params_tensor.shape[:-1])
|
|
230
|
+
batch_size = int(np.prod(batch_shape)) if batch_shape else 1
|
|
231
|
+
params_flat = params_tensor.reshape(batch_size, n_params).to(device=device, dtype=dtype)
|
|
232
|
+
return params_flat, batch_shape
|
|
233
|
+
|
|
234
|
+
if not isinstance(params_values, (list, tuple)):
|
|
235
|
+
params_values = [params_values]
|
|
236
|
+
|
|
237
|
+
if len(params_values) != n_params:
|
|
238
|
+
raise ValueError(f"Expected {n_params} params; got {len(params_values)}")
|
|
239
|
+
|
|
240
|
+
def _to_tensor(value):
|
|
241
|
+
"""Best-effort conversion of scalars / numpy arrays (incl. object dtype) to torch tensors."""
|
|
242
|
+
if torch.is_tensor(value):
|
|
243
|
+
return value.to(device=device)
|
|
244
|
+
|
|
245
|
+
# First try the fast path.
|
|
246
|
+
try:
|
|
247
|
+
return torch.as_tensor(value, device=device)
|
|
248
|
+
except Exception:
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
# Numpy arrays with dtype=object frequently come from SymPy objects.
|
|
252
|
+
if isinstance(value, np.ndarray):
|
|
253
|
+
arr = value
|
|
254
|
+
else:
|
|
255
|
+
try:
|
|
256
|
+
arr = np.asarray(value)
|
|
257
|
+
except Exception:
|
|
258
|
+
arr = None
|
|
259
|
+
|
|
260
|
+
if isinstance(arr, np.ndarray):
|
|
261
|
+
if arr.dtype == object:
|
|
262
|
+
# Try float first (most common), fall back to complex.
|
|
263
|
+
try:
|
|
264
|
+
arr = arr.astype(np.float64)
|
|
265
|
+
except Exception:
|
|
266
|
+
try:
|
|
267
|
+
arr = np.vectorize(float, otypes=[np.float64])(arr)
|
|
268
|
+
except Exception:
|
|
269
|
+
arr = np.vectorize(complex, otypes=[np.complex128])(arr)
|
|
270
|
+
return torch.as_tensor(arr, device=device)
|
|
271
|
+
|
|
272
|
+
# Scalar fallback: coerce to builtin numeric types.
|
|
273
|
+
try:
|
|
274
|
+
return torch.as_tensor(complex(value), device=device)
|
|
275
|
+
except Exception:
|
|
276
|
+
return torch.as_tensor(float(value), device=device)
|
|
277
|
+
|
|
278
|
+
batch_shape = None
|
|
279
|
+
for param_value in params_values:
|
|
280
|
+
if torch.is_tensor(param_value) and param_value.ndim > 0:
|
|
281
|
+
batch_shape = tuple(param_value.shape)
|
|
282
|
+
break
|
|
283
|
+
if not TorchExpr._is_scalar_like(param_value):
|
|
284
|
+
param_tensor = _to_tensor(param_value)
|
|
285
|
+
if param_tensor.ndim > 0:
|
|
286
|
+
batch_shape = tuple(param_tensor.shape)
|
|
287
|
+
break
|
|
288
|
+
if batch_shape is None:
|
|
289
|
+
batch_shape = tuple()
|
|
290
|
+
|
|
291
|
+
params_list = []
|
|
292
|
+
for param_value in params_values:
|
|
293
|
+
param_tensor = _to_tensor(param_value)
|
|
294
|
+
if param_tensor.ndim == 0:
|
|
295
|
+
if batch_shape:
|
|
296
|
+
param_tensor = param_tensor.expand(batch_shape)
|
|
297
|
+
else:
|
|
298
|
+
if tuple(param_tensor.shape) != batch_shape:
|
|
299
|
+
raise ValueError(
|
|
300
|
+
f"All parameter tensors must have the same shape; got {tuple(param_tensor.shape)} vs {batch_shape}"
|
|
301
|
+
)
|
|
302
|
+
params_list.append(param_tensor)
|
|
303
|
+
|
|
304
|
+
params_tensor = torch.stack(params_list, dim=-1)
|
|
305
|
+
if dtype is not None:
|
|
306
|
+
params_tensor = params_tensor.to(dtype=dtype)
|
|
307
|
+
batch_size = int(np.prod(batch_shape)) if batch_shape else 1
|
|
308
|
+
params_flat = params_tensor.reshape(batch_size, n_params)
|
|
309
|
+
return params_flat, batch_shape
|
|
310
|
+
|
|
311
|
+
# ------------------------------------------------------------------
|
|
312
|
+
# Convenience methods: numeric integration on this TorchExpr
|
|
313
|
+
# ------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
def torchquad_integrate(self, params_values=None, method=None, N: int = 21, dtype=None):
|
|
316
|
+
"""Integrate this ``TorchExpr`` using the torchquad-based integrator.
|
|
317
|
+
|
|
318
|
+
This is the simplest integration entry point. It is intended for
|
|
319
|
+
low-dimensional problems where a single torchquad integral is
|
|
320
|
+
sufficient.
|
|
321
|
+
|
|
322
|
+
Parameters
|
|
323
|
+
----------
|
|
324
|
+
params_values : list | tuple | torch.Tensor | None, optional
|
|
325
|
+
Numerical values for the symbolic parameters. The accepted
|
|
326
|
+
formats match those of ``torchquad_integrate``:
|
|
327
|
+
|
|
328
|
+
- ``None`` if there are no parameters,
|
|
329
|
+
- a Python sequence (list/tuple) of numbers/tensors, or
|
|
330
|
+
- a tensor whose last dimension is ``n_params``.
|
|
331
|
+
method : torchquad integrator instance or None, optional
|
|
332
|
+
If ``None``, a default ``Simpson`` integrator from torchquad is
|
|
333
|
+
constructed internally.
|
|
334
|
+
N : int, optional
|
|
335
|
+
Resolution parameter passed through to torchquad. Larger values
|
|
336
|
+
increase accuracy but also the number of function evaluations.
|
|
337
|
+
dtype : torch.dtype or None, optional
|
|
338
|
+
Floating dtype used for internal computations. This controls
|
|
339
|
+
how ``domain_points`` and parameter values are coerced before
|
|
340
|
+
evaluation. If ``None``, torchquad's defaults are used.
|
|
341
|
+
|
|
342
|
+
Returns
|
|
343
|
+
-------
|
|
344
|
+
re, im : torch.Tensor
|
|
345
|
+
Real and imaginary parts of the integral. If the integrand is
|
|
346
|
+
real-valued, ``im`` will be a zero tensor.
|
|
347
|
+
|
|
348
|
+
USAGE
|
|
349
|
+
=====
|
|
350
|
+
1. Simple 1D integral without parameters::
|
|
351
|
+
|
|
352
|
+
````python
|
|
353
|
+
import symtorch
|
|
354
|
+
from sympy import symbols, Integral, exp, oo
|
|
355
|
+
|
|
356
|
+
x = symbols("x", real=True)
|
|
357
|
+
integral_expr = Integral(exp(-x**2), (x, -oo, oo))
|
|
358
|
+
|
|
359
|
+
lt = symtorch.SymTorch()
|
|
360
|
+
texpr = lt.torchify(integral_expr)
|
|
361
|
+
re, im = texpr.torchquad_integrate(N=121)
|
|
362
|
+
````
|
|
363
|
+
|
|
364
|
+
2. Integral with parameters (e.g. Fourier transform)::
|
|
365
|
+
|
|
366
|
+
````python
|
|
367
|
+
import symtorch
|
|
368
|
+
from sympy import symbols, Integral, exp, I, oo
|
|
369
|
+
import torch
|
|
370
|
+
|
|
371
|
+
x, k = symbols("x k", real=True)
|
|
372
|
+
integral_expr = Integral(exp(-x**2) * exp(I * k * x), (x, -oo, oo))
|
|
373
|
+
|
|
374
|
+
lt = symtorch.SymTorch()
|
|
375
|
+
texpr = lt.torchify(integral_expr)
|
|
376
|
+
|
|
377
|
+
# Evaluate at k = 0.5
|
|
378
|
+
re, im = texpr.torchquad_integrate(params_values=[0.5], N=121)
|
|
379
|
+
````
|
|
380
|
+
"""
|
|
381
|
+
from torchquad import Simpson
|
|
382
|
+
|
|
383
|
+
if method is None:
|
|
384
|
+
method = Simpson()
|
|
385
|
+
|
|
386
|
+
param_vals = list(params_values) if params_values else []
|
|
387
|
+
|
|
388
|
+
def _as_torch_scalar(value, *, device, dtype):
|
|
389
|
+
"""Best-effort conversion of Python/numpy/mpmath/sympy scalars to torch tensors.
|
|
390
|
+
|
|
391
|
+
torch.as_tensor() cannot infer dtype for some scalar-like types
|
|
392
|
+
(e.g. mpmath.mpf). We coerce those to built-in complex/float.
|
|
393
|
+
"""
|
|
394
|
+
if torch.is_tensor(value):
|
|
395
|
+
return value.to(device=device, dtype=dtype) if dtype is not None else value.to(device=device)
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
return torch.as_tensor(value, device=device, dtype=dtype) if dtype is not None else torch.as_tensor(value, device=device)
|
|
399
|
+
except Exception:
|
|
400
|
+
# Fallback for types like mpmath.mpf/mpc, sympy Float, etc.
|
|
401
|
+
try:
|
|
402
|
+
# Avoid forcing a real dtype on complex values.
|
|
403
|
+
return torch.as_tensor(complex(value), device=device)
|
|
404
|
+
except Exception:
|
|
405
|
+
return (
|
|
406
|
+
torch.as_tensor(float(value), device=device, dtype=dtype)
|
|
407
|
+
if dtype is not None
|
|
408
|
+
else torch.as_tensor(float(value), device=device)
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
def integrand(domain_points):
|
|
412
|
+
points = domain_points.to(dtype=dtype) if dtype is not None else domain_points
|
|
413
|
+
param_vals_local = [
|
|
414
|
+
_as_torch_scalar(param_value, device=points.device, dtype=dtype)
|
|
415
|
+
for param_value in param_vals
|
|
416
|
+
]
|
|
417
|
+
values = self.func(*[points[:, i] for i in range(points.shape[1])], *param_vals_local)
|
|
418
|
+
out = torch.as_tensor(values, device=points.device)
|
|
419
|
+
if dtype is not None and out.is_floating_point():
|
|
420
|
+
out = out.to(dtype=dtype)
|
|
421
|
+
return out
|
|
422
|
+
|
|
423
|
+
result = method.integrate(
|
|
424
|
+
integrand,
|
|
425
|
+
dim=self.dim,
|
|
426
|
+
N=N,
|
|
427
|
+
integration_domain=self.domain,
|
|
428
|
+
)
|
|
429
|
+
return (
|
|
430
|
+
result.real if torch.is_complex(result) else result,
|
|
431
|
+
result.imag if torch.is_complex(result) else torch.zeros_like(result),
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
def torch_integrate_batched(
|
|
435
|
+
self,
|
|
436
|
+
*,
|
|
437
|
+
params_values = None,
|
|
438
|
+
method: str | Callable = "simpson",
|
|
439
|
+
N: int = 121,
|
|
440
|
+
chunk_size_params: int = 256,
|
|
441
|
+
chunk_size_points: int | None = None,
|
|
442
|
+
device=None,
|
|
443
|
+
dtype=None,
|
|
444
|
+
):
|
|
445
|
+
"""Batched tensor-product quadrature integration for this ``TorchExpr``.
|
|
446
|
+
|
|
447
|
+
This method is intended for situations where you want to evaluate
|
|
448
|
+
the same integral for many different parameter values (for example,
|
|
449
|
+
on a 2D or 3D mesh).
|
|
450
|
+
|
|
451
|
+
Parameters
|
|
452
|
+
----------
|
|
453
|
+
params_values : tensor | list | tuple
|
|
454
|
+
Numerical parameters. See ``torch_integrate_batched``
|
|
455
|
+
for a full description of the accepted shapes and types.
|
|
456
|
+
method : str or callable, optional
|
|
457
|
+
Quadrature rule name (``"simpson"``, ``"trapezoid"``,
|
|
458
|
+
``"gauss-legendre"``) or a callable returning
|
|
459
|
+
``(coords_1d, weights_1d)`` for ``(a, b, N)``.
|
|
460
|
+
N : int, optional
|
|
461
|
+
Odd number of Simpson points per dimension.
|
|
462
|
+
chunk_size_params : int, optional
|
|
463
|
+
Number of parameter points processed in one chunk. Reducing
|
|
464
|
+
this lowers peak memory usage at the cost of more Python loops.
|
|
465
|
+
chunk_size_points : int or None, optional
|
|
466
|
+
Maximum number of grid points processed per chunk. ``None``
|
|
467
|
+
means "all at once".
|
|
468
|
+
device : torch.device or None, optional
|
|
469
|
+
Device used for all internal tensors. If ``None``, CUDA is
|
|
470
|
+
used when available, otherwise CPU.
|
|
471
|
+
dtype : torch.dtype or None, optional
|
|
472
|
+
Floating dtype for internal computations. Defaults to
|
|
473
|
+
``float32`` on CUDA and ``float64`` on CPU.
|
|
474
|
+
|
|
475
|
+
Returns
|
|
476
|
+
-------
|
|
477
|
+
re, im : torch.Tensor
|
|
478
|
+
Tensors with the same batch shape as the input parameters,
|
|
479
|
+
containing the real and imaginary parts of the integral.
|
|
480
|
+
|
|
481
|
+
USAGE
|
|
482
|
+
=====
|
|
483
|
+
1. 1D integral without parameters (single value)::
|
|
484
|
+
|
|
485
|
+
````python
|
|
486
|
+
import symtorch
|
|
487
|
+
from sympy import symbols, Integral, exp, oo
|
|
488
|
+
|
|
489
|
+
x = symbols("x", real=True)
|
|
490
|
+
integral_expr = Integral(exp(-x**2), (x, -oo, oo))
|
|
491
|
+
|
|
492
|
+
lt = symtorch.SymTorch()
|
|
493
|
+
texpr = lt.torchify(integral_expr)
|
|
494
|
+
|
|
495
|
+
# No parameters → pass ``None`` for ``params_values``
|
|
496
|
+
re, im = texpr.torch_integrate_batched(
|
|
497
|
+
params_values=None,
|
|
498
|
+
N=121,
|
|
499
|
+
)
|
|
500
|
+
````
|
|
501
|
+
|
|
502
|
+
2. 1D integral evaluated on a grid of parameter values::
|
|
503
|
+
|
|
504
|
+
````python
|
|
505
|
+
import symtorch
|
|
506
|
+
from sympy import symbols, Integral, exp, I, oo
|
|
507
|
+
import torch
|
|
508
|
+
|
|
509
|
+
x, k = symbols("x k", real=True)
|
|
510
|
+
integral_expr = Integral(exp(-x**2) * exp(I * k * x), (x, -oo, oo))
|
|
511
|
+
|
|
512
|
+
lt = symtorch.SymTorch()
|
|
513
|
+
texpr = lt.torchify(integral_expr)
|
|
514
|
+
|
|
515
|
+
# Parameter grid: 250 points between -5 and 5
|
|
516
|
+
k_grid = torch.linspace(-5.0, 5.0, 250).unsqueeze(-1) # shape (250, 1)
|
|
517
|
+
|
|
518
|
+
re, im = texpr.torch_integrate_batched(
|
|
519
|
+
params_values=k_grid,
|
|
520
|
+
N=51,
|
|
521
|
+
chunk_size_params=64,
|
|
522
|
+
chunk_size_points=10_000,
|
|
523
|
+
)
|
|
524
|
+
````
|
|
525
|
+
"""
|
|
526
|
+
if self.dim <= 0:
|
|
527
|
+
raise ValueError(f"self.dim must be positive; got dim={self.dim}")
|
|
528
|
+
if self.n_params < 0:
|
|
529
|
+
raise ValueError(f"self.n_params must be non-negative; got n_params={self.n_params}")
|
|
530
|
+
if len(self.domain) != self.dim:
|
|
531
|
+
raise ValueError(f"self.domain must have length dim={self.dim}; got {len(self.domain)}")
|
|
532
|
+
|
|
533
|
+
if device is None:
|
|
534
|
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
535
|
+
if dtype is None:
|
|
536
|
+
dtype = torch.float32 if device.type == "cuda" else torch.float64
|
|
537
|
+
|
|
538
|
+
if chunk_size_params <= 0:
|
|
539
|
+
raise ValueError(f"chunk_size_params must be positive; got {chunk_size_params}")
|
|
540
|
+
if chunk_size_points is not None and chunk_size_points <= 0:
|
|
541
|
+
raise ValueError(f"chunk_size_points must be positive; got {chunk_size_points}")
|
|
542
|
+
|
|
543
|
+
params_flat, batch_shape = self._normalize_params_values(
|
|
544
|
+
params_values,
|
|
545
|
+
self.n_params,
|
|
546
|
+
device=device,
|
|
547
|
+
dtype=dtype,
|
|
548
|
+
)
|
|
549
|
+
batch_size = int(params_flat.shape[0])
|
|
550
|
+
|
|
551
|
+
rule = self._resolve_quadrature_rule(method)
|
|
552
|
+
|
|
553
|
+
coords_1d = []
|
|
554
|
+
weights_1d = []
|
|
555
|
+
for (lower_bound, upper_bound) in self.domain:
|
|
556
|
+
lower_bound = float(lower_bound)
|
|
557
|
+
upper_bound = float(upper_bound)
|
|
558
|
+
coords, weights = rule(lower_bound, upper_bound, N, device=device, dtype=dtype)
|
|
559
|
+
coords = torch.as_tensor(coords, device=device, dtype=dtype).reshape(-1)
|
|
560
|
+
weights = torch.as_tensor(weights, device=device, dtype=dtype).reshape(-1)
|
|
561
|
+
if coords.numel() != weights.numel():
|
|
562
|
+
raise ValueError(
|
|
563
|
+
f"Quadrature rule returned mismatched coordinates/weights lengths: {coords.numel()} vs {weights.numel()}"
|
|
564
|
+
)
|
|
565
|
+
coords_1d.append(coords)
|
|
566
|
+
weights_1d.append(weights)
|
|
567
|
+
|
|
568
|
+
if self.dim == 1:
|
|
569
|
+
grid = coords_1d[0].unsqueeze(1)
|
|
570
|
+
weights = weights_1d[0]
|
|
571
|
+
else:
|
|
572
|
+
grid = torch.cartesian_prod(*coords_1d)
|
|
573
|
+
weights = weights_1d[0]
|
|
574
|
+
for weight_1d in weights_1d[1:]:
|
|
575
|
+
weights = torch.kron(weights, weight_1d)
|
|
576
|
+
n_points = int(grid.shape[0])
|
|
577
|
+
|
|
578
|
+
if chunk_size_points is None:
|
|
579
|
+
chunk_size_points = n_points
|
|
580
|
+
|
|
581
|
+
re_out = torch.empty(batch_size, device=device, dtype=dtype)
|
|
582
|
+
im_out = torch.empty(batch_size, device=device, dtype=dtype)
|
|
583
|
+
|
|
584
|
+
for start_param in range(0, batch_size, chunk_size_params):
|
|
585
|
+
stop_param = min(batch_size, start_param + chunk_size_params)
|
|
586
|
+
param_chunk = params_flat[start_param:stop_param, :]
|
|
587
|
+
param_args = [param_chunk[:, index].unsqueeze(0) for index in range(self.n_params)]
|
|
588
|
+
|
|
589
|
+
re_acc = torch.zeros(stop_param - start_param, device=device, dtype=dtype)
|
|
590
|
+
im_acc = torch.zeros(stop_param - start_param, device=device, dtype=dtype)
|
|
591
|
+
|
|
592
|
+
for start_point in range(0, n_points, chunk_size_points):
|
|
593
|
+
stop_point = min(n_points, start_point + chunk_size_points)
|
|
594
|
+
grid_chunk = grid[start_point:stop_point, :]
|
|
595
|
+
weight_chunk = weights[start_point:stop_point].unsqueeze(1)
|
|
596
|
+
|
|
597
|
+
var_args = [grid_chunk[:, index].unsqueeze(1) for index in range(self.dim)]
|
|
598
|
+
values = self.func(*var_args, *param_args)
|
|
599
|
+
values = torch.as_tensor(values, device=device)
|
|
600
|
+
|
|
601
|
+
if values.ndim == 1:
|
|
602
|
+
values = values.unsqueeze(1)
|
|
603
|
+
elif (
|
|
604
|
+
values.ndim == 2
|
|
605
|
+
and values.shape[0] == (stop_param - start_param)
|
|
606
|
+
and values.shape[1] == (stop_point - start_point)
|
|
607
|
+
):
|
|
608
|
+
values = values.t().contiguous()
|
|
609
|
+
|
|
610
|
+
if values.shape[0] != (stop_point - start_point) or values.shape[1] != (stop_param - start_param):
|
|
611
|
+
raise RuntimeError(
|
|
612
|
+
f"Unexpected integrand output shape {tuple(values.shape)}; expected ({stop_point - start_point}, {stop_param - start_param})"
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
if torch.is_complex(values):
|
|
616
|
+
re_acc += (weight_chunk * values.real).sum(dim=0)
|
|
617
|
+
im_acc += (weight_chunk * values.imag).sum(dim=0)
|
|
618
|
+
else:
|
|
619
|
+
re_acc += (weight_chunk * values).sum(dim=0)
|
|
620
|
+
|
|
621
|
+
re_out[start_param:stop_param] = re_acc
|
|
622
|
+
im_out[start_param:stop_param] = im_acc
|
|
623
|
+
|
|
624
|
+
re_out = re_out.reshape(batch_shape)
|
|
625
|
+
im_out = im_out.reshape(batch_shape)
|
|
626
|
+
return re_out, im_out
|
|
627
|
+
|
|
628
|
+
def torch_integrate_batched_simpson(
|
|
629
|
+
self,
|
|
630
|
+
*,
|
|
631
|
+
params_values = None,
|
|
632
|
+
N: int = 121,
|
|
633
|
+
chunk_size_params: int = 256,
|
|
634
|
+
chunk_size_points: int | None = None,
|
|
635
|
+
device=None,
|
|
636
|
+
dtype=None,
|
|
637
|
+
):
|
|
638
|
+
"""Backward-compatible wrapper for ``torch_integrate_batched(method='simpson')``."""
|
|
639
|
+
return self.torch_integrate_batched(
|
|
640
|
+
params_values=params_values,
|
|
641
|
+
method="simpson",
|
|
642
|
+
N=N,
|
|
643
|
+
chunk_size_params=chunk_size_params,
|
|
644
|
+
chunk_size_points=chunk_size_points,
|
|
645
|
+
device=device,
|
|
646
|
+
dtype=dtype,
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
class SymTorch:
|
|
652
|
+
def __init__(self):
|
|
653
|
+
pass
|
|
654
|
+
|
|
655
|
+
@staticmethod
|
|
656
|
+
def _normalize_cov_method(change_of_variables_method: str) -> str:
|
|
657
|
+
method = str(change_of_variables_method).strip().lower()
|
|
658
|
+
aliases = {
|
|
659
|
+
"tangent": "tangent",
|
|
660
|
+
"tan": "tangent",
|
|
661
|
+
"algebraic": "algebraic",
|
|
662
|
+
"rational": "algebraic",
|
|
663
|
+
"tanh-sinh": "tanh-sinh",
|
|
664
|
+
"tanh_sinh": "tanh-sinh",
|
|
665
|
+
"tanhsinh": "tanh-sinh",
|
|
666
|
+
}
|
|
667
|
+
if method not in aliases:
|
|
668
|
+
raise ValueError(
|
|
669
|
+
"Unknown change_of_variables_method. Expected one of {'tangent', 'algebraic', 'tanh-sinh'}. "
|
|
670
|
+
f"Got: {change_of_variables_method}"
|
|
671
|
+
)
|
|
672
|
+
return aliases[method]
|
|
673
|
+
|
|
674
|
+
@staticmethod
|
|
675
|
+
def _inset_open_interval(lower: float, upper: float, eps: float) -> list[float]:
|
|
676
|
+
return [float(lower + eps), float(upper - eps)]
|
|
677
|
+
|
|
678
|
+
def _transform_limit_with_method(self, variable, lower, upper, *, change_of_variables_method: str, eps: float):
|
|
679
|
+
"""Return transformed variable, substitution, jacobian and finite domain."""
|
|
680
|
+
method = self._normalize_cov_method(change_of_variables_method)
|
|
681
|
+
|
|
682
|
+
if lower != -oo and upper != oo:
|
|
683
|
+
return variable, variable, Integer(1), [float(lower), float(upper)]
|
|
684
|
+
|
|
685
|
+
t_v = Symbol(f"t_{variable.name}", real=True)
|
|
686
|
+
open_interval = None
|
|
687
|
+
|
|
688
|
+
if method == "tangent":
|
|
689
|
+
if lower == -oo and upper == oo:
|
|
690
|
+
mapped = tan(t_v)
|
|
691
|
+
open_interval = (float(-pi / 2), float(pi / 2))
|
|
692
|
+
elif lower != -oo and upper == oo:
|
|
693
|
+
mapped = lower + tan(t_v) ** 2
|
|
694
|
+
open_interval = (0.0, float(pi / 2))
|
|
695
|
+
elif lower == -oo and upper != oo:
|
|
696
|
+
mapped = upper - tan(t_v) ** 2
|
|
697
|
+
open_interval = (0.0, float(pi / 2))
|
|
698
|
+
else:
|
|
699
|
+
raise ValueError("Unexpected limit pattern in tangent transform")
|
|
700
|
+
|
|
701
|
+
elif method == "algebraic":
|
|
702
|
+
if lower == -oo and upper == oo:
|
|
703
|
+
mapped = t_v / (1 - t_v ** 2)
|
|
704
|
+
open_interval = (-1.0, 1.0)
|
|
705
|
+
elif lower != -oo and upper == oo:
|
|
706
|
+
mapped = lower + t_v / (1 - t_v)
|
|
707
|
+
open_interval = (0.0, 1.0)
|
|
708
|
+
elif lower == -oo and upper != oo:
|
|
709
|
+
mapped = upper - t_v / (1 - t_v)
|
|
710
|
+
open_interval = (0.0, 1.0)
|
|
711
|
+
else:
|
|
712
|
+
raise ValueError("Unexpected limit pattern in algebraic transform")
|
|
713
|
+
|
|
714
|
+
elif method == "tanh-sinh":
|
|
715
|
+
# Finite-interval parameter t in (-1,1) or (0,1) mapped through sinh(atanh(t)).
|
|
716
|
+
core = sinh(atanh(t_v))
|
|
717
|
+
if lower == -oo and upper == oo:
|
|
718
|
+
mapped = core
|
|
719
|
+
open_interval = (-1.0, 1.0)
|
|
720
|
+
elif lower != -oo and upper == oo:
|
|
721
|
+
mapped = lower + core ** 2
|
|
722
|
+
open_interval = (0.0, 1.0)
|
|
723
|
+
elif lower == -oo and upper != oo:
|
|
724
|
+
mapped = upper - core ** 2
|
|
725
|
+
open_interval = (0.0, 1.0)
|
|
726
|
+
else:
|
|
727
|
+
raise ValueError("Unexpected limit pattern in tanh-sinh transform")
|
|
728
|
+
|
|
729
|
+
else:
|
|
730
|
+
raise ValueError(f"Unsupported change_of_variables_method: {method}")
|
|
731
|
+
|
|
732
|
+
jacobian = diff(mapped, t_v)
|
|
733
|
+
domain = self._inset_open_interval(open_interval[0], open_interval[1], eps)
|
|
734
|
+
return t_v, mapped, jacobian, domain
|
|
735
|
+
|
|
736
|
+
def _merge_lambdify_modules(self, modules, extra_mapping):
|
|
737
|
+
"""Merge lambdify modules with an extra mapping dict at highest priority."""
|
|
738
|
+
if not extra_mapping:
|
|
739
|
+
return modules
|
|
740
|
+
|
|
741
|
+
if modules is None:
|
|
742
|
+
modules = self._default_modules()
|
|
743
|
+
|
|
744
|
+
if isinstance(modules, (list, tuple)):
|
|
745
|
+
merged = list(modules)
|
|
746
|
+
merged.insert(0, dict(extra_mapping))
|
|
747
|
+
return merged
|
|
748
|
+
|
|
749
|
+
return [dict(extra_mapping), modules]
|
|
750
|
+
|
|
751
|
+
def _build_nested_definite_integral_callable(self, texpr: TorchExpr, n_params: int, *, inner_N: int = 61):
|
|
752
|
+
"""Create a callable for lambdify that evaluates a definite nested integral numerically."""
|
|
753
|
+
|
|
754
|
+
def eval_nested(*args):
|
|
755
|
+
if len(args) != n_params:
|
|
756
|
+
raise ValueError(f"Expected {n_params} nested integral params; got {len(args)}")
|
|
757
|
+
|
|
758
|
+
if n_params == 0:
|
|
759
|
+
re_val, im_val = texpr.torchquad_integrate(N=inner_N)
|
|
760
|
+
re_tensor = torch.as_tensor(re_val)
|
|
761
|
+
im_tensor = torch.as_tensor(im_val, device=re_tensor.device)
|
|
762
|
+
return re_tensor + 1j * im_tensor
|
|
763
|
+
|
|
764
|
+
args_tensors = [torch.as_tensor(arg) for arg in args]
|
|
765
|
+
broadcasted = torch.broadcast_tensors(*args_tensors)
|
|
766
|
+
target_device = broadcasted[0].device
|
|
767
|
+
batch_shape = tuple(broadcasted[0].shape)
|
|
768
|
+
flat_params = [tensor.reshape(-1) for tensor in broadcasted]
|
|
769
|
+
batch_size = int(flat_params[0].shape[0])
|
|
770
|
+
|
|
771
|
+
out_re = []
|
|
772
|
+
out_im = []
|
|
773
|
+
for idx in range(batch_size):
|
|
774
|
+
params_i = [flat_params[param_index][idx] for param_index in range(n_params)]
|
|
775
|
+
re_val, im_val = texpr.torchquad_integrate(params_values=params_i, N=inner_N)
|
|
776
|
+
out_re.append(torch.as_tensor(re_val, device=target_device))
|
|
777
|
+
out_im.append(torch.as_tensor(im_val, device=target_device))
|
|
778
|
+
|
|
779
|
+
re_tensor = torch.stack(out_re).reshape(batch_shape)
|
|
780
|
+
im_tensor = torch.stack(out_im).reshape(batch_shape).to(device=re_tensor.device)
|
|
781
|
+
return re_tensor + 1j * im_tensor
|
|
782
|
+
|
|
783
|
+
return eval_nested
|
|
784
|
+
|
|
785
|
+
def _replace_nested_definite_integrals(
|
|
786
|
+
self,
|
|
787
|
+
expr,
|
|
788
|
+
*,
|
|
789
|
+
inner_N: int = 61,
|
|
790
|
+
change_of_variables_method: str = "tangent",
|
|
791
|
+
cov_eps: float = 1e-7,
|
|
792
|
+
):
|
|
793
|
+
"""Replace nested definite Integrals with numeric callables.
|
|
794
|
+
|
|
795
|
+
Any nested integral with non-definite limits triggers a ValueError.
|
|
796
|
+
"""
|
|
797
|
+
if not hasattr(expr, "has") or not expr.has(Integral):
|
|
798
|
+
return expr, {}
|
|
799
|
+
|
|
800
|
+
expr_work = expr
|
|
801
|
+
nested_mapping = {}
|
|
802
|
+
counter = 0
|
|
803
|
+
|
|
804
|
+
while expr_work.has(Integral):
|
|
805
|
+
leaf_integrals = [itg for itg in expr_work.atoms(Integral) if not itg.function.has(Integral)]
|
|
806
|
+
if not leaf_integrals:
|
|
807
|
+
break
|
|
808
|
+
|
|
809
|
+
for inner_integral in leaf_integrals:
|
|
810
|
+
for lim in inner_integral.limits:
|
|
811
|
+
if len(lim) != 3:
|
|
812
|
+
raise ValueError(
|
|
813
|
+
"The integrand must not contain unevaluated indefinite integrals. "
|
|
814
|
+
f"Found nested integral with non-definite limit: {inner_integral}"
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
integration_vars = [lim[0] for lim in inner_integral.limits]
|
|
818
|
+
param_symbols = sorted(
|
|
819
|
+
list(inner_integral.free_symbols - set(integration_vars)),
|
|
820
|
+
key=lambda symbol: symbol.name,
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
nested_texpr = self.torchify(
|
|
824
|
+
inner_integral,
|
|
825
|
+
change_of_variables_method=change_of_variables_method,
|
|
826
|
+
cov_eps=cov_eps,
|
|
827
|
+
)
|
|
828
|
+
nested_name = f"_nested_definite_integral_{counter}"
|
|
829
|
+
counter += 1
|
|
830
|
+
|
|
831
|
+
nested_mapping[nested_name] = self._build_nested_definite_integral_callable(
|
|
832
|
+
nested_texpr,
|
|
833
|
+
len(param_symbols),
|
|
834
|
+
inner_N=inner_N,
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
placeholder = Function(nested_name)(*param_symbols)
|
|
838
|
+
expr_work = expr_work.xreplace({inner_integral: placeholder})
|
|
839
|
+
|
|
840
|
+
if expr_work.has(Integral):
|
|
841
|
+
raise ValueError(
|
|
842
|
+
"The integrand must not contain unevaluated integrals. "
|
|
843
|
+
"Only nested definite integrals are supported."
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
return expr_work, nested_mapping
|
|
847
|
+
|
|
848
|
+
def _default_modules(self):
|
|
849
|
+
"""Internal helper: default SymPy → torch mapping for ``lambdify``.
|
|
850
|
+
|
|
851
|
+
The returned list is passed directly to SymPy's ``lambdify`` as the
|
|
852
|
+
``modules`` argument. It exposes a minimal subset of functions
|
|
853
|
+
implemented with torch operations so that the resulting numerical
|
|
854
|
+
function is differentiable and GPU-friendly where possible.
|
|
855
|
+
|
|
856
|
+
Keeping this mapping in a separate method avoids cluttering
|
|
857
|
+
:meth:`torchify` and makes it easy to customize or extend in user
|
|
858
|
+
code by subclassing ``SymTorch``.
|
|
859
|
+
"""
|
|
860
|
+
|
|
861
|
+
def safe_sqrt(x):
|
|
862
|
+
return torch.sqrt(torch.as_tensor(x, dtype=torch.float64))
|
|
863
|
+
|
|
864
|
+
def safe_erf(x):
|
|
865
|
+
"""Error function that supports real torch tensors and complex inputs."""
|
|
866
|
+
if torch.is_tensor(x):
|
|
867
|
+
if torch.is_complex(x):
|
|
868
|
+
# torch.erf is not implemented for complex tensors on CPU.
|
|
869
|
+
out = scipy_special.erf(x.detach().cpu().numpy())
|
|
870
|
+
return torch.as_tensor(out, device=x.device, dtype=x.dtype)
|
|
871
|
+
return torch.erf(x)
|
|
872
|
+
return scipy_special.erf(x)
|
|
873
|
+
|
|
874
|
+
def safe_erfc(x):
|
|
875
|
+
"""Complementary error function with complex-input fallback."""
|
|
876
|
+
if torch.is_tensor(x):
|
|
877
|
+
if torch.is_complex(x):
|
|
878
|
+
out = scipy_special.erfc(x.detach().cpu().numpy())
|
|
879
|
+
return torch.as_tensor(out, device=x.device, dtype=x.dtype)
|
|
880
|
+
return torch.erfc(x)
|
|
881
|
+
return scipy_special.erfc(x)
|
|
882
|
+
|
|
883
|
+
return [{
|
|
884
|
+
# Trigonometric
|
|
885
|
+
"sin": torch.sin, "cos": torch.cos, "tan": torch.tan,
|
|
886
|
+
"asin": torch.asin, "acos": torch.acos, "atan": torch.atan,
|
|
887
|
+
"atan2": torch.atan2,
|
|
888
|
+
# Hyperbolic
|
|
889
|
+
"sinh": torch.sinh, "cosh": torch.cosh, "tanh": torch.tanh,
|
|
890
|
+
"asinh": torch.asinh, "acosh": torch.acosh, "atanh": torch.atanh,
|
|
891
|
+
# Exponentials / logs
|
|
892
|
+
"exp": torch.exp, "log": torch.log, "ln": torch.log,
|
|
893
|
+
"log10": torch.log10, "log2": torch.log2,
|
|
894
|
+
# Special functions used by symbolic integral evaluation
|
|
895
|
+
"erf": safe_erf, "erfc": safe_erfc,
|
|
896
|
+
# Roots / powers
|
|
897
|
+
"sqrt": safe_sqrt, "Pow": torch.pow,
|
|
898
|
+
# Misc
|
|
899
|
+
"Abs": torch.abs, "sign": torch.sign,
|
|
900
|
+
"conjugate": torch.conj, "conj": torch.conj,
|
|
901
|
+
"floor": torch.floor, "ceiling": torch.ceil,
|
|
902
|
+
"Min": torch.minimum, "Max": torch.maximum,
|
|
903
|
+
# Piecewise / heaviside
|
|
904
|
+
"Heaviside": lambda x: torch.heaviside(x, torch.zeros_like(x)),
|
|
905
|
+
# Constants
|
|
906
|
+
"pi": getattr(torch, "pi", float(np.pi)),
|
|
907
|
+
"E": float(np.e),
|
|
908
|
+
}]
|
|
909
|
+
|
|
910
|
+
def torchify(
|
|
911
|
+
self,
|
|
912
|
+
expr,
|
|
913
|
+
*,
|
|
914
|
+
variables=None,
|
|
915
|
+
limits=None,
|
|
916
|
+
params=None,
|
|
917
|
+
modules=None,
|
|
918
|
+
change_of_variables_method: str = "tangent",
|
|
919
|
+
cov_eps: float = 1e-7,
|
|
920
|
+
):
|
|
921
|
+
"""Convert a SymPy object into a torch-compatible callable or TorchExpr.
|
|
922
|
+
|
|
923
|
+
This mirrors the original functional ``torchify`` but lives as a
|
|
924
|
+
method on ``SymTorch``. It performs change-of-variables for
|
|
925
|
+
infinite / semi-infinite limits at the SymPy level (once), then
|
|
926
|
+
``lambdify``'s the transformed expression.
|
|
927
|
+
|
|
928
|
+
``expr`` can be:
|
|
929
|
+
|
|
930
|
+
- a plain SymPy expression (e.g. ``exp(-x**2)``),
|
|
931
|
+
- a SymPy ``Integral`` (``Integral(f(x), (x, -oo, oo))``), or
|
|
932
|
+
- a SymPy ``Eq`` whose right-hand side is an ``Integral``.
|
|
933
|
+
|
|
934
|
+
If an ``Integral`` (or equation with an integral) is passed, the
|
|
935
|
+
integrand and limits are extracted internally, so you no longer
|
|
936
|
+
need to manually use ``.function`` / ``.limits``.
|
|
937
|
+
|
|
938
|
+
Parameters
|
|
939
|
+
----------
|
|
940
|
+
expr : sympy.Expr | sympy.Integral | sympy.Eq
|
|
941
|
+
Symbolic expression (may be complex-valued) or an integral.
|
|
942
|
+
variables : list[sympy.Symbol] or None
|
|
943
|
+
Integration variables (order matters). If ``expr`` is an
|
|
944
|
+
``Integral`` and ``variables`` is ``None``, they are inferred
|
|
945
|
+
from the integral limits.
|
|
946
|
+
limits : list[tuple] or None
|
|
947
|
+
``(var, lower, upper)`` for each variable. ``oo`` / ``-oo``
|
|
948
|
+
trigger automatic change of variables. If ``expr`` is an
|
|
949
|
+
``Integral`` and ``limits`` is ``None``, they are taken from
|
|
950
|
+
``expr.limits``.
|
|
951
|
+
params : list[sympy.Symbol] or None
|
|
952
|
+
Extra symbolic parameters that appear in the transformed
|
|
953
|
+
expression but are **not** integrated over. If ``None``, they
|
|
954
|
+
are inferred automatically from the free symbols.
|
|
955
|
+
modules : list[dict] or None
|
|
956
|
+
Custom ``lambdify`` module list. ``None`` → default torch
|
|
957
|
+
mapping from ``_default_modules``.
|
|
958
|
+
change_of_variables_method : str, optional
|
|
959
|
+
Method used to transform infinite/semi-infinite domains to finite
|
|
960
|
+
ones. Supported values: ``"tangent"``, ``"algebraic"``,
|
|
961
|
+
``"tanh-sinh"``.
|
|
962
|
+
cov_eps : float, optional
|
|
963
|
+
Small inward shift for transformed open intervals to avoid
|
|
964
|
+
endpoint singular evaluations.
|
|
965
|
+
|
|
966
|
+
Returns
|
|
967
|
+
-------
|
|
968
|
+
TorchExpr
|
|
969
|
+
If limits are provided (directly or via an ``Integral``), ready
|
|
970
|
+
for numerical integration.
|
|
971
|
+
callable
|
|
972
|
+
If ``limits`` is ``None``, a plain torch-ready function
|
|
973
|
+
(no integration).
|
|
974
|
+
|
|
975
|
+
USAGE
|
|
976
|
+
=====
|
|
977
|
+
1. Plain function (no integration)::
|
|
978
|
+
|
|
979
|
+
````python
|
|
980
|
+
import symtorch
|
|
981
|
+
from sympy import symbols, exp
|
|
982
|
+
|
|
983
|
+
x = symbols("x", real=True)
|
|
984
|
+
|
|
985
|
+
lt = symtorch.SymTorch()
|
|
986
|
+
f = lt.torchify(exp(-x**2), variables=[x])
|
|
987
|
+
````
|
|
988
|
+
|
|
989
|
+
2. Integral with finite limits::
|
|
990
|
+
|
|
991
|
+
````python
|
|
992
|
+
import symtorch
|
|
993
|
+
from sympy import symbols, Integral, exp
|
|
994
|
+
|
|
995
|
+
x = symbols("x", real=True)
|
|
996
|
+
integral = Integral(exp(-x**2), (x, 0, 1))
|
|
997
|
+
|
|
998
|
+
lt = symtorch.SymTorch()
|
|
999
|
+
texpr = lt.torchify(integral)
|
|
1000
|
+
````
|
|
1001
|
+
|
|
1002
|
+
3. Integral with infinite limits (automatic change of variables)::
|
|
1003
|
+
|
|
1004
|
+
````python
|
|
1005
|
+
import symtorch
|
|
1006
|
+
from sympy import symbols, Integral, exp, oo
|
|
1007
|
+
|
|
1008
|
+
x = symbols("x", real=True)
|
|
1009
|
+
integral = Integral(exp(-x**2), (x, -oo, oo))
|
|
1010
|
+
|
|
1011
|
+
lt = symtorch.SymTorch()
|
|
1012
|
+
texpr = lt.torchify(integral)
|
|
1013
|
+
````
|
|
1014
|
+
|
|
1015
|
+
4. Equation with integral on the right-hand side::
|
|
1016
|
+
|
|
1017
|
+
````python
|
|
1018
|
+
import symtorch
|
|
1019
|
+
from sympy import symbols, Eq, Integral, exp, oo
|
|
1020
|
+
|
|
1021
|
+
x, y = symbols("x y", real=True)
|
|
1022
|
+
eq = Eq(y, Integral(exp(-x**2), (x, -oo, oo)))
|
|
1023
|
+
|
|
1024
|
+
lt = symtorch.SymTorch()
|
|
1025
|
+
texpr = lt.torchify(eq) # integral auto-extracted
|
|
1026
|
+
````
|
|
1027
|
+
"""
|
|
1028
|
+
|
|
1029
|
+
def _absorb_scalar_mul_into_integral(expr_candidate):
|
|
1030
|
+
"""Rewrite `prefactor * Integral(...)` as `Integral(prefactor*integrand, ...)`.
|
|
1031
|
+
|
|
1032
|
+
This is a convenience for common patterns like `Integral(f, ...)/(2*pi)`
|
|
1033
|
+
which SymPy stores as `Mul(Integral(...), 1/(2*pi))`.
|
|
1034
|
+
|
|
1035
|
+
The rewrite is conservative: the prefactor is only absorbed when
|
|
1036
|
+
it is independent of the integral's dummy variables.
|
|
1037
|
+
"""
|
|
1038
|
+
|
|
1039
|
+
if not isinstance(expr_candidate, Mul):
|
|
1040
|
+
return expr_candidate
|
|
1041
|
+
|
|
1042
|
+
integral_factors = [arg for arg in expr_candidate.args if isinstance(arg, Integral)]
|
|
1043
|
+
if len(integral_factors) != 1:
|
|
1044
|
+
return expr_candidate
|
|
1045
|
+
|
|
1046
|
+
integral_factor = integral_factors[0]
|
|
1047
|
+
prefactor = Mul(*[arg for arg in expr_candidate.args if arg is not integral_factor])
|
|
1048
|
+
|
|
1049
|
+
dummy_vars = [lim[0] for lim in integral_factor.limits]
|
|
1050
|
+
if not prefactor.free_symbols.isdisjoint(set(dummy_vars)):
|
|
1051
|
+
return expr_candidate
|
|
1052
|
+
|
|
1053
|
+
return Integral(prefactor * integral_factor.function, *integral_factor.limits)
|
|
1054
|
+
|
|
1055
|
+
# Detect integral / equation-with-integral and extract base expr/limits.
|
|
1056
|
+
integral = None
|
|
1057
|
+
if isinstance(expr, Integral):
|
|
1058
|
+
integral = expr
|
|
1059
|
+
elif isinstance(expr, Eq):
|
|
1060
|
+
if isinstance(expr.rhs, Integral):
|
|
1061
|
+
integral = expr.rhs
|
|
1062
|
+
else:
|
|
1063
|
+
rhs2 = _absorb_scalar_mul_into_integral(expr.rhs)
|
|
1064
|
+
if isinstance(rhs2, Integral):
|
|
1065
|
+
integral = rhs2
|
|
1066
|
+
else:
|
|
1067
|
+
expr2 = _absorb_scalar_mul_into_integral(expr)
|
|
1068
|
+
if isinstance(expr2, Integral):
|
|
1069
|
+
integral = expr2
|
|
1070
|
+
|
|
1071
|
+
if integral is not None:
|
|
1072
|
+
base_expr = integral.function
|
|
1073
|
+
nested_modules = {}
|
|
1074
|
+
if base_expr.has(Integral):
|
|
1075
|
+
# Evaluate nested definite integrals numerically using torchquad.
|
|
1076
|
+
base_expr, nested_modules = self._replace_nested_definite_integrals(
|
|
1077
|
+
base_expr,
|
|
1078
|
+
change_of_variables_method=change_of_variables_method,
|
|
1079
|
+
cov_eps=cov_eps,
|
|
1080
|
+
)
|
|
1081
|
+
if limits is None:
|
|
1082
|
+
limits = list(integral.limits)
|
|
1083
|
+
if variables is None and limits is not None:
|
|
1084
|
+
variables = [lim[0] for lim in limits]
|
|
1085
|
+
else:
|
|
1086
|
+
base_expr = expr
|
|
1087
|
+
nested_modules = {}
|
|
1088
|
+
|
|
1089
|
+
if variables is None:
|
|
1090
|
+
raise ValueError("'variables' must be provided when expr is not an Integral")
|
|
1091
|
+
|
|
1092
|
+
if modules is None:
|
|
1093
|
+
modules = self._default_modules()
|
|
1094
|
+
modules = self._merge_lambdify_modules(modules, nested_modules)
|
|
1095
|
+
|
|
1096
|
+
variables = list(variables)
|
|
1097
|
+
|
|
1098
|
+
# ------------------------------------------------------------------
|
|
1099
|
+
# Simple case: no limits → plain lambdify
|
|
1100
|
+
# ------------------------------------------------------------------
|
|
1101
|
+
if limits is None:
|
|
1102
|
+
return lambdify(tuple(variables), base_expr, modules=modules)
|
|
1103
|
+
|
|
1104
|
+
# ------------------------------------------------------------------
|
|
1105
|
+
# With limits: symbolic change of variables (done once, fast forever)
|
|
1106
|
+
# ------------------------------------------------------------------
|
|
1107
|
+
limit_map = {lim[0]: (lim[1], lim[2]) for lim in limits}
|
|
1108
|
+
|
|
1109
|
+
new_vars = []
|
|
1110
|
+
domain = []
|
|
1111
|
+
subs_map = {}
|
|
1112
|
+
jacobian = Integer(1)
|
|
1113
|
+
|
|
1114
|
+
for v in variables:
|
|
1115
|
+
lower, upper = limit_map[v]
|
|
1116
|
+
t_var, mapped_expr, jac_expr, domain_entry = self._transform_limit_with_method(
|
|
1117
|
+
v,
|
|
1118
|
+
lower,
|
|
1119
|
+
upper,
|
|
1120
|
+
change_of_variables_method=change_of_variables_method,
|
|
1121
|
+
eps=cov_eps,
|
|
1122
|
+
)
|
|
1123
|
+
new_vars.append(t_var)
|
|
1124
|
+
domain.append(domain_entry)
|
|
1125
|
+
if t_var != v:
|
|
1126
|
+
subs_map[v] = mapped_expr
|
|
1127
|
+
jacobian *= jac_expr
|
|
1128
|
+
|
|
1129
|
+
expr_work = base_expr.subs(subs_map) * jacobian
|
|
1130
|
+
|
|
1131
|
+
if params is None:
|
|
1132
|
+
params = sorted(
|
|
1133
|
+
list(expr_work.free_symbols - set(new_vars)),
|
|
1134
|
+
key=lambda s: s.name,
|
|
1135
|
+
)
|
|
1136
|
+
else:
|
|
1137
|
+
params = list(params)
|
|
1138
|
+
|
|
1139
|
+
# Lambdify: new_vars first, then params
|
|
1140
|
+
arglist = tuple(new_vars + params)
|
|
1141
|
+
func = lambdify(arglist, expr_work, modules=modules)
|
|
1142
|
+
|
|
1143
|
+
return TorchExpr(
|
|
1144
|
+
func=func,
|
|
1145
|
+
domain=domain,
|
|
1146
|
+
dim=len(new_vars),
|
|
1147
|
+
n_params=len(params),
|
|
1148
|
+
sympy_expr=expr_work,
|
|
1149
|
+
variables=new_vars,
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
# ---------------------------------------------------------------------------
|
|
1154
|
+
# Functional compatibility helpers
|
|
1155
|
+
# ---------------------------------------------------------------------------
|
|
1156
|
+
def torchify(
|
|
1157
|
+
expr,
|
|
1158
|
+
variables=None,
|
|
1159
|
+
limits=None,
|
|
1160
|
+
params=None,
|
|
1161
|
+
modules=None,
|
|
1162
|
+
change_of_variables_method: str = "tangent",
|
|
1163
|
+
cov_eps: float = 1e-7,
|
|
1164
|
+
):
|
|
1165
|
+
"""Convert a SymPy object into a torch-compatible callable or ``TorchExpr``.
|
|
1166
|
+
|
|
1167
|
+
This functional wrapper is kept for backward compatibility with the
|
|
1168
|
+
original ``SymTorch`` API. It delegates to ``SymTorch().torchify(...)``
|
|
1169
|
+
so the functional and object-oriented styles share the same core logic.
|
|
1170
|
+
|
|
1171
|
+
Parameters
|
|
1172
|
+
----------
|
|
1173
|
+
expr : sympy.Expr | sympy.Integral | sympy.Eq
|
|
1174
|
+
Symbolic expression or integral-like SymPy object.
|
|
1175
|
+
variables : list[sympy.Symbol] or None, optional
|
|
1176
|
+
Variables passed through to ``SymTorch.torchify``.
|
|
1177
|
+
limits : list[tuple] or None, optional
|
|
1178
|
+
Integration limits passed through to ``SymTorch.torchify``.
|
|
1179
|
+
params : list[sympy.Symbol] or None, optional
|
|
1180
|
+
Additional symbolic parameters.
|
|
1181
|
+
modules : list[dict] or None, optional
|
|
1182
|
+
Custom ``lambdify`` modules mapping.
|
|
1183
|
+
|
|
1184
|
+
Returns
|
|
1185
|
+
-------
|
|
1186
|
+
TorchExpr | callable
|
|
1187
|
+
Same return contract as ``SymTorch().torchify(...)``.
|
|
1188
|
+
|
|
1189
|
+
USAGE
|
|
1190
|
+
=====
|
|
1191
|
+
1. Functional style kept in parallel with the OOP API::
|
|
1192
|
+
|
|
1193
|
+
````python
|
|
1194
|
+
from symtorch import torchify
|
|
1195
|
+
from sympy import symbols, Integral, exp, oo
|
|
1196
|
+
|
|
1197
|
+
x = symbols("x", real=True)
|
|
1198
|
+
integral_expr = Integral(exp(-x**2), (x, -oo, oo))
|
|
1199
|
+
|
|
1200
|
+
texpr = torchify(integral_expr)
|
|
1201
|
+
````
|
|
1202
|
+
"""
|
|
1203
|
+
return SymTorch().torchify(
|
|
1204
|
+
expr,
|
|
1205
|
+
variables=variables,
|
|
1206
|
+
limits=limits,
|
|
1207
|
+
params=params,
|
|
1208
|
+
modules=modules,
|
|
1209
|
+
change_of_variables_method=change_of_variables_method,
|
|
1210
|
+
cov_eps=cov_eps,
|
|
1211
|
+
)
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
def torchquad_integrate(texpr: TorchExpr, params_values=None, method=None, N: int = 21, dtype=None):
|
|
1215
|
+
"""Numerically integrate a ``TorchExpr`` using torchquad.
|
|
1216
|
+
|
|
1217
|
+
This functional wrapper is kept for backward compatibility with the
|
|
1218
|
+
original ``SymTorch`` API. New code can call
|
|
1219
|
+
:meth:`TorchExpr.torchquad_integrate` directly.
|
|
1220
|
+
|
|
1221
|
+
Parameters
|
|
1222
|
+
----------
|
|
1223
|
+
texpr : TorchExpr
|
|
1224
|
+
Object returned by ``SymTorch().torchify(..., limits=...)``.
|
|
1225
|
+
params_values : list | tuple | torch.Tensor | None, optional
|
|
1226
|
+
Numerical parameter values in the same order expected by
|
|
1227
|
+
``texpr.func``.
|
|
1228
|
+
method : torchquad integrator instance or None, optional
|
|
1229
|
+
If ``None``, a default ``Simpson`` integrator is created.
|
|
1230
|
+
N : int, optional
|
|
1231
|
+
Resolution parameter passed to torchquad.
|
|
1232
|
+
dtype : torch.dtype or None, optional
|
|
1233
|
+
Floating dtype used for internal computations. Passed through to
|
|
1234
|
+
:meth:`TorchExpr.torchquad_integrate`.
|
|
1235
|
+
|
|
1236
|
+
Returns
|
|
1237
|
+
-------
|
|
1238
|
+
re, im : torch.Tensor
|
|
1239
|
+
Real and imaginary parts of the integral.
|
|
1240
|
+
|
|
1241
|
+
USAGE
|
|
1242
|
+
=====
|
|
1243
|
+
1. Functional style kept in parallel with the OOP API::
|
|
1244
|
+
|
|
1245
|
+
````python
|
|
1246
|
+
import symtorch
|
|
1247
|
+
from symtorch import torchquad_integrate
|
|
1248
|
+
from sympy import symbols, Integral, exp, oo
|
|
1249
|
+
|
|
1250
|
+
x = symbols("x", real=True)
|
|
1251
|
+
integral_expr = Integral(exp(-x**2), (x, -oo, oo))
|
|
1252
|
+
|
|
1253
|
+
lt = symtorch.SymTorch()
|
|
1254
|
+
texpr = lt.torchify(integral_expr)
|
|
1255
|
+
re, im = torchquad_integrate(texpr, N=121)
|
|
1256
|
+
````
|
|
1257
|
+
"""
|
|
1258
|
+
return texpr.torchquad_integrate(params_values=params_values, method=method, N=N, dtype=dtype)
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
def _simpson_weights_1d(a: float, b: float, N: int, *, device=None, dtype=None) -> torch.Tensor:
|
|
1262
|
+
"""Backward-compatible Simpson weights wrapper."""
|
|
1263
|
+
_coords, weights = TorchExpr._rule_simpson(a, b, N, device=device, dtype=dtype)
|
|
1264
|
+
return weights
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
def _is_scalar_like(x) -> bool:
|
|
1268
|
+
"""Functional wrapper around ``TorchExpr._is_scalar_like``."""
|
|
1269
|
+
return TorchExpr._is_scalar_like(x)
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
def _normalize_params_values(params_values, n_params: int, *, device, dtype):
|
|
1273
|
+
"""Functional wrapper around ``TorchExpr._normalize_params_values``."""
|
|
1274
|
+
return TorchExpr._normalize_params_values(params_values, n_params, device=device, dtype=dtype)
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def torch_integrate_batched(
|
|
1278
|
+
texpr: TorchExpr,
|
|
1279
|
+
params_values=None,
|
|
1280
|
+
*,
|
|
1281
|
+
method: str | Callable = "simpson",
|
|
1282
|
+
N: int = 121,
|
|
1283
|
+
chunk_size_params: int = 256,
|
|
1284
|
+
chunk_size_points: int | None = None,
|
|
1285
|
+
device=None,
|
|
1286
|
+
dtype=None,
|
|
1287
|
+
):
|
|
1288
|
+
"""Batched tensor-product quadrature integration for any dimension.
|
|
1289
|
+
|
|
1290
|
+
This functional wrapper is kept so existing code that used the
|
|
1291
|
+
original function-based API continues to work. New code can call
|
|
1292
|
+
:meth:`TorchExpr.torch_integrate_batched` directly.
|
|
1293
|
+
|
|
1294
|
+
Parameters
|
|
1295
|
+
----------
|
|
1296
|
+
texpr : TorchExpr
|
|
1297
|
+
Object returned by ``SymTorch().torchify(..., limits=...)``.
|
|
1298
|
+
params_values : tensor | list | tuple | None
|
|
1299
|
+
Numerical parameter values.
|
|
1300
|
+
N : int, optional
|
|
1301
|
+
Odd number of Simpson points per dimension.
|
|
1302
|
+
chunk_size_params : int, optional
|
|
1303
|
+
Number of parameter points processed per chunk.
|
|
1304
|
+
chunk_size_points : int or None, optional
|
|
1305
|
+
Number of sample points processed per chunk.
|
|
1306
|
+
device : torch.device or None, optional
|
|
1307
|
+
Device used for internal tensors.
|
|
1308
|
+
dtype : torch.dtype or None, optional
|
|
1309
|
+
Floating dtype used for internal tensors.
|
|
1310
|
+
|
|
1311
|
+
Returns
|
|
1312
|
+
-------
|
|
1313
|
+
re, im : torch.Tensor
|
|
1314
|
+
Real and imaginary parts of the integral.
|
|
1315
|
+
|
|
1316
|
+
USAGE
|
|
1317
|
+
=====
|
|
1318
|
+
1. Functional style kept in parallel with the OOP API::
|
|
1319
|
+
|
|
1320
|
+
````python
|
|
1321
|
+
import symtorch
|
|
1322
|
+
from symtorch import torch_integrate_batched_simpson
|
|
1323
|
+
from sympy import symbols, Integral, exp, oo
|
|
1324
|
+
|
|
1325
|
+
x = symbols("x", real=True)
|
|
1326
|
+
integral_expr = Integral(exp(-x**2), (x, -oo, oo))
|
|
1327
|
+
|
|
1328
|
+
lt = symtorch.SymTorch()
|
|
1329
|
+
texpr = lt.torchify(integral_expr)
|
|
1330
|
+
re, im = torch_integrate_batched_simpson(texpr, params_values=None, N=121)
|
|
1331
|
+
````
|
|
1332
|
+
"""
|
|
1333
|
+
return texpr.torch_integrate_batched(
|
|
1334
|
+
params_values=params_values,
|
|
1335
|
+
method=method,
|
|
1336
|
+
N=N,
|
|
1337
|
+
chunk_size_params=chunk_size_params,
|
|
1338
|
+
chunk_size_points=chunk_size_points,
|
|
1339
|
+
device=device,
|
|
1340
|
+
dtype=dtype,
|
|
1341
|
+
)
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def torch_integrate_batched_simpson(
|
|
1345
|
+
texpr: TorchExpr,
|
|
1346
|
+
params_values=None,
|
|
1347
|
+
*,
|
|
1348
|
+
N: int = 121,
|
|
1349
|
+
chunk_size_params: int = 256,
|
|
1350
|
+
chunk_size_points: int | None = None,
|
|
1351
|
+
device=None,
|
|
1352
|
+
dtype=None,
|
|
1353
|
+
):
|
|
1354
|
+
"""Backward-compatible wrapper for ``torch_integrate_batched(method='simpson')``."""
|
|
1355
|
+
return texpr.torch_integrate_batched(
|
|
1356
|
+
params_values=params_values,
|
|
1357
|
+
method="simpson",
|
|
1358
|
+
N=N,
|
|
1359
|
+
chunk_size_params=chunk_size_params,
|
|
1360
|
+
chunk_size_points=chunk_size_points,
|
|
1361
|
+
device=device,
|
|
1362
|
+
dtype=dtype,
|
|
1363
|
+
)
|