Decompositions of symmetric tensors#

QR decompositions#

The function below takes tensor a with 4 legs, decompose it using QR and contracts the resulting Q and R tensors back into a.

def run_qr_combine(a):
    """ decompose and contracts tensor ``a`` using qr decomposition """
    assert a.ndim == 4

    Q, R = yastn.linalg.qr(a, axes=((3, 1), (2, 0)))
    QR = yastn.tensordot(Q, R, axes=(2, 0))
    QR = QR.transpose(axes=(3, 1, 2, 0))
    assert yastn.norm(a - QR) < tol  # == 0.0
    assert Q.is_consistent()
    assert R.is_consistent()
    check_diag_R_nonnegative(R.fuse_legs(axes=(0, (1, 2)), mode='hard'))

    # change signature of new leg; and position of new leg
    Q2, R2 = yastn.qr(a, axes=((3, 1), (2, 0)), sQ=-1, Qaxis=0, Raxis=-1)
    QR2 = yastn.tensordot(R2, Q2, axes=(2, 0)).transpose(axes=(1, 3, 0, 2))
    assert yastn.norm(a - QR2) < tol  # == 0.0
    assert Q2.is_consistent()
    assert R2.is_consistent()
    check_diag_R_nonnegative(R2.fuse_legs(axes=((0, 1), 2), mode='hard'))

SVD decompositions and truncation#

def test_svd_truncate():
    #
    # start with random tensor with 4 legs
    legs = [yastn.Leg(config_U1, s=1, t=(0, 1), D=(5, 6)),
            yastn.Leg(config_U1, s=1, t=(-1, 0), D=(5, 6)),
            yastn.Leg(config_U1, s=-1, t=(-1, 0, 1), D=(2, 3, 4)),
            yastn.Leg(config_U1, s=-1, t=(-1, 0, 1), D=(2, 3, 4))]
    a = yastn.rand(config=config_U1, n=1, legs=legs)

    #
    # fixing singular values for testing
    # creat new tensor *a* that will be used for testing
    U, S, V = yastn.linalg.svd(a, axes=((0, 1), (2, 3)), sU=-1)
    S.set_block(ts=(-2, -2), Ds=4, val=[2**(-ii - 6) for ii in range(4)])
    S.set_block(ts=(-1, -1), Ds=12, val=[2**(-ii - 2) for ii in range(12)])
    S.set_block(ts=(0, 0), Ds=25, val=[2**(-ii - 1) for ii in range(25)])
    a = yastn.ncon([U, S, V], [(-1, -2, 1), (1, 2), (2, -3, -4)])

    #
    # opts for truncation; truncates below (global) tolerance tol with respect to largest singular value
    # up to a total of D_total singular values, with at mot D_block singular values for each charge sector.
    opts = {'tol': 0.01, 'D_block': 100, 'D_total': 12}
    U1, S1, V1 = yastn.linalg.svd_with_truncation(a, axes=((0, 1), (2, 3)), sU=-1, **opts)  # nU=True, total charge of *a* goes with U
    assert S1.get_blocks_charge() == ((-2, -2), (-1, -1), (0, 0))
    assert S1.s[0] == 1 and U1.n == a.n and V1.n == (0,) and S1.get_shape() == (12, 12)

    #
    # svd_with_truncation is a shorthand for
    U, S, V = yastn.linalg.svd_with_truncation(a, axes=((0, 1), (2, 3)), sU=-1)  # nU=True
    mask = yastn.linalg.truncation_mask(S, **opts)
    U1p, S1p, V1p = mask.apply_mask(U, S, V, axes=(2, 0, 0))
    assert all((x - y).norm() < tol for x, y in ((U1, U1p), (S1, S1p), (V1, V1p)))

    #
    # specific charges of S1 depend on signature of a new leg comming from U,
    # and where charge of tensor 'a' is attached
    U1, S1, V1 = yastn.linalg.svd_with_truncation(a, axes=((0, 1), (2, 3)), **opts)  # sU=1, nU=True
    assert S1.get_blocks_charge() == ((0, 0), (1, 1), (2, 2))
    assert S1.s[0] == -1 and U1.n == a.n and V1.n == (0,) and S1.get_shape() == (12, 12)

    U1, S1, V1 = yastn.linalg.svd_with_truncation(a, axes=((0, 1), (2, 3)), sU=-1, nU=False, **opts)
    assert S1.get_blocks_charge() == ((-1, -1), (0, 0), (1, 1))
    assert S1.s[0] == 1 and U1.n == (0,) and V1.n == a.n and S1.get_shape() == (12, 12)

    U1, S1, V1 = yastn.linalg.svd_with_truncation(a, axes=((0, 1), (2, 3)), sU=1, nU=False, **opts)
    assert S1.get_blocks_charge() == ((-1, -1), (0, 0), (1, 1))
    assert S1.s[0] == -1 and U1.n == (0,) and V1.n == a.n and S1.get_shape() == (12, 12)

    #
    # different truncation options
    opts = {'tol': 0.02, 'D_block': 5, 'D_total': 100}
    _, S1, _ = yastn.linalg.svd_with_truncation(a, axes=((0, 1), (2, 3)), sU=1, **opts)
    assert S1.get_shape() == (11, 11)

    #
    # tol_block decides on block truncation tolerance with respect to largest singular value of each block
    opts = {'tol': 0, 'tol_block': 0.2, 'D_total': 100}
    _, S1, _ = yastn.linalg.svd_with_truncation(a, axes=((0, 1), (2, 3)), sU=1, **opts)
    assert S1.get_shape() == (9, 9)

    #
    # we can specify D_block and tol_block for each charge sector independently
    opts = {'D_block': {(0,): 2, (-1,): 3}, 'tol_block': {(0,): 0.6, (-1,): 0.1}}
    _, S1, _ = yastn.linalg.svd_with_truncation(a, axes=((0, 1), (2, 3)), sU=-1, **opts)
    assert S1.get_shape() == (4, 4)

    #
    # Here use truncate_multiplets option to shift the cut to largest D to retain multiplet
    opts = {'D_total': 10, "truncate_multiplets": True}
    _, S1, _ = yastn.linalg.svd_with_truncation(a, axes=((0, 1), (2, 3)), sU=-1, **opts)
    assert S1.get_shape() == (12, 12)

    #
    # empty tensor
    opts = {'D_total': 0}
    _, S2, _ = yastn.linalg.svd_with_truncation(a, axes=((0, 1), (2, 3)), nU=False, sU=-1, **opts)
    assert S2.norm() < tol and S2.size == 0

