Skip to content

qten.phys.spin

Module reference for qten.phys.spin.

spin

Spin-1/2 irrep labels and \(SU(2)\) lifts of spatial point-group rotations.

Spin is a typed U(1) irrep for use inside U1Basis, replacing ad-hoc "up" / "down" strings. Putting Spin.up or Spin.down on a basis state is what makes a Hilbert space spinful; see qten.phys and qten.pointgroups for the constructor-level workflow.

Crystal rotations act on spin through the spin-1/2 cover \(u(g)\in SU(2)\) of the proper part of the stored 3D rotation \(R(g)\in O(3)\): [ R_+(g)=(\det R(g))\,R(g),\qquad u(g)=\cos(\theta/2)\,I-i\sin(\theta/2)\,\hat n\cdot\boldsymbol\sigma, ] with \(\cos\theta=(\operatorname{tr}R_+-1)/2\) and \(\theta\in[0,\pi]\). Both signs \(\pm u\) cover the same \(R_+\); the principal branch keeps \(\operatorname{Re}\operatorname{tr}u\ge 0\). The chosen section is a 2-cocycle, \(u(g)u(h)=\omega(g,h)\,u(gh)\) with \(\omega(g,h)\in\{\pm 1\}\).

A generic \(SU(2)\) matrix maps one spin state to a superposition, so the single-outcome contract PointGroupOpr @ Spin -> Spin cannot express the full action and raises. Use expand_spin together with hilbert_repr. The lift is named SU2_SECTION_CONVENTION (qten-su2-principal-v1).

SU2_SECTION_CONVENTION module-attribute

SU2_SECTION_CONVENTION = 'qten-su2-principal-v1'

Spin dataclass

Spin(ms: Rational)

Bases: Operable

Spin-1/2 projection label \(m_s = \pm 1/2\).

Use as a typed irrep inside U1Basis:

psi = U1Basis.new(site, Spin.up)

Attributes:

Name Type Description
ms Rational

Magnetic quantum number. Must be +1/2 or -1/2.

ms instance-attribute

ms: Rational

up class-attribute

up: Spin

down class-attribute

down: Spin

is_up property

is_up: bool

is_down property

is_down: bool

__post_init__

__post_init__() -> None
Source code in src/qten/phys/spin.py
74
75
76
77
78
def __post_init__(self) -> None:
    ms = sy.Rational(self.ms)
    if ms not in _SPIN_MS:
        raise ValueError(f"Spin.ms must be ±1/2, got {self.ms}")
    object.__setattr__(self, "ms", ms)

__str__

__str__() -> str
Source code in src/qten/phys/spin.py
88
89
def __str__(self) -> str:
    return "up" if self.is_up else "down"

__repr__

__repr__() -> str
Source code in src/qten/phys/spin.py
91
92
def __repr__(self) -> str:
    return f"Spin.{'up' if self.is_up else 'down'}"

SpinAction dataclass

SpinAction(
    kind: Literal["electron", "trivial"] = "electron",
)

Internal spin policy for a point operation.

Not a user-facing setter and not part of the qten.phys export list. Read element.spin instead.

kind class-attribute instance-attribute

kind: Literal['electron', 'trivial'] = 'electron'

of classmethod

of(g: 'PointGroupElement | PointGroupOpr') -> 'SpinAction'
Source code in src/qten/phys/spin.py
109
110
111
112
113
114
115
116
117
@classmethod
def of(cls, g: "PointGroupElement | PointGroupOpr") -> "SpinAction":
    from ..pointgroups.elements import PointGroupOpr

    element = g.g if isinstance(g, PointGroupOpr) else g
    kind = element.spin
    if kind not in {"electron", "trivial"}:
        raise ValueError(f"Unknown spin policy {kind!r}.")
    return cls(kind=cast(Literal["electron", "trivial"], kind))

proper_rotation_matrix

proper_rotation_matrix(
    R: Matrix,
) -> sy.ImmutableDenseMatrix

Return the proper \(SO(3)\) factor used for the spinor lift.

Returns \(R_+=(\det R)\,R\). For \(\det R=+1\) this is \(R\). For an improper isometry (\(\det R=-1\)) it is \(-R\) (now det \(+1\)): \(R=i\circ R_+\) with spatial inversion \(i=-I\), and inversion does not act on spin-1/2, so the lift uses \(R_+\) only.

