Skip to content

qten.phys

Package reference for qten.phys.

phys

Physics-facing labels and operator assembly.

This package sits between qten.symbolics and qten.linalg. Use it for two things:

  • free-fermion bonds that become tensors
  • spin-1/2 labels that decide whether a point group must lift to \(SU(2)\)
Spin and point groups

Spin is a typed irrep inside U1Basis. Putting Spin.up / Spin.down on a site is the definition of a spinful Hilbert space. Point-group code later asks contains_spin; you do not call .with_spin on the group.

Electron spin always lives in \(\mathbb{C}^2\). The spatial model can be 1D, 2D, or 3D. The map from a point operation \(g\) to spin is the principal \(SU(2)\) lift of the stored 3D rotation \(R(g)\in O(3)\), not a padded copy of the small spatial matrix. Improper isometries first drop inversion, \(R_+(g)=(\det R(g))\,R(g)\in SO(3)\), because spatial inversion does not act on spin-1/2. Then [ u(g)=\cos(\theta/2)\,I-i\sin(\theta/2)\,\hat n\cdot\boldsymbol\sigma, ] where \(R_+(g)\) has axis \(\hat n\) and angle \(\theta\in[0,\pi]\). The cover is two-to-one: \(u\) and \(-u\) determine the same \(R_+\). QTen takes the principal branch \(\operatorname{Re}\operatorname{tr}u\ge 0\).

from qten.phys import Spin, su2_of_point_group
from qten.pointgroups import pointgroup
from qten.symbolics import U1Basis

psi = U1Basis.new(site, Spin.up)   # this basis state is spinful
g = pointgroup("C4v", plane="xy")  # geometry is fixed here
u = su2_of_point_group(g.elements()[1])  # 2x2 SU(2) factor

Inspect a lift when you need the matrix. Projection and representation assembly live in qten.pointgroups: hilbert_repr builds \(D(g)=D_{\mathrm{orb}}(g)\otimes u(g)\) on a spinful space.

Rare exception, still at construction: pointgroup("C4v", spin="trivial") sets \(u(g)=I\) (flavor spin / no SOC). Do not redefine an already built group.

Spin APIs
Free-fermion assembly
  • Bond stores a weighted directed transition between two U1Basis states.
  • FFObservable accumulates bond terms and converts them into a rank-2 Tensor.

Exported API

Bond dataclass

Bond(coef: Number, base: BaseType)

Bases: Multiple[Tuple[U1Basis, U1Basis]]

Weighted directed transition between two U1Basis states.

Bond specializes Multiple for the common physics case where the base object is an ordered pair (src, dst) of basis states. The scalar coefficient is kept separate from the endpoint states so symbolic manipulations can preserve the structural meaning of the bond until a later tensor-construction step.

In an FFObservable, the ordered pair is interpreted as a matrix element from src to dst. The observable is responsible for adding the Hermitian conjugate contribution when converting the collected bonds to a tensor.

In index notation this directed entry is \(O_{\mathrm{src},\mathrm{dst}}\).

Attributes:

Name Type Description
coef Number

Symbolic coefficient multiplying the bond contribution.

base Tuple[U1Basis, U1Basis]

Ordered pair (src, dst) describing the directed basis-state transition represented by the bond.

See Also

qten.phys.FFObservable Observable builder that consumes Bond instances.

coef instance-attribute

coef: Number

Numeric SymPy coefficient applied to base, kept separate so callers can accumulate scalar factors without eagerly rebuilding symbolic expressions.

base instance-attribute

base: BaseType

The symbolic or scalar object being multiplied by coef, preserved in structured form for later operator application or simplification.

FFObservable

FFObservable()

Free-fermionic observable assembled from symbolic Bond terms.

The observable stores a list of weighted basis-state transitions and can convert them into a Hermitian matrix representation on the minimal HilbertSpace spanned by the bond endpoints after ray reduction.

Each accumulated Bond contributes one directed matrix element. During tensor conversion, endpoint basis states are reduced to their rays, repeated rays are coalesced into a single Hilbert-space basis, and off-diagonal entries are mirrored by complex conjugation.

In matrix notation, inserting a bond from \(|i\rangle\) to \(|j\rangle\) with coefficient \(c\) writes \(O_{ij} = c\) and, for \(i \ne j\), the Hermitian completion \(O_{ji} = c^{*}\).

Notes

Bonds are retained internally in insertion order so the generated Hilbert space is deterministic for a fixed sequence of add_bond calls.