scipy.sparse.linalg.eigs#

Calculate the dominant eigenvector of a transfer matrix by employing the Krylov-base eigs method available in SciPy. Tensor operations can be similarly passed to other SciPy methods, though this is limited to the NumPy backend.

@pytest.mark.skipif(not config_U1.backend.BACKEND_ID=="numpy", reason="uses scipy for raw data")
def test_eigs_simple():
    legs = [yastn.Leg(config_U1, s=1, t=(-1, 0, 1), D=(2, 3, 2)),
            yastn.Leg(config_U1, s=1, t=(0, 1), D=(1, 1)),
            yastn.Leg(config_U1, s=-1, t=(-1, 0, 1), D=(2, 3, 2))]
    a = yastn.rand(config=config_U1, legs=legs)  # could be mps tensor
    a, _ = yastn.qr(a, axes=((0, 1), 2), sQ=-1)  # orthonormalize

    # dense transfer matrix build from a; reference solution
    tm = yastn.ncon([a, a.conj()], [(-1, 1, -3), (-2, 1, -4)])
    tm = tm.fuse_legs(axes=((2, 3), (0, 1)), mode='hard')
    tmn = tm.to_numpy()
    w_ref, v_ref = eigs(tmn, k=1, which='LM')  # use scipy.sparse.linalg.eigs

    # initializing random tensor matching tm from left
    # we add an extra 3-rd leg carrying charges -1, 0, 1
    # to calculate eigs over those 3 subspaces in one go
    legs = [a.get_legs(0).conj(),
            a.get_legs(0),
            yastn.Leg(a.config, s=1, t=(-1, 0, 1), D=(1, 1, 1))]
    v0 = yastn.rand(config=a.config, legs=legs)
    # define a wrapper that goes r1d -> yastn.tensor -> tm @ yastn.tensor -> r1d
    r1d, meta = yastn.compress_to_1d(v0)
    def f(x):
        t = yastn.decompress_from_1d(x, meta=meta)
        t2 = yastn.ncon([t, a, a.conj()], [(1, 3, -3), (1, 2, -1), (3, 2, -2)])
        t3, _ = yastn.compress_to_1d(t2, meta=meta)
        return t3
    ff = LinearOperator(shape=(len(r1d), len(r1d)), matvec=f, dtype=np.float64)
    # scipy.sparse.linalg.eigs that goes though yastn symmetric tensor
    wa, va1d = eigs(ff, v0=r1d, k=1, which='LM', tol=1e-10)
    # transform eigenvectors into yastn tensors
    va = [yastn.decompress_from_1d(x, meta) for x in va1d.T]
    # we can remove zero blocks now, as there are eigenvectors with well defined charge
    # (though we might get superposition of symmetry sectors in case of degeneracy)
    va = [x.remove_zero_blocks() for x in va]

    # we can also limit ourselves directly to eigenvectors with desired charge, here 0.
    legs = [a.get_legs(0).conj(),
            a.get_legs(0)]
    v0 = yastn.rand(config=a.config, legs=legs, n=0)
    r1d, meta = yastn.compress_to_1d(v0)
    def f(x):
        t = yastn.decompress_from_1d(x, meta=meta)
        t2 = yastn.ncon([t, a, a.conj()], [(1, 3), (1, 2, -1), (3, 2, -2)])
        t3, _ = yastn.compress_to_1d(t2, meta=meta)
        return t3
    ff = LinearOperator(shape=(len(r1d), len(r1d)), matvec=f, dtype=np.float64)
    wb, vb1d = eigs(ff, v0=r1d, k=1, which='LM', tol=1e-10)  # scipy.sparse.linalg.eigs
    vb = [yastn.decompress_from_1d(x, meta) for x in vb1d.T]  # eigenvectors as yastn tensors

    # dominant eigenvalue should have amplitude 1 (likely degenerate in our example)
    assert all(pytest.approx(abs(x), rel=tol) == 1.0 for x in (w_ref, wa, wb))
    print("va -> ", va.pop())
    print("vb -> ", vb.pop())