Merge pull request #86 from qiboteam/matcha

implementing the Quantum Matcha Tea backend
This commit is contained in:
Matteo Robbiati
2025-02-12 13:27:24 +01:00
committed by GitHub
21 changed files with 2405 additions and 278 deletions

View File

@@ -18,12 +18,6 @@ repos:
hooks:
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/PyCQA/docformatter
rev: v1.7.5
hooks:
- id: docformatter
additional_dependencies: [tomli]
args: [--in-place, --config, ./pyproject.toml]
- repo: https://github.com/asottile/pyupgrade
rev: v3.19.1
hooks:
@@ -32,17 +26,4 @@ repos:
rev: v2.5.0
hooks:
- id: pycln
args:
- --config=pyproject.toml
- --all
- repo: https://github.com/adamchainz/blacken-docs
rev: 1.19.1
hooks:
- id: blacken-docs
- repo: https://github.com/pycqa/pydocstyle
rev: 6.3.0
hooks:
- id: pydocstyle
args:
- --select=D103,D200,D206,D300,D301
files: ^src/
args: [--config=pyproject.toml]

BIN
doc/source/QiboTN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

View File

@@ -1,6 +1,7 @@
Getting started
===============
In this section we present the basic aspects of the Qibotn design and provide installation instructions.
Please visit the following sections to understand how ``qibotn`` works.

View File

