Skip to content

Quick Examples

This page provides quick examples to get you started with Nicole. After understanding the Core Concepts, these examples will help you create and manipulate symmetry-aware tensors.

Basic U(1) Tensor

Create a simple tensor with U(1) symmetry (e.g., particle number conservation):

# Create a U(1) symmetric index
group = U1Group()
index = Index(
    Direction.OUT,
    group,
    sectors=(
        Sector(charge=0, dim=2),   # neutral sector, 2 states
        Sector(charge=1, dim=1),   # charge +1, 1 state
        Sector(charge=-1, dim=1),  # charge -1, 1 state
    )
)

# Create a random tensor with this symmetry
tensor = Tensor.random([index, index.flip()], itags=["i", "j"], seed=42)
print(tensor)
  info:  2x { 3 x 1 }  having 'A',   Tensor,  { i*, j }
  data:  2-D float64 (48 B)    4 x 4 => 4 x 4  @ norm = 1.23835

     1.  1x1     |  1x1     [ -1 ; -1 ]  -0.1863     
     2.  2x2     |  1x1     [  0 ;  0 ]    32 B      
     3.  1x1     |  1x1     [  1 ;  1 ]   -1.123     

Understanding the Output

When you print a tensor, Nicole displays:

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

     1.  1x1     |  1x1     [ -1 ; -1 ]   -1.302
     2.  2x2     |  1x1     [  0 ;  0 ]    32 B
     3.  1x1     |  1x1     [  1 ;  1 ]   -1.951
  • info: Tensor shape, symmetry type, and index tags (* marks OUT direction)
  • data: Data type, total bytes, multiplets and states, Frobenius norm
  • blocks: Each line shows block dimensions, multiplet info, charges, and value or memory

Creating Different Tensors

Zero and Random Tensors

index_out = Index(
    Direction.OUT,
    group,
    sectors=(
        Sector(charge=0, dim=2),
        Sector(charge=1, dim=1),
        Sector(charge=-1, dim=1),
    )
)
index_in = Index(
    Direction.IN,
    group,
    sectors=(
        Sector(charge=0, dim=3),
        Sector(charge=1, dim=2),
        Sector(charge=-1, dim=1),
    )
)

# Create a zero tensor
tensor_zero = Tensor.zeros([index_out, index_in], itags=["i", "j"])
print(f"Zero tensor:\n{tensor_zero}\n")

# Create a random tensor
tensor_random = Tensor.random([index_out, index_in], itags=["i", "j"], seed=42)
print(f"Random tensor:\n{tensor_random}")
Zero tensor:

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

     1.  1x1     |  1x1     [ -1 ; -1 ]       0.     
     2.  2x3     |  1x1     [  0 ;  0 ]    48 B      
     3.  1x2     |  1x1     [  1 ;  1 ]    16 B      

Random tensor:

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

     1.  1x1     |  1x1     [ -1 ; -1 ]   0.4617     
     2.  2x3     |  1x1     [  0 ;  0 ]    48 B      
     3.  1x2     |  1x1     [  1 ;  1 ]    16 B      

Identity Tensors

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

# Create identity tensor
I = identity(idx, itags=("i", "j"))
print(I)
  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.     

Basic Operations

Arithmetic Operations

Tensors with the same index structure support element-wise arithmetic:

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

A = Tensor.random([idx, idx.flip()], itags=["i", "j"], seed=1)
B = Tensor.random([idx, idx.flip()], itags=["i", "j"], seed=2)

# Addition and subtraction
C = A + B
D = A - B

# Scalar multiplication
E = 2.5 * A

# Norm
print(f"Norm of A: {A.norm()}")
Norm of A: 1.0500921279267068

Tensor Contraction

Contract two tensors along matching indices:

idx_out = Index(Direction.OUT, group, sectors=(Sector(0, 2), Sector(1, 1)))
idx_in = Index(Direction.IN, group, sectors=(Sector(0, 2), Sector(1, 1)))

# Create tensors
A = Tensor.random([idx_out, idx_out], itags=["i", "mid"], seed=10)
B = Tensor.random([idx_in, idx_out], itags=["mid", "j"], seed=11)

# Automatic contraction on matching tags with opposite directions
result = contract(A, B)
print(f"Automatic contraction result:\n{result}\n")