Source code in src/qten/phys/spin.py
183
184
185
186
187
188
189
190
191
192
193
194
195
def proper_rotation_matrix(R: sy.Matrix) -> sy.ImmutableDenseMatrix:
    r"""
    Return the proper \(SO(3)\) factor used for the spinor lift.

    Returns \(R_+=(\det R)\,R\). For \(\det R=+1\) this is \(R\). For an
    improper isometry (\(\det R=-1\)) it is \(-R\) (now det \(+1\)):
    \(R=i\circ R_+\) with spatial inversion \(i=-I\), and inversion does
    not act on spin-1/2, so the lift uses \(R_+\) only.
    """
    M, det_sign = _validated_o3_matrix(R, require_proper=False)
    if det_sign == 1:
        return M
    return sy.ImmutableDenseMatrix(-M)

principal_su2_from_rows

principal_su2_from_rows(
    rotation: list[list[float]],
) -> list[list[complex]]

Principal-branch \(SU(2)\) lift of a real \(3\times 3\) \(O(3)\) matrix given as rows.

Same section as su2_from_so3: first form \(R_+=(\det R)\,R\), then the quaternion with \(w=\cos(\theta/2)\ge 0\). Numeric helper for inexact matrices. Prefer su2_of_point_group at the point-group API.

Source code in src/qten/phys/spin.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
def principal_su2_from_rows(rotation: list[list[float]]) -> list[list[complex]]:
    r"""Principal-branch \(SU(2)\) lift of a real \(3\times 3\) \(O(3)\) matrix given as rows.

    Same section as [`su2_from_so3`][qten.phys.spin.su2_from_so3]: first form
    \(R_+=(\det R)\,R\), then the quaternion with \(w=\cos(\theta/2)\ge 0\).
    Numeric helper for inexact matrices. Prefer
    [`su2_of_point_group`][qten.phys.spin.su2_of_point_group] at the
    point-group API.
    """
    r = [list(row) for row in rotation]
    det = (
        r[0][0] * (r[1][1] * r[2][2] - r[1][2] * r[2][1])
        - r[0][1] * (r[1][0] * r[2][2] - r[1][2] * r[2][0])
        + r[0][2] * (r[1][0] * r[2][1] - r[1][1] * r[2][0])
    )
    if det < 0.0:
        r = [[-entry for entry in row] for row in r]

    trace = r[0][0] + r[1][1] + r[2][2]

    if trace > 0.0:
        scale = 2.0 * max(0.0, trace + 1.0) ** 0.5
        w = 0.25 * scale
        x = (r[2][1] - r[1][2]) / scale
        y = (r[0][2] - r[2][0]) / scale
        z = (r[1][0] - r[0][1]) / scale
    elif r[0][0] >= r[1][1] and r[0][0] >= r[2][2]:
        scale = 2.0 * max(0.0, 1.0 + r[0][0] - r[1][1] - r[2][2]) ** 0.5
        x = 0.25 * scale
        w = (r[2][1] - r[1][2]) / scale
        y = (r[0][1] + r[1][0]) / scale
        z = (r[0][2] + r[2][0]) / scale
    elif r[1][1] >= r[2][2]:
        scale = 2.0 * max(0.0, 1.0 + r[1][1] - r[0][0] - r[2][2]) ** 0.5
        y = 0.25 * scale
        w = (r[0][2] - r[2][0]) / scale
        x = (r[0][1] + r[1][0]) / scale
        z = (r[1][2] + r[2][1]) / scale
    else:
        scale = 2.0 * max(0.0, 1.0 + r[2][2] - r[0][0] - r[1][1]) ** 0.5
        z = 0.25 * scale
        w = (r[1][0] - r[0][1]) / scale
        x = (r[0][2] + r[2][0]) / scale
        y = (r[1][2] + r[2][1]) / scale

    norm = (w * w + x * x + y * y + z * z) ** 0.5
    if norm == 0.0:
        raise ValueError(f"Could not extract a quaternion from rotation {r}")
    w, x, y, z = (component / norm for component in (w, x, y, z))

    # Choose the principal SO(3) branch. Both signs are valid lifts, but this
    # convention preserves continuity from the identity for angles below π.
    if w < 0.0:
        w, x, y, z = (-component for component in (w, x, y, z))

    return [
        [w - 1j * z, -y - 1j * x],
        [y - 1j * x, w + 1j * z],
    ]

su2_from_so3