@@ -1,12 +1,18 @@
Quick start
===========
In this section, we provide an example of two qubit ciruit simulation using qibotn package in Qibo simulator. First, the backend is to be set with appropriate run card settings, followed by the circuit simulation using Qibo documentation.
In this section, we provide examples on how to use Qibotn to execute tensor network
simulation of quantum circuit. First, we show how to use the Cutensornet and Quimb
backends, while in a second moment we show a complete example of usage of the Quantum
Matcha Tea Backend.
Setting the backend
"""""""""""""""""""
Setting the backend with Cutensornet and Quimb
""""""""""""""""""""""""""""""""""""""""""""""
QiboTN offers two backends: cutensornet (using cuQuantum library) and qutensornet (using Quimb library) for tensor network based simulations. At present, cutensornet backend works only for GPUs whereas qutensornet for CPUs. The backend can be set using the following command line.
Among the backends provided by Qibotn, we have cutensornet (using cuQuantum library)
and qutensornet (using Quimb library) for tensor network based simulations.
At present, cutensornet backend works only for GPUs whereas qutensornet for CPUs.
These backend can be set using the following command line.
To use cuQuantum library, cutensornet can be specified as follows::
@@ -79,3 +85,54 @@ The following is a basic example to execute a two qubit circuit and print the fi
# Print the final state
print(result.state())
Using the Quantum Matcha Tea backend
""""""""""""""""""""""""""""""""""""
In the following we show an example of how the Quantum Matcha Tea backend can be
used to execute a quantum circuit::
# We need Qibo to setup the circuit and the backend
from qibo import Circuit, gates
from qibo.models.encodings import ghz_state
from qibo.backends import construct_backend
# We need Quantum Matcha Tea to customize the tensor network simulation
from qmatchatea import QCConvergenceParameters
# Set the number of qubits
nqubits = 40
# Construct a circuit preparing a Quantum Fourier Transform
circuit = ghz_state(nqubits)
# Construct the backend
backend = construct_backend(backend="qibotn", platform="qmatchatea")
# Customize the low-level backend preferences according to Qibo's formalism
backend.set_device("/CPU:1")
backend.set_precision("double")
# Customize the tensor network simulation itself
backend.configure_tn_simulation(
ansatz = "MPS",
convergence_params = QCConvergenceParameters(max_bond_dimension=50, cut_ratio=1e-6)
)
# Execute the tensor network simulation
outcome = backend.execute_circuit(
circuit = circuit,
nshots=1024,
)
# Print some results
print(outcome.probabilities())
# Should print something like: {'0000000000000000000000000000000000000000': 0.5000000000000001, '1111111111111111111111111111111111111111': 0.5000000000000001}
print(outcome.frequencies())
# Should print something like: {'0000000000000000000000000000000000000000': 488, '1111111111111111111111111111111111111111': 536}
By default, the simulator is choosing a specific method to compute the probabilities,
and for further information we recommend the user to refer to our High-Level-API
docstrings: :doc:`/api-reference/qibotn.backends`.

View File

@@ -4,29 +4,54 @@
What is QiboTN?
===============
QiboTN is the dedicated `Qibo <https://github.com/qiboteam/qibo>`_ backend to support large-scale simulation of quantum circuits and acceleration.
Qibotn is an high-level library which integrates tensor network simulation within
the `Qibo <https://github.com/qiboteam/qibo>`_ ecosystem.
Supported Computation:
If you are familiar with Qibo, you will be well aware of the modularity we provide
through the use of our backends: after building a specific algorithm or quantum
circuit, any of our backends can be selected to perform operations on the
desired hardware (classical or quantum).
- Tensornet (TN)
- Matrix Product States (MPS)
Here, we extend this modularity to one of the most famous quantum inspired simulation
technique.
Tensor Network contractions to:
We do this by relying on well-known and maintained packages, and integrating their
operation into our own dedicated backends.
- dense vectors
- expecation values of given Pauli string
.. image:: QiboTN.png
As shown in the figure above, we currently support three different backends, which
correspond to the three mentioned packages:
- `cuQuantum <https://github.com/NVIDIA/cuQuantum>`_: an NVIDIA SDK of optimized libraries and tools for accelerating quantum computing workflows (we refer to the specific `Cutensornet <https://docs.nvidia.com/cuda/cuquantum/latest/cutensornet/index.html>`_ library);
- `quimb <https://quimb.readthedocs.io/en/latest/>`_: an easy but fast python library for quantum information many-body calculations, focusing primarily on tensor networks;
- `Quantum Matcha Tea <https://www.quantumtea.it/>`_: a logical quantum computer emulator powered by matrix product states.
.. warning::
There are currently two ways to use the three backends (`qmatchatea` is
slightly different from the others), but we are working to standardize the interface.
Thanks to the mentioned packages, we currently support some tensor network ansatze:
Matrix Product States (MPS) on any mentioned backend, Tree Tensor Networks (TTN)
through the Quantum Matcha Tea backend and a more general Tensor Network (TN) ansatz through
Cutensornet and Quimb.
Supported simulation features
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
We support Tensor Network contractions to:
- dense vectors (all the backends)
- expecation values of given Pauli string (Cutensornet and Qmatchatea)
The supported HPC configurations are:
- single-node CPU
- single-node GPU or GPUs
- multi-node multi-GPU with Message Passing Interface (MPI)
- multi-node multi-GPU with NVIDIA Collective Communications Library (NCCL)
- single-node CPU through Quimb and Qmatchatea
- single-node GPU or GPUs through Cutensornet and Qmatchatea
- multi-node multi-GPU with Message Passing Interface (MPI) through Cutensornet
- multi-node multi-GPU with NVIDIA Collective Communications Library (NCCL) through Cutensornet
Currently, the supported tensor network libraries are:
- `cuQuantum <https://github.com/NVIDIA/cuQuantum>`_, an NVIDIA SDK of optimized libraries and tools for accelerating quantum computing workflows.
- `quimb <https://quimb.readthedocs.io/en/latest/>`_, an easy but fast python library for quantum information many-body calculations, focusing primarily on tensor networks.
How to Use the Documentation
============================

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

1031
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,8 +25,10 @@ qibo = "^0.2.8"
quimb = { version = "^1.6.0", extras = ["tensor"] }
cupy-cuda11x = { version = "^11.6.0", optional = true }
cuquantum-python-cu11 = { version = "^23.3.0", optional = true }
qmatchatea = { version = "^1.1.4", optional = true }
mpi4py = { version = "^3.1.5", optional = true }
[tool.poetry.extras]
cuda = ["cupy-cuda11x", "cuquantum-python-cu11", "mpi4py"]

View File

@@ -2,12 +2,11 @@ from typing import Union
from qibo.config import raise_error
from qibotn.backends.abstract import QibotnBackend
from qibotn.backends.cutensornet import CuTensorNet # pylint: disable=E0401
from qibotn.backends.quimb import QuimbBackend # pylint: disable=E0401
QibotnBackend = Union[CuTensorNet, QuimbBackend]
PLATFORMS = ("cutensornet", "qutensornet")
PLATFORMS = ("cutensornet", "qutensornet", "qmatchatea")
class MetaBackend:
@@ -28,10 +27,14 @@ class MetaBackend:
return CuTensorNet(runcard)
elif platform == "qutensornet": # pragma: no cover
return QuimbBackend(runcard)
elif platform == "qmatchatea": # pragma: no cover
from qibotn.backends.qmatchatea import QMatchaTeaBackend
return QMatchaTeaBackend()
else:
raise_error(
NotImplementedError,
f"Unsupported platform {platform}, please pick one in (`cutensornet`, `qutensornet)",
f"Unsupported platform {platform}, please pick one in {PLATFORMS}",
)
def list_available(self) -> dict:

View File

@@ -0,0 +1,35 @@
from abc import ABC
from qibo.config import raise_error
class QibotnBackend(ABC):
def __init__(self):
super().__init__()
def apply_gate(self, gate, state, nqubits): # pragma: no cover
raise_error(NotImplementedError, "QiboTN cannot apply gates directly.")
def apply_gate_density_matrix(self, gate, state, nqubits): # pragma: no cover
raise_error(NotImplementedError, "QiboTN cannot apply gates directly.")
def assign_measurements(self, measurement_map, circuit_result):
raise_error(NotImplementedError, "Not implemented in QiboTN.")
def set_precision(self, precision):
if precision != self.precision:
super().set_precision(precision)
self._setup_backend_specifics()
def set_device(self, device):
self.device = device
self._setup_backend_specifics()
def configure_tn_simulation(self, **config):
"""Configure the TN simulation that will be performed."""
pass
def _setup_backend_specifics(self):
"""Configure the backend specific according to the used package."""
pass

View File

@@ -1,12 +1,14 @@
import numpy as np
from qibo.backends.numpy import NumpyBackend
from qibo.backends import NumpyBackend
from qibo.config import raise_error
from qibo.result import QuantumState
from qibotn.backends.abstract import QibotnBackend
CUDA_TYPES = {}
class CuTensorNet(NumpyBackend): # pragma: no cover
class CuTensorNet(QibotnBackend, NumpyBackend): # pragma: no cover
# CI does not test for GPU
"""Creates CuQuantum backend for QiboTN."""
@@ -77,23 +79,10 @@ class CuTensorNet(NumpyBackend): # pragma: no cover
),
}
def apply_gate(self, gate, state, nqubits): # pragma: no cover
raise_error(NotImplementedError, "QiboTN cannot apply gates directly.")
def apply_gate_density_matrix(self, gate, state, nqubits): # pragma: no cover
raise_error(NotImplementedError, "QiboTN cannot apply gates directly.")
def assign_measurements(self, measurement_map, circuit_result):
raise_error(NotImplementedError, "Not implemented in QiboTN.")
def __del__(self):
if hasattr(self, "cutn"):
self.cutn.destroy(self.handle)
def set_precision(self, precision):
if precision != self.precision:
super().set_precision(precision)
def cuda_type(self, dtype="complex64"):
"""Get CUDA Type.

