|
16 | 16 | from pytensor.tensor import math as ptm |
17 | 17 | from pytensor.tensor.basic import as_tensor_variable, diagonal |
18 | 18 | from pytensor.tensor.blockwise import Blockwise |
19 | | -from pytensor.tensor.type import Variable, dvector, lscalar, matrix, scalar, vector |
| 19 | +from pytensor.tensor.type import ( |
| 20 | + Variable, |
| 21 | + dvector, |
| 22 | + lscalar, |
| 23 | + matrix, |
| 24 | + scalar, |
| 25 | + tensor3, |
| 26 | + vector, |
| 27 | +) |
20 | 28 |
|
21 | 29 |
|
22 | 30 | class MatrixPinv(Op): |
@@ -297,31 +305,68 @@ def slogdet(x: TensorLike) -> tuple[ptb.TensorVariable, ptb.TensorVariable]: |
297 | 305 | class Eig(Op): |
298 | 306 | """ |
299 | 307 | Compute the eigenvalues and right eigenvectors of a square array. |
300 | | -
|
301 | 308 | """ |
302 | 309 |
|
303 | | - __props__: tuple[str, ...] = () |
304 | | - gufunc_signature = "(m,m)->(m),(m,m)" |
| 310 | + __props__: tuple[str, ...] = ("return_components",) |
305 | 311 | gufunc_spec = ("numpy.linalg.eig", 1, 2) |
306 | 312 |
|
| 313 | + def __init__(self, return_components: bool = False): |
| 314 | + self.return_components = return_components |
| 315 | + if return_components: |
| 316 | + signature = "(m,m)->(a,m),(a,m,m)" |
| 317 | + else: |
| 318 | + signature = "(m,m)->(m),(m,m)" |
| 319 | + self.gufunc_signature = signature |
| 320 | + |
307 | 321 | def make_node(self, x): |
308 | 322 | x = as_tensor_variable(x) |
309 | 323 | assert x.ndim == 2 |
310 | | - w = vector(dtype=x.dtype) |
311 | | - v = matrix(dtype=x.dtype) |
| 324 | + |
| 325 | + if self.return_components: |
| 326 | + w = matrix(dtype=x.dtype) |
| 327 | + v = tensor3(dtype=x.dtype) |
| 328 | + |
| 329 | + else: |
| 330 | + w = vector(dtype=x.dtype) |
| 331 | + v = matrix(dtype=x.dtype) |
| 332 | + |
312 | 333 | return Apply(self, [x], [w, v]) |
313 | 334 |
|
314 | 335 | def perform(self, node, inputs, outputs): |
315 | 336 | (x,) = inputs |
316 | 337 | (w, v) = outputs |
317 | | - w[0], v[0] = (z.astype(x.dtype) for z in np.linalg.eig(x)) |
| 338 | + if self.return_components: |
| 339 | + w_res, v_res = np.linalg.eig(x) |
| 340 | + w[0] = np.stack([w_res.real, w_res.imag], axis=0) |
| 341 | + v[0] = np.stack([v_res.real, v_res.imag], axis=0) |
| 342 | + else: |
| 343 | + w[0], v[0] = (z.astype(x.dtype) for z in np.linalg.eig(x)) |
318 | 344 |
|
319 | 345 | def infer_shape(self, fgraph, node, shapes): |
320 | 346 | n = shapes[0][0] |
| 347 | + if self.return_components: |
| 348 | + return [(2, n), (2, n, n)] |
321 | 349 | return [(n,), (n, n)] |
322 | 350 |
|
323 | 351 |
|
324 | | -eig = Blockwise(Eig()) |
| 352 | +def eig(x: TensorLike, return_components: bool = False): |
| 353 | + """ |
| 354 | + Return the eigenvalues and right eigenvectors of a square array. |
| 355 | +
|
| 356 | + Parameters |
| 357 | + ---------- |
| 358 | + x: TensorLike |
| 359 | + Square matrix, or array of such matrices |
| 360 | + return_components: bool, optional |
| 361 | + By default, only the real component of the eigenvalues and eigenvectors are returned, as Pytensor does not |
| 362 | + allow the dtype of a variable to change during graph execution. |
| 363 | +
|
| 364 | + To circumvent this, if `return_components` is set to True, the real and imaginary *components* of the |
| 365 | + eigenvalues are returned as a concatenated array of shape (2, n), where the first row contains the real parts |
| 366 | + and the second row contains the imaginary parts. Similarly, the eigenvectors are returned as an array of shape |
| 367 | + (2, n, n). |
| 368 | + """ |
| 369 | + return Blockwise(Eig(return_components=return_components))(x) |
325 | 370 |
|
326 | 371 |
|
327 | 372 | class Eigh(Eig): |
|
0 commit comments