Basic tensor initialization and creation operations#
In all examples, start by importing the library and setting configuration options to use a NumPy backend (the default option). Other tensor configuration options will take their default values.
import yastn
import pytest
config_kwargs = {"backend": "np"}
Create tensors from scratch#
def test_syntax_tensor_creation_operations(config_kwargs):
#
# Initialize several rank-4 tensors, with the following signature
# ___
# (-) 0--<--| a |--<--1 (+)
# (+) 2-->--|___|-->--3 (-)
#
# The signatures can be interpreted as tensor legs being directed:
# ingoing for (+) or outgoing for (-).
#
# The symmetry, U1, is specified in config_U1.
config_U1 = yastn.make_config(sym='U1', **config_kwargs)
#
# `t` gives charge sectors and `D` gives dimensions for each sector.
leg1 = yastn.Leg(config_U1, s=-1, t=(-1, 0, 1), D=(1, 2, 3))
leg2 = yastn.Leg(config_U1, s=1, t=(-1, 1, 2), D=(4, 5, 6))
leg3 = yastn.Leg(config_U1, s=1, t=(-1, 1, 2), D=(7, 8, 9))
leg4 = yastn.Leg(config_U1, s=-1, t=(-1, 1, 2), D=(10, 11, 12))
# Upon creation, all blocks that respect charge conservation are
# initialized and filled with either random numbers, ones, or zeros.
a = yastn.rand(config=config_U1, legs=[leg1, leg2, leg3, leg4])
b = yastn.ones(config=config_U1, legs=[leg1, leg2, leg3, leg4])
c = yastn.zeros(config=config_U1, legs=[leg1, leg2, leg3, leg4])
# Diagonal tensors infer the second leg from the provided leg.
d = yastn.rand(config=config_U1, legs=leg1, isdiag=True)
e = yastn.eye(config=config_U1, legs=leg1)
Create an empty tensor and fill it block by block#
def test_syntax_create_empty_tensor_and_fill(config_kwargs):
#
# Create an empty rank-4 tensor with this signature.
# ___
# (-) 0--<--| a |--<--1 (+)
# (+) 2-->--|___|-->--3 (-)
#
# Then fill selected blocks with random values. The block charges,
# ts, are given as a tuple with one entry per tensor leg. The block
# dimensions, Ds, follow the same order.
#
config_U1 = yastn.make_config(sym='U1', **config_kwargs)
d = yastn.Tensor(config=config_U1, s=(-1, 1, 1, -1))
d.set_block(ts=(1, -1, 2, 0), Ds=(2, 4, 9, 2), val='rand')
d.set_block(ts=(2, 0, 2, 0), Ds=(3, 3, 9, 2), val='rand')
# Reusing a charge sector with a different dimension raises an error.
with pytest.raises(yastn.YastnError,
match="Provided Ds is not consistent with " \
"dimensions of existing legs."):
d.set_block(ts=(2, 1, 2, 1), Ds=(3, 3, 10, 2), val='rand')
Clone, detach, or copy tensors#
We switch to the torch backend for gradient support.
config_kwargs = {"backend": "torch"}
@no_numpy_test
def test_clone_copy(config_kwargs):
#
# Create a random U1-symmetric tensor and enable autograd tracking.
#
config = yastn.make_config(sym='U1', **config_kwargs)
leg1 = yastn.Leg(config, s=1, t=(-1, 0, 1), D=(2, 3, 4))
leg2 = yastn.Leg(config, s=1, t=(-1, 1, 2), D=(2, 4, 5))
a = yastn.rand(config=config, legs=[leg1, leg1.conj(), leg2.conj(), leg2])
a.requires_grad_(True)
#
# Clone tensor a to obtain a new tensor b with identical values.
# The tensors a and b do not share data, so their blocks are independent.
# Further operations on b are differentiated correctly when computing gradients
# with respect to a.
b = a.clone()
assert b.requires_grad
assert yastn.are_independent(a, b)
#
# A tensor tracked by autograd can be detached from the computational
# graph. This is useful when one wants to perform computations outside
# autograd. The original tensor and the detached tensor still share data blocks.
c = a.detach()
assert not c.requires_grad
assert not yastn.are_independent(a, c)
#
# A copy of the tensor is detached from the computational graph and does
# not share data with the original tensor.
d = a.copy()
assert not d.requires_grad
assert yastn.are_independent(a, d)
Serialization of symmetric tensors#
def test_syntax_tensor_export_import_operations(config_kwargs):
# Serialize and deserialize a symmetric tensor.
config_U1 = yastn.make_config(sym='U1', **config_kwargs)
legs = [yastn.Leg(config_U1, s=-1, t=(-1, 0, 1), D=(1, 2, 3)),
yastn.Leg(config_U1, s=1, t=(-1, 1, 2), D=(4, 5, 6)),
yastn.Leg(config_U1, s=-1, t=(-1, 1, 2), D=(7, 8, 9))]
a = yastn.rand(config=config_U1, legs=legs)
dictionary = a.to_dict()
tensor = yastn.from_dict(d=dictionary, config=config_U1)
# `config` can override the config stored in the serialized dictionary.
# Split into raw block data and metadata, then recombine.
vector, meta = yastn.split_data_and_meta(a.to_dict(level=0), squeeze=True)
# Ensure that the tensor structure is embedded in the provided metadata.
vector, meta = yastn.split_data_and_meta(a.to_dict(level=0, meta=meta), squeeze=True)
tensor = yastn.Tensor.from_dict(yastn.combine_data_and_meta(vector, meta))
Direct access to blocks#
def test_syntax_block_access(config_kwargs):
config_U1 = yastn.make_config(sym='U1', **config_kwargs)
legs = [yastn.Leg(config_U1, s=-1, t=(-1, 0, 1), D=(1, 2, 3)),
yastn.Leg(config_U1, s=1, t=(-1, 1, 2), D=(4, 5, 6)),
yastn.Leg(config_U1, s=-1, t=(-1, 1, 2), D=(7, 8, 9))]
a = yastn.rand(config=config_U1, legs=legs)
# Access an existing block by its charge key and verify its shape.
assert a[(1, 2, 1)].shape == (3, 6, 8)
# Modify the block in place.
a[(1, 2, 1)] = a[(1, 2, 1)] * 2
# Accessing a missing block raises YastnError.
with pytest.raises(yastn.YastnError,
match="Tensor does not have the block specified by key."):
a[(0, 3, 3)]