View File

@@ -0,0 +1,315 @@
"""Implementation of Quantum Matcha Tea backend."""
import re
from dataclasses import dataclass
import numpy as np
import qiskit
import qmatchatea
import qtealeaves
from qibo.backends import NumpyBackend
from qibo.config import raise_error
from qibotn.backends.abstract import QibotnBackend
from qibotn.result import TensorNetworkResult
@dataclass
class QMatchaTeaBackend(QibotnBackend, NumpyBackend):
def __init__(self):
super().__init__()
self.name = "qibotn"
self.platform = "qmatchatea"
# Set default configurations
self.configure_tn_simulation()
self._setup_backend_specifics()
def configure_tn_simulation(
self,
ansatz: str = "MPS",
max_bond_dimension: int = 10,
cut_ratio: float = 1e-9,
trunc_tracking_mode: str = "C",
svd_control: str = "A",
ini_bond_dimension: int = 1,
):
"""Configure TN simulation given Quantum Matcha Tea interface.
Args:
ansatz (str): tensor network ansatz. It can be tree tensor network "TTN"
or Matrix Product States "MPS" (default).
max_bond_dimension : int, optional Maximum bond dimension of the problem. Default to 10.
cut_ratio : float, optional
Cut ratio for singular values. If :math:`\\lambda_n/\\lambda_1 <` cut_ratio then
:math:`\\lambda_n` is neglected. Default to 1e-9.
trunc_tracking_mode : str, optional
Modus for storing truncation, 'M' for maximum, 'C' for
cumulated (default).
svd_ctrl : character, optional
Control for the SVD algorithm. Available:
- "A" : automatic. Some heuristic is run to choose the best mode for the algorithm.
- "V" : gesvd. Safe but slow method.
- "D" : gesdd. Fast iterative method. It might fail. Resort to gesvd if it fails
- "E" : eigenvalue decomposition method. Faster on GPU. Available only when
contracting the singular value to left or right
- "X" : sparse eigenvalue decomposition method. Used when you reach the maximum
bond dimension.
- "R" : random svd method. Used when you reach the maximum bond dimension.
Default to 'A'.
ini_bond_dimension: int, optional
Initial bond dimension of the simulation. It is used if the initial state is random.
Default to 1.
"""
self.convergence_params = qmatchatea.QCConvergenceParameters(
max_bond_dimension=max_bond_dimension,
cut_ratio=cut_ratio,
trunc_tracking_mode=trunc_tracking_mode,
svd_ctrl=svd_control,
ini_bond_dimension=ini_bond_dimension,
)
self.ansatz = ansatz
def _setup_backend_specifics(self):
"""Configure qmatchatea QCBackend object."""
qmatchatea_device = (
"cpu" if "CPU" in self.device else "gpu" if "GPU" in self.device else None
)
qmatchatea_precision = (
"C"
if self.precision == "single"
else "Z" if self.precision == "double" else "A"
)
# TODO: once MPI is available for Python, integrate it here
self.qmatchatea_backend = qmatchatea.QCBackend(
backend="PY", # The only alternative is Fortran, but we use Python here
precision=qmatchatea_precision,
device=qmatchatea_device,
ansatz=self.ansatz,
)
def execute_circuit(
self,
circuit,
initial_state=None,
nshots=None,
prob_type=None,
return_array=False,
**prob_kwargs,
):
"""Execute a Qibo quantum circuit using tensor network simulation.
This method returns a ``TensorNetworkResult`` object, which provides:
- Reconstruction of the system state (if the system size is < 20).
- Frequencies (if the number of shots is specified).
- Probabilities computed using various methods.
The following probability computation methods are available, as implemented
in Quantum Matcha Tea:
- **"E" (Even):** Probabilities are computed by evenly descending the probability tree,
pruning branches (states) with probabilities below a threshold.
- **"G" (Greedy):** Probabilities are computed by following the most probable states
in descending order until reaching a given coverage (sum of probabilities).
- **"U" (Unbiased):** An optimal probability measure that is unbiased and designed
for best performance. See https://arxiv.org/abs/2401.10330 for details.
Args:
circuit: A Qibo circuit to execute.
initial_state: The initial state of the system (default is the vacuum state
for tensor network simulations).
nshots: The number of shots for shot-noise simulation (optional).
prob_type: The probability computation method. Must be one of:
- "E" (Even)
- "G" (Greedy)
- "U" (Unbiased) [default].
prob_kwargs: Additional parameters required for probability computation:
- For "U", requires ``num_samples``.
- For "E" and "G", requires ``prob_threshold``.
Returns:
TensorNetworkResult: An object with methods to reconstruct the state,
compute probabilities, and generate frequencies.
"""
# TODO: verify if the QCIO mechanism of matcha is supported by Fortran only
# as written in the docstrings or by Python too (see ``io_info`` argument of
# ``qmatchatea.interface.run_simulation`` function)
if initial_state is not None:
raise_error(
NotImplementedError,
f"Backend {self} currently does not support initial state.",
)
if prob_type == None:
prob_type = "U"
prob_kwargs = {"num_samples": 500}
# TODO: check
circuit = self._qibocirc_to_qiskitcirc(circuit)
run_qk_params = qmatchatea.preprocessing.qk_transpilation_params(False)
# Initialize the TNObservable object
observables = qtealeaves.observables.TNObservables()
# Shots
if nshots is not None:
observables += qtealeaves.observables.TNObsProjective(num_shots=nshots)
# Probabilities
observables += qtealeaves.observables.TNObsProbabilities(
prob_type=prob_type,
**prob_kwargs,
)
# State
observables += qtealeaves.observables.TNState2File(name="temp", formatting="U")
results = qmatchatea.run_simulation(
circ=circuit,
convergence_parameters=self.convergence_params,
transpilation_parameters=run_qk_params,
backend=self.qmatchatea_backend,
observables=observables,
)
if circuit.num_qubits < 20 and return_array:
statevector = results.statevector
else:
statevector = None
return TensorNetworkResult(
nqubits=circuit.num_qubits,
backend=self,
measures=results.measures,
measured_probabilities=results.measure_probabilities,
prob_type=prob_type,
statevector=statevector,
)
def expectation(self, circuit, observable):
"""Compute the expectation value of a Qibo-friendly ``observable`` on
the Tensor Network constructed from a Qibo ``circuit``.
This method takes a Qibo-style symbolic Hamiltonian (e.g., `X(0)*Z(1) + 2.0*Y(2)*Z(0)`)
as the observable, converts it into a Quantum Matcha Tea (qmatchatea) observable
(using `TNObsTensorProduct` and `TNObsWeightedSum`), and computes its expectation
value using the provided circuit.
Args:
circuit: A Qibo quantum circuit object on which the expectation value
is computed. The circuit should be compatible with the qmatchatea
Tensor Network backend.
observable: The observable whose expectation value we want to compute.
This must be provided in the symbolic Hamiltonian form supported by Qibo
(e.g., `X(0)*Y(1)` or `Z(0)*Z(1) + 1.5*Y(2)`).
Returns:
qibotn.TensorNetworkResult class, providing methods to retrieve
probabilities, frequencies and state always according to the chosen
simulation setup.
"""
# From Qibo to Qiskit
circuit = self._qibocirc_to_qiskitcirc(circuit)
run_qk_params = qmatchatea.preprocessing.qk_transpilation_params(False)
operators = qmatchatea.QCOperators()
observables = qtealeaves.observables.TNObservables()
# Add custom observable
observables += self._qiboobs_to_qmatchaobs(hamiltonian=observable)
results = qmatchatea.run_simulation(
circ=circuit,
convergence_parameters=self.convergence_params,
transpilation_parameters=run_qk_params,
backend=self.qmatchatea_backend,
observables=observables,
operators=operators,
)
return np.real(results.observables["custom_hamiltonian"])
def _qibocirc_to_qiskitcirc(self, qibo_circuit) -> qiskit.QuantumCircuit:
"""Convert a Qibo Circuit into a Qiskit Circuit."""
# Convert the circuit to QASM 2.0 to qiskit
qasm_circuit = qibo_circuit.to_qasm()
qiskit_circuit = qiskit.QuantumCircuit.from_qasm_str(qasm_circuit)
# Transpile the circuit to adapt it to the linear structure of the MPS,
# with the constraint of having only the gates basis_gates
qiskit_circuit = qmatchatea.preprocessing.preprocess(
qiskit_circuit,
qk_params=qmatchatea.preprocessing.qk_transpilation_params(),
)
return qiskit_circuit
def _qiboobs_to_qmatchaobs(self, hamiltonian, observable_name="custom_hamiltonian"):
"""
Convert a Qibo SymbolicHamiltonian into a qmatchatea TNObsWeightedSum observable.
The SymbolicHamiltonian is expected to have a collection of terms, where each term has:
- `coefficient`: A numeric value.
- `factors`: A list of factors, each a string such as "X2" or "Z0", representing an operator
and the qubit it acts on.
Args:
hamiltonian (qibo.SymbolicHamiltonian): The symbolic Hamiltonian containing the terms.
observable_name (str): The name for the resulting TNObsWeightedSum observable.
Returns:
TNObsWeightedSum: An observable suitable for use with qmatchatea.
"""
coeff_list = []
tensor_product_obs = None
# Regex to split an operator factor (e.g., "X2" -> operator "X", qubit 2)
factor_pattern = re.compile(r"([^\d]+)(\d+)")
# Iterate over each term in the symbolic Hamiltonian
for i, term in enumerate(hamiltonian.terms):
# Store the term's coefficient
coeff_list.append(term.coefficient)
operator_names = []
acting_on_qubits = []
# Process each factor in the term
for factor in term.factors:
# Assume each factor is a string like "Y2" or "Z0"
match = factor_pattern.match(str(factor))
if match:
operator_name = match.group(1)
qubit_index = int(match.group(2))
operator_names.append(operator_name)
acting_on_qubits.append([qubit_index])
else:
raise ValueError(
f"Factor '{str(factor)}' does not match the expected format."
)
# Create a TNObsTensorProduct for this term.
term_tensor_prod = qtealeaves.observables.TNObsTensorProduct(
name=f"term_{i}",
operators=operator_names,
sites=acting_on_qubits,
)
# Combine tensor products from each term
if tensor_product_obs is None:
tensor_product_obs = term_tensor_prod
else:
tensor_product_obs += term_tensor_prod
# Combine all terms into a weighted sum observable
obs_sum = qtealeaves.observables.TNObsWeightedSum(
name=observable_name,
tp_operators=tensor_product_obs,
coeffs=coeff_list,
use_itpo=False,
)
return obs_sum