su2_from_so3(R: Matrix) -> sy.ImmutableDenseMatrix

Lift an \(SO(3)\) matrix to one \(SU(2)\) factor \(u(R)\).

Writes \(R\) in axis-angle form, \(\cos\theta=(\operatorname{tr}R-1)/2\), \(\theta\in[0,\pi]\), and returns [ u=\cos(\theta/2)\,I-i\sin(\theta/2)\,\hat n\cdot\boldsymbol\sigma. ] Both \(\pm u\) cover the same \(R\). The principal branch is the one continuous from the identity, equivalently \(\operatorname{Re}\operatorname{tr}u\ge 0\). At \(\theta=\pi\) the two signs are equally valid; an axis convention picks one.

Parameters:

Name Type Description Default
R Matrix

Proper \(3\times 3\) rotation matrix (\(\det = +1\)).

required

Returns:

Type Description
ImmutableDenseMatrix

\(2\times 2\) unitary matrix with determinant \(1\).

Source code in src/qten/phys/spin.py
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
def su2_from_so3(R: sy.Matrix) -> sy.ImmutableDenseMatrix:
    r"""
    Lift an \(SO(3)\) matrix to one \(SU(2)\) factor \(u(R)\).

    Writes \(R\) in axis-angle form,
    \(\cos\theta=(\operatorname{tr}R-1)/2\), \(\theta\in[0,\pi]\), and
    returns
    \[
    u=\cos(\theta/2)\,I-i\sin(\theta/2)\,\hat n\cdot\boldsymbol\sigma.
    \]
    Both \(\pm u\) cover the same \(R\). The principal branch is the one
    continuous from the identity, equivalently
    \(\operatorname{Re}\operatorname{tr}u\ge 0\). At \(\theta=\pi\) the
    two signs are equally valid; an axis convention picks one.

    Parameters
    ----------
    R : sy.Matrix
        Proper \(3\times 3\) rotation matrix (\(\det = +1\)).

    Returns
    -------
    sy.ImmutableDenseMatrix
        \(2\times 2\) unitary matrix with determinant \(1\).
    """
    M, _ = _validated_o3_matrix(R, require_proper=True)
    if any(entry.free_symbols for entry in M):
        raise ValueError(
            "Parameterized symbolic rotations are not supported; substitute "
            "numerical or exact constant parameter values before lifting to SU(2)."
        )
    if any(entry.atoms(sy.Float) for entry in M):
        return _numeric_su2_from_so3(M)
    return _su2_from_so3_cached(_matrix_cache_key(M))

su2_of_point_group

su2_of_point_group(
    g: "PointGroupElement | PointGroupOpr",
) -> sy.ImmutableDenseMatrix

Return the \(SU(2)\) factor of a point operation in the Cartesian spin frame.

Reads the stored \(R(g)\) (rotation3, or the 3D irrep when that is already Cartesian) and returns \(u(g)=u\bigl(R_+(g)\bigr)\) with \(R_+=(\det R)\,R\). A group built with spin="trivial" returns \(I\). A 1D or 2D element with no rotation3 raises: the lift is not a padded copy of the spatial matrix.

Parameters:

Name Type Description Default
g PointGroupElement | PointGroupOpr

Point operation whose stored 3D rotation is lifted.

required

Returns:

Type Description
ImmutableDenseMatrix

\(2\times 2\) \(SU(2)\) matrix, or the identity when the spin policy is trivial.

Source code in src/qten/phys/spin.py
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
def su2_of_point_group(
    g: "PointGroupElement | PointGroupOpr",
) -> sy.ImmutableDenseMatrix:
    r"""
    Return the \(SU(2)\) factor of a point operation in the Cartesian spin frame.

    Reads the stored \(R(g)\) (`rotation3`, or the 3D `irrep` when that is
    already Cartesian) and returns
    \(u(g)=u\bigl(R_+(g)\bigr)\) with
    \(R_+=(\det R)\,R\). A group built with `spin="trivial"` returns \(I\).
    A 1D or 2D element with no `rotation3` raises: the lift is not a padded
    copy of the spatial matrix.

    Parameters
    ----------
    g : PointGroupElement | PointGroupOpr
        Point operation whose stored 3D rotation is lifted.

    Returns
    -------
    sy.ImmutableDenseMatrix
        \(2\times 2\) \(SU(2)\) matrix, or the identity when the spin policy is
        trivial.
    """
    if SpinAction.of(g).kind == "trivial":
        return sy.ImmutableDenseMatrix.eye(2)
    cartesian = _canonical_cartesian_rotation(g)
    return su2_from_so3(proper_rotation_matrix(cartesian))