# Or specify contraction pairs explicitly
result2 = contract(A, B, axes=(1, 0))
print(f"Explicit contraction result:\n{result2}")
Automatic contraction result:

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

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

Explicit contraction result:

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

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

Trace

Take the trace of a tensor:

left = Index(Direction.OUT, group, sectors=(Sector(0, 2), Sector(1, 1)))
right = Index(Direction.IN, group, sectors=(Sector(0, 2), Sector(1, 1)))

T = Tensor.random([left, right], itags=["i", "j"], seed=99)

# Trace over both indices
scalar = trace(T, axes=(0, 1))
print(f"Trace result: {scalar.item()}")
Trace result: 0.6904861815584418

Permutation and Transpose

Reorder tensor axes:

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

T = Tensor.random([idx, idx.flip(), idx], itags=["i", "j", "k"], seed=5)

# Permute axes
T_perm = permute(T, [2, 0, 1])  # k, i, j
print(f"Permuted (k, i, j):\n{T_perm}\n")

# Transpose (reverses axis order by default)
T_trans = transpose(T)  # k, j, i
print(f"Transposed (k, j, i):\n{T_trans}")
Permuted (k, i, j):

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

     1.  2x2x2   |  1x1x1   [ 0 ; 0 ; 0 ]    64 B      

Transposed (k, j, i):

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

     1.  2x2x2   |  1x1x1   [ 0 ; 0 ; 0 ]    64 B      

Conjugation

idx = Index(Direction.OUT, group, sectors=(Sector(0, 2),))
T = Tensor.random([idx, idx.flip()], itags=["i", "j"], seed=43)

# Conjugate tensor (flips all index directions)
T_conj = conj(T)
print(f"Conjugated tensor (directions flipped: OUT→IN, IN→OUT):\n{T_conj}")
Conjugated tensor (directions flipped: OUT→IN, IN→OUT):

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

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

Tensor Decomposition (SVD)

Decompose a tensor using SVD:

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

T = Tensor.random([idx, idx.flip()], itags=["i", "j"], seed=7)

# Perform SVD: T ≈ U @ S @ Vh
U, S, Vh = decomp(T, axes=0, mode="SVD")

print(f"U = \n{U}\n")
print(f"S = \n{S}\n")
print(f"Vh = \n{Vh}")
U = 

  info:  2x { 2 x 1 }  having 'A',   Tensor,  { i*, _bond_L* }
  data:  2-D float64 (104 B)    5 x 5 => 5 x 5  @ norm = 2.23607

     1.  3x3     |  1x1     [ 0 ;  0 ]    72 B      
     2.  2x2     |  1x1     [ 1 ; -1 ]    32 B      

S = 

  info:  2x { 2 x 1 }  having 'A',  Diagonal,  { _bond_L, _bond_R }
  data:  2-D float64 (104 B)    5 x 5 => 5 x 5  @ norm = 3.32699

     1.  2x2     |  1x1     [ -1 ; 1 ]    32 B      
     2.  3x3     |  1x1     [  0 ; 0 ]    72 B      

Vh = 

  info:  2x { 2 x 1 }  having 'A',   Tensor,  { _bond_R*, j }
  data:  2-D float64 (104 B)    5 x 5 => 5 x 5  @ norm = 2.23607

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

SU(2) Tensor

Nicole natively supports SU(2) non-Abelian symmetry via SU2Group. Charges are non-negative integers representing twice the spin (2j convention):

# spin-1/2 ⊗ spin-1/2 fuses into singlet (j=0) or triplet (j=1)
print(f"Fusion channels: {SU2Group().fuse_channels(1, 1)}")  # (0, 2) in 2j notation
print(f"irrep_dim of 2j=2: {SU2Group().irrep_dim(2)}")       # 3 (triplet)
Fusion channels: (0, 2)
irrep_dim of 2j=2: 3
# Index with a spin-1/2 sector (one doublet) and a spin-1 sector (one triplet)
su2_index = Index(
    Direction.OUT,
    SU2Group(),
    sectors=(
        Sector(charge=1, dim=1),  # spin-1/2: 1 doublet
        Sector(charge=2, dim=1),  # spin-1:   1 triplet
    )
)