View File

@@ -1,9 +1,11 @@
from qibo.backends.numpy import NumpyBackend
from qibo.backends import NumpyBackend
from qibo.config import raise_error
from qibo.result import QuantumState
from qibotn.backends.abstract import QibotnBackend
class QuimbBackend(NumpyBackend):
class QuimbBackend(QibotnBackend, NumpyBackend):
def __init__(self, runcard):
super().__init__()
@@ -36,19 +38,6 @@ class QuimbBackend(NumpyBackend):
self.platform = "QuimbBackend"
self.versions["quimb"] = self.quimb.__version__
def apply_gate(self, gate, state, nqubits): # pragma: no cover
raise_error(NotImplementedError, "QiboTN cannot apply gates directly.")
def apply_gate_density_matrix(self, gate, state, nqubits): # pragma: no cover
raise_error(NotImplementedError, "QiboTN cannot apply gates directly.")
def assign_measurements(self, measurement_map, circuit_result):
raise_error(NotImplementedError, "Not implemented in QiboTN.")
def set_precision(self, precision):
if precision != self.precision:
super().set_precision(precision)
def execute_circuit(
self, circuit, initial_state=None, nshots=None, return_array=False
): # pragma: no cover

66
src/qibotn/result.py Normal file
View File

@@ -0,0 +1,66 @@
from copy import deepcopy
from dataclasses import dataclass
from typing import Union
from numpy import ndarray
from qibo.config import raise_error
from qibotn.backends.abstract import QibotnBackend
@dataclass
class TensorNetworkResult:
"""
Object to store and process the output of a Tensor Network simulation of a quantum circuit.
Args:
nqubits (int): number of qubits involved in the simulation;
backend (QibotnBackend): specific backend on which the simulation has been performed;
measures (dict): measures (if performed) during the tensor network simulation;
measured_probabilities (Union[dict, ndarray]): probabilities of the final state
according to the simulation;
prob_type (str): string identifying the method used to compute the probabilities.
Especially useful in case the `QmatchateaBackend` is selected.
statevector (ndarray): if computed, the reconstructed statevector.
"""
nqubits: int
backend: QibotnBackend
measures: dict
measured_probabilities: Union[dict, ndarray]
prob_type: str
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}
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[self.prob_type]
return self.backend.cast(list(probabilities.values()), dtype="double")
def frequencies(self):
"""Return frequencies if a certain number of shots has been set."""
if self.measures is None:
raise_error(
ValueError,
f"To access frequencies, circuit has to be executed with a given number of shots != None",
)
return self.measures
def state(self):
"""Return the statevector if the number of qubits is less than 20."""
if self.nqubits < 20:
return self.statevector
raise_error(
NotImplementedError,
f"Tensor network simulation cannot be used to reconstruct statevector for >= 20 .",
)

