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)