From f0828c946b36987b1c2db7ccb36453b1effe779f Mon Sep 17 00:00:00 2001 From: Mattia Robbiano Date: Sat, 24 May 2025 00:51:55 +0200 Subject: [PATCH 1/4] refactor: standardized quimb interface with qmatchatea --- src/qibotn/backends/__init__.py | 51 +---------- src/qibotn/backends/quimb.py | 153 +++++++++++++++++++++----------- src/qibotn/result.py | 4 +- 3 files changed, 106 insertions(+), 102 deletions(-) diff --git a/src/qibotn/backends/__init__.py b/src/qibotn/backends/__init__.py index 8fdd367..4942a45 100644 --- a/src/qibotn/backends/__init__.py +++ b/src/qibotn/backends/__init__.py @@ -1,50 +1,5 @@ -from typing import Union +import importlib.metadata as im -from qibo.config import raise_error +from qibotn.backends import MetaBackend -from qibotn.backends.abstract import QibotnBackend -from qibotn.backends.cutensornet import CuTensorNet # pylint: disable=E0401 -from qibotn.backends.quimb import QuimbBackend # pylint: disable=E0401 - -PLATFORMS = ("cutensornet", "qutensornet", "qmatchatea") - - -class MetaBackend: - """Meta-backend class which takes care of loading the qibotn backends.""" - - @staticmethod - def load(platform: str, runcard: dict = None) -> QibotnBackend: - """Loads the backend. - - Args: - platform (str): Name of the backend to load: either `cutensornet` or `qutensornet`. - runcard (dict): Dictionary containing the simulation settings. - Returns: - qibo.backends.abstract.Backend: The loaded backend. - """ - - if platform == "cutensornet": # pragma: no cover - 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 {PLATFORMS}", - ) - - def list_available(self) -> dict: - """Lists all the available qibotn backends.""" - available_backends = {} - for platform in PLATFORMS: - try: - MetaBackend.load(platform=platform) - available = True - except: - available = False - available_backends[platform] = available - return available_backends +__version__ = im.version(__package__) diff --git a/src/qibotn/backends/quimb.py b/src/qibotn/backends/quimb.py index 13a8cf1..8ce49ce 100644 --- a/src/qibotn/backends/quimb.py +++ b/src/qibotn/backends/quimb.py @@ -1,75 +1,124 @@ +from collections import Counter + +import quimb.tensor as qtn from qibo.backends import NumpyBackend from qibo.config import raise_error from qibo.result import QuantumState from qibotn.backends.abstract import QibotnBackend - +from qibotn.result import TensorNetworkResult class QuimbBackend(QibotnBackend, NumpyBackend): - def __init__(self, runcard): + def __init__(self): super().__init__() - import quimb # pylint: disable=import-error - - if runcard is not None: - self.MPI_enabled = runcard.get("MPI_enabled", False) - self.NCCL_enabled = runcard.get("NCCL_enabled", False) - self.expectation_enabled = runcard.get("expectation_enabled", False) - - mps_enabled_value = runcard.get("MPS_enabled") - if mps_enabled_value is True: - self.mps_opts = {"method": "svd", "cutoff": 1e-6, "cutoff_mode": "abs"} - elif mps_enabled_value is False: - self.mps_opts = None - elif isinstance(mps_enabled_value, dict): - self.mps_opts = mps_enabled_value - else: - raise TypeError("MPS_enabled has an unexpected type") - - else: - self.MPI_enabled = False - self.MPS_enabled = False - self.NCCL_enabled = False - self.expectation_enabled = False - self.mps_opts = None self.name = "qibotn" - self.quimb = quimb - self.platform = "QuimbBackend" - self.versions["quimb"] = self.quimb.__version__ + self.platform = "quimb" - def execute_circuit( - self, circuit, initial_state=None, nshots=None, return_array=False - ): # pragma: no cover - """Executes a quantum circuit. + self.configure_tn_simulation() + self.setup_backend_specifics() + + def configure_tn_simulation( + self, + ansatz: str = "MPS", + max_bond_dimension: int = 10, + ): + """ + Configure tensor network simulation. Args: - circuit (:class:`qibo.models.circuit.Circuit`): Circuit to execute. - initial_state (:class:`qibo.models.circuit.Circuit`): Circuit to prepare the initial state. - If ``None`` the default ``|00...0>`` state is used. + ansatz : str, optional + The tensor network ansatz to use. Currently, only "MPS" is supported. Default is "MPS". + max_bond_dimension : int, optional + The maximum bond dimension for the MPS ansatz. Default is 10. + + Notes: + - The ansatz determines the tensor network structure used for simulation. Currently, only "MPS" is supported. + - The `max_bond_dimension` parameter controls the maximum allowed bond dimension for the MPS ansatz. + """ + self.ansatz = ansatz + self.max_bond_dimension = max_bond_dimension + + def setup_backend_specifics(self, qimb_backend="numpy"): + """Setup backend specifics. + Args: + qimb_backend: str + The backend to use for the quimb tensor network simulation. + """ + self.backend = qimb_backend + + def execute_circuit( + self, + circuit, + initial_state=None, + nshots=None, + return_array=False, + **prob_kwargs, + ): + """ + Execute a quantum circuit using the specified tensor network ansatz and initial state. + + Args: + circuit : QuantumCircuit + The quantum circuit to be executed. + initial_state : array-like, optional + The initial state of the quantum system. Only supported for Matrix Product States (MPS) ansatz. + nshots : int, optional + The number of shots for sampling the circuit. If None, no sampling is performed, and the full statevector is used. + return_array : bool, optional + If True, returns the statevector as a dense array. Default is False. + **prob_kwargs : dict, optional + Additional keyword arguments for probability computation (currently unused). Returns: - QuantumState or numpy.ndarray: If `return_array` is False, returns a QuantumState object representing the quantum state. If `return_array` is True, returns a numpy array representing the quantum state. + TensorNetworkResult + An object containing the results of the circuit execution, including: + - nqubits: Number of qubits in the circuit. + - backend: The backend used for execution. + - measures: The measurement frequencies if nshots is specified, otherwise None. + - measured_probabilities: A dictionary of computational basis states and their probabilities. + - prob_type: The type of probability computation used (currently "default"). + - statevector: The final statevector as a dense array if return_array is True, otherwise None. + + Raises: + ValueError + If an initial state is provided but the ansatz is not "MPS". + + Notes: + - The ansatz determines the tensor network structure used for simulation. Currently, only "MPS" is supported. + - If `initial_state` is provided, it must be compatible with the MPS ansatz. + - The `nshots` parameter enables sampling from the circuit's output distribution. If not specified, the full statevector is computed. """ - import qibotn.eval_qu as eval - - if self.MPI_enabled == True: - raise_error(NotImplementedError, "QiboTN quimb backend cannot support MPI.") - if self.NCCL_enabled == True: + if initial_state is not None and self.ansatz == "MPS": + initial_state = qtn.tensor_1d.MatrixProductState.from_dense( + initial_state, 2 + ) # 2 is the physical dimension + elif initial_state is not None: raise_error( - NotImplementedError, "QiboTN quimb backend cannot support NCCL." - ) - if self.expectation_enabled == True: - raise_error( - NotImplementedError, "QiboTN quimb backend cannot support expectation" + ValueError, "Initial state not None supported only for MPS ansatz." ) - state = eval.dense_vector_tn_qu( - circuit.to_qasm(), initial_state, self.mps_opts, backend="numpy" + circ_ansatz = ( + qtn.circuit.CircuitMPS if self.ansatz == "MPS" else qtn.circuit.Circuit + ) + circ_quimb = circ_ansatz.from_openqasm2_str( + circuit.to_qasm(), psi0=initial_state ) - if return_array: - return state.flatten() - else: - return QuantumState(state.flatten()) + frequencies = Counter(circ_quimb.sample(nshots)) if nshots is not None else None + main_frequencies = {state: count for state, count in frequencies.most_common(n=100)} + computational_states = [state for state in main_frequencies.keys()] + amplitudes = {state: circ_quimb.amplitude(state) for state in computational_states} + measured_probabilities = {state: abs(amplitude) ** 2 for state, amplitude in amplitudes.items()} + + statevector = circ_quimb.to_dense() if return_array else None + return TensorNetworkResult( + nqubits=circuit.nqubits, + backend=self, + measures=frequencies, + measured_probabilities=measured_probabilities, + prob_type="default", + statevector=statevector, + ) diff --git a/src/qibotn/result.py b/src/qibotn/result.py index a37cc63..33be61c 100644 --- a/src/qibotn/result.py +++ b/src/qibotn/result.py @@ -44,8 +44,8 @@ class TensorNetworkResult: 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") + probabilities = self.measured_probabilities + return probabilities def frequencies(self): """Return frequencies if a certain number of shots has been set.""" From 5c249cb72729c855ff6d74e5b886b2aea9f3ce23 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 22:54:46 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/qibotn/backends/quimb.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/qibotn/backends/quimb.py b/src/qibotn/backends/quimb.py index 8ce49ce..b3b0b72 100644 --- a/src/qibotn/backends/quimb.py +++ b/src/qibotn/backends/quimb.py @@ -8,6 +8,7 @@ from qibo.result import QuantumState from qibotn.backends.abstract import QibotnBackend from qibotn.result import TensorNetworkResult + class QuimbBackend(QibotnBackend, NumpyBackend): def __init__(self): @@ -108,11 +109,17 @@ class QuimbBackend(QibotnBackend, NumpyBackend): ) frequencies = Counter(circ_quimb.sample(nshots)) if nshots is not None else None - main_frequencies = {state: count for state, count in frequencies.most_common(n=100)} + main_frequencies = { + state: count for state, count in frequencies.most_common(n=100) + } computational_states = [state for state in main_frequencies.keys()] - amplitudes = {state: circ_quimb.amplitude(state) for state in computational_states} - measured_probabilities = {state: abs(amplitude) ** 2 for state, amplitude in amplitudes.items()} - + amplitudes = { + state: circ_quimb.amplitude(state) for state in computational_states + } + measured_probabilities = { + state: abs(amplitude) ** 2 for state, amplitude in amplitudes.items() + } + statevector = circ_quimb.to_dense() if return_array else None return TensorNetworkResult( nqubits=circuit.nqubits, From f9d1d0b6b00214b3f2ed32852b9519add26dddc1 Mon Sep 17 00:00:00 2001 From: Mattia Robbiano Date: Thu, 29 May 2025 15:23:19 +0200 Subject: [PATCH 3/4] refactor adapted to pull request comments --- src/qibotn/backends/__init__.py | 51 +++++++++++++++++++++++++++++++-- src/qibotn/backends/quimb.py | 8 ++++-- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/qibotn/backends/__init__.py b/src/qibotn/backends/__init__.py index 4942a45..11eaf02 100644 --- a/src/qibotn/backends/__init__.py +++ b/src/qibotn/backends/__init__.py @@ -1,5 +1,50 @@ -import importlib.metadata as im +from typing import Union -from qibotn.backends import MetaBackend +from qibo.config import raise_error -__version__ = im.version(__package__) +from qibotn.backends.abstract import QibotnBackend +from qibotn.backends.cutensornet import CuTensorNet # pylint: disable=E0401 +from qibotn.backends.quimb import QuimbBackend # pylint: disable=E0401 + +PLATFORMS = ("cutensornet", "qutensornet", "qmatchatea") + + +class MetaBackend: + """Meta-backend class which takes care of loading the qibotn backends.""" + + @staticmethod + def load(platform: str, runcard: dict = None) -> QibotnBackend: + """Loads the backend. + + Args: + platform (str): Name of the backend to load: either `cutensornet` or `qutensornet`. + runcard (dict): Dictionary containing the simulation settings. + Returns: + qibo.backends.abstract.Backend: The loaded backend. + """ + + if platform == "cutensornet": # pragma: no cover + return CuTensorNet(runcard) + elif platform == "quimb": # 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 {PLATFORMS}", + ) + + def list_available(self) -> dict: + """Lists all the available qibotn backends.""" + available_backends = {} + for platform in PLATFORMS: + try: + MetaBackend.load(platform=platform) + available = True + except: + available = False + available_backends[platform] = available + return available_backends \ No newline at end of file diff --git a/src/qibotn/backends/quimb.py b/src/qibotn/backends/quimb.py index b3b0b72..d55bbab 100644 --- a/src/qibotn/backends/quimb.py +++ b/src/qibotn/backends/quimb.py @@ -24,6 +24,7 @@ class QuimbBackend(QibotnBackend, NumpyBackend): self, ansatz: str = "MPS", max_bond_dimension: int = 10, + n_most_frequent_states: int = 100, ): """ Configure tensor network simulation. @@ -40,6 +41,7 @@ class QuimbBackend(QibotnBackend, NumpyBackend): """ self.ansatz = ansatz self.max_bond_dimension = max_bond_dimension + self.n_most_frequent_states = n_most_frequent_states def setup_backend_specifics(self, qimb_backend="numpy"): """Setup backend specifics. @@ -69,6 +71,8 @@ class QuimbBackend(QibotnBackend, NumpyBackend): The number of shots for sampling the circuit. If None, no sampling is performed, and the full statevector is used. return_array : bool, optional If True, returns the statevector as a dense array. Default is False. + n_most_frequent_states : int, optional + The number of most frequent computational basis states to return. Default is 100. **prob_kwargs : dict, optional Additional keyword arguments for probability computation (currently unused). @@ -109,9 +113,7 @@ class QuimbBackend(QibotnBackend, NumpyBackend): ) frequencies = Counter(circ_quimb.sample(nshots)) if nshots is not None else None - main_frequencies = { - state: count for state, count in frequencies.most_common(n=100) - } + main_frequencies = {state: count for state, count in frequencies.most_common(self.n_most_frequent_states)} computational_states = [state for state in main_frequencies.keys()] amplitudes = { state: circ_quimb.amplitude(state) for state in computational_states From dff0f1cf8f402124e941f1a2c101f35590016e6c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 29 May 2025 13:27:36 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/qibotn/backends/__init__.py | 2 +- src/qibotn/backends/quimb.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/qibotn/backends/__init__.py b/src/qibotn/backends/__init__.py index 11eaf02..1547a3d 100644 --- a/src/qibotn/backends/__init__.py +++ b/src/qibotn/backends/__init__.py @@ -47,4 +47,4 @@ class MetaBackend: except: available = False available_backends[platform] = available - return available_backends \ No newline at end of file + return available_backends diff --git a/src/qibotn/backends/quimb.py b/src/qibotn/backends/quimb.py index d55bbab..fdbb387 100644 --- a/src/qibotn/backends/quimb.py +++ b/src/qibotn/backends/quimb.py @@ -113,7 +113,10 @@ class QuimbBackend(QibotnBackend, NumpyBackend): ) frequencies = Counter(circ_quimb.sample(nshots)) if nshots is not None else None - main_frequencies = {state: count for state, count in frequencies.most_common(self.n_most_frequent_states)} + main_frequencies = { + state: count + for state, count in frequencies.most_common(self.n_most_frequent_states) + } computational_states = [state for state in main_frequencies.keys()] amplitudes = { state: circ_quimb.amplitude(state) for state in computational_states