Initialize an empty observable with no bond contributions.

New terms can be added with add_bond().

Source code in src/qten/phys/_ff_observables.py
67
68
69
70
71
72
73
def __init__(self):
    """
    Initialize an empty observable with no bond contributions.

    New terms can be added with [`add_bond()`][qten.phys.FFObservable.add_bond].
    """
    self._bonds = []

add_bond

add_bond(bond: Bond)

Append a bond contribution to the observable.

Supported forms
  • add_bond(bond)
  • add_bond(coef, src, dst)

Parameters:

Name Type Description Default
bond Bond

Already-constructed bond term for the single-argument form.

required

Returns:

Type Description
None

The observable is updated in place.

Raises:

Type Description
TypeError

If bond is not an instance of Bond.

Source code in src/qten/phys/_ff_observables.py
 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
@multimethod
def add_bond(self, bond: Bond):
    """
    Append a bond contribution to the observable.

    Supported forms
    ---------------
    - `add_bond(bond)`
    - `add_bond(coef, src, dst)`

    Parameters
    ----------
    bond : Bond
        Already-constructed bond term for the single-argument form.

    Returns
    -------
    None
        The observable is updated in place.

    Raises
    ------
    TypeError
        If `bond` is not an instance of `Bond`.
    """
    if not isinstance(bond, Bond):
        raise TypeError(f"Expected a Bond instance, got {type(bond).__name__}")
    self._bonds.append(bond)

to_tensor

to_tensor(*, device: Optional[Device] = None) -> Tensor

Convert the accumulated bonds into a Hermitian matrix Tensor.

Each bond contributes the matrix element induced by its coefficient and endpoint basis-state amplitudes. Basis states are first reduced to their ray representatives, and the output HilbertSpace is built from the resulting insertion-ordered unique rays. Off-diagonal entries are mirrored by complex conjugation so the returned tensor is Hermitian.

The resulting data satisfy \(O_{ij} = O_{ji}^{*}\).

Parameters:

Name Type Description Default
device Device | None

Logical device on which to allocate the output tensor data. If omitted, PyTorch uses its default device.

None

Returns:

Type Description
Tensor

Rank-2 tensor whose dimensions are the minimal Hilbert space spanned by the bond endpoint rays.

Source code in src/qten/phys/_ff_observables.py
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
def to_tensor(self, *, device: Optional[Device] = None) -> Tensor:
    r"""
    Convert the accumulated bonds into a Hermitian matrix [`Tensor`][qten.linalg.tensors.Tensor].

    Each bond contributes the matrix element induced by its coefficient and
    endpoint basis-state amplitudes. Basis states are first reduced to their
    ray representatives, and the output [`HilbertSpace`][qten.symbolics.hilbert_space.HilbertSpace] is built from the
    resulting insertion-ordered unique rays. Off-diagonal entries are
    mirrored by complex conjugation so the returned tensor is Hermitian.

    The resulting data satisfy \(O_{ij} = O_{ji}^{*}\).

    Parameters
    ----------
    device : Device | None
        Logical device on which to allocate the output tensor data. If
        omitted, PyTorch uses its default device.

    Returns
    -------
    Tensor
        Rank-2 tensor whose dimensions are the minimal Hilbert space
        spanned by the bond endpoint rays.
    """
    basis_index: dict[U1Basis, int] = {}
    basis_order: list[U1Basis] = []
    bond_entries: list[tuple[int, int, complex]] = []

    for bond in self._bonds:
        left, right = bond.base
        left_ray = left.rays()
        right_ray = right.rays()

        i = basis_index.setdefault(left_ray, len(basis_order))
        if i == len(basis_order):
            basis_order.append(left_ray)

        j = basis_index.setdefault(right_ray, len(basis_order))
        if j == len(basis_order):
            basis_order.append(right_ray)

        value = complex(bond.coef * left.coef * sy.conjugate(right.coef))
        bond_entries.append((i, j, value))

    space = HilbertSpace.new(basis_order)
    precision = get_precision_config()
    torch_device = device.torch_device() if device is not None else None
    data = torch.zeros(
        (space.dim, space.dim), dtype=precision.torch_complex, device=torch_device
    )

    for i, j, value in bond_entries:
        if i == j:
            data[i, i] += value.real
        else:
            data[i, j] += value
            data[j, i] += value.conjugate()

    return Tensor(data=data, dims=(space, space))

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'}"

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)

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)

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)

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_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)]

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))