66
tests/conftest.py Normal file
View File

@@ -0,0 +1,66 @@
"""conftest.py.
Pytest fixtures.
"""
import sys
import pytest
# backends to be tested
# TODO: add cutensornet and quimb here as well
BACKENDS = ["qmatchatea"]
def get_backend(backend_name):
from qibotn.backends.qmatchatea import QMatchaTeaBackend
NAME2BACKEND = {
"qmatchatea": QMatchaTeaBackend,
}
return NAME2BACKEND[backend_name]()
AVAILABLE_BACKENDS = []
for backend_name in BACKENDS:
try:
_backend = get_backend(backend_name)
AVAILABLE_BACKENDS.append(backend_name)
except (ModuleNotFoundError, ImportError):
pass
def pytest_runtest_setup(item):
ALL = {"darwin", "linux"}
supported_platforms = ALL.intersection(mark.name for mark in item.iter_markers())
plat = sys.platform
if supported_platforms and plat not in supported_platforms: # pragma: no cover
# case not covered by workflows
pytest.skip(f"Cannot run test on platform {plat}.")
@pytest.fixture
def backend(backend_name):
yield get_backend(backend_name)
def pytest_runtest_setup(item):
ALL = {"darwin", "linux"}
supported_platforms = ALL.intersection(mark.name for mark in item.iter_markers())
plat = sys.platform
if supported_platforms and plat not in supported_platforms: # pragma: no cover
# case not covered by workflows
pytest.skip(f"Cannot run test on platform {plat}.")
def pytest_configure(config):
config.addinivalue_line("markers", "linux: mark test to run only on linux")
def pytest_generate_tests(metafunc):
module_name = metafunc.module.__name__
if "backend_name" in metafunc.fixturenames:
metafunc.parametrize("backend_name", AVAILABLE_BACKENDS)

