Source code for tn4ml.models.mps

from typing import Any

import autoray as a
import jax.numpy as jnp
import numpy as np
import quimb.tensor as qtn
from jax.nn.initializers import Initializer
from quimb import *

from ..initializers import randn
from .model import Model
from .tn import TensorNetwork


[docs] class MatrixProductState(Model, qtn.MatrixProductState): """A Trainable MatrixProductState class. See :class:`quimb.tensor.tensor_1d.MatrixProductState` for explanation of other attributes and methods. """
[docs] def __init__(self, arrays, **kwargs): """Initialize the MatrixProductState. Parameters ---------- arrays : list of array_like The list of tensors, each of shape ``(D, D, d)``, where ``D`` is the bond dimension and ``d`` is the physical dimension. **kwargs : dict Additional arguments to be passed to the parent class. """ Model.__init__(self) qtn.MatrixProductState.__init__(self, arrays, **kwargs)
[docs] def normalize(self, insert=None): """Normalize tensors of :class:`tn4ml.models.mps.MatrixProductState`.""" if self.L > 200: # for large systems for i, tensor in enumerate(self.tensors): if i == 0: self.left_canonize_site(i) elif i == self.L - 1: tensor.modify(data=tensor.data / jnp.linalg.norm(tensor.data)) else: tensor.modify(data=tensor.data / jnp.linalg.norm(tensor.data)) self.left_canonize_site(i) else: norm = self.norm() if insert is None: for tensor in self.tensors: tensor.modify(data=tensor.data / a.do("power", norm, 1 / self.L)) else: self.tensors[insert].modify(data=self.tensors[insert].data / norm)
def trainable_wrapper(mps: qtn.MatrixProductState, **kwargs) -> MatrixProductState: """Create a trainable wrapper around qtn.MatrixProductState. Parameters ---------- mps : :class:`quimb.tensor.MatrixProductState` Matrix Product State to be trained. Returns ------- :class:`tn4ml.models.mps.MatrixProductState` """ tensors = mps.arrays return MatrixProductState(tensors, **kwargs) def generate_shape( method: str, L: int, bond_dim: int = 2, phys_dim: int = 2, cyclic: bool = False, position: int | None = None, class_index: int | None = None, class_dim: int | None = None, ) -> tuple: """Return a tensor shape. Parameters ---------- method : str Method on how to create shapes of tensors. 'even' = exact dimensions as given by parameters, anything else = truncated dimensions. L : int Number of tensors. bond_dim : int Dimension of virtual indices between tensors. *Default = 4*. phys_dim : int Dimension of physical index for individual tensor. cyclic : bool Flag for indicating if MatrixProductState this tensor is part of is cyclic. *Default=False*. position : int Position of tensor in MatrixProductState. class_index : int Index of tensor that is the output node. For classification tasks only. class_dim : int Dimension of output node, or number of classes for classification. Returns ------- tuple """ if method == "even": shape = ( (bond_dim, bond_dim, phys_dim, class_dim) if class_index is not None and position == class_index else (bond_dim, bond_dim, phys_dim) ) if position == 1: shape = ( (1, bond_dim, phys_dim, class_dim) if class_index is not None and position == class_index else (1, bond_dim, phys_dim) ) if position == L: shape = ( (bond_dim, 1, phys_dim, class_dim) if class_index is not None and position == class_index else (bond_dim, 1, phys_dim) ) else: assert not cyclic j = (L + 1 - abs(2 * position - L - 1)) // 2 if position > L // 2 else position chir = min(bond_dim, phys_dim**j) chil = min(bond_dim, phys_dim ** (j - 1)) if position > L // 2: (chil, chir) = (chir, chil) if position == 1: shape = ( (1, chir, phys_dim, class_dim) if class_index is not None and position == class_index else (1, chir, phys_dim) ) elif position == L: shape = ( (chil, 1, phys_dim, class_dim) if class_index is not None and position == class_index else (chil, 1, phys_dim) ) else: shape = ( (chil, chir, phys_dim, class_dim) if class_index is not None and position == class_index else (chil, chir, phys_dim) ) return shape def generate_ind( L: int, shape: tuple, position: int, cyclic: bool = False, class_index: int | None = None, ) -> tuple: """Return the names of the tensor indices. Parameters ---------- shape : tuple Shape of tensor. position : int Position of tensor in MatrixProductState. Goes from 1 to L included. cyclic : bool Flag for indicating if MatrixProductState this tensor is part of is cyclic. *Default=False*. class_index : int Index of tensor that is the output node (that is having index for number of classes). For classification tasks only. Returns ------- tuple String names of indices. """ ind: tuple if len(shape) == 3: if position == 1: if class_index == position: ind = (f"bond_{position - 1}", f"k{position - 1}", f"b{position - 1}") else: ind = ( f"bond_{position - 2}", f"bond_{position - 1}", f"k{position - 1}", ) elif position == L: if cyclic and class_index != position: raise ValueError("Cyclic MPS cannot have class_dim") ind = ( (f"bond_{position - 2}", f"k{position - 1}", f"b{position - 1}") if class_index == position else ( f"bond_{position - 2}", f"bond_{position - 1}", f"k{position - 1}", ) ) else: ind = (f"bond_{position - 2}", f"bond_{position - 1}", f"k{position - 1}") else: ind = ( f"bond_{position - 2}", f"bond_{position - 1}", f"k{position - 1}", f"b{position - 1}", ) return ind def MPS_initialize( # noqa: N802 L: int, arrays: list | None = None, initializer: Initializer = None, key: Any = None, dtype: Any = jnp.float_, shape_method: str = "even", bond_dim: int = 4, phys_dim: int = 2, cyclic: bool = False, add_identity: bool = False, add_to_output: bool = False, boundary: str = "obc", class_index: int | None = None, class_dim: int | None = None, tags_id: str = "I{}", compress: bool = False, insert: int | None = None, canonical_center: int | None = None, **kwargs, ): """Initialize :class:`tn4ml.models.mps.MatrixProductState`. Parameters ---------- L : int Number of tensors. initializer : :class:`jax.nn.initializers.Initializer`` Type of tensor initialization function. key : Array Argument key is a PRNG key (e.g. from `jax.random.key()`), used to generate random numbers to initialize the array. dtype : Any Type of tensor data (from `jax.numpy.float_`) shape_method : str Method to generate shapes for tensors. bond_dim : int Dimension of virtual indices between tensors. *Default = 4*. phys_dim : int Dimension of physical index for individual tensor. cyclic : bool Flag for indicating if MatrixProductState is cyclic. *Default=False*. add_identity : bool Flag to add identity to tensors diagonal elements. add_to_output : bool Flag for adding identity to diagonal elements of tensors with output indices. *Default=False*. boundary : str Boundary condition of MatrixProductState. *Default = 'obc'*. obc = open boundary condition. pbc = periodic boundary condition. class_index : int Index of tensor that is the output node for class. For classification tasks only. class_dim : int Dimension of output node, or number of classes for classification. compress : bool Flag to truncate bond dimensions. insert : int Index of tensor divided by norm. When `None` the norm division is distributed across all tensors canonical_center : int If not `None` then create canonical form around canonical center index. kwargs : dict Additional arguments. Returns ------- :class:`tn4ml.models.mps.MatrixProductState` """ if cyclic and shape_method != "even": raise NotImplementedError("Change shape_method to 'even'.") if ( initializer is not None and callable(initializer) and "rand_unitary" in getattr(initializer, "__qualname__", "") ): if add_identity: raise ValueError("rand_unitary initializer does not support add_identity.") if compress: raise ValueError("rand_unitary initializer does not support compress.") if insert: raise ValueError("rand_unitary initializer does not support insert.") if boundary == "obc": boundary = None if arrays is not None: # This means MPS for classification needs to be created with qtn.tensor_1d.TensorNetwork1DFlat class assert ( class_index is not None ) # class_index is required when arrays or shapes are provided if initializer is None: initializer = randn() if class_index is not None: # MPS for classification if class_index > L: raise ValueError("class_index should be less than L.") tensors = [] if arrays is not None: for i, array in enumerate(arrays): ind = generate_ind(L, array.shape, i + 1, cyclic, class_index) tensors.append(qtn.Tensor(array, inds=ind, tags=tags_id.format(i))) else: for i in range(1, L + 1): shape = generate_shape( shape_method, L, bond_dim, phys_dim, cyclic, i, class_index, class_dim, ) ind = generate_ind(L, shape, i, cyclic, class_index) if callable(initializer) and "rand_unitary" in getattr( initializer, "__qualname__", "" ): if i < class_index or i > class_index: array = initializer(key, shape, dtype) elif i == class_index: # Output node rng = np.random.default_rng() array = jnp.asarray(rng.normal(0.0, 1.0, shape), dtype) else: raise ValueError( "Check value of class_index. It should be less than L." ) else: array = initializer(key, shape, dtype) if add_identity: if len(array.shape) == 3: copy_array = jnp.copy(array) copy_array = copy_array.at[:, :, 0].add( jnp.eye(array.shape[0], array.shape[1], dtype=dtype) ) array = copy_array elif len(array.shape) == 4: # output node if add_to_output: copy_array = jnp.copy(array) identity = jnp.eye( array.shape[0], array.shape[1], dtype=dtype ) identity = jnp.expand_dims(identity, axis=2) identity = jnp.broadcast_to( identity, ( copy_array.shape[0], copy_array.shape[1], copy_array.shape[3], ), ) copy_array = copy_array.at[:, :, 0, :].add(identity) array = copy_array else: raise ValueError( "Tensors need to always be 3D or 4D in MPS for classification." ) if boundary == "obc": aux_array = jnp.zeros(array.shape, dtype=dtype) if i == 1: # Left node aux_array = aux_array.at[:, 0, :].set(array[:, 0, :]) array = aux_array elif i == L: # Right node aux_array = aux_array.at[0, :, :].set(array[0, :, :]) array = aux_array tensors.append(qtn.Tensor(array, inds=ind, tags=tags_id.format(i - 1))) mps = TensorNetwork(tensors, cyclic=cyclic, site_tag_id=tags_id, **kwargs) if L > 200: # for large systems for i, tensor in enumerate(mps.tensors): if i == 0: mps.left_canonize_site(i) elif i == L - 1: tensor.modify(data=tensor.data / jnp.linalg.norm(tensor.data)) else: tensor.modify(data=tensor.data / jnp.linalg.norm(tensor.data)) mps.left_canonize_site(i) if canonical_center is not None: mps.canonicalize(canonical_center, inplace=True) mps.normalize(insert=canonical_center) else: # normalize if canonical_center is None: mps.normalize() else: mps.canonize(canonical_center, inplace=True) mps.normalize(insert=canonical_center) else: # MPS for regression if arrays is not None: tensors = [] for array in arrays: tensors.append(jnp.squeeze(array)) else: tensors = [] for i in range(1, L + 1): shape = generate_shape(shape_method, L, bond_dim, phys_dim, cyclic, i) tensor = initializer(key, shape, dtype) if callable(initializer) and "rand_unitary" not in getattr( initializer, "__qualname__", "" ): if add_identity: if len(tensor.shape) == 3: copy_tensor = jnp.copy(tensor) copy_tensor.at[:, :, 0].add( jnp.eye(tensor.shape[0], tensor.shape[1], dtype=dtype) ) tensor = copy_tensor else: raise ValueError( "There was an error in generating shape. They should be 3D" ) if boundary == "obc": aux_tensor = jnp.zeros(tensor.shape, dtype=dtype) if i == 1: # Left node aux_tensor = aux_tensor.at[:, 0, :].set(tensor[:, 0, :]) tensor = aux_tensor elif i == L: # Right node aux_tensor = aux_tensor.at[0, :, :].set(tensor[0, :, :]) tensor = aux_tensor tensors.append(jnp.squeeze(tensor)) if ( not ( callable(initializer) and "rand_unitary" in getattr(initializer, "__qualname__", "") ) and insert and insert < L and shape_method == "even" ): tensors[insert] /= jnp.sqrt(phys_dim) mps = MatrixProductState(tensors, **kwargs) if compress: if shape_method == "even": mps.compress(form="flat", max_bond=bond_dim) # limit bond_dim else: raise ValueError('Compress only works with shape_method = "even".') if L > 200: # for large systems for i, tensor in enumerate(mps.tensors): if i == 0: mps.left_canonize_site(i) elif i == L - 1: tensor.modify(data=tensor.data / jnp.linalg.norm(tensor.data)) else: tensor.modify(data=tensor.data / jnp.linalg.norm(tensor.data)) mps.left_canonize_site(i) if canonical_center is not None: mps.canonicalize(canonical_center, inplace=True) mps.normalize(insert=canonical_center) else: if canonical_center is None: norm = mps.norm() for tensor in mps.tensors: tensor.modify(data=tensor.data / a.do("power", norm, 1 / L)) else: mps.canonicalize(canonical_center, inplace=True) mps.normalize(insert=canonical_center) return mps