su2_numeric

su2_numeric(
    g: "PointGroupElement | PointGroupOpr",
) -> list[list[complex]]

Complex \(2\times 2\) \(SU(2)\) factor for fast Hilbert-space assembly.

Source code in src/qten/phys/spin.py
491
492
493
494
495
496
def su2_numeric(
    g: "PointGroupElement | PointGroupOpr",
) -> list[list[complex]]:
    r"""Complex \(2\times 2\) \(SU(2)\) factor for fast Hilbert-space assembly."""
    u = su2_of_point_group(g)
    return [[complex(sy.N(u[i, j])) for j in range(2)] for i in range(2)]

expand_spin

expand_spin(
    g: "PointGroupElement | PointGroupOpr", spin: Spin
) -> Tuple[Tuple[sy.Expr, Spin], ...]

Expand \(u(g)|s\rangle\) in the \(\{|\uparrow\rangle,|\downarrow\rangle\}\) basis.

Columns of \(u(g)\) are ordered \((\uparrow,\downarrow)\), so [ u(g)|s\rangle=\sum_{s'}u(g)_{s's}\,|s'\rangle ] with only the nonzero amplitudes returned.

Parameters:

Name Type Description Default
g PointGroupElement | PointGroupOpr

Point operation whose \(SU(2)\) factor is applied.

required
spin Spin

Input spin-1/2 label.

required

Returns:

Type Description
tuple[tuple[Expr, Spin], ...]

Nonzero (amplitude, Spin) pairs.

Source code in src/qten/phys/spin.py
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
def expand_spin(
    g: "PointGroupElement | PointGroupOpr", spin: Spin
) -> Tuple[Tuple[sy.Expr, Spin], ...]:
    r"""
    Expand \(u(g)|s\rangle\) in the \(\{|\uparrow\rangle,|\downarrow\rangle\}\) basis.

    Columns of \(u(g)\) are ordered \((\uparrow,\downarrow)\), so
    \[
    u(g)|s\rangle=\sum_{s'}u(g)_{s's}\,|s'\rangle
    \]
    with only the nonzero amplitudes returned.

    Parameters
    ----------
    g : PointGroupElement | PointGroupOpr
        Point operation whose \(SU(2)\) factor is applied.
    spin : Spin
        Input spin-1/2 label.

    Returns
    -------
    tuple[tuple[sy.Expr, Spin], ...]
        Nonzero `(amplitude, Spin)` pairs.
    """
    u = su2_of_point_group(g)
    col = 0 if spin.is_up else 1
    out: list[tuple[sy.Expr, Spin]] = []
    for row, target in enumerate((Spin.up, Spin.down)):
        amp = sy.simplify(u[row, col])
        if amp != 0:
            out.append((amp, target))
    if not out:
        raise RuntimeError(f"SU(2) image of {spin} under {g} vanished")
    return tuple(out)

as_spin

as_spin(rep: object) -> Spin | None

Return a Spin label, or None if rep is not a spin-1/2 irrep.

Accepts Spin and the leftover strings "up" / "down" so old bases still count as spinful instead of silently selecting ordinary projectors. The string "spin-up" is not a spin label.

Source code in src/qten/phys/spin.py
535
536
537
538
539
540
541
542
543
544
545
546
547
def as_spin(rep: object) -> Spin | None:
    """Return a Spin label, or None if `rep` is not a spin-1/2 irrep.

    Accepts [`Spin`][qten.phys.spin.Spin] and the leftover strings ``"up"`` /
    ``"down"`` so old bases still count as spinful instead of silently
    selecting ordinary projectors. The string ``"spin-up"`` is not a spin
    label.
    """
    if type(rep) is Spin:
        return rep
    if type(rep) is str and rep in {"up", "down"}:
        return Spin.up if rep == "up" else Spin.down
    return None

contains_spin

contains_spin(space: 'HilbertSpace') -> bool

Return True if any basis state carries a spin-1/2 irrep.

Source code in src/qten/phys/spin.py
550
551
552
def contains_spin(space: "HilbertSpace") -> bool:
    """Return True if any basis state carries a spin-1/2 irrep."""
    return any(as_spin(rep) is not None for psi in space.elements() for rep in psi.base)