Skip to content

Build Operators

Physical operators in quantum many-body systems must respect symmetries and charge conservation. This page demonstrates how to construct common operators—identity, isometry, and ladder operators—as symmetry-aware tensors.

Key operators:

  • Identity: Diagonal operators mapping each state to itself
  • Isometry: Fusion tensors that combine multiple indices
  • Number operator: Diagonal operator returning charge values
  • Ladder operators: Creation and annihilation with auxiliary indices for charge conservation
  • Spin operators: \(S_z\) (diagonal z-component)

Operators with auxiliary indices (like creation/annihilation or spin raising/lowering) need an extra index to ensure the total charge is conserved. These auxiliary indices have specific charges that balance the charge transfer between physical states.

Identity Operator

group = U1Group()
idx = Index(Direction.OUT, group, sectors=(Sector(0, 2), Sector(1, 1)))

# Create identity operator
I = identity(idx, itags=("i", "j"))

print(f"Identity has {len(I.data)} blocks\n{I}")
Identity has 2 blocks

  info:  2x { 2 x 1 }  having 'A',   Tensor,  { i*, j }
  data:  2-D float64 (40 B)    3 x 3 => 3 x 3  @ norm = 1.73205

     1.  2x2     |  1x1     [ 0 ; 0 ]    32 B      
     2.  1x1     |  1x1     [ 1 ; 1 ]       1.     

Fusion Isometry

# Fuse two indices into one
idx1 = Index(Direction.OUT, group, sectors=(Sector(0, 2), Sector(1, 1)))
idx2 = Index(Direction.OUT, group, sectors=(Sector(0, 1), Sector(1, 2)))

# Create fusion tensor
iso = isometry(idx1, idx2, itags=("i", "j", "ij"))

print(f"Isometry indices: {iso.itags}")
print(f"Fused index dim: {iso.indices[2].dim}\n{iso}")
Isometry indices: ('i', 'j', 'ij')
Fused index dim: 9

  info:  3x { 4 x 1 }  having 'A',   Tensor,  { i*, j*, ij }
  data:  3-D float64 (264 B)    3 x 3 x 9 => 3 x 3 x 9  @ norm = 3

     1.  2x1x2   |  1x1x1   [ 0 ; 0 ; 0 ]    32 B      
     2.  2x2x5   |  1x1x1   [ 0 ; 1 ; 1 ]   160 B      
     3.  1x1x5   |  1x1x1   [ 1 ; 0 ; 1 ]    40 B      
     4.  1x2x2   |  1x1x1   [ 1 ; 1 ; 2 ]    32 B      

Number Operator

# Diagonal operator that returns the charge value
idx_num = Index(Direction.OUT, group, sectors=(Sector(0, 1), Sector(1, 1), Sector(2, 1)))

# Manually construct number operator
N_data = {}
for sector in idx_num.sectors:
    charge = sector.charge
    dim = sector.dim
    # Diagonal matrix with charge values
    N_data[(charge, charge)] = torch.eye(dim) * charge

N = Tensor(indices=(idx_num, idx_num.flip()), itags=("out", "in"), data=N_data)

print(f"Number operator:\n{N}")
Number operator:

  info:  2x { 3 x 1 }  having 'A',   Tensor,  { out*, in }
  data:  2-D float64 (12 B)    3 x 3 => 3 x 3  @ norm = 2.23607

     1.  1x1     |  1x1     [ 0 ; 0 ]       0.     
     2.  1x1     |  1x1     [ 1 ; 1 ]       1.     
     3.  1x1     |  1x1     [ 2 ; 2 ]       2.     

Ladder Operators

# Creation and annihilation operators for bosons
# a†|n⟩ = √(n+1)|n+1⟩
# a|n⟩ = √n|n-1⟩
# Need auxiliary indices to conserve total charge

n_max = 3
idx_ladder = Index(Direction.OUT, group, sectors=tuple(Sector(n, 1) for n in range(n_max + 1)))

# Auxiliary index with charge +1 for creation
idx_aux_plus = Index(Direction.OUT, group, sectors=(Sector(1, 1),))

# Creation operator a†: (out, in, aux+1)
a_dag_data = {}
for n in range(n_max):
    # Connects |n⟩ to |n+1⟩, charge conserved: (n+1) + (-n) + (-1) = 0
    a_dag_data[(n + 1, n, 1)] = torch.tensor([[[torch.sqrt(torch.tensor(float(n + 1)))]]])

a_dag = Tensor(
    indices=(idx_ladder, idx_ladder.flip(), idx_aux_plus.flip()),
    itags=("out", "in", "aux"),
    data=a_dag_data
)

print(f"Creation operator has {len(a_dag.data)} blocks\n{a_dag}\n")

# Auxiliary index with charge -1 for annihilation
idx_aux_minus = Index(Direction.OUT, group, sectors=(Sector(-1, 1),))

# Annihilation operator a: (out, in, aux-1)
a_data = {}
for n in range(1, n_max + 1):
    # Connects |n⟩ to |n-1⟩, charge conserved: (n-1) + (-n) + (1) = 0
    a_data[(n - 1, n, -1)] = torch.tensor([[[torch.sqrt(torch.tensor(float(n)))]]])

a = Tensor(
    indices=(idx_ladder, idx_ladder.flip(), idx_aux_minus.flip()),
    itags=("out", "in", "aux"),
    data=a_data
)

print(f"Annihilation operator has {len(a.data)} blocks\n{a}")
Creation operator has 3 blocks

  info:  3x { 3 x 1 }  having 'A',   Tensor,  { out*, in, aux }
  data:  3-D float64 (12 B)    3 x 3 x 1 => 3 x 3 x 1  @ norm = 2.44949

     1.  1x1x1   |  1x1x1   [ 1 ; 0 ; 1 ]       1.     
     2.  1x1x1   |  1x1x1   [ 2 ; 1 ; 1 ]    1.414     
     3.  1x1x1   |  1x1x1   [ 3 ; 2 ; 1 ]    1.732     

Annihilation operator has 3 blocks

  info:  3x { 3 x 1 }  having 'A',   Tensor,  { out*, in, aux }
  data:  3-D float64 (12 B)    3 x 3 x 1 => 3 x 3 x 1  @ norm = 2.44949

     1.  1x1x1   |  1x1x1   [ 0 ; 1 ; -1 ]       1.     
     2.  1x1x1   |  1x1x1   [ 1 ; 2 ; -1 ]    1.414     
     3.  1x1x1   |  1x1x1   [ 2 ; 3 ; -1 ]    1.732     

Spin Operators

# Sz operator for spin-1/2
# |↓⟩ has Sz = -1/2, |↑⟩ has Sz = +1/2
# Using units where 2*Sz is an integer

idx_spin = Index(Direction.OUT, group, sectors=(Sector(-1, 1), Sector(1, 1)))

# Sz is diagonal
Sz_data = {
    (-1, -1): torch.tensor([[-0.5]]),  # |↓⟩
    (1, 1): torch.tensor([[0.5]]),     # |↑⟩
}

Sz = Tensor(indices=(idx_spin, idx_spin.flip()), itags=("out", "in"), data=Sz_data)

print(f"Sz operator:\n{Sz}")
Sz operator:

  info:  2x { 2 x 1 }  having 'A',   Tensor,  { out*, in }
  data:  2-D float64 (8 B)    2 x 2 => 2 x 2  @ norm = 0.707107

     1.  1x1     |  1x1     [ -1 ; -1 ]     -0.5     
     2.  1x1     |  1x1     [  1 ;  1 ]      0.5     

See Also