简化代码;加入.venv下内容
Some checks failed
Build wheels / build (ubuntu-latest, 3.11) (push) Has been cancelled
Build wheels / build (ubuntu-latest, 3.12) (push) Has been cancelled
Build wheels / build (ubuntu-latest, 3.13) (push) Has been cancelled
Tests / check (push) Has been cancelled
Tests / build (ubuntu-latest, 3.11) (push) Has been cancelled
Tests / build (ubuntu-latest, 3.12) (push) Has been cancelled
Tests / build (ubuntu-latest, 3.13) (push) Has been cancelled
Some checks failed
Build wheels / build (ubuntu-latest, 3.11) (push) Has been cancelled
Build wheels / build (ubuntu-latest, 3.12) (push) Has been cancelled
Build wheels / build (ubuntu-latest, 3.13) (push) Has been cancelled
Tests / check (push) Has been cancelled
Tests / build (ubuntu-latest, 3.11) (push) Has been cancelled
Tests / build (ubuntu-latest, 3.12) (push) Has been cancelled
Tests / build (ubuntu-latest, 3.13) (push) Has been cancelled
This commit is contained in:
1288
.venv/lib/python3.12/site-packages/cotengra/contract.py
Normal file
1288
.venv/lib/python3.12/site-packages/cotengra/contract.py
Normal file
File diff suppressed because it is too large
Load Diff
4130
.venv/lib/python3.12/site-packages/cotengra/core.py
Normal file
4130
.venv/lib/python3.12/site-packages/cotengra/core.py
Normal file
File diff suppressed because it is too large
Load Diff
1168
.venv/lib/python3.12/site-packages/cotengra/hyperoptimizers/hyper.py
Normal file
1168
.venv/lib/python3.12/site-packages/cotengra/hyperoptimizers/hyper.py
Normal file
File diff suppressed because it is too large
Load Diff
583
.venv/lib/python3.12/site-packages/cotengra/parallel.py
Normal file
583
.venv/lib/python3.12/site-packages/cotengra/parallel.py
Normal file
@@ -0,0 +1,583 @@
|
||||
"""Interface for parallelism."""
|
||||
|
||||
import atexit
|
||||
import collections
|
||||
import functools
|
||||
import importlib
|
||||
import inspect
|
||||
import numbers
|
||||
import operator
|
||||
import warnings
|
||||
|
||||
_AUTO_BACKEND = None
|
||||
|
||||
# check for loky, joblib (vendors loky), then default to concurrent.futures
|
||||
have_loky = importlib.util.find_spec("loky") is not None
|
||||
have_joblib = importlib.util.find_spec("joblib") is not None
|
||||
if have_loky or have_joblib:
|
||||
_DEFAULT_BACKEND = "loky"
|
||||
else:
|
||||
_DEFAULT_BACKEND = "concurrent.futures"
|
||||
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def choose_default_num_workers():
|
||||
import os
|
||||
|
||||
if "COTENGRA_NUM_WORKERS" in os.environ:
|
||||
return int(os.environ["COTENGRA_NUM_WORKERS"])
|
||||
|
||||
if "OMP_NUM_THREADS" in os.environ:
|
||||
return int(os.environ["OMP_NUM_THREADS"])
|
||||
|
||||
return os.cpu_count()
|
||||
|
||||
|
||||
def get_pool(n_workers=None, maybe_create=False, backend=None):
|
||||
"""Get a parallel pool."""
|
||||
if backend is None:
|
||||
backend = _DEFAULT_BACKEND
|
||||
|
||||
if backend == "dask":
|
||||
return _get_pool_dask(n_workers=n_workers, maybe_create=maybe_create)
|
||||
|
||||
if backend == "ray":
|
||||
return _get_pool_ray(n_workers=n_workers, maybe_create=maybe_create)
|
||||
|
||||
# above backends are distributed, don't specify n_workers
|
||||
if n_workers is None:
|
||||
n_workers = choose_default_num_workers()
|
||||
|
||||
if backend == "loky":
|
||||
get_reusable_executor = get_loky_get_reusable_executor()
|
||||
return get_reusable_executor(max_workers=n_workers)
|
||||
|
||||
if backend == "concurrent.futures":
|
||||
return _get_process_pool_cf(n_workers=n_workers)
|
||||
|
||||
if backend == "threads":
|
||||
return _get_thread_pool_cf(n_workers=n_workers)
|
||||
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def _infer_backed_cached(pool_class):
|
||||
if pool_class.__name__ == "RayExecutor":
|
||||
return "ray"
|
||||
|
||||
path = pool_class.__module__.split(".")
|
||||
|
||||
if path[0] == "concurrent":
|
||||
return "concurrent.futures"
|
||||
|
||||
if path[0] == "joblib":
|
||||
return "loky"
|
||||
|
||||
if path[0] == "distributed":
|
||||
return "dask"
|
||||
|
||||
return path[0]
|
||||
|
||||
|
||||
def _infer_backend(pool):
|
||||
"""Return the backend type of ``pool`` - cached for speed."""
|
||||
return _infer_backed_cached(pool.__class__)
|
||||
|
||||
|
||||
def get_n_workers(pool=None):
|
||||
"""Extract how many workers our pool has (mostly for working out how many
|
||||
tasks to pre-dispatch).
|
||||
"""
|
||||
if pool is None:
|
||||
pool = get_pool()
|
||||
|
||||
try:
|
||||
return pool._max_workers
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
backend = _infer_backend(pool)
|
||||
|
||||
if backend == "dask":
|
||||
workers = pool.scheduler_info(n_workers=-1)["workers"]
|
||||
return sum(int(w.get("nthreads", 1) or 1) for w in workers.values())
|
||||
|
||||
if backend == "ray":
|
||||
while True:
|
||||
try:
|
||||
return int(get_ray().available_resources()["CPU"])
|
||||
except KeyError:
|
||||
import time
|
||||
|
||||
time.sleep(1e-3)
|
||||
|
||||
if backend == "mpi4py":
|
||||
from mpi4py import MPI
|
||||
|
||||
return MPI.COMM_WORLD.size
|
||||
|
||||
raise ValueError(f"Can't find number of workers in pool {pool}.")
|
||||
|
||||
|
||||
def parse_parallel_arg(parallel):
|
||||
""" """
|
||||
global _AUTO_BACKEND
|
||||
|
||||
if parallel == "auto":
|
||||
return get_pool(maybe_create=False, backend=_AUTO_BACKEND)
|
||||
|
||||
if parallel is False:
|
||||
return None
|
||||
|
||||
if parallel is True:
|
||||
if _AUTO_BACKEND is None:
|
||||
_AUTO_BACKEND = _DEFAULT_BACKEND
|
||||
parallel = _AUTO_BACKEND
|
||||
|
||||
if isinstance(parallel, numbers.Integral):
|
||||
_AUTO_BACKEND = _DEFAULT_BACKEND
|
||||
return get_pool(
|
||||
n_workers=parallel, maybe_create=True, backend=_DEFAULT_BACKEND
|
||||
)
|
||||
|
||||
if parallel == "loky":
|
||||
return get_pool(maybe_create=True, backend="loky")
|
||||
|
||||
if parallel == "concurrent.futures":
|
||||
return get_pool(maybe_create=True, backend="concurrent.futures")
|
||||
|
||||
if parallel == "threads":
|
||||
return get_pool(maybe_create=True, backend="threads")
|
||||
|
||||
if parallel == "dask":
|
||||
_AUTO_BACKEND = "dask"
|
||||
return get_pool(maybe_create=True, backend="dask")
|
||||
|
||||
if parallel == "ray":
|
||||
_AUTO_BACKEND = "ray"
|
||||
return get_pool(maybe_create=True, backend="ray")
|
||||
|
||||
return parallel
|
||||
|
||||
|
||||
def set_parallel_backend(backend):
|
||||
"""Create a parallel pool of type ``backend`` which registers it as the
|
||||
default for ``'auto'`` parallel.
|
||||
"""
|
||||
return parse_parallel_arg(backend)
|
||||
|
||||
|
||||
def maybe_leave_pool(pool):
|
||||
"""Logic required for nested parallelism in dask.distributed."""
|
||||
if _infer_backend(pool) == "dask":
|
||||
return _maybe_leave_pool_dask()
|
||||
|
||||
|
||||
def maybe_rejoin_pool(is_worker, pool):
|
||||
"""Logic required for nested parallelism in dask.distributed."""
|
||||
if is_worker and _infer_backend(pool) == "dask":
|
||||
_rejoin_pool_dask()
|
||||
|
||||
|
||||
def submit(pool, fn, *args, **kwargs):
|
||||
"""Interface for submitting ``fn(*args, **kwargs)`` to ``pool``."""
|
||||
if _infer_backend(pool) == "dask":
|
||||
kwargs.setdefault("pure", False)
|
||||
return pool.submit(fn, *args, **kwargs)
|
||||
|
||||
|
||||
def scatter(pool, data):
|
||||
"""Interface for maybe turning ``data`` into a remote object or reference."""
|
||||
if _infer_backend(pool) in ("dask", "ray"):
|
||||
return pool.scatter(data)
|
||||
return data
|
||||
|
||||
|
||||
def can_scatter(pool):
|
||||
"""Whether ``pool`` can make objects remote."""
|
||||
return _infer_backend(pool) in ("dask", "ray")
|
||||
|
||||
|
||||
def should_nest(pool):
|
||||
"""Given argument ``pool`` should we try nested parallelism."""
|
||||
if pool is None:
|
||||
return False
|
||||
backend = _infer_backend(pool)
|
||||
if backend in ("ray", "dask"):
|
||||
return backend
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------- loky ----------------------------------- #
|
||||
|
||||
|
||||
@functools.lru_cache(1)
|
||||
def get_loky_get_reusable_executor():
|
||||
try:
|
||||
from loky import get_reusable_executor
|
||||
except ImportError:
|
||||
from joblib.externals.loky import get_reusable_executor
|
||||
return get_reusable_executor
|
||||
|
||||
|
||||
# --------------------------- concurrent.futures ---------------------------- #
|
||||
|
||||
|
||||
class CachedProcessPoolExecutor:
|
||||
def __init__(self):
|
||||
self._pool = None
|
||||
self._n_workers = -1
|
||||
atexit.register(self.shutdown)
|
||||
|
||||
def __call__(self, n_workers=None):
|
||||
if n_workers != self._n_workers:
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
|
||||
self.shutdown()
|
||||
self._pool = ProcessPoolExecutor(n_workers)
|
||||
self._n_workers = n_workers
|
||||
return self._pool
|
||||
|
||||
def is_initialized(self):
|
||||
return self._pool is not None
|
||||
|
||||
def shutdown(self):
|
||||
if self._pool is not None:
|
||||
self._pool.shutdown()
|
||||
self._pool = None
|
||||
|
||||
def __del__(self):
|
||||
self.shutdown()
|
||||
|
||||
|
||||
ProcessPoolHandler = CachedProcessPoolExecutor()
|
||||
|
||||
|
||||
def _get_process_pool_cf(n_workers=None):
|
||||
return ProcessPoolHandler(n_workers)
|
||||
|
||||
|
||||
class CachedThreadPoolExecutor:
|
||||
def __init__(self):
|
||||
self._pool = None
|
||||
self._n_workers = -1
|
||||
atexit.register(self.shutdown)
|
||||
|
||||
def __call__(self, n_workers=None):
|
||||
if n_workers != self._n_workers:
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
self.shutdown()
|
||||
self._pool = ThreadPoolExecutor(n_workers)
|
||||
self._n_workers = n_workers
|
||||
return self._pool
|
||||
|
||||
def is_initialized(self):
|
||||
return self._pool is not None
|
||||
|
||||
def shutdown(self):
|
||||
if self._pool is not None:
|
||||
self._pool.shutdown()
|
||||
self._pool = None
|
||||
|
||||
def __del__(self):
|
||||
self.shutdown()
|
||||
|
||||
|
||||
ThreadPoolHandler = CachedThreadPoolExecutor()
|
||||
|
||||
|
||||
def _get_thread_pool_cf(n_workers=None):
|
||||
return ThreadPoolHandler(n_workers)
|
||||
|
||||
|
||||
# ---------------------------------- DASK ----------------------------------- #
|
||||
|
||||
|
||||
def _get_pool_dask(n_workers=None, maybe_create=False):
|
||||
"""Maybe get an existing or create a new dask.distrbuted client.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n_workers : None or int, optional
|
||||
The number of workers to request if creating a new client.
|
||||
maybe_create : bool, optional
|
||||
Whether to create an new local cluster and client if no existing client
|
||||
is found.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None or dask.distributed.Client
|
||||
"""
|
||||
try:
|
||||
from dask.distributed import get_client
|
||||
except ImportError:
|
||||
if not maybe_create:
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
|
||||
try:
|
||||
client = get_client()
|
||||
except ValueError:
|
||||
if not maybe_create:
|
||||
return None
|
||||
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from dask.distributed import Client, LocalCluster
|
||||
|
||||
local_directory = tempfile.mkdtemp()
|
||||
lc = LocalCluster(
|
||||
n_workers=n_workers,
|
||||
threads_per_worker=1,
|
||||
local_directory=local_directory,
|
||||
memory_limit=0,
|
||||
)
|
||||
client = Client(lc)
|
||||
|
||||
warnings.warn(
|
||||
"Parallel specified but no existing global dask client found... "
|
||||
"created one (with {} workers).".format(get_n_workers(client))
|
||||
)
|
||||
|
||||
@atexit.register
|
||||
def delete_local_dask_directory():
|
||||
shutil.rmtree(local_directory, ignore_errors=True)
|
||||
|
||||
if n_workers is not None:
|
||||
current_n_workers = get_n_workers(client)
|
||||
if n_workers != current_n_workers:
|
||||
warnings.warn(
|
||||
"Found existing client (with {} workers which) doesn't match "
|
||||
"the requested {}... using it instead.".format(
|
||||
current_n_workers, n_workers
|
||||
)
|
||||
)
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def _maybe_leave_pool_dask():
|
||||
try:
|
||||
from dask.distributed import secede
|
||||
|
||||
secede() # for nested parallelism
|
||||
is_dask_worker = True
|
||||
except (ImportError, ValueError):
|
||||
is_dask_worker = False
|
||||
return is_dask_worker
|
||||
|
||||
|
||||
def _rejoin_pool_dask():
|
||||
from dask.distributed import rejoin
|
||||
|
||||
rejoin()
|
||||
|
||||
|
||||
# ----------------------------------- RAY ----------------------------------- #
|
||||
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def get_ray():
|
||||
""" """
|
||||
import ray
|
||||
|
||||
return ray
|
||||
|
||||
|
||||
class RayFuture:
|
||||
"""Basic ``concurrent.futures`` like future wrapping a ray ``ObjectRef``."""
|
||||
|
||||
__slots__ = ("_obj", "_cancelled")
|
||||
|
||||
def __init__(self, obj):
|
||||
self._obj = obj
|
||||
self._cancelled = False
|
||||
|
||||
def result(self, timeout=None):
|
||||
return get_ray().get(self._obj, timeout=timeout)
|
||||
|
||||
def done(self):
|
||||
return self._cancelled or bool(
|
||||
get_ray().wait([self._obj], timeout=0)[0]
|
||||
)
|
||||
|
||||
def cancel(self):
|
||||
get_ray().cancel(self._obj)
|
||||
self._cancelled = True
|
||||
|
||||
|
||||
def _unpack_futures_tuple(x):
|
||||
return tuple(map(_unpack_futures, x))
|
||||
|
||||
|
||||
def _unpack_futures_list(x):
|
||||
return list(map(_unpack_futures, x))
|
||||
|
||||
|
||||
def _unpack_futures_dict(x):
|
||||
return {k: _unpack_futures(v) for k, v in x.items()}
|
||||
|
||||
|
||||
def _unpack_futures_identity(x):
|
||||
return x
|
||||
|
||||
|
||||
_unpack_dispatch = collections.defaultdict(
|
||||
lambda: _unpack_futures_identity,
|
||||
{
|
||||
RayFuture: operator.attrgetter("_obj"),
|
||||
tuple: _unpack_futures_tuple,
|
||||
list: _unpack_futures_list,
|
||||
dict: _unpack_futures_dict,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _unpack_futures(x):
|
||||
"""Allows passing futures by reference - takes e.g. args and kwargs and
|
||||
replaces all ``RayFuture`` objects with their underyling ``ObjectRef``
|
||||
within all nested tuples, lists and dicts.
|
||||
|
||||
[Subclassing ``ObjectRef`` might avoid needing this.]
|
||||
"""
|
||||
return _unpack_dispatch[x.__class__](x)
|
||||
|
||||
|
||||
@functools.lru_cache(2**14)
|
||||
def get_remote_fn(fn, **remote_opts):
|
||||
"""Cached retrieval of remote function."""
|
||||
ray = get_ray()
|
||||
if remote_opts:
|
||||
return ray.remote(**remote_opts)(fn)
|
||||
return ray.remote(fn)
|
||||
|
||||
|
||||
@functools.lru_cache(2**14)
|
||||
def get_fn_as_remote_object(fn):
|
||||
ray = get_ray()
|
||||
return ray.put(fn)
|
||||
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def get_deploy(**remote_opts):
|
||||
"""Alternative for 'non-function' callables - e.g. partial
|
||||
functions - pass the callable object too.
|
||||
"""
|
||||
ray = get_ray()
|
||||
|
||||
def deploy(fn, *args, **kwargs):
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
if remote_opts:
|
||||
return ray.remote(**remote_opts)(deploy)
|
||||
return ray.remote(deploy)
|
||||
|
||||
|
||||
class RayExecutor:
|
||||
"""Basic ``concurrent.futures`` like interface using ``ray``."""
|
||||
|
||||
def __init__(self, *args, default_remote_opts=None, **kwargs):
|
||||
ray = get_ray()
|
||||
if not ray.is_initialized():
|
||||
ray.init(*args, **kwargs)
|
||||
|
||||
self.default_remote_opts = (
|
||||
{} if default_remote_opts is None else dict(default_remote_opts)
|
||||
)
|
||||
|
||||
def _maybe_inject_remote_opts(self, remote_opts=None):
|
||||
"""Return the default remote options, possibly overriding some with
|
||||
those supplied by a ``submit call``.
|
||||
"""
|
||||
ropts = self.default_remote_opts
|
||||
if remote_opts is not None:
|
||||
ropts = {**ropts, **remote_opts}
|
||||
return ropts
|
||||
|
||||
def submit(self, fn, *args, pure=False, remote_opts=None, **kwargs):
|
||||
"""Remotely run ``fn(*args, **kwargs)``, returning a ``RayFuture``."""
|
||||
# want to pass futures by reference
|
||||
args = _unpack_futures_tuple(args)
|
||||
kwargs = _unpack_futures_dict(kwargs)
|
||||
|
||||
ropts = self._maybe_inject_remote_opts(remote_opts)
|
||||
|
||||
# this is the same test ray uses to accept functions
|
||||
if inspect.isfunction(fn):
|
||||
# can use the faster cached remote function
|
||||
obj = get_remote_fn(fn, **ropts).remote(*args, **kwargs)
|
||||
else:
|
||||
fn_obj = get_fn_as_remote_object(fn)
|
||||
obj = get_deploy(**ropts).remote(fn_obj, *args, **kwargs)
|
||||
|
||||
return RayFuture(obj)
|
||||
|
||||
def map(self, func, *iterables, remote_opts=None):
|
||||
"""Remote map ``func`` over arguments ``iterables``."""
|
||||
ropts = self._maybe_inject_remote_opts(remote_opts)
|
||||
remote_fn = get_remote_fn(func, **ropts)
|
||||
objs = tuple(map(remote_fn.remote, *iterables))
|
||||
ray = get_ray()
|
||||
return map(ray.get, objs)
|
||||
|
||||
def scatter(self, data):
|
||||
"""Push ``data`` into the distributed store, returning an ``ObjectRef``
|
||||
that can be supplied to ``submit`` calls for example.
|
||||
"""
|
||||
ray = get_ray()
|
||||
return ray.put(data)
|
||||
|
||||
def shutdown(self):
|
||||
"""Shutdown the parent ray cluster, this ``RayExecutor`` instance
|
||||
itself does not need any cleanup.
|
||||
"""
|
||||
get_ray().shutdown()
|
||||
|
||||
|
||||
_RAY_EXECUTOR = None
|
||||
|
||||
|
||||
def _get_pool_ray(n_workers=None, maybe_create=False):
|
||||
"""Maybe get an existing or create a new RayExecutor, thus initializing,
|
||||
ray.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n_workers : None or int, optional
|
||||
The number of workers to request if creating a new client.
|
||||
maybe_create : bool, optional
|
||||
Whether to create initialize ray and return a RayExecutor if not
|
||||
initialized already.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None or RayExecutor
|
||||
"""
|
||||
try:
|
||||
import ray
|
||||
except ImportError:
|
||||
if not maybe_create:
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
|
||||
global _RAY_EXECUTOR
|
||||
|
||||
if (_RAY_EXECUTOR is None) or (not ray.is_initialized()):
|
||||
if not maybe_create:
|
||||
return None
|
||||
_RAY_EXECUTOR = RayExecutor(num_cpus=n_workers)
|
||||
|
||||
if n_workers is not None:
|
||||
current_n_workers = get_n_workers(_RAY_EXECUTOR)
|
||||
if n_workers != current_n_workers:
|
||||
warnings.warn(
|
||||
"Found initialized ray (with {} workers which) doesn't match "
|
||||
"the requested {}... sticking with old number.".format(
|
||||
current_n_workers, n_workers
|
||||
)
|
||||
)
|
||||
|
||||
return _RAY_EXECUTOR
|
||||
1009
.venv/lib/python3.12/site-packages/qmatchatea/py_emulator.py
Normal file
1009
.venv/lib/python3.12/site-packages/qmatchatea/py_emulator.py
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,691 @@
|
||||
# This code is part of qtealeaves.
|
||||
#
|
||||
# This code is licensed under the Apache License, Version 2.0. You may
|
||||
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
||||
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
||||
#
|
||||
# Any modifications or derivative works of this code must retain this
|
||||
# copyright notice, and modified files need to carry a notice indicating
|
||||
# that they have been altered from the originals.
|
||||
|
||||
"""
|
||||
The module contains a the MPI version of the MPS simulator.
|
||||
|
||||
Code for the MPI simulations should be run as:
|
||||
|
||||
.. code-block::
|
||||
mpiexec -n 4 python my_mpi_script.py
|
||||
|
||||
where we used 4 processes as an example.
|
||||
"""
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
|
||||
from qtealeaves.convergence_parameters import TNConvergenceParameters
|
||||
from qtealeaves.tensors import TensorBackend
|
||||
from qtealeaves.tooling.mpisupport import MPI, TN_MPI_TYPES
|
||||
|
||||
from .mps_simulator import MPS
|
||||
|
||||
__all__ = ["MPIMPS"]
|
||||
|
||||
|
||||
def _mpi_array_dtype(array):
|
||||
"""Return the MPI dtype for numpy arrays and CPU tensor buffers."""
|
||||
dtype = array.dtype
|
||||
if hasattr(dtype, "str"):
|
||||
return TN_MPI_TYPES[dtype.str]
|
||||
|
||||
# qredtea torch singular values are raw torch.Tensor objects, not
|
||||
# QteaTorchTensor instances, so they do not expose dtype_mpi().
|
||||
import torch
|
||||
|
||||
return {
|
||||
torch.complex128: MPI.DOUBLE_COMPLEX,
|
||||
torch.complex64: MPI.COMPLEX,
|
||||
torch.float64: MPI.DOUBLE_PRECISION,
|
||||
torch.float32: MPI.REAL,
|
||||
torch.int64: MPI.INT,
|
||||
}[dtype]
|
||||
|
||||
|
||||
def _mpi_send_array(comm, array, to_):
|
||||
if hasattr(array, "resolve_conj"):
|
||||
array = array.resolve_conj().contiguous()
|
||||
comm.Send([array, _mpi_array_dtype(array)], to_)
|
||||
|
||||
|
||||
def _mpi_empty_like(array, shape):
|
||||
if hasattr(array, "resolve_conj"):
|
||||
import torch
|
||||
|
||||
return torch.empty(shape, dtype=array.dtype, device="cpu")
|
||||
return np.empty(shape, array.dtype)
|
||||
|
||||
|
||||
def _mpi_recv_array(comm, template, shape, from_):
|
||||
array = _mpi_empty_like(template, shape)
|
||||
comm.Recv([array, _mpi_array_dtype(array)], from_)
|
||||
if hasattr(template, "device") and hasattr(array, "to"):
|
||||
array = array.to(device=template.device)
|
||||
return array
|
||||
|
||||
|
||||
# pylint: disable-next=too-many-instance-attributes
|
||||
class MPIMPS(MPS):
|
||||
"""
|
||||
MPI version of the MPS emulator that divides the MPS between the different nodes
|
||||
|
||||
Parameters
|
||||
----------
|
||||
num_sites: int
|
||||
Number of sites
|
||||
convergence_parameters: :py:class:`TNConvergenceParameters`
|
||||
Class for handling convergence parameters. In particular, in the MPS simulator we are
|
||||
interested in:
|
||||
- the *maximum bond dimension* :math:`\\chi`;
|
||||
- the *cut ratio* :math:`\\epsilon` after which the singular
|
||||
values are neglected, i.e. if :math:`\\lamda_1` is the
|
||||
bigger singular values then after an SVD we neglect all the
|
||||
singular values such that :math:`\\frac{\\lambda_i}{\\lambda_1}\\leq\\epsilon`
|
||||
local_dim: int or list of ints, optional
|
||||
Local dimension of the degrees of freedom. Default to 2.
|
||||
If a list is given, then it must have length num_sites.
|
||||
initialize: str, optional
|
||||
The method for the initialization. Default to "vacuum"
|
||||
Available:
|
||||
- "vacuum", for the |000...0> state
|
||||
- "random", for a random state at given bond dimension
|
||||
tensor_backend : `None` or instance of :class:`TensorBackend`
|
||||
Default for `None` is :class:`QteaTensor` with np.complex128 on CPU.
|
||||
|
||||
"""
|
||||
|
||||
# pylint: disable-next=too-many-arguments
|
||||
def __init__(
|
||||
self,
|
||||
num_sites,
|
||||
convergence_parameters,
|
||||
local_dim=2,
|
||||
initialize="vacuum",
|
||||
tensor_backend=None,
|
||||
):
|
||||
if MPI is None:
|
||||
raise ImportError("No module mpi4py found in python environment")
|
||||
# MPI variables
|
||||
# pylint: disable-next=c-extension-no-member
|
||||
self.comm = MPI.COMM_WORLD
|
||||
self.size = self.comm.Get_size()
|
||||
self.rank = self.comm.Get_rank()
|
||||
self.tot_sites = num_sites
|
||||
|
||||
# Number of sites in the local MPS
|
||||
modulus = num_sites % self.size
|
||||
local_num_size = int(np.floor(num_sites // self.size))
|
||||
self.indexes = [0] + [
|
||||
local_num_size + 1 if ii < modulus else local_num_size
|
||||
for ii in range(self.size)
|
||||
]
|
||||
local_num_size = self.indexes[self.rank + 1]
|
||||
|
||||
# indexes takes into account which indexes are in each core
|
||||
self.indexes = np.cumsum(self.indexes)
|
||||
|
||||
# The par_map is a dicrionary where the index is the position of the
|
||||
# sites in the full chain, while the value the position on the
|
||||
# subchain in this process
|
||||
self.par_map = dict(
|
||||
zip(
|
||||
np.arange(
|
||||
self.indexes[self.rank], self.indexes[self.rank + 1], dtype=int
|
||||
),
|
||||
np.arange(local_num_size, dtype=int),
|
||||
)
|
||||
)
|
||||
|
||||
# Auxiliary site for the boundaries
|
||||
if self.rank < self.size - 1:
|
||||
local_num_size += 1
|
||||
|
||||
if not np.isscalar(local_dim):
|
||||
local_dim = local_dim[
|
||||
self.indexes[self.rank] : self.indexes[self.rank + 1]
|
||||
+ int(self.rank != (self.size - 1))
|
||||
]
|
||||
|
||||
super().__init__(
|
||||
local_num_size,
|
||||
convergence_parameters,
|
||||
local_dim=local_dim,
|
||||
initialize=initialize,
|
||||
tensor_backend=tensor_backend,
|
||||
)
|
||||
|
||||
# MPS initializetion not aware of device
|
||||
self.convert(self.tensor_backend.dtype, self.tensor_backend.memory_device)
|
||||
|
||||
@property
|
||||
def mpi_dtype(self):
|
||||
"""Return the MPI version of the MPS dtype (going via first tensor)"""
|
||||
return TN_MPI_TYPES[np.dtype(self[0].dtype).str]
|
||||
|
||||
def get_tensor_of_site(self, idx):
|
||||
"""Retrieve tensor of specifc site."""
|
||||
return self[self.par_map[idx]]
|
||||
|
||||
def apply_one_site_operator(self, op, pos):
|
||||
"""
|
||||
Applies a one operator `op` to the site `pos` of the MPIMPS.
|
||||
Instead of communicating the changes on the boundaries we
|
||||
perform an additional contraction.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
op: numpy array shape (local_dim, local_dim)
|
||||
Matrix representation of the quantum gate
|
||||
pos: int
|
||||
Position of the qubit where to apply `op`.
|
||||
"""
|
||||
# Apply the gate on the right MPS
|
||||
if pos in self.par_map:
|
||||
super().apply_one_site_operator(op, self.par_map[pos])
|
||||
|
||||
# For one-qubit gates it is more convenient to apply them both to
|
||||
# the real and auxiliary qubits if they are on the boundaries
|
||||
elif pos - 1 in self.par_map:
|
||||
super().apply_one_site_operator(op, self.num_sites - 1)
|
||||
|
||||
return None
|
||||
|
||||
# pylint: disable-next=too-many-arguments
|
||||
def apply_two_site_operator(self, op, pos, swap=False, svd=None, parallel=None):
|
||||
"""
|
||||
Applies a two-site operator `op` to the site `pos`, `pos+1` of the MPS.
|
||||
Then, perform the necessary communications between the interested
|
||||
process and the process
|
||||
|
||||
Parameters
|
||||
----------
|
||||
op: numpy array shape (local_dim, local_dim, local_dim, local_dim)
|
||||
Matrix representation of the quantum gate
|
||||
pos: int or list of ints
|
||||
Position of the qubit where to apply `op`. If a list is passed,
|
||||
the two sites should be adjacent. The first index is assumed to
|
||||
be the control, and the second the target. The swap argument is
|
||||
overwritten if a list is passed.
|
||||
swap: bool
|
||||
If True swaps the operator. This means that instead of the
|
||||
first contraction in the following we get the second.
|
||||
It is written is a list of pos is passed.
|
||||
svd : None
|
||||
Required for compatibility. Can be only True.
|
||||
parallel: None
|
||||
Required for compatibility. Can be only True
|
||||
|
||||
Returns
|
||||
-------
|
||||
singular_values_cutted: ndarray
|
||||
Array of singular values cutted, normalized to the biggest singular value
|
||||
|
||||
"""
|
||||
if not np.isscalar(pos) and len(pos) == 2:
|
||||
pos = min(pos[0], pos[1])
|
||||
elif not np.isscalar(pos):
|
||||
raise ValueError(
|
||||
f"pos should be only scalar or len 2 array-like, not len {len(pos)}"
|
||||
)
|
||||
|
||||
# Hardcoded but necessary for compatibility
|
||||
svd = True
|
||||
if parallel is None:
|
||||
parallel_env = os.environ.get("QTEALEAVES_MPIMPS_PARALLEL", "1").lower()
|
||||
parallel = parallel_env not in ("0", "false", "no", "off")
|
||||
|
||||
if pos in self.par_map:
|
||||
res = super().apply_two_site_operator(
|
||||
op, self.par_map[pos], swap, svd=svd, parallel=parallel
|
||||
)
|
||||
|
||||
# Send the information back to the auxiliary if it was the first site
|
||||
if self.par_map[pos] == 0 and self.rank > 0:
|
||||
self.mpi_send_tensor(self[0], to_=self.rank - 1)
|
||||
_mpi_send_array(self.comm, self.singvals[1], self.rank - 1)
|
||||
|
||||
# Send the information towards the next if it was the last site
|
||||
elif self.par_map[pos] == self.num_sites - 2 and self.rank < self.size - 1:
|
||||
self.mpi_send_tensor(self[self.num_sites - 1], to_=self.rank + 1)
|
||||
_mpi_send_array(
|
||||
self.comm, self.singvals[self.num_sites - 1], self.rank + 1
|
||||
)
|
||||
|
||||
else:
|
||||
res = []
|
||||
# Receive the information from the MPS on the right
|
||||
if pos == self.indexes[self.rank + 1] and self.rank < self.size - 1:
|
||||
tens = self.mpi_receive_tensor(from_=self.rank + 1)
|
||||
|
||||
self[self.num_sites - 1] = tens
|
||||
|
||||
singvals = _mpi_recv_array(
|
||||
self.comm,
|
||||
self.singvals[self.num_sites],
|
||||
tens.shape[2],
|
||||
self.rank + 1,
|
||||
)
|
||||
self._singvals[self.num_sites] = singvals
|
||||
|
||||
# Receive the information from the MPS from the left
|
||||
if pos == self.indexes[self.rank] - 1 and self.rank > 0:
|
||||
tens = self.mpi_receive_tensor(from_=self.rank - 1)
|
||||
self[0] = tens
|
||||
|
||||
singvals = _mpi_recv_array(
|
||||
self.comm,
|
||||
self.singvals[0],
|
||||
tens.shape[0],
|
||||
self.rank - 1,
|
||||
)
|
||||
self._singvals[0] = singvals
|
||||
|
||||
return res
|
||||
|
||||
def apply_projective_operator(self, site, selected_output=None, remove=False):
|
||||
"""
|
||||
Apply a projective operator to the site **site**, and give the measurement as output.
|
||||
You can also decide to select a given output for the measurement, if the probability is
|
||||
non-zero. Finally, you have the possibility of removing the site after the measurement.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
site: int
|
||||
Index of the site you want to measure
|
||||
selected_output: int, optional
|
||||
If provided, the selected state is measured. Throw an error if the probability of the
|
||||
state is 0
|
||||
remove: bool, optional
|
||||
If True, the measured index is traced away after the measurement. Default to False.
|
||||
|
||||
Returns
|
||||
-------
|
||||
meas_state: int | None
|
||||
Measured state or None if site not in this part of the MPI-MPS.
|
||||
state_prob : float | None
|
||||
Probability of measuring the output state or None if site not
|
||||
in this part of the MPI-MPS.
|
||||
"""
|
||||
self.reinstall_isometry_serial()
|
||||
if site in self.par_map:
|
||||
res = super().apply_projective_operator(
|
||||
self.par_map[site], selected_output, remove
|
||||
)
|
||||
else:
|
||||
res = (None, None)
|
||||
|
||||
# Move informations to further right
|
||||
self.reinstall_isometry_serial(left=False, from_site=site)
|
||||
# Move information to the left
|
||||
self.reinstall_isometry_serial()
|
||||
|
||||
return res
|
||||
|
||||
# pylint: disable-next=arguments-differ
|
||||
def reinstall_isometry_serial(self, left=False, from_site=None):
|
||||
"""
|
||||
Reinstall the isometry center on position 0 of the full MPS.
|
||||
|
||||
This step is serial because we have to serially pass the information
|
||||
along the MPS. It cannot be parallelized.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
left: bool, optional
|
||||
If True, reinstall the isometry to the left.
|
||||
If False, to the right. Defaulto to False
|
||||
from_site: int, optional
|
||||
The site from which the isometrization should start.
|
||||
By default None, i.e. the other end of the MPS chain.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if from_site is None:
|
||||
from_site = self.num_sites - 1 if left else 0
|
||||
extrem = np.nonzero(from_site <= self.indexes)[0][0]
|
||||
|
||||
if left:
|
||||
boundaries = (extrem, -1, -1)
|
||||
tidx = 0
|
||||
to_ = self.rank - 1
|
||||
from_ = self.rank + 1
|
||||
else:
|
||||
boundaries = (extrem, self.size, 1)
|
||||
tidx = self.num_sites - 1
|
||||
to_ = self.rank + 1
|
||||
from_ = self.rank - 1
|
||||
|
||||
for ii in range(*boundaries):
|
||||
if self.rank == ii:
|
||||
self._first_non_orthogonal_left = self.num_sites - 1
|
||||
self._first_non_orthogonal_right = self.num_sites - 1
|
||||
requires_singvals = self._requires_singvals
|
||||
self._requires_singvals = True
|
||||
if left:
|
||||
self.right_canonize(0, False, True)
|
||||
else:
|
||||
self.left_canonize(self.num_sites - 1, False, True)
|
||||
self._requires_singvals = requires_singvals
|
||||
|
||||
# Send tensor
|
||||
if (self.rank > 0 and left) or (self.rank + 1 < self.size and not left):
|
||||
self.mpi_send_tensor(self[tidx], to_=to_)
|
||||
|
||||
elif (self.rank == ii - 1 and left) or (self.rank == ii + 1 and not left):
|
||||
# Receive tensor
|
||||
tens = self.mpi_receive_tensor(from_=from_)
|
||||
self[self.num_sites - 1 - tidx] = tens
|
||||
|
||||
# pylint: disable-next=arguments-differ
|
||||
def reinstall_isometry_parallel(self, num_cycles):
|
||||
"""
|
||||
Reinstall the isometry by applying identities to all even sites and
|
||||
to all odd sites, and repeating for `num_cycles` cycles.
|
||||
The reinstallation is exact for `num_cycles=num_sites/2`.
|
||||
Method from https://arxiv.org/abs/2312.02667
|
||||
|
||||
This step is serial because we have to serially pass the information
|
||||
along the MPS. It cannot be parallelized.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
num_cycles: int
|
||||
Number of cycles for reinstalling the isometry
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
for _ in range(num_cycles):
|
||||
# Apply on all even sites
|
||||
for ii in range(0, self.tot_sites - 1, 2):
|
||||
self.apply_two_site_operator(
|
||||
self[0].eye_like(4), ii, svd=True, parallel=True
|
||||
)
|
||||
# Apply on all odd sites
|
||||
for ii in range(1, self.tot_sites - 1, 2):
|
||||
self.apply_two_site_operator(
|
||||
self[0].eye_like(4), ii, svd=True, parallel=True
|
||||
)
|
||||
|
||||
def mpi_gather_tn(self):
|
||||
"""
|
||||
Gather the tensors on process 0.
|
||||
We do not use MPI.comm.Gather because we would gather lists of np.arrays
|
||||
without using the np.array advantages, making it slower than the single
|
||||
communications.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list on np.ndarray or None
|
||||
List of tensors on the rank 0 process, None on the others
|
||||
"""
|
||||
self.comm.Barrier()
|
||||
if self.rank != 0:
|
||||
num_tensors = (
|
||||
self.num_sites if self.rank == self.size - 1 else self.num_sites - 1
|
||||
)
|
||||
for jj in range(num_tensors):
|
||||
self.mpi_send_tensor(self[jj], to_=0)
|
||||
tensor_list = None
|
||||
else:
|
||||
tensor_list = [None for _ in range(self.tot_sites)]
|
||||
tensor_list[: self.num_sites - 1] = self.tensors[:-1]
|
||||
|
||||
tidx = self.num_sites - 1
|
||||
for ii in range(1, self.size):
|
||||
num_tensors = self.indexes[ii + 1] - self.indexes[ii]
|
||||
for jj in range(num_tensors):
|
||||
tens = self.mpi_receive_tensor(from_=ii)
|
||||
tensor_list[tidx + jj] = tens
|
||||
tidx += num_tensors
|
||||
|
||||
self.comm.Barrier()
|
||||
|
||||
return tensor_list
|
||||
|
||||
def mpi_scatter_tn(self, tensor_list):
|
||||
"""
|
||||
Scatter the tensors on process 0.
|
||||
We do not use MPI.comm.Scatter because we would gather lists of np.arrays
|
||||
without using the np.array advantages, making it slower than the single
|
||||
communications.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tensor_list : list of lists of np.ndarrays
|
||||
The index i of the list is sent to the rank i
|
||||
|
||||
Returns
|
||||
-------
|
||||
list on np.ndarray or None
|
||||
List of tensors on the rank 0 process, None on the others
|
||||
"""
|
||||
self.comm.Barrier()
|
||||
if self.rank == 0:
|
||||
for ridx, sub_tensorlist in enumerate(tensor_list[1:]):
|
||||
for idx, tens in enumerate(sub_tensorlist):
|
||||
self.mpi_send_tensor(tens, to_=ridx + 1)
|
||||
|
||||
tensor_list = tensor_list[0]
|
||||
else:
|
||||
num_tensors = len(tensor_list[self.rank])
|
||||
tensor_list = [None for _ in range(num_tensors)]
|
||||
for idx in range(num_tensors):
|
||||
tens = self.mpi_receive_tensor(from_=0)
|
||||
tensor_list[idx] = tens
|
||||
|
||||
self.comm.Barrier()
|
||||
|
||||
return tensor_list
|
||||
|
||||
def to_tensor_list(self):
|
||||
"""
|
||||
Return the tensor list of the full MPS. Thus, here there are
|
||||
communications between the different processes and all the tensorlist
|
||||
is returned on process 0
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of np.ndarray or None
|
||||
List of tensors on the rank 0 process, None on the others
|
||||
"""
|
||||
return self.mpi_gather_tn()
|
||||
|
||||
def to_statevector(self, qiskit_order=False, max_qubit_equivalent=20):
|
||||
"""
|
||||
Serially compute the statevector
|
||||
|
||||
Parameters
|
||||
----------
|
||||
qiskit_order: bool, optional
|
||||
weather to use qiskit ordering or the theoretical one. For
|
||||
example the state |011> has 0 in the first position for the
|
||||
theoretical ordering, while for qiskit ordering it is on the
|
||||
last position.
|
||||
max_qubit_equivalent: int, optional
|
||||
Maximum number of qubit sites the MPS can have and still be
|
||||
transformed into a statevector.
|
||||
If the number of sites is greater, it will throw an exception.
|
||||
Default to 20.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.ndarray or None
|
||||
Statevector on process 0, None on the others
|
||||
"""
|
||||
|
||||
tensorlist = self.to_tensor_list()
|
||||
if self.rank == 0:
|
||||
mps = MPS.from_tensor_list(tensorlist)
|
||||
statevect = mps.to_statevector(qiskit_order, max_qubit_equivalent)
|
||||
else:
|
||||
statevect = None
|
||||
|
||||
return statevect
|
||||
|
||||
@classmethod
|
||||
def from_tensor_list(
|
||||
cls,
|
||||
tensor_list,
|
||||
conv_params=None,
|
||||
tensor_backend=None,
|
||||
target_device=None,
|
||||
):
|
||||
"""
|
||||
Initialize the MPS tensors using a list of correctly shaped tensors
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tensor_list : list of ndarrays or cupy arrays
|
||||
List of tensor for initializing the MPS
|
||||
conv_params : :py:class:`TNConvergenceParameters`, optional
|
||||
Convergence parameters for the new MPS. If None, the maximum bond
|
||||
bond dimension possible is assumed, and a cut_ratio=1e-9.
|
||||
Default to None.
|
||||
tensor_backend : `None` or instance of :class:`TensorBackend`
|
||||
Default for `None` is :class:`QteaTensor` with np.complex128 on CPU.
|
||||
target_device: None | str, optional
|
||||
If `None`, take memory device of tensor backend.
|
||||
If string is `any`, do not convert. Otherwise,
|
||||
use string as device string.
|
||||
|
||||
Returns
|
||||
-------
|
||||
obj : :py:class:`MPIMPS`
|
||||
The MPIMPS class
|
||||
"""
|
||||
mismatches = [
|
||||
tensor_list[ii].shape[2] != tensor_list[ii + 1].shape[0]
|
||||
for ii in range(len(tensor_list) - 1)
|
||||
]
|
||||
if any(mismatches):
|
||||
msg = f"Mismatches for tensors equals to True: {mismatches}."
|
||||
raise ValueError(f"Dimension mismatch when constructing MPS:{msg}")
|
||||
|
||||
if conv_params is None:
|
||||
max_bond_dim = max(elem.shape[2] for elem in tensor_list)
|
||||
conv_params = TNConvergenceParameters(max_bond_dimension=int(max_bond_dim))
|
||||
if tensor_backend is None:
|
||||
# Have to resolve it here in case target device is not given
|
||||
tensor_backend = TensorBackend()
|
||||
if target_device is None:
|
||||
target_device = tensor_backend.memory_device
|
||||
elif target_device == "any":
|
||||
target_device = None
|
||||
|
||||
local_dim = [elem.shape[1] for elem in tensor_list]
|
||||
obj = cls(
|
||||
len(tensor_list), conv_params, local_dim, tensor_backend=tensor_backend
|
||||
)
|
||||
|
||||
# Convert data type (lateron device if GPU enabled?)
|
||||
for elem in tensor_list:
|
||||
elem.convert(obj.tensor_backend.dtype, target_device)
|
||||
|
||||
if obj.rank == 0:
|
||||
tensorlist = [
|
||||
tensor_list[
|
||||
obj.indexes[rank] : obj.indexes[rank + 1]
|
||||
+ int(rank != obj.size - 1)
|
||||
]
|
||||
for rank in range(obj.size)
|
||||
]
|
||||
else:
|
||||
list_sizes = obj.indexes[1:] - obj.indexes[:-1] + 1
|
||||
list_sizes[-1] -= 1
|
||||
tensorlist = [
|
||||
[None for _ in range(list_sizes[rank])] for rank in range(obj.size)
|
||||
]
|
||||
|
||||
tensor_list = obj.mpi_scatter_tn(tensorlist)
|
||||
obj._tensors = tensor_list
|
||||
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def from_statevector(
|
||||
cls,
|
||||
statevector,
|
||||
local_dim=2,
|
||||
conv_params=None,
|
||||
tensor_backend=None,
|
||||
):
|
||||
"""Serially decompose the statevector and then initialize the MPS"""
|
||||
mps = MPS.from_statevector(
|
||||
statevector, local_dim, conv_params, tensor_backend=tensor_backend
|
||||
)
|
||||
|
||||
return cls.from_tensor_list(
|
||||
mps.to_tensor_list(), conv_params, tensor_backend=tensor_backend
|
||||
)
|
||||
|
||||
# ---------------------------
|
||||
# ----- MEASURE METHODS -----
|
||||
# ---------------------------
|
||||
|
||||
def meas_local(self, op_list):
|
||||
"""
|
||||
Measure a local observable along all sites of the MPS
|
||||
|
||||
Parameters
|
||||
----------
|
||||
op_list : list of :class:`_AbstractQteaTensor`
|
||||
local operator to measure on each site
|
||||
|
||||
Return
|
||||
------
|
||||
measures : ndarray, shape (num_sites)
|
||||
Measures of the local operator along each site on rank-0
|
||||
"""
|
||||
res = super().meas_local(op_list)
|
||||
|
||||
# Call back on the site 0 the results
|
||||
if self.rank != 0:
|
||||
self.comm.Send([res, self.mpi_dtype[res.dtype.str]], 0)
|
||||
tot_res = None
|
||||
else:
|
||||
tot_res = np.empty(self.tot_sites, dtype=res.dtype)
|
||||
tot_res[: self.num_sites - 1] = res[:-1]
|
||||
|
||||
tidx = self.num_sites - 1
|
||||
for ii in range(1, self.size):
|
||||
num_tensors = self.indexes[ii] - self.indexes[ii - 1]
|
||||
self.comm.Recv(
|
||||
[tot_res[tidx : tidx + num_tensors], self.mpi_dtype[res.dtype.str]],
|
||||
ii,
|
||||
)
|
||||
tidx += num_tensors
|
||||
|
||||
return tot_res
|
||||
|
||||
def _get_eff_op_on_pos(self, pos):
|
||||
"""
|
||||
Obtain the list of effective operators adjacent
|
||||
to the position pos and the index where they should
|
||||
be contracted
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pos : int
|
||||
Index of the tensor w.r.t. which we have to retrieve
|
||||
the effective operators
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of IndexedOperators
|
||||
List of effective operators
|
||||
list of ints
|
||||
Indexes where the operators should be contracted
|
||||
"""
|
||||
raise NotImplementedError("This function has to be overwritten")
|
||||
5061
.venv/lib/python3.12/site-packages/quimb/tensor/tn1d/core.py
Normal file
5061
.venv/lib/python3.12/site-packages/quimb/tensor/tn1d/core.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,29 @@
|
||||
import importlib.metadata as im
|
||||
|
||||
from qibotn.backends import MetaBackend
|
||||
|
||||
__version__ = im.version(__package__)
|
||||
|
||||
_LAZY_EXPORTS = {
|
||||
"MetaBackend": ("qibotn.backends", "MetaBackend"),
|
||||
"cpu_backend": ("qibotn.expectation_runner", "cpu_backend"),
|
||||
"cpu_expectation": ("qibotn.expectation_runner", "cpu_expectation"),
|
||||
"mps_expectation": ("qibotn.expectation_runner", "mps_expectation"),
|
||||
"cpu_runcard": ("qibotn.expectation_runner", "cpu_runcard"),
|
||||
"pauli_pattern": ("qibotn.observables", "pauli_pattern"),
|
||||
"pauli_sum": ("qibotn.observables", "pauli_sum"),
|
||||
}
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
try:
|
||||
module_name, object_name = _LAZY_EXPORTS[name]
|
||||
except KeyError:
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
|
||||
|
||||
from importlib import import_module
|
||||
|
||||
value = getattr(import_module(module_name), object_name)
|
||||
globals()[name] = value
|
||||
return value
|
||||
|
||||
|
||||
__all__ = sorted([*_LAZY_EXPORTS, "__version__"])
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
from typing import Union
|
||||
|
||||
from qibo.config import raise_error
|
||||
|
||||
from qibotn.backends.abstract import QibotnBackend
|
||||
from qibotn.backends.cpu import CpuTensorNet
|
||||
from qibotn.backends.cutensornet import CuTensorNet # pylint: disable=E0401
|
||||
|
||||
PLATFORMS = ("cutensornet", "cpu", "quimb", "qmatchatea", "vidal")
|
||||
|
||||
@@ -24,8 +20,12 @@ class MetaBackend:
|
||||
"""
|
||||
|
||||
if platform == "cutensornet": # pragma: no cover
|
||||
from qibotn.backends.cutensornet import CuTensorNet
|
||||
|
||||
return CuTensorNet(runcard)
|
||||
elif platform == "cpu":
|
||||
from qibotn.backends.cpu import CpuTensorNet
|
||||
|
||||
return CpuTensorNet(runcard)
|
||||
elif platform == "quimb": # pragma: no cover
|
||||
import qibotn.backends.quimb as qmb
|
||||
@@ -55,8 +55,8 @@ class MetaBackend:
|
||||
for platform in PLATFORMS:
|
||||
try:
|
||||
MetaBackend.load(platform=platform)
|
||||
available = True
|
||||
except:
|
||||
available = False
|
||||
available_backends[platform] = available
|
||||
except (ImportError, NotImplementedError, TypeError, ValueError):
|
||||
available_backends[platform] = False
|
||||
else:
|
||||
available_backends[platform] = True
|
||||
return available_backends
|
||||
|
||||
@@ -15,14 +15,9 @@ from qibo.config import raise_error
|
||||
from qibotn.backends.abstract import QibotnBackend
|
||||
from qibotn.backends.vidal import (
|
||||
_observable_mpo_tensors,
|
||||
_operator_terms_to_mpo,
|
||||
_symbolic_hamiltonian_to_operator_terms,
|
||||
_unsupported_reason,
|
||||
)
|
||||
from qibotn.backends.vidal_mpi_segment import SegmentVidalMPIExecutor
|
||||
from qibotn.backends.vidal_tebd import VidalTEBDExecutor
|
||||
from qibotn.observables import check_observable
|
||||
from qibotn.result import TensorNetworkResult
|
||||
|
||||
|
||||
def _as_bool_or_dict(value, name):
|
||||
@@ -282,79 +277,35 @@ class CpuTensorNet(QibotnBackend, NumpyBackend):
|
||||
):
|
||||
if compile_circuit is None:
|
||||
compile_circuit = self.compile_circuit
|
||||
if preprocess:
|
||||
if self.MPI_enabled:
|
||||
from mpi4py import MPI
|
||||
|
||||
self.rank = MPI.COMM_WORLD.Get_rank()
|
||||
|
||||
from qibotn.backends.vidal import VidalBackend
|
||||
|
||||
backend = VidalBackend()
|
||||
backend.configure_tn_simulation(
|
||||
max_bond_dimension=self.max_bond_dimension,
|
||||
cut_ratio=self.cut_ratio,
|
||||
tensor_module=self.tensor_module,
|
||||
compile_circuit=compile_circuit,
|
||||
mpi_approach="CT" if self.MPI_enabled else "SR",
|
||||
mpi_term_batch_size=self.mpi_term_batch_size,
|
||||
fallback=False,
|
||||
)
|
||||
value = backend.expectation(
|
||||
circuit,
|
||||
observable,
|
||||
preprocess=True,
|
||||
compile_circuit=compile_circuit,
|
||||
)
|
||||
self.rank = getattr(backend, "rank", self.rank)
|
||||
self.last_truncation_error = getattr(
|
||||
backend, "last_truncation_error", np.nan
|
||||
)
|
||||
self.last_max_truncation_error = getattr(
|
||||
backend, "last_max_truncation_error", np.nan
|
||||
)
|
||||
return value
|
||||
|
||||
mpo_tensors = _observable_mpo_tensors(observable, circuit.nqubits)
|
||||
if self.MPI_enabled:
|
||||
from mpi4py import MPI
|
||||
|
||||
comm = MPI.COMM_WORLD
|
||||
self.rank = comm.Get_rank()
|
||||
executor = SegmentVidalMPIExecutor(
|
||||
nqubits=circuit.nqubits,
|
||||
max_bond=self.max_bond_dimension,
|
||||
cut_ratio=self.cut_ratio,
|
||||
tensor_module=self.tensor_module,
|
||||
comm=comm,
|
||||
)
|
||||
executor.run_circuit(circuit)
|
||||
self.last_truncation_error = float(executor.global_truncation_error())
|
||||
self.last_max_truncation_error = float(
|
||||
executor.global_max_truncation_error()
|
||||
)
|
||||
if mpo_tensors is not None:
|
||||
value = executor.expectation_mpo_root(mpo_tensors)
|
||||
else:
|
||||
terms = _symbolic_hamiltonian_to_operator_terms(observable)
|
||||
value = executor.expectation_mpo_root(
|
||||
_operator_terms_to_mpo(terms, circuit.nqubits)
|
||||
)
|
||||
return np.nan if self.rank != 0 else value
|
||||
self.rank = MPI.COMM_WORLD.Get_rank()
|
||||
|
||||
executor = VidalTEBDExecutor(
|
||||
nqubits=circuit.nqubits,
|
||||
max_bond=self.max_bond_dimension,
|
||||
from qibotn.backends.vidal import VidalBackend
|
||||
|
||||
backend = VidalBackend()
|
||||
backend.configure_tn_simulation(
|
||||
max_bond_dimension=self.max_bond_dimension,
|
||||
cut_ratio=self.cut_ratio,
|
||||
tensor_module=self.tensor_module,
|
||||
compile_circuit=compile_circuit,
|
||||
mpi_approach="CT" if self.MPI_enabled else "SR",
|
||||
mpi_term_batch_size=self.mpi_term_batch_size,
|
||||
fallback=False,
|
||||
)
|
||||
executor.run_circuit(circuit)
|
||||
self.last_truncation_error = float(executor.truncation_error)
|
||||
self.last_max_truncation_error = float(executor.max_truncation_error)
|
||||
if mpo_tensors is not None:
|
||||
return executor.expectation_mpo(mpo_tensors)
|
||||
terms = _symbolic_hamiltonian_to_operator_terms(observable)
|
||||
return executor.expectation_mpo(_operator_terms_to_mpo(terms, circuit.nqubits))
|
||||
value = backend.expectation(
|
||||
circuit,
|
||||
observable,
|
||||
preprocess=preprocess,
|
||||
compile_circuit=compile_circuit,
|
||||
)
|
||||
self.rank = getattr(backend, "rank", self.rank)
|
||||
self.last_truncation_error = getattr(backend, "last_truncation_error", np.nan)
|
||||
self.last_max_truncation_error = getattr(
|
||||
backend, "last_max_truncation_error", np.nan
|
||||
)
|
||||
return value
|
||||
|
||||
def _quimb_backend(self):
|
||||
import qibotn.backends.quimb as qmb
|
||||
|
||||
@@ -12,6 +12,50 @@ from qibotn.benchmark_cases import exact_pauli_sum
|
||||
from qibotn.observables import check_observable
|
||||
|
||||
|
||||
def cpu_runcard(
|
||||
observable=None,
|
||||
*,
|
||||
ansatz: str = "tn",
|
||||
mpi: bool = False,
|
||||
bond: int | None = 1024,
|
||||
cut_ratio: float | None = 1e-12,
|
||||
tensor_module: str = "torch",
|
||||
quimb_backend: str = "torch",
|
||||
dtype: str = "complex128",
|
||||
torch_threads: int | None = 8,
|
||||
parallel_opts: dict | None = None,
|
||||
compile_circuit: bool = False,
|
||||
preprocess: bool = False,
|
||||
):
|
||||
"""Build the small CPU backend runcard used throughout qibotn."""
|
||||
return {
|
||||
"MPI_enabled": mpi,
|
||||
"MPS_enabled": ansatz.lower() == "mps",
|
||||
"NCCL_enabled": False,
|
||||
"expectation_enabled": observable if observable is not None else False,
|
||||
"max_bond_dimension": bond,
|
||||
"cut_ratio": cut_ratio,
|
||||
"tensor_module": tensor_module,
|
||||
"quimb_backend": quimb_backend,
|
||||
"dtype": dtype,
|
||||
"torch_threads": torch_threads,
|
||||
"parallel_opts": parallel_opts or {},
|
||||
"compile_circuit": compile_circuit,
|
||||
"preprocess": preprocess,
|
||||
}
|
||||
|
||||
|
||||
def cpu_backend(**kwargs):
|
||||
"""Return a configured qibotn CPU backend.
|
||||
|
||||
Example:
|
||||
``backend = cpu_backend(ansatz="mps", bond=512, torch_threads=8)``
|
||||
"""
|
||||
from qibotn.backends.cpu import CpuTensorNet
|
||||
|
||||
return CpuTensorNet(cpu_runcard(**kwargs))
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExpectationConfig:
|
||||
ansatz: str = "tn"
|
||||
@@ -33,6 +77,15 @@ class ExpectationResult:
|
||||
parallel_stats: list | None = None
|
||||
|
||||
|
||||
def _config_from_kwargs(**kwargs):
|
||||
fields = ExpectationConfig.__dataclass_fields__
|
||||
config_kwargs = {name: kwargs.pop(name) for name in list(kwargs) if name in fields}
|
||||
if kwargs:
|
||||
unknown = ", ".join(sorted(kwargs))
|
||||
raise TypeError(f"Unknown expectation option(s): {unknown}")
|
||||
return ExpectationConfig(**config_kwargs)
|
||||
|
||||
|
||||
def exact_for_observable(circuit, observable, nqubits):
|
||||
if isinstance(observable, dict) and "terms" in observable:
|
||||
terms = [
|
||||
@@ -49,19 +102,18 @@ def exact_for_observable(circuit, observable, nqubits):
|
||||
|
||||
|
||||
def run_cpu_expectation(circuit, observable, config):
|
||||
runcard = {
|
||||
"MPI_enabled": config.mpi,
|
||||
"MPS_enabled": config.ansatz.lower() == "mps",
|
||||
"NCCL_enabled": False,
|
||||
"expectation_enabled": observable,
|
||||
"max_bond_dimension": config.bond,
|
||||
"cut_ratio": config.cut_ratio,
|
||||
"tensor_module": config.tensor_module,
|
||||
"quimb_backend": config.quimb_backend,
|
||||
"dtype": config.dtype,
|
||||
"torch_threads": config.torch_threads,
|
||||
"parallel_opts": config.parallel_opts or {},
|
||||
}
|
||||
runcard = cpu_runcard(
|
||||
observable,
|
||||
ansatz=config.ansatz,
|
||||
mpi=config.mpi,
|
||||
bond=config.bond,
|
||||
cut_ratio=config.cut_ratio,
|
||||
tensor_module=config.tensor_module,
|
||||
quimb_backend=config.quimb_backend,
|
||||
dtype=config.dtype,
|
||||
torch_threads=config.torch_threads,
|
||||
parallel_opts=config.parallel_opts,
|
||||
)
|
||||
backend = construct_backend(
|
||||
backend="qibotn",
|
||||
platform="cpu",
|
||||
@@ -80,3 +132,26 @@ def run_cpu_expectation(circuit, observable, config):
|
||||
rank=rank,
|
||||
parallel_stats=list(stats) if stats is not None else None,
|
||||
)
|
||||
|
||||
|
||||
def cpu_expectation(circuit, observable=None, *, return_result=False, **kwargs):
|
||||
"""Compute a CPU TN/MPS expectation with concise keyword options.
|
||||
|
||||
This is the preferred API for small scripts. Common options are
|
||||
``ansatz="tn" | "mps"``, ``bond``, ``cut_ratio``, ``mpi``,
|
||||
``torch_threads``, ``quimb_backend`` and ``parallel_opts``.
|
||||
"""
|
||||
config = _config_from_kwargs(**kwargs)
|
||||
result = run_cpu_expectation(circuit, observable, config)
|
||||
return result if return_result else result.value
|
||||
|
||||
|
||||
def mps_expectation(circuit, observable=None, *, return_result=False, **kwargs):
|
||||
"""Compute expectation using the CPU Vidal/MPS path when possible."""
|
||||
kwargs.setdefault("ansatz", "mps")
|
||||
return cpu_expectation(
|
||||
circuit,
|
||||
observable,
|
||||
return_result=return_result,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -4,6 +4,30 @@ from qibo import hamiltonians
|
||||
from qibo.symbols import I, X, Y, Z
|
||||
|
||||
|
||||
def pauli_pattern(pattern):
|
||||
"""Return the compact qibotn representation of a repeated Pauli string."""
|
||||
return {"pauli_string_pattern": pattern}
|
||||
|
||||
|
||||
def pauli_sum(*terms):
|
||||
"""Return the compact qibotn representation of a Pauli sum.
|
||||
|
||||
Each term is ``(coefficient, operators)`` where operators are pairs like
|
||||
``("X", 0)``. Example:
|
||||
|
||||
``pauli_sum((0.5, [("X", 0), ("Z", 1)]), (-1.0, [("Z", 3)]))``
|
||||
"""
|
||||
return {
|
||||
"terms": [
|
||||
{
|
||||
"coefficient": coeff,
|
||||
"operators": [(name, int(site)) for name, site in operators],
|
||||
}
|
||||
for coeff, operators in terms
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def check_observable(observable, circuit_nqubit):
|
||||
"""Checks the type of observable and returns the appropriate Hamiltonian."""
|
||||
if observable is None:
|
||||
@@ -20,11 +44,10 @@ def check_observable(observable, circuit_nqubit):
|
||||
|
||||
def build_observable(circuit_nqubit):
|
||||
"""Construct the default benchmark observable used by qibotn."""
|
||||
hamiltonian_form = 0
|
||||
for i in range(circuit_nqubit):
|
||||
hamiltonian_form += 0.5 * X(i % circuit_nqubit) * Z((i + 1) % circuit_nqubit)
|
||||
|
||||
return hamiltonians.SymbolicHamiltonian(form=hamiltonian_form)
|
||||
form = sum(
|
||||
0.5 * X(i) * Z((i + 1) % circuit_nqubit) for i in range(circuit_nqubit)
|
||||
)
|
||||
return hamiltonians.SymbolicHamiltonian(form=form)
|
||||
|
||||
|
||||
def create_hamiltonian_from_dict(data, circuit_nqubit):
|
||||
@@ -50,7 +73,6 @@ def create_hamiltonian_from_dict(data, circuit_nqubit):
|
||||
term_expr = full_term_expr[0]
|
||||
for op in full_term_expr[1:]:
|
||||
term_expr *= op
|
||||
|
||||
terms.append(coeff * term_expr)
|
||||
|
||||
if not terms:
|
||||
@@ -84,23 +106,20 @@ def create_hamiltonian_from_pauli_pattern(pattern, circuit_nqubit):
|
||||
continue
|
||||
factor = pauli_gates[name](qubit)
|
||||
expr = factor if expr is None else expr * factor
|
||||
|
||||
if expr is None:
|
||||
expr = I(0)
|
||||
|
||||
return hamiltonians.SymbolicHamiltonian(form=expr)
|
||||
return hamiltonians.SymbolicHamiltonian(form=expr or I(0))
|
||||
|
||||
|
||||
def build_random_circuit(nqubits, nlayers, seed=42):
|
||||
"""Build a random circuit with RY+RZ+CNOT layers for benchmarks."""
|
||||
import numpy as np
|
||||
from qibo import Circuit, gates
|
||||
np.random.seed(seed)
|
||||
|
||||
rng = np.random.default_rng(seed)
|
||||
c = Circuit(nqubits)
|
||||
for _ in range(nlayers):
|
||||
for q in range(nqubits):
|
||||
c.add(gates.RY(q, theta=np.random.uniform(0, 2*np.pi)))
|
||||
c.add(gates.RZ(q, theta=np.random.uniform(0, 2*np.pi)))
|
||||
c.add(gates.RY(q, theta=rng.uniform(0, 2 * np.pi)))
|
||||
c.add(gates.RZ(q, theta=rng.uniform(0, 2 * np.pi)))
|
||||
for q in range(nqubits):
|
||||
c.add(gates.CNOT(q % nqubits, (q + 1) % nqubits))
|
||||
return c
|
||||
|
||||
@@ -32,20 +32,19 @@ class TensorNetworkResult:
|
||||
statevector: ndarray
|
||||
|
||||
def __post_init__(self):
|
||||
# TODO: define the general convention when using backends different from qmatchatea
|
||||
if self.measured_probabilities is None:
|
||||
self.measured_probabilities = {"default": self.measured_probabilities}
|
||||
self.measured_probabilities = {}
|
||||
|
||||
def probabilities(self):
|
||||
"""Return calculated probabilities according to the given method."""
|
||||
if self.prob_type == "U":
|
||||
measured_probabilities = deepcopy(self.measured_probabilities)
|
||||
for bitstring, prob in self.measured_probabilities[self.prob_type].items():
|
||||
measured_probabilities[self.prob_type][bitstring] = prob[1] - prob[0]
|
||||
probabilities = measured_probabilities[self.prob_type]
|
||||
else:
|
||||
probabilities = self.measured_probabilities
|
||||
return probabilities
|
||||
if self.prob_type != "U":
|
||||
return self.measured_probabilities
|
||||
|
||||
measured_probabilities = deepcopy(self.measured_probabilities)
|
||||
values = measured_probabilities.get(self.prob_type, {})
|
||||
for bitstring, prob in values.items():
|
||||
values[bitstring] = prob[1] - prob[0]
|
||||
return values
|
||||
|
||||
def frequencies(self):
|
||||
"""Return frequencies if a certain number of shots has been set."""
|
||||
|
||||
@@ -9,6 +9,7 @@ from qibotn.benchmark_cases import (
|
||||
build_circuit as build_benchmark_circuit,
|
||||
exact_pauli_sum,
|
||||
)
|
||||
from qibotn import cpu_expectation, mps_expectation, pauli_pattern, pauli_sum
|
||||
|
||||
|
||||
def build_circuit(nqubits=6):
|
||||
@@ -46,6 +47,37 @@ def test_cpu_generic_tn_expectation_matches_statevector():
|
||||
assert math.isclose(value, exact, abs_tol=1e-12)
|
||||
|
||||
|
||||
def test_public_cpu_expectation_api_matches_statevector():
|
||||
circuit = build_circuit()
|
||||
observable = pauli_sum((0.5, [("X", 0), ("Z", 1)]), (-0.25, [("Z", 5)]))
|
||||
exact = exact_pauli_sum(
|
||||
circuit,
|
||||
[(0.5, (("X", 0), ("Z", 1))), (-0.25, (("Z", 5),))],
|
||||
circuit.nqubits,
|
||||
)
|
||||
|
||||
value = cpu_expectation(circuit, observable, torch_threads=1)
|
||||
|
||||
assert math.isclose(value, exact, abs_tol=1e-12)
|
||||
|
||||
|
||||
def test_public_mps_expectation_api_accepts_pauli_pattern():
|
||||
circuit = build_circuit()
|
||||
exact_hamiltonian = hamiltonians.SymbolicHamiltonian(
|
||||
form=X(1) * Z(2) * X(4) * Z(5)
|
||||
)
|
||||
exact = exact_hamiltonian.expectation_from_state(circuit().state(numpy=True))
|
||||
|
||||
value = mps_expectation(
|
||||
circuit,
|
||||
pauli_pattern("IXZ"),
|
||||
bond=64,
|
||||
torch_threads=1,
|
||||
)
|
||||
|
||||
assert math.isclose(value, exact, abs_tol=1e-12)
|
||||
|
||||
|
||||
def test_cpu_mps_expectation_matches_statevector():
|
||||
circuit = build_circuit()
|
||||
observable = build_observable(circuit.nqubits)
|
||||
|
||||
Reference in New Issue
Block a user