View File

@@ -0,0 +1,91 @@
import math
import pytest
from qibo import Circuit, gates, hamiltonians
from qibo.symbols import X, Z
from qibotn.backends.qmatchatea import QMatchaTeaBackend
def build_observable(nqubits):
"""Helper function to construct a target observable."""
hamiltonian_form = 0
for i in range(nqubits):
hamiltonian_form += 0.5 * X(i % nqubits) * Z((i + 1) % nqubits)
hamiltonian = hamiltonians.SymbolicHamiltonian(form=hamiltonian_form)
return hamiltonian, hamiltonian_form
def build_GHZ(nqubits):
"""Helper function to construct a layered quantum circuit."""
circ = Circuit(nqubits)
circ.add(gates.H(0))
[circ.add(gates.CNOT(q, q + 1)) for q in range(nqubits - 1)]
return circ
def construct_targets(nqubits):
"""Construct strings of 1s and 0s of size `nqubits`."""
ones = "1" * nqubits
zeros = "0" * nqubits
return ones, zeros
@pytest.mark.parametrize("nqubits", [2, 10, 40])
def test_probabilities(backend, nqubits):
circ = build_GHZ(nqubits=nqubits)
if isinstance(backend, QMatchaTeaBackend):
# unbiased prob
out_u = backend.execute_circuit(
circuit=circ,
prob_type="U",
num_samples=1000,
).probabilities()
math.isclose(out_u[0], 0.5, abs_tol=1e-7)
math.isclose(out_u[1], 0.5, abs_tol=1e-7)
out_g = backend.execute_circuit(
circuit=circ,
prob_type="G",
prob_threshold=1.0,
).probabilities()
math.isclose(out_g[0], 0.5, abs_tol=1e-7)
math.isclose(out_g[1], 0.5, abs_tol=1e-7)
out_e = backend.execute_circuit(
circuit=circ,
prob_type="E",
prob_threshold=0.2,
).probabilities()
math.isclose(out_e[0], 0.5, abs_tol=1e-7)
math.isclose(out_e[1], 0.5, abs_tol=1e-7)
@pytest.mark.parametrize("nqubits", [2, 10, 40])
@pytest.mark.parametrize("nshots", [100, 1000])
def test_shots(backend, nqubits, nshots):
circ = build_GHZ(nqubits=nqubits)
ones, zeros = construct_targets(nqubits)
# For p = 0.5, sigma = sqrt(nshots * 0.5 * 0.5) = sqrt(nshots)/2.
sigma_threshold = 3 * (math.sqrt(nshots) / 2)
outcome = backend.execute_circuit(circ, nshots=nshots)
frequencies = outcome.frequencies()
shots_ones = frequencies.get(ones, 0)
shots_zeros = frequencies.get(zeros, 0)
# Check that the counts for both outcomes are within the 3-sigma threshold of nshots/2.
assert (
abs(shots_ones - (nshots / 2)) < sigma_threshold
), f"Count for {ones} deviates too much: {shots_ones} vs expected {nshots/2}"
assert (
abs(shots_zeros - (nshots / 2)) < sigma_threshold
), f"Count for {zeros} deviates too much: {shots_zeros} vs expected {nshots/2}"

