Spaces:
Sleeping
Sleeping
File size: 6,052 Bytes
51b20c8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | """Rotation / SU(2) operations for spinors.
Rotation generators accept scalar or batched inputs and broadcast: an axis of
shape ``[..., 3]`` with an angle of shape ``[...]`` gives matrices of shape
``[..., 2, 2]``. Everything is differentiable.
"""
from typing import Optional, Sequence, Union
import torch
from .core import Spinor
from .ops import inner_product
from .utils import (
TensorLike,
complex_dtype_of,
real_dtype_of,
resolve_dtype_and_device,
)
DeviceLike = Optional[Union[str, torch.device]]
def generate_su2_rotation(
axis: TensorLike,
angle: Union[float, torch.Tensor],
*,
dtype: Optional[torch.dtype] = None,
device: DeviceLike = None,
) -> torch.Tensor:
"""
SU(2) rotation matrix ``R = cos(θ/2) I − i sin(θ/2) (n·σ)`` for axis ``n``.
Args:
axis: Rotation axis, shape ``[..., 3]``. Need not be normalised.
angle: Rotation angle in radians, broadcastable against ``axis[..., 0]``.
dtype: Output complex dtype (``complex64``/``complex128``). Defaults to
the precision of the floating inputs, else the torch default.
device: Output device. Defaults to the inputs' device, else CPU.
Returns:
Complex tensor of shape ``[..., 2, 2]`` where ``[...]`` is the broadcast
of the axis and angle batch shapes.
"""
real_dtype, device = resolve_dtype_and_device([axis, angle], dtype, device)
axis = torch.as_tensor(axis, dtype=real_dtype, device=device)
angle = torch.as_tensor(angle, dtype=real_dtype, device=device)
if axis.ndim == 0 or axis.shape[-1] != 3:
raise ValueError(f"axis must have shape [..., 3], got {tuple(axis.shape)}")
axis_norm = torch.linalg.vector_norm(axis, dim=-1, keepdim=True)
if bool((axis_norm == 0).any()):
raise ValueError("Rotation axis must be non-zero")
n = axis / axis_norm
cos_half = torch.cos(angle / 2)
sin_half = torch.sin(angle / 2)
nx, ny, nz = n.unbind(-1)
c, sx, sy, sz = torch.broadcast_tensors(
cos_half, sin_half * nx, sin_half * ny, sin_half * nz
)
# R = c·I − i·s·(n·σ) with n·σ = [[nz, nx − i·ny], [nx + i·ny, −nz]]:
# R00 = c − i·s·nz R01 = −s·ny − i·s·nx
# R10 = s·ny − i·s·nx R11 = c + i·s·nz
real = torch.stack([torch.stack([c, -sy], -1), torch.stack([sy, c], -1)], -2)
imag = torch.stack([torch.stack([-sz, -sx], -1), torch.stack([-sx, sz], -1)], -2)
return torch.complex(real, imag)
def generate_su2_rotation_from_axis_angle(
axis_angle: TensorLike,
*,
dtype: Optional[torch.dtype] = None,
device: DeviceLike = None,
) -> torch.Tensor:
"""
SU(2) rotation matrix from an axis-angle vector of shape ``[..., 3]``.
The vector's direction is the axis and its magnitude the angle; a zero
vector gives the identity. Differentiable w.r.t. ``axis_angle``.
"""
real_dtype, device = resolve_dtype_and_device([axis_angle], dtype, device)
v = torch.as_tensor(axis_angle, dtype=real_dtype, device=device)
if v.ndim == 0 or v.shape[-1] != 3:
raise ValueError(f"axis_angle must have shape [..., 3], got {tuple(v.shape)}")
angle = torch.linalg.vector_norm(v, dim=-1)
is_zero = angle == 0
safe_angle = torch.where(is_zero, torch.ones_like(angle), angle)
axis = v / safe_angle.unsqueeze(-1)
# Any axis works for a zero angle; substitute z to avoid a zero axis.
z_axis = torch.zeros_like(axis)
z_axis[..., 2] = 1
axis = torch.where(is_zero.unsqueeze(-1), z_axis, axis)
return generate_su2_rotation(axis, angle, dtype=dtype, device=device)
def rotate(spinor: Spinor, matrix: torch.Tensor) -> Spinor:
"""
Apply a rotation matrix: ``R @ s`` over the component axis.
``matrix`` has shape ``[..., N, N]`` and broadcasts against the spinor's
batch shape. dtypes are promoted, so a ``complex128`` matrix can act on a
``complex64`` (or real) spinor.
"""
n = spinor.dim
if matrix.ndim < 2 or matrix.shape[-2:] != (n, n):
raise ValueError(
f"Rotation matrix must have shape [..., {n}, {n}], got {tuple(matrix.shape)}"
)
out_dtype = torch.promote_types(spinor.dtype, matrix.dtype)
m = matrix.to(dtype=out_dtype, device=spinor.device)
s = spinor.data.to(out_dtype)
result = torch.matmul(m, s.unsqueeze(-1)).squeeze(-1)
return spinor._wrap(result)
def global_phase(spinor: Spinor, angle: Union[float, torch.Tensor]) -> Spinor:
"""
Multiply by the global phase ``exp(i·angle)``.
``angle`` may be a scalar or a tensor of batch shape ``[...]`` giving one
phase per spinor. The result is complex.
"""
angle = torch.as_tensor(angle, device=spinor.device)
if not angle.is_floating_point():
angle = angle.to(real_dtype_of(spinor.dtype))
phase = torch.polar(torch.ones_like(angle), angle)
if phase.ndim > 0:
phase = phase.unsqueeze(-1)
out_dtype = torch.promote_types(complex_dtype_of(spinor.dtype), phase.dtype)
return spinor._wrap(spinor.data.to(out_dtype) * phase)
def contract(
s1: Spinor,
s2: Spinor,
indices: Optional[Union[int, Sequence[Sequence[int]]]] = None,
conjugate: bool = True,
) -> torch.Tensor:
"""
Contract ``s1†`` (or ``s1`` if ``conjugate=False``) with ``s2``.
With ``indices=None`` this is the batched ``inner_product`` over the
component axis. Otherwise ``indices`` is passed to ``torch.tensordot`` as
its ``dims`` argument — an int ``k`` contracts the last ``k`` axes of ``s1``
with the first ``k`` of ``s2``, or a pair of axis lists contracts those
axes pairwise — and the remaining axes of both operands are kept in order.
"""
if indices is None:
return inner_product(s1, s2) if conjugate else torch.sum(s1.data * s2.data, -1)
out_dtype = torch.promote_types(s1.dtype, s2.dtype)
left = torch.conj(s1.data) if conjugate else s1.data
return torch.tensordot(left.to(out_dtype), s2.data.to(out_dtype), dims=indices)
|