Skip to content

Indexing and Block Access

Symmetry-aware tensors store data in multiple blocks, each labeled by quantum numbers. This page shows how to access and manipulate these blocks and their associated indices.

Key operations:

  • Block access: Retrieve individual blocks by integer index or charge key
  • Iteration: Loop over all blocks in a tensor
  • Block filtering: Create new tensors containing only specific blocks
  • Index manipulation: Inspect properties, modify tags, flip directions
  • Trivial indices: Insert auxiliary indices for tensor network operations

Understanding block structure is essential for debugging, analyzing tensor properties, and performing advanced manipulations.

Accessing Blocks

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

# Display tensor to see block structure
print(f"T = \n{T}\n")

# Access blocks by integer index (1-based, matching display)
first_block = T.block(1)
print(f"First block shape: {first_block.shape}")

# Get block key (charges)
key_1 = T.key(1)
print(f"First block charges: {key_1}\n")

# Access blocks directly via data dictionary
if (0, 0) in T.data:
    block_00 = T.data[(0, 0)]
    print(f"Block (0,0) shape: {block_00.shape}")
T = 

  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     

First block shape: torch.Size([1, 1])
First block charges: (-1, -1)

Block (0,0) shape: torch.Size([2, 2])

Iterating Over Blocks

# Method 1: Using sorted_keys property
for key in T.sorted_keys:
    block = T.data[key]
    print(f"Block {key}: shape {block.shape}, norm {torch.linalg.norm(block):.4f}")

print()

# Method 2: Using integer indices
for i in range(1, len(T.data) + 1):
    key = T.key(i)
    block = T.block(i)
    print(f"Block {i} has charges {key}")

print()

# Method 3: Direct dictionary iteration
for key, block in T.data.items():
    print(f"Charges {key}: {block.shape}")
Block (-1, -1): shape torch.Size([1, 1]), norm 0.1863
Block (0, 0): shape torch.Size([2, 2]), norm 0.4878
Block (1, 1): shape torch.Size([1, 1]), norm 1.1229

Block 1 has charges (-1, -1)
Block 2 has charges (0, 0)
Block 3 has charges (1, 1)

Charges (0, 0): torch.Size([2, 2])
Charges (1, 1): torch.Size([1, 1])
Charges (-1, -1): torch.Size([1, 1])

Extracting Block Subsets

# Get a single block (using integer)
T_single = filter_blocks(T, 1)
print(f"Single block: {len(T_single.data)}\n")

# Get multiple blocks (using list)
T_sub = filter_blocks(T, [1, 2])
print(f"Original blocks: {len(T.data)}")
print(f"Subset blocks: {len(T_sub.data)}\n")

# Note: filter_blocks automatically removes unused sectors from indices

# Extract specific charge sectors
# Find blocks with positive charges only
positive_blocks = []
for i in range(1, len(T.data) + 1):
    key = T.key(i)
    if all(q >= 0 for q in key):
        positive_blocks.append(i)

if positive_blocks:
    T_positive = filter_blocks(T, positive_blocks)
    print(f"Positive charge blocks: {len(T_positive.data)}")
Single block: 1

Original blocks: 3
Subset blocks: 2

Positive charge blocks: 2

Displaying Index Information

# The Index class has a __str__ method for display
idx_display = T.indices[0]
print(f"First index:\n{idx_display}\n")

# Second index
idx_display2 = T.indices[1]
print(f"Second index:\n{idx_display2}")
First index:

  Index having 'U1' with -

      Charge  Dims
         0     2
         1     1
        -1     1

Second index:

  Index having 'U1' with +

      Charge  Dims
         0     2
         1     1
        -1     1

Index Properties

# Get index information
idx_inspect = T.indices[0]
print(f"Direction: {idx_inspect.direction}")
print(f"Group: {idx_inspect.group.name}")
print(f"Number of sectors: {len(idx_inspect.sectors)}")
print(f"Total dimension: {idx_inspect.dim}\n")

# List all sectors
for sector in idx_inspect.sectors:
    print(f"Charge {sector.charge}: dimension {sector.dim}")

print()

# Get charges and dimensions
charges = idx_inspect.charges()
dim_map = idx_inspect.sector_dim_map()
print(f"Charges: {charges}")
print(f"Dimension map: {dim_map}")
Direction: -1
Group: U1
Number of sectors: 3
Total dimension: 4

