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