47
tests/test_expectation.py Normal file
View File

@@ -0,0 +1,47 @@
import math
import random
import pytest
from qibo import Circuit, construct_backend, gates, hamiltonians
from qibo.symbols import X, Z
def build_observable(nqubits):
"""Helper function to construct a target observable."""
hamiltonian_form = 0
for i in range(nqubits):
hamiltonian_form += 0.5 * X(i % nqubits) * Z((i + 1) % nqubits)
hamiltonian = hamiltonians.SymbolicHamiltonian(form=hamiltonian_form)
return hamiltonian, hamiltonian_form
def build_circuit(nqubits, nlayers, seed=42):
"""Helper function to construct a layered quantum circuit."""
random.seed(seed)
circ = Circuit(nqubits)
for _ in range(nlayers):
for q in range(nqubits):
circ.add(gates.RY(q=q, theta=random.uniform(-math.pi, math.pi)))
circ.add(gates.RZ(q=q, theta=random.uniform(-math.pi, math.pi)))
[circ.add(gates.CNOT(q % nqubits, (q + 1) % nqubits) for q in range(nqubits))]
circ.add(gates.M(*range(nqubits)))
return circ
@pytest.mark.parametrize("nqubits", [2, 5, 10])
def test_observable_expval(backend, nqubits):
numpy_backend = construct_backend("numpy")
ham, ham_form = build_observable(nqubits)
circ = build_circuit(nqubits=nqubits, nlayers=1)
exact_expval = numpy_backend.calculate_expectation_state(
hamiltonian=ham,
state=circ().state(),
normalize=False,
)
tn_expval = backend.expectation(circuit=circ, observable=ham_form)
assert math.isclose(exact_expval, tn_expval, abs_tol=1e-7)