T = Tensor.random([su2_index, su2_index.flip()], itags=["i", "j"], seed=42)
print(T)
  info:  2x { 2 x 1 }  having 'SU2',   Tensor,  { i*, j }
  data:  2-D float64 (16 B)    2 x 2 => 5 x 5  @ norm = 0.360489

     1.  1x1     |  2x2     [ 1 ; 1 ]   0.3367  {+}
     2.  1x1     |  3x3     [ 2 ; 2 ]   0.1288  {+}

All standard operations — contract, decomp, permute, conj, and the rest — work identically for SU(2) tensors.

Working with Multiple Symmetries

Use ProductGroup to combine multiple symmetries:

# Create U(1) × Z(2) group (particle number and parity)
group = ProductGroup([U1Group(), Z2Group()])

# Create index with composite charges (particle, parity)
index = Index(
    Direction.OUT,
    group,
    sectors=(
        Sector((0, 0), 1),  # vacuum: no particles, even parity
        Sector((1, 1), 2),  # one particle, odd parity
        Sector((2, 0), 1),  # two particles, even parity
    )
)

# Create and manipulate tensors as before
T = Tensor.random([index, index.flip()], itags=["i", "j"], seed=42)
print(T)
  info:  2x { 3 x 2 }  having 'U1×Z2',   Tensor,  { i*, j }
  data:  2-D float64 (48 B)    4 x 4 => 4 x 4  @ norm = 1.23835

     1.  1x1     |  1x1     [ 0 0 ; 0 0 ]   0.3367     
     2.  2x2     |  1x1     [ 1 1 ; 1 1 ]    32 B      
     3.  1x1     |  1x1     [ 2 0 ; 2 0 ]  -0.1863     

Advanced: Accessing Blocks

# Access blocks by integer index (1-indexed)
first_block = tensor.block(1)

# Get block key (charges)
key = tensor.key(1)
print(f"Block 1 has charges: {key}")

# Iterate over all blocks
for key, block_array in tensor.data.items():
    print(f"Block {key}: shape {block_array.shape}")
Block 1 has charges: (-1, -1)
Block (0, 0): shape torch.Size([2, 2])
Block (1, 1): shape torch.Size([1, 1])
Block (-1, -1): shape torch.Size([1, 1])

Tips for Getting Started

  1. Index Tags: Use descriptive tags like "left", "right", "phys" to make code more readable
  2. Seeds: Always use seeds with Tensor.random() for reproducible results in development
  3. Display: Call print(tensor) frequently to visualize block structure and verify charge conservation
  4. Direction: Remember that OUT and IN directions must be opposite for valid contractions
  5. Performance: Nicole only stores and operates on admissible blocks, automatically saving memory

Common Patterns

Matrix-Matrix Multiplication

# Create compatible tensors (2-index each)
M1 = Tensor.random([idx_out, idx_in], itags=["i", "j"], seed=20)
M2 = Tensor.random([idx_out, idx_in], itags=["j", "k"], seed=21)

# Contract (like matrix multiplication)
result = contract(M1, M2)  # Contracts on "j"
print(f"Result of M1 @ M2:\n{result}")
Result of M1 @ M2:

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

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

Building Tensor Networks

# Three tensors forming a chain
A = Tensor.random([idx_out, idx_out], itags=["i", "bond1"], seed=30)
B = Tensor.random([idx_in, idx_out], itags=["bond1", "bond2"], seed=31)
C = Tensor.random([idx_in, idx_out], itags=["bond2", "j"], seed=32)

# Contract step by step
AB = contract(A, B)
ABC = contract(AB, C)
print(f"Final contracted tensor:\n{ABC}")
Final contracted tensor:

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

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

Next Steps

  • API Reference: Detailed documentation of all functions and classes
  • Examples: More complex use cases organized by topic

Troubleshooting

ValueError: Sector dim must be positive

Ensure all sector dimensions are positive integers when creating indices.

ValueError: Duplicate sector charge

Each charge can appear only once per index. Check your sector definitions for duplicates.

Contraction fails

Verify that:

  • Tags match between the two tensors
  • Directions are opposite (OUT ↔ IN)
  • Symmetry groups are compatible

For more help, check the API Reference or open an issue on GitHub.