Charge 0: dimension 2
Charge 1: dimension 1
Charge -1: dimension 1

Charges: (0, 1, -1)
Dimension map: {0: 2, 1: 1, -1: 1}

Modifying Index Tags

# Change tags to make contractions clearer
T_tag = Tensor.random([idx, idx.flip(), idx], itags=["i", "j", "k"], seed=99)
print(f"Original tags: {T_tag.itags}")

# Mode 1: Retag by name using dictionary mapping
T_tag.retag({"i": "left", "k": "right"})
print(f"After retag by name: {T_tag.itags}")

# Mode 2: Retag all at once with full list
T_tag.retag(["a", "b", "c"])
print(f"After retag all: {T_tag.itags}")

# Mode 3: Retag by position
T_tag.retag([0, 2], ["x", "z"])
print(f"After retag by position: {T_tag.itags}")
Original tags: ('i', 'j', 'k')
After retag by name: ('left', 'j', 'right')
After retag all: ('a', 'b', 'c')
After retag by position: ('x', 'b', 'z')

Flipping and Dualizing Indices

# Flip: reverse direction only
idx_out = Index(Direction.OUT, group, sectors=(Sector(1, 2),))
idx_in = idx_out.flip()

print(f"Original: {idx_out.direction}")  # OUT
print(f"Flipped: {idx_in.direction}")    # IN
print(f"Charges unchanged: {idx_out.sectors == idx_in.sectors}\n")

# Dual: reverse direction AND conjugate charges
idx_dual = idx_out.dual()
print(f"Dual direction: {idx_dual.direction}")  # IN
# For U(1), charges are negated
print()

# Check sectors
print("Original sectors:")
for s in idx_out.sectors:
    print(f"  Charge {s.charge}, dim {s.dim}")

print("\nDual sectors:")
for s in idx_dual.sectors:
    print(f"  Charge {s.charge}, dim {s.dim}")
Original: -1
Flipped: 1
Charges unchanged: True

Dual direction: 1

Original sectors:
  Charge 1, dim 2

Dual sectors:
  Charge -1, dim 2

Inserting Trivial Indices

# Create a tensor to demonstrate inserting a trivial index
T_insert = Tensor.random([idx, idx.flip()], itags=["i", "j"], seed=7)
print(f"Before: {len(T_insert.indices)} indices")
print(f"Tags: {T_insert.itags}")
print(T_insert)
Before: 2 indices
Tags: ('i', 'j')

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

     1.  1x1     |  1x1     [ -1 ; -1 ]  -0.8948     
     2.  2x2     |  1x1     [  0 ;  0 ]    32 B      
     3.  1x1     |  1x1     [  1 ;  1 ]    1.691     
# Insert a trivial index (neutral charge, dimension 1) at position 1
T_insert.insert_index(position=1, direction=Direction.OUT, itag="trivial")
print(f"After: {len(T_insert.indices)} indices")
print(f"Tags: {T_insert.itags}")
print(T_insert)
After: 3 indices
Tags: ('i', 'trivial', 'j')

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

     1.  1x1x1   |  1x1x1   [ -1 ; 0 ; -1 ]  -0.8948     
     2.  2x1x2   |  1x1x1   [  0 ; 0 ;  0 ]    32 B      
     3.  1x1x1   |  1x1x1   [  1 ; 0 ;  1 ]    1.691     

Block Memory Usage

# Check memory usage per block
print("Block memory usage:")
for i in range(1, len(T.data) + 1):
    key = T.key(i)
    block = T.block(i)
    mem_bytes = block.nbytes
    mem_kb = mem_bytes / 1024
    print(f"Block {i} {key}: {block.shape} -> {mem_bytes} B ({mem_kb:.2f} KB)")

# Total memory
total_bytes = sum(block.nbytes for block in T.data.values())
print(f"\nTotal block memory: {total_bytes} B ({total_bytes/1024:.2f} KB)")
Block memory usage:
Block 1 (-1, -1): torch.Size([1, 1]) -> 8 B (0.01 KB)
Block 2 (0, 0): torch.Size([2, 2]) -> 32 B (0.03 KB)
Block 3 (1, 1): torch.Size([1, 1]) -> 8 B (0.01 KB)

Total block memory: 48 B (0.05 KB)

See Also