From 1b82ca9020abe9444fc7c1fd7e33529a64f6ea07 Mon Sep 17 00:00:00 2001 From: Sankalp Sanand Date: Tue, 26 Sep 2023 21:47:50 -0400 Subject: [PATCH] adding new files --- covalent/executor/qbase.py | 343 +++++++++++ covalent/executor/utils/context.py | 45 ++ covalent/quantum/__init__.py | 21 + covalent/quantum/qclient/__init__.py | 22 + covalent/quantum/qclient/base_client.py | 54 ++ covalent/quantum/qclient/core.py | 72 +++ covalent/quantum/qclient/local_client.py | 63 ++ covalent/quantum/qcluster/__init__.py | 26 + covalent/quantum/qcluster/base.py | 103 ++++ covalent/quantum/qcluster/clusters.py | 104 ++++ .../quantum/qcluster/default_selectors.py | 57 ++ covalent/quantum/qcluster/simulator.py | 103 ++++ covalent/quantum/qserver/__init__.py | 22 + covalent/quantum/qserver/core.py | 409 +++++++++++++ covalent/quantum/qserver/database.py | 98 +++ covalent/quantum/qserver/serialize.py | 230 +++++++ covalent/quantum/qserver/servers/__init__.py | 21 + covalent/quantum/qserver/servers/local.py | 23 + covalent/quantum/qserver/utils.py | 81 +++ ...3d_add_qelectron_data_exists_flag_to_db.py | 60 ++ covalent_ui/webapp/src/assets/codeview.svg | 3 + .../src/assets/qelectron/circuit-large.svg | 9 + .../webapp/src/assets/qelectron/circuit.svg | 13 + .../webapp/src/assets/qelectron/filter.svg | 3 + .../webapp/src/assets/qelectron/qelectron.svg | 4 + .../webapp/src/assets/qelectron/view.svg | 3 + .../webapp/src/assets/status/running.svg | 3 + .../src/components/common/QElectronCard.js | 152 +++++ .../src/components/common/QElectronDrawer.js | 213 +++++++ .../src/components/common/QElectronTab.js | 99 +++ .../src/components/common/QElectronTopBar.js | 145 +++++ .../components/common/QElelctronAccordion.js | 97 +++ .../common/__tests__/QElectronCard.test.js | 57 ++ .../common/__tests__/QElectronDrawer.test.js | 104 ++++ .../common/__tests__/QElectronTab.test.js | 54 ++ .../common/__tests__/QElectronTopBar.test.js | 54 ++ .../__tests__/QElelctronAccordion.test.js | 61 ++ .../src/components/qelectron/Circuit.js | 223 +++++++ .../src/components/qelectron/Executor.js | 48 ++ .../src/components/qelectron/Overview.js | 139 +++++ .../src/components/qelectron/QElectronList.js | 575 ++++++++++++++++++ .../qelectron/__tests__/Circuit.test.js | 66 ++ .../qelectron/__tests__/Executor.test.js | 83 +++ .../qelectron/__tests__/Overview.test.js | 52 ++ .../qelectron/__tests__/QElectronList.test.js | 125 ++++ covalent_ui/webapp/src/utils/style.css | 13 + doc/source/api/executors/braketqubit.rst | 122 ++++ doc/source/api/executors/ibmq.rst | 122 ++++ doc/source/api/executors/localbraketqubit.rst | 92 +++ doc/source/api/executors/qiskit.rst | 170 ++++++ doc/source/api/executors/simulator.rst | 56 ++ doc/source/api/qclusters.rst | 6 + doc/source/api/qelectrons.rst | 6 + 53 files changed, 4929 insertions(+) create mode 100644 covalent/executor/qbase.py create mode 100644 covalent/executor/utils/context.py create mode 100644 covalent/quantum/__init__.py create mode 100644 covalent/quantum/qclient/__init__.py create mode 100644 covalent/quantum/qclient/base_client.py create mode 100644 covalent/quantum/qclient/core.py create mode 100644 covalent/quantum/qclient/local_client.py create mode 100644 covalent/quantum/qcluster/__init__.py create mode 100644 covalent/quantum/qcluster/base.py create mode 100644 covalent/quantum/qcluster/clusters.py create mode 100644 covalent/quantum/qcluster/default_selectors.py create mode 100644 covalent/quantum/qcluster/simulator.py create mode 100644 covalent/quantum/qserver/__init__.py create mode 100644 covalent/quantum/qserver/core.py create mode 100644 covalent/quantum/qserver/database.py create mode 100644 covalent/quantum/qserver/serialize.py create mode 100644 covalent/quantum/qserver/servers/__init__.py create mode 100644 covalent/quantum/qserver/servers/local.py create mode 100644 covalent/quantum/qserver/utils.py create mode 100644 covalent_migrations/versions/de0a6c0a3e3d_add_qelectron_data_exists_flag_to_db.py create mode 100644 covalent_ui/webapp/src/assets/codeview.svg create mode 100644 covalent_ui/webapp/src/assets/qelectron/circuit-large.svg create mode 100644 covalent_ui/webapp/src/assets/qelectron/circuit.svg create mode 100644 covalent_ui/webapp/src/assets/qelectron/filter.svg create mode 100644 covalent_ui/webapp/src/assets/qelectron/qelectron.svg create mode 100644 covalent_ui/webapp/src/assets/qelectron/view.svg create mode 100644 covalent_ui/webapp/src/assets/status/running.svg create mode 100644 covalent_ui/webapp/src/components/common/QElectronCard.js create mode 100644 covalent_ui/webapp/src/components/common/QElectronDrawer.js create mode 100644 covalent_ui/webapp/src/components/common/QElectronTab.js create mode 100644 covalent_ui/webapp/src/components/common/QElectronTopBar.js create mode 100644 covalent_ui/webapp/src/components/common/QElelctronAccordion.js create mode 100644 covalent_ui/webapp/src/components/common/__tests__/QElectronCard.test.js create mode 100644 covalent_ui/webapp/src/components/common/__tests__/QElectronDrawer.test.js create mode 100644 covalent_ui/webapp/src/components/common/__tests__/QElectronTab.test.js create mode 100644 covalent_ui/webapp/src/components/common/__tests__/QElectronTopBar.test.js create mode 100644 covalent_ui/webapp/src/components/common/__tests__/QElelctronAccordion.test.js create mode 100644 covalent_ui/webapp/src/components/qelectron/Circuit.js create mode 100644 covalent_ui/webapp/src/components/qelectron/Executor.js create mode 100644 covalent_ui/webapp/src/components/qelectron/Overview.js create mode 100644 covalent_ui/webapp/src/components/qelectron/QElectronList.js create mode 100644 covalent_ui/webapp/src/components/qelectron/__tests__/Circuit.test.js create mode 100644 covalent_ui/webapp/src/components/qelectron/__tests__/Executor.test.js create mode 100644 covalent_ui/webapp/src/components/qelectron/__tests__/Overview.test.js create mode 100644 covalent_ui/webapp/src/components/qelectron/__tests__/QElectronList.test.js create mode 100644 covalent_ui/webapp/src/utils/style.css create mode 100644 doc/source/api/executors/braketqubit.rst create mode 100644 doc/source/api/executors/ibmq.rst create mode 100644 doc/source/api/executors/localbraketqubit.rst create mode 100644 doc/source/api/executors/qiskit.rst create mode 100644 doc/source/api/executors/simulator.rst create mode 100644 doc/source/api/qclusters.rst create mode 100644 doc/source/api/qelectrons.rst diff --git a/covalent/executor/qbase.py b/covalent/executor/qbase.py new file mode 100644 index 000000000..856ce8730 --- /dev/null +++ b/covalent/executor/qbase.py @@ -0,0 +1,343 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +import asyncio +import time +from abc import ABC, abstractmethod +from concurrent.futures import ThreadPoolExecutor +from functools import lru_cache +from threading import Thread +from typing import Any, Dict, List, Optional, Sequence, Union + +import orjson +import pennylane as qml +from mpire import WorkerPool +from pydantic import BaseModel, Extra, Field, root_validator # pylint: disable=no-name-in-module + +from .._shared_files.qinfo import QElectronInfo, QNodeSpecs + +__all__ = [ + "BaseQExecutor", + "BaseProcessPoolQExecutor", + "AsyncBaseQExecutor", + "BaseThreadPoolQExecutor", +] + +SHOTS_DEFAULT = -1 + + +def orjson_dumps(v, *, default): + return orjson.dumps(v, default=default).decode() # pylint: disable=no-member + + +@lru_cache +def get_process_pool(num_processes=None): + return WorkerPool(n_jobs=num_processes) + + +@lru_cache +def get_thread_pool(max_workers=None): + return ThreadPoolExecutor(max_workers=max_workers) + + +@lru_cache +def get_asyncio_event_loop(): + """ + Returns an asyncio event loop running in a separate thread. + """ + + def _run_loop(_loop): + asyncio.set_event_loop(_loop) + _loop.run_forever() + + loop = asyncio.new_event_loop() + thread = Thread(target=_run_loop, args=(loop,), daemon=True) + thread.start() + + # Create function attribute so reference to thread is not lost. + get_asyncio_event_loop.thread = thread + + return loop + + +class BaseQExecutor(ABC, BaseModel): + """ + Base class for all Quantum Executors. + """ + + shots: Union[None, int, Sequence[int], Sequence[Union[int, Sequence[int]]]] = SHOTS_DEFAULT + shots_converter: Optional[type] = None + persist_data: bool = True + + # Executors need to contain certain information about original QNode, in order + # to produce correct results. These attributes below contain that information. + # They are set inside the `QServer` and will be `None` client-side. + qelectron_info: Optional[QElectronInfo] = None + qnode_specs: Optional[QNodeSpecs] = None + + @property + def override_shots(self) -> Union[int, None]: + """ + Fallback to the QNode device's shots if no user-specified shots on executor. + """ + + if self.shots is SHOTS_DEFAULT: + # No user-specified shots. Use the original QNode device's shots instead. + shots = self.qelectron_info.device_shots + shots_converter = self.qelectron_info.device_shots_type + return shots_converter(shots) if shots_converter is not None else shots + if self.shots is None: + # User has specified `shots=None` on executor. + return None + + if isinstance(self.shots, Sequence) and self.shots_converter is not None: + return self.shots_converter(self.shots) + + # User has specified `shots` as an int. + return self.shots + + class Config: + extra = Extra.allow + + @root_validator(pre=True) + def set_name(cls, values): + # pylint: disable=no-self-argument + # Set the `name` attribute to the class name + values["name"] = cls.__name__ + return values + + @abstractmethod + def batch_submit(self, qscripts_list): + raise NotImplementedError + + @abstractmethod + def batch_get_results(self, futures_list): + raise NotImplementedError + + def run_circuit(self, qscript, device, result_obj: "QCResult") -> "QCResult": + start_time = time.perf_counter() + results = qml.execute([qscript], device, gradient_fn="best") + end_time = time.perf_counter() + + result_obj.results = results + result_obj.execution_time = end_time - start_time + + return result_obj + + def dict(self, *args, **kwargs): + dict_ = super().dict(*args, **kwargs) + + # Ensure shots is a hashable value. + shots = dict_.get("shots") + if isinstance(shots, Sequence): + dict_["shots"] = tuple(shots) + + # Set shots converter to recover original sequence type. + shots_converter = dict_.get("shots_converter") + if shots_converter is None: + dict_["shots_converter"] = type(shots) + + return dict_ + + +class QCResult(BaseModel): + """ + Container for results from `run_circuit` methods. Standardizes output and allows + metadata to be updated at various points. + """ + + results: Optional[Any] = None + execution_time: float = None + metadata: Dict[str, Any] = Field(default_factory=lambda: {"execution_metadata": []}) + + def expand(self) -> List["QCResult"]: + """ + Expand result object into a list of result objects, one for each execution. + """ + result_objs = [] + for i, result in enumerate(self.results): + # Copy other non-execution metadata. + _result_obj = QCResult( + results=[result], execution_time=self.execution_time, metadata={} + ) + + # Handle single and multi-component metadata. + execution_metadata = self.metadata["execution_metadata"] + if len(self.metadata["execution_metadata"]) > 0: + execution_metadata = execution_metadata[i] + + # Populate corresponding metadata. + _result_obj.metadata.update( + execution_metadata=[execution_metadata], + device_name=self.metadata["device_name"], + executor_name=self.metadata["executor_name"], + executor_backend_name=self.metadata["executor_backend_name"], + ) + + result_objs.append(_result_obj) + + return result_objs + + @classmethod + def with_metadata(cls, *, device_name: str, executor: BaseQExecutor): + """ + Create a blank instance with pre-set metadata. + """ + result_obj = cls() + backend_name = executor.backend if hasattr(executor, "backend") else "" + result_obj.metadata.update( + device_name=device_name, + executor_name=executor.__class__.__name__, + executor_backend_name=backend_name, + ) + return result_obj + + +class SyncBaseQExecutor(BaseQExecutor): + device: Optional[str] = "default.qubit" + + def run_all_circuits(self, qscripts_list) -> List[QCResult]: + result_objs: List[QCResult] = [] + + for qscript in qscripts_list: + dev = qml.device( + self.device, + wires=self.qelectron_info.device_wires, + shots=self.qelectron_info.device_shots, + ) + + result_obj = QCResult.with_metadata(device_name=dev.short_name, executor=self) + result_obj = self.run_circuit(qscript, dev, result_obj) + result_objs.append(result_obj) + + return result_objs + + def batch_submit(self, qscripts_list): + # Offload execution of all circuits to the same thread + # so that the qserver isn't blocked by their completion. + pool = get_thread_pool() + fut = pool.submit(self.run_all_circuits, qscripts_list) + dummy_futures = [fut] * len(qscripts_list) + return dummy_futures + + def batch_get_results(self, futures_list): + return futures_list[0].result() + + +class AsyncBaseQExecutor(BaseQExecutor): + """ + Executor that uses `asyncio` to handle multiple job submissions + """ + + device: Optional[str] = "default.qubit" + + def batch_submit(self, qscripts_list): + futures = [] + loop = get_asyncio_event_loop() + for qscript in qscripts_list: + dev = qml.device( + self.device, + wires=self.qelectron_info.device_wires, + shots=self.qelectron_info.device_shots, + ) + + result_obj = QCResult.with_metadata( + device_name=dev.short_name, + executor=self, + ) + fut = loop.create_task(self.run_circuit(qscript, dev, result_obj)) + futures.append(fut) + + return futures + + def batch_get_results(self, futures_list: List): + loop = get_asyncio_event_loop() + task = asyncio.run_coroutine_threadsafe(self._get_result(futures_list), loop) + return task.result() + + async def _get_result(self, futures_list: List) -> List[QCResult]: + return await asyncio.gather(*futures_list) + + async def run_circuit(self, qscript, device, result_obj) -> QCResult: + await asyncio.sleep(0) + start_time = time.perf_counter() + results = qml.execute([qscript], device, gradient_fn="best") + end_time = time.perf_counter() + + result_obj.results = results + result_obj.execution_time = end_time - start_time + + return result_obj + + +class BaseProcessPoolQExecutor(BaseQExecutor): + device: Optional[str] = "default.qubit" + num_processes: int = 10 + + def batch_submit(self, qscripts_list): + pool = get_process_pool(self.num_processes) + + futures = [] + for qscript in qscripts_list: + dev = qml.device( + self.device, + wires=self.qelectron_info.device_wires, + shots=self.qelectron_info.device_shots, + ) + + result_obj = QCResult.with_metadata( + device_name=dev.short_name, + executor=self, + ) + fut = pool.apply_async(self.run_circuit, args=(qscript, dev, result_obj)) + futures.append(fut) + + return futures + + def batch_get_results(self, futures_list: List) -> List[QCResult]: + return [fut.get() for fut in futures_list] + + +class BaseThreadPoolQExecutor(BaseQExecutor): + device: Optional[str] = "default.qubit" + num_threads: int = 10 + + def batch_submit(self, qscripts_list): + pool = get_thread_pool(self.num_threads) + + futures = [] + for qscript in qscripts_list: + dev = qml.device( + self.device, + wires=self.qelectron_info.device_wires, + shots=self.qelectron_info.device_shots, + ) + + result_obj = QCResult.with_metadata( + device_name=dev.short_name, + executor=self, + ) + fut = pool.submit(self.run_circuit, qscript, dev, result_obj) + futures.append(fut) + + return futures + + def batch_get_results(self, futures_list: List) -> List[QCResult]: + return [fut.result() for fut in futures_list] diff --git a/covalent/executor/utils/context.py b/covalent/executor/utils/context.py new file mode 100644 index 000000000..a3c0e1969 --- /dev/null +++ b/covalent/executor/utils/context.py @@ -0,0 +1,45 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +from contextlib import contextmanager + +from pydantic import BaseModel + + +class Context(BaseModel): + node_id: int = None + dispatch_id: str = None + + +def get_context(): + return current_context + + +@contextmanager +def set_context(node_id: int, dispatch_id: str): + global current_context + global unset_context + current_context = Context(node_id=node_id, dispatch_id=dispatch_id) + yield + current_context = unset_context + + +unset_context = Context() +current_context = unset_context diff --git a/covalent/quantum/__init__.py b/covalent/quantum/__init__.py new file mode 100644 index 000000000..48984cda2 --- /dev/null +++ b/covalent/quantum/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +from .qcluster import QCluster, Simulator diff --git a/covalent/quantum/qclient/__init__.py b/covalent/quantum/qclient/__init__.py new file mode 100644 index 000000000..47d66bb77 --- /dev/null +++ b/covalent/quantum/qclient/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +from .base_client import BaseQClient +from .local_client import LocalQClient diff --git a/covalent/quantum/qclient/base_client.py b/covalent/quantum/qclient/base_client.py new file mode 100644 index 000000000..1454ccf07 --- /dev/null +++ b/covalent/quantum/qclient/base_client.py @@ -0,0 +1,54 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +from abc import ABC, abstractmethod, abstractproperty + + +class BaseQClient(ABC): + @abstractmethod + def submit(self, qscripts, executors, qelectron_info, qnode_specs): + raise NotImplementedError + + @abstractmethod + def get_results(self, batch_id): + raise NotImplementedError + + @abstractproperty + def selector(self): + raise NotImplementedError + + @abstractproperty + def database(self): + raise NotImplementedError + + # The following methods are abstract because the qserver + # is expecting serialized inputs and will be sending + # back serialized outputs, thus even if these methods + # essentially just pass through, for e.g in the LocalQClient's + # case, they are still to be implemented by the child class and + # should use the same seriliazing/deserializing method as is being + # used by the equivalent qserver. + @abstractmethod + def serialize(self, obj): + raise NotImplementedError + + @abstractmethod + def deserialize(self, ser_obj): + raise NotImplementedError diff --git a/covalent/quantum/qclient/core.py b/covalent/quantum/qclient/core.py new file mode 100644 index 000000000..acaf0f834 --- /dev/null +++ b/covalent/quantum/qclient/core.py @@ -0,0 +1,72 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +from typing import List + +import pennylane as qml + +from .local_client import LocalQClient + + +class MiddleWare: + def __init__(self) -> None: + self.qclient = LocalQClient() + + def __new__(cls): + # Making this a singleton class + if not hasattr(cls, "instance"): + cls.instance = super(MiddleWare, cls).__new__(cls) + return cls.instance + + # The following attributes are properties + # because the qclient might change over time + # and every time it gets changed, we shouldn't + # have to set things like: + # self.database = self.qclient.database + # Thus, we override the access of these attributes + # and return/set these dynamically depending upon + # what the qclient is at that point in time. + + @property + def selector(self): + return self.qclient.selector + + @selector.setter + def selector(self, selector_func): + self.qclient.selector = selector_func + + @property + def database(self): + return self.qclient.database + + def run_circuits_async( + self, + qscripts: List[qml.tape.qscript.QuantumScript], + executors, + qelectron_info, + qnode_specs, + ): + return self.qclient.submit(qscripts, executors, qelectron_info, qnode_specs) + + def get_results(self, batch_id): + return self.qclient.get_results(batch_id) + + +middleware = MiddleWare() diff --git a/covalent/quantum/qclient/local_client.py b/covalent/quantum/qclient/local_client.py new file mode 100644 index 000000000..a10ba2ebc --- /dev/null +++ b/covalent/quantum/qclient/local_client.py @@ -0,0 +1,63 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +from ..._shared_files.utils import cloudpickle_deserialize, cloudpickle_serialize +from ..qserver import LocalQServer +from .base_client import BaseQClient + +# Since in the local case, the server and client are the same +# thus the "server" class's functions are directly accessed + + +class LocalQClient(BaseQClient): + def __init__(self) -> None: + self.qserver = LocalQServer() + + @property + def selector(self): + return self.deserialize(self.qserver.selector) + + @selector.setter + def selector(self, selector_func): + self.qserver.selector = self.serialize(selector_func) + + @property + def database(self): + return self.deserialize(self.qserver.database) + + def submit(self, qscripts, executors, qelectron_info, qnode_specs): + ser_qscripts = self.serialize(qscripts) + ser_executors = self.serialize(executors) + ser_qelectron_info = self.serialize(qelectron_info) + ser_qnode_specs = self.serialize(qnode_specs) + + return self.qserver.submit( + ser_qscripts, ser_executors, ser_qelectron_info, ser_qnode_specs + ) + + def get_results(self, batch_id): + ser_results = self.qserver.get_results(batch_id) + return self.deserialize(ser_results) + + def serialize(self, obj): + return cloudpickle_serialize(obj) + + def deserialize(self, ser_obj): + return cloudpickle_deserialize(ser_obj) diff --git a/covalent/quantum/qcluster/__init__.py b/covalent/quantum/qcluster/__init__.py new file mode 100644 index 000000000..1f3893a16 --- /dev/null +++ b/covalent/quantum/qcluster/__init__.py @@ -0,0 +1,26 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +""" +Defines QExecutors and provides a "manager" to get all available QExecutors +""" + +from .clusters import QCluster +from .simulator import Simulator diff --git a/covalent/quantum/qcluster/base.py b/covalent/quantum/qcluster/base.py new file mode 100644 index 000000000..eed5e371d --- /dev/null +++ b/covalent/quantum/qcluster/base.py @@ -0,0 +1,103 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +import asyncio +from abc import ABC, abstractmethod +from concurrent.futures import Future +from typing import Callable, List, Sequence, Union + +from mpire.async_result import AsyncResult +from pydantic import BaseModel, Extra + +from ...executor.qbase import AsyncBaseQExecutor, BaseQExecutor, QCResult + + +class AsyncBaseQCluster(AsyncBaseQExecutor): + executors: Sequence[BaseQExecutor] + selector: Union[str, Callable] + + _selector_serialized: bool = False + + @abstractmethod + def serialize_selector(self) -> None: + """ + Serializes the cluster's selector function. + """ + raise NotImplementedError + + @abstractmethod + def deserialize_selector(self) -> Union[str, Callable]: + """ + Deserializes the cluster's selector function. + """ + raise NotImplementedError + + @abstractmethod + def dict(self, *args, **kwargs) -> dict: + """ + Custom dict method to create a hashable `executors` attribute. + """ + raise NotImplementedError + + @abstractmethod + def get_selector(self): + """ + Returns the deserialized selector function. + """ + raise NotImplementedError + + async def _get_result(self, futures_list: List) -> List[QCResult]: + """ + Override the base method to handle the case where the `futures_list` + contains a mix of object types from various executors. + """ + results_and_times = [] + for fut in futures_list: + if isinstance(fut, asyncio.Task): + results_and_times.append(await fut) + elif isinstance(fut, Future): + results_and_times.append(fut.result()) + elif isinstance(fut, AsyncResult): + results_and_times.append(fut.get()) + else: + results_and_times.append(fut) + + return results_and_times + + +class BaseQSelector(ABC, BaseModel): + name: str = "base_qselector" + + def __call__(self, qscript, executors): + """ " + Interface used by the quantum server. + """ + return self.selector_function(qscript, executors) + + @abstractmethod + def selector_function(self, qscript, executors): + """ + Implement selection logic here. + """ + raise NotImplementedError + + class Config: + # Allows defining extra state fields in subclasses. + extra = Extra.allow diff --git a/covalent/quantum/qcluster/clusters.py b/covalent/quantum/qcluster/clusters.py new file mode 100644 index 000000000..ce3a0198c --- /dev/null +++ b/covalent/quantum/qcluster/clusters.py @@ -0,0 +1,104 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +import base64 +from typing import Callable, Union + +from ..._shared_files.utils import cloudpickle_deserialize, cloudpickle_serialize +from .base import AsyncBaseQCluster, BaseQExecutor +from .default_selectors import selector_map + +__all__ = [ + "QCluster", +] + + +class QCluster(AsyncBaseQCluster): + """ + A cluster of quantum executors. + + Args: + executors: A sequence of quantum executors. + selector: A callable that selects an executor, or one of the strings "cyclic" + or "random". The "cyclic" selector (default) cycles through `executors` + and returns the next executor for each circuit. The "random" selector + chooses an executor from `executors` at random for each circuit. Any + user-defined selector must be callable with two positional arguments, + a circuit and a list of executors. A selector must also return exactly + one executor. + """ + + selector: Union[str, Callable] = "cyclic" + + # Flag used to indicate whether `self.selector` is currently serialized. + _selector_serialized: bool = False + + def batch_submit(self, qscripts_list): + if self._selector_serialized: + self.selector = self.deserialize_selector() + + selector = self.get_selector() + selected_executor: BaseQExecutor = selector(qscripts_list, self.executors) + + # Copy server-side set attributes into selector executor. + selected_executor.qelectron_info = self.qelectron_info.copy() + return selected_executor.batch_submit(qscripts_list) + + def serialize_selector(self) -> None: + if self._selector_serialized: + return + + # serialize to bytes with cloudpickle + self.selector = cloudpickle_serialize(self.selector) + + # convert to string to make JSON-able + self.selector = base64.b64encode(self.selector).decode("utf-8") + self._selector_serialized = True + + def deserialize_selector(self) -> Union[str, Callable]: + if not self._selector_serialized: + return self.selector + + # Deserialize the selector function (or string). + selector = cloudpickle_deserialize(base64.b64decode(self.selector.encode("utf-8"))) + + self._selector_serialized = False + return selector + + def dict(self, *args, **kwargs) -> dict: + # override `dict` method to convert dict attributes to JSON strings + dict_ = super(AsyncBaseQCluster, self).dict(*args, **kwargs) + dict_.update(executors=tuple(ex.json() for ex in self.executors)) + return dict_ + + def get_selector(self) -> Callable: + """ + Wraps `self.selector` to return defaults corresponding to string values. + + This method is called inside `batch_submit`. + """ + self.selector = self.deserialize_selector() + + if isinstance(self.selector, str): + # use default selector + selector_cls = selector_map[self.selector] + self.selector = selector_cls() + + return self.selector diff --git a/covalent/quantum/qcluster/default_selectors.py b/covalent/quantum/qcluster/default_selectors.py new file mode 100644 index 000000000..07d833964 --- /dev/null +++ b/covalent/quantum/qcluster/default_selectors.py @@ -0,0 +1,57 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +# pylint: disable=too-few-public-methods + +import random + +from .base import BaseQSelector + + +class RandomSelector(BaseQSelector): + """ + A selector that randomly selects an executor. + """ + + name: str = "random" + + def selector_function(self, qscript, executors): + return random.choice(executors) + + +class CyclicSelector(BaseQSelector): + """ + A selector that cycles in order through the available executors. + """ + + name: str = "cyclic" + + _counter: int = 0 + + def selector_function(self, qscript, executors): + executor = executors[self._counter % len(executors)] + self._counter += 1 + return executor + + +selector_map = { + "cyclic": CyclicSelector, + "random": RandomSelector, +} diff --git a/covalent/quantum/qcluster/simulator.py b/covalent/quantum/qcluster/simulator.py new file mode 100644 index 000000000..df84dae4e --- /dev/null +++ b/covalent/quantum/qcluster/simulator.py @@ -0,0 +1,103 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +from typing import Union + +from pydantic import validator + +from ...executor.qbase import ( + BaseProcessPoolQExecutor, + BaseQExecutor, + BaseThreadPoolQExecutor, + SyncBaseQExecutor, +) + +SIMULATOR_DEVICES = [ + "default.qubit", + "default.qubit.autograd", + "default.qubit.jax", + "default.qubit.tf", + "default.qubit.torch", + "default.gaussian", + "lightning.qubit", +] + + +class Simulator(BaseQExecutor): + """ + A quantum executor that uses the specified Pennylane device to execute circuits. + Parallelizes circuit execution on the specified `device` using either threads + or processes. + + Keyword Args: + device: A valid string corresponding to a Pennylane device. Simulation-based + devices (e.g. "default.qubit" and "lightning.qubit") are recommended. + Defaults to "default.qubit" or "default.gaussian" depending on the + decorated QNode's device. + parallel: The type of parallelism to use. Valid values are "thread" and + "process". Passing any other value will result in synchronous execution. + Defaults to "thread". + workers: The number of threads or processes to use. Defaults to 10. + shots: The number of shots to use for the execution device. Overrides the + :code:`shots` value from the original device if set to :code:`None` or + a positive :code:`int`. The shots setting from the original device is + is used by default. + """ + + device: str = "default.qubit" + parallel: Union[bool, str] = "thread" + workers: int = 10 + + @validator("device") + def validate_device(cls, device): # pylint: disable=no-self-argument + """ + Check that the `device` attribute is NOT a provider or hardware device. + """ + if device not in SIMULATOR_DEVICES: + valid_devices = ", ".join(SIMULATOR_DEVICES[::-1] + [f"or {SIMULATOR_DEVICES[-1]}"]) + raise ValueError(f"Simulator device must be {valid_devices}.") + return device + + def batch_submit(self, qscripts_list): + # Defer to original QNode's device type in special cases. + if self.qelectron_info.device_name in ["default.gaussian"]: + device = self.qelectron_info.device_name + else: + device = self.device + + # Select backend batching the chosen method of parallelism. + if self.parallel == "process": + self._backend = BaseProcessPoolQExecutor(num_processes=self.workers, device=device) + elif self.parallel == "thread": + self._backend = BaseThreadPoolQExecutor(num_threads=self.workers, device=device) + else: + self._backend = SyncBaseQExecutor(device=device) + + # Pass on server-set settings from original device. + updates = {"device_name": device, "device_shots": self.override_shots} + self._backend.qelectron_info = self.qelectron_info.copy(update=updates) + self._backend.qnode_specs = self.qnode_specs.copy() + + return self._backend.batch_submit(qscripts_list) + + def batch_get_results(self, futures_list): + return self._backend.batch_get_results(futures_list) + + _backend: BaseQExecutor = None diff --git a/covalent/quantum/qserver/__init__.py b/covalent/quantum/qserver/__init__.py new file mode 100644 index 000000000..add9fe547 --- /dev/null +++ b/covalent/quantum/qserver/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +from .core import QServer +from .servers import LocalQServer diff --git a/covalent/quantum/qserver/core.py b/covalent/quantum/qserver/core.py new file mode 100644 index 000000000..ab3364f37 --- /dev/null +++ b/covalent/quantum/qserver/core.py @@ -0,0 +1,409 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +""" +Quantum Server Implementation: Handles the async execution of quantum circuits. +""" + +import datetime +import uuid +from asyncio import Task +from typing import Callable, List, Tuple + +from pennylane.tape import QuantumScript + +from ..._shared_files.qinfo import QElectronInfo, QNodeSpecs +from ..._shared_files.utils import ( + cloudpickle_deserialize, + cloudpickle_serialize, + select_first_executor, +) +from ...executor.utils import get_context +from ..qcluster.base import AsyncBaseQCluster, BaseQExecutor +from .database import Database +from .utils import CircuitInfo, get_cached_executor, get_circuit_id + + +class FuturesTable: + """ + Container for async task futures corresponding to a sub-batch of executing + qscripts, as identified by a batch UUID. + """ + + def __init__(self): + self._ef_pairs = {} + + def add_executor_future_pairs( + self, + executor_future_pairs: List[Tuple[BaseQExecutor, Task]], + submission_order: List[int], + ) -> str: + """ + Add a list of futures to the table and return a corresponding UUID. + """ + batch_id = str(uuid.uuid4()) + self._ef_pairs[batch_id] = (executor_future_pairs, submission_order) + return batch_id + + def pop_executor_future_pairs( + self, + batch_id: str, + ) -> Tuple[List[Tuple[BaseQExecutor, Task]], List[int]]: + """ + Retrieve a list of futures from the table using a UUID. + """ + return self._ef_pairs.pop(batch_id) + + +class QServer: + """ + Initialize a QServer instance with a given selector function. + """ + + # def __init__(self, selector: BaseQSelector = None) -> None: + def __init__(self, selector: Callable = None) -> None: + self.futures_table = FuturesTable() + + # self._selector = selector or SimpleSelector(selector_function=select_first_executor) + self._selector = selector or select_first_executor + self._database = Database() + + @property + def selector(self): + """ + Executor selector function for the Quantum server. + """ + return self._selector + + @selector.setter + def selector(self, ser_selector): + self._selector = self.deserialize(ser_selector) + + @property + def database(self): + """Return the database for reading.""" + return self.serialize(self._database) + + def select_executors( + self, + qscripts: List[QuantumScript], + executors: List[BaseQExecutor], + qnode_specs: QNodeSpecs, + ): + """ + Links qscripts with an executor + based on the self.selector function + """ + + linked_executors = [] + for qscript in qscripts: + selected_executor = self.selector(qscript, executors) + + # Use cached executor. + selected_executor = get_cached_executor(**selected_executor.dict()) + + if isinstance(selected_executor, AsyncBaseQCluster): + # Apply QCluster's selector as well. + qcluster = selected_executor + selected_executor = qcluster.get_selector()(qscript, qcluster.executors) + + # Use cached executor. + selected_executor = get_cached_executor(**selected_executor.dict()) + + # This is the only place where the qnode_specs are set. + selected_executor.qnode_specs = qnode_specs.copy() + + # An example `linked_executors` will look like: + # [exec_4, exec_4, exec_2, exec_3] + # Their indices corresponding to the indices of `qscripts`. + linked_executors.append(selected_executor) + + return linked_executors + + def submit_to_executors( + self, + qscripts: List[QuantumScript], + linked_executors: List[BaseQExecutor], + qelectron_info: QElectronInfo, + ): + """ + Generates futures for scheduled execution + of qscripts on respective executors + """ + + # Since we will be modifying the qscripts list (or sometimes tuple). + qscripts = list(qscripts).copy() + + submission_order = [] + executor_qscript_sub_batch_pairs = [] + for i, qscript in enumerate(qscripts): + if qscript is None: + continue + + # Generate a sub batch of qscripts to be executed on the same executor + qscript_sub_batch = [linked_executors[i], {i: qscript}] + + # The qscript submission order is stored in this list to ensure that + # the final result is recombined correctly, even if task-circuit + # correspondence is not one-to-one. See, for example, PR #13. + submission_order.append(i) + + for j in range(i + 1, len(qscripts)): + if linked_executors[i] == linked_executors[j]: + qscript_sub_batch[1][j] = qscripts[j] + qscripts[j] = None + submission_order.append(j) + + # An example `qscript_sub_batch` will look like: + # [exec_4, {0: qscript_1, 3: qscript_4}] + + executor_qscript_sub_batch_pairs.append(qscript_sub_batch) + + # An example `executor_qscript_sub_batch_pairs` will look like: + # [ + # [exec_4, {0: qscript_1, 3: qscript_4}], + # [exec_2, {2: qscript_3}], + # [exec_3, {4: qscript_5}], + # ] + + # Generating futures from each executor: + executor_future_pairs = [] + for executor, qscript_sub_batch in executor_qscript_sub_batch_pairs: + executor.qelectron_info = qelectron_info.copy() + qscript_futures = executor.batch_submit(qscript_sub_batch.values()) + + futures_dict = dict(zip(qscript_sub_batch.keys(), qscript_futures)) + # An example `futures_dict` will look like: + # {0: future_1, 3: future_4} + + executor_future_pairs.append([executor, futures_dict]) + + # An example `executor_future_pairs` will look like: + # [[exec_4, {0: future_1, 3: future_4}], [exec_2, {2: future_3}], [exec_3, {4: future_5}]] + + return executor_future_pairs, submission_order + + def submit( + self, + qscripts: List[QuantumScript], + executors: List[BaseQExecutor], + qelectron_info: QElectronInfo, + qnode_specs: QNodeSpecs, + ): + # pylint: disable=too-many-locals + """ + Submit a list of QuantumScripts to the server for execution. + + Args: + qscripts: A list of QuantumScripts to run. + executors: The executors to choose from to use for running the QuantumScripts. + qelectron_info: Information about the qelectron as provided by the user. + qnode_specs: Specifications of the qnode. + + Returns: + str: A UUID corresponding to the batch of submitted QuantumScripts. + """ + + # Get current electron's context + context = get_context() + + # Get qelectron info, qnode specs, quantum scripts, and executors + qelectron_info = self.deserialize(qelectron_info) + qnode_specs = self.deserialize(qnode_specs) + qscripts = self.deserialize(qscripts) + executors = self.deserialize(executors) + + # Generate a list of executors for each qscript. + linked_executors = self.select_executors(qscripts, executors, qnode_specs) + + # Assign qscript sub-batches to unique executors. + executor_future_pairs, submission_order = self.submit_to_executors( + qscripts, linked_executors, qelectron_info + ) + + # Get batch ID for N qscripts being async-executed on M <= N executors. + batch_id = self.futures_table.add_executor_future_pairs( + executor_future_pairs, submission_order + ) + + # Storing the qscripts, executors, and metadata in the database + batch_time = str(datetime.datetime.now()) + key_value_pairs = [[], []] + + for i, qscript in enumerate(qscripts): + circuit_id = get_circuit_id(batch_id, i) + key_value_pairs[0].append(circuit_id) + circuit_info = CircuitInfo( + electron_node_id=context.node_id, + dispatch_id=context.dispatch_id, + circuit_name=qelectron_info.name, + circuit_description=qelectron_info.description, + circuit_diagram=qscript.draw(), + qnode_specs=qnode_specs, + qexecutor=linked_executors[i], + save_time=batch_time, + circuit_id=circuit_id, + qscript=qscript.graph.serialize() if linked_executors[i].persist_data else None, + ).dict() + + key_value_pairs[1].append(circuit_info) + + # An example `key_value_pairs` will look like: + # [ + # [ + # "circuit_0-uuid", + # "circuit_1-uuid", + # + # ... + # ], + # [ + # {"electron_node_id": "node_1", + # "dispatch_id": "uuid", + # "circuit_name": "qscript_name", + # "circuit_description": "qscript_description", + # "qnode_specs": {"qnode_specs": "specs"}, + # "qexecutor": "executor_1", + # "save_time": "2021-01-01 00:00:00", + # "circuit_id": "circuit_0-uuid", + # "qscript": "qscript_1"}, + + # {"electron_node_id": "node_1", + # "dispatch_id": "uuid", + # "circuit_name": "qscript_name", + # "circuit_description": "qscript_description", + # "qnode_specs": {"qnode_specs": "specs"}, + # "qexecutor": "executor_2", + # "save_time": "2021-01-01 00:00:00", + # "circuit_id": "circuit_1-uuid", + # "qscript": "qscript_2"}, + # + # ... + # ], + # ] + + self._database.set( + *key_value_pairs, dispatch_id=context.dispatch_id, node_id=context.node_id + ) + + return batch_id + + def get_results(self, batch_id): + # pylint: disable=too-many-locals + """ + Retrieve the results of previously submitted QuantumScripts from the server. + + Args: + batch_id: The UUID corresponding to the batch of submitted QuantumScripts. + + Returns: + List: An ordered list of results for the submitted QuantumScripts. + """ + + # Get current electron's context + context = get_context() + + results_dict = {} + key_value_pairs = [[], []] + executor_future_pairs, submission_order = self.futures_table.pop_executor_future_pairs( + batch_id + ) + + # ids of (e)xecutor_(f)uture_(p)airs, hence `idx_efp` + qscript_submission_index = 0 + for idx_efp, (executor, futures_sub_batch) in enumerate(executor_future_pairs): + result_objs = executor.batch_get_results(futures_sub_batch.values()) + + # Adding results according to the order of the qscripts + # ids of (f)utures_(s)ub_(b)atch, hence `idx_fsb` + for idx_fsb, circuit_number in enumerate(futures_sub_batch.keys()): + result_obj = result_objs[idx_fsb] + + # Expand `result_obj` in case contains multiple circuits. + # Loop through sub-results to store separately in db. + for result_number, sub_result_obj in enumerate(result_obj.expand()): + qscript_number = submission_order[qscript_submission_index] + + # Use tuple of integers for key to enable later multi-factor sort. + key = (qscript_number, circuit_number, result_number) + results_dict[key] = sub_result_obj.results[0] + qscript_submission_index += 1 + + # To store the results in the database + circuit_id = get_circuit_id(batch_id, circuit_number + result_number) + key_value_pairs[0].append(circuit_id) + key_value_pairs[1].append( + { + "execution_time": sub_result_obj.execution_time, + "result": sub_result_obj.results if executor.persist_data else None, + "result_metadata": sub_result_obj.metadata + if executor.persist_data + else None, + } + ) + + # An example `key_value_pairs` will look like: + # [ + # [ + # "result_circuit_0-uuid", + # "result_circuit_1-uuid", + # ], + # [ + # {"execution_time": "2021-01-01 00:00:00", + # "result": [result_11, ...], + # "result_metadata": [{}, ...]}, + # + # {"execution_time": "2021-01-01 00:00:00", + # "result": [result_21, ...], + # "result_metadata": [{}, ...]}, + # ], + # ] + + # Deleting the futures once their results have been retrieved + del executor_future_pairs[idx_efp][1] + + # After deletion of one `future_sub_batch`, the `executor_future_pairs` will look like: + # [[exec_4], [exec_2, {2: future_3}], [exec_3, {4: future_5}]] + + self._database.set( + *key_value_pairs, dispatch_id=context.dispatch_id, node_id=context.node_id + ) + + # An example `results_dict` will look like: + # {0: result_1, 3: result_4, 2: result_3, 4: result_5} + + # Perform multi-factor sort on `results_dict`. + batch_results = list(dict(sorted(results_dict.items())).values()) + + # An example `batch_results` will look like: + # [result_1, result_2, result_3, result_4, result_5] + + return self.serialize(batch_results) + + def serialize(self, obj): + """ + Serialize an object. + """ + return cloudpickle_serialize(obj) + + def deserialize(self, obj): + """ + Deserialize an object. + """ + return cloudpickle_deserialize(obj) diff --git a/covalent/quantum/qserver/database.py b/covalent/quantum/qserver/database.py new file mode 100644 index 000000000..6ed7caded --- /dev/null +++ b/covalent/quantum/qserver/database.py @@ -0,0 +1,98 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +from pathlib import Path + +from ..._shared_files.config import get_config +from .serialize import JsonLmdb, Strategy +from .utils import CircuitInfo + + +def set_serialization_strategy(strategy_name): + """ + Select a serialization strategy for the database + """ + Database.serialization_strategy = strategy_name + + +class Database: + # dash-separated names result in fallback strategy with try/except loops, + # other valid strategy names uses the one strategy every time + # see `covalent_qelectron/quantum_server/serialize.py` + serialization_strategy = (Strategy.PICKLE, Strategy.ORJSON) + + @property + def strategy_name(self): + # using a property here for dynamic access + # allows runtime strategy selection with `set_serialization_strategy()` + return Database.serialization_strategy + + def __init__(self, db_dir=None): + if db_dir: + self.db_dir = Path(db_dir) + else: + self.db_dir = Path(get_config("dispatcher")["qelectron_db_path"]) + + def _get_db_path(self, dispatch_id, node_id, *, mkdir=False): + dispatch_id = "default-dispatch" if dispatch_id is None else dispatch_id + node_id = "default-node" if node_id is None else node_id + db_path = self.db_dir.joinpath(dispatch_id, f"node-{node_id}") + if mkdir: + db_path.mkdir(parents=True, exist_ok=True) + + return db_path.resolve().absolute() + + def _open(self, dispatch_id, node_id, mkdir=False): + db_path = self._get_db_path(dispatch_id, node_id, mkdir=mkdir) + + if not db_path.exists(): + raise FileNotFoundError(f"Missing database directory {db_path}.") + + return JsonLmdb.open_with_strategy( + file=str(db_path), flag="c", strategy_name=self.strategy_name + ) + + def set(self, keys, values, *, dispatch_id, node_id): + with self._open(dispatch_id, node_id, mkdir=True) as db: + for i, circuit_id in enumerate(keys): + stored_val: dict = db.get(circuit_id, None) + if stored_val is None: + continue + + stored_val.update(values[i]) + values[i] = stored_val + + db.update(dict(zip(keys, values))) + + def get_circuit_ids(self, *, dispatch_id, node_id): + with self._open(dispatch_id, node_id) as db: + return list(db.keys()) + + def get_circuit_info(self, circuit_id, *, dispatch_id, node_id): + with self._open(dispatch_id, node_id) as db: + return CircuitInfo(**db.get(circuit_id, None)) + + def get_db(self, *, dispatch_id, node_id): + db_copy = {} + with self._open(dispatch_id, node_id) as db: + for key, value in db.items(): + db_copy[key] = value + + return db_copy diff --git a/covalent/quantum/qserver/serialize.py b/covalent/quantum/qserver/serialize.py new file mode 100644 index 000000000..8140d03a5 --- /dev/null +++ b/covalent/quantum/qserver/serialize.py @@ -0,0 +1,230 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +""" +Implement several different serialization methods for QNode output data written +to the database. +""" +import warnings +import zlib +from abc import ABC, abstractmethod +from enum import Enum +from typing import Sequence, Union + +import cloudpickle as pickle +import lmdb +import orjson +from lmdbm.lmdbm import Lmdb, remove_lmdbm + + +class _Serializer(ABC): + """ + base class for serializer strategies + """ + + @property + @abstractmethod + def name(self) -> str: + """ + strategy name + """ + + @abstractmethod + def pre_value(self, value): + """ + returns processed value to be written to db + """ + raise NotImplementedError() + + @abstractmethod + def post_value(self, value): + """ + post-process value read from db and return + """ + raise NotImplementedError() + + +class _OrjsonStrategy(_Serializer): + """ + uses `orjson` and `zlib` to serialize/deserialize + """ + + name = "orjson" + + def pre_value(self, value): + return zlib.compress(orjson.dumps(value, option=orjson.OPT_SERIALIZE_NUMPY)) + + def post_value(self, value): + return orjson.loads(zlib.decompress(value)) + + +class _PickleStrategy(_Serializer): + """ + uses `cloudpickle` and `zlib` to serialize/deserialize + """ + + name = "pickle" + + def pre_value(self, value): + return zlib.compress(pickle.dumps(value)) + + def post_value(self, value): + return pickle.loads(zlib.decompress(value)) + + +class _FallbackStrategy(_Serializer): + """ + tries multiple strategies until success + """ + + name = "fallback" + + def __init__(self, strategies): + self.strategies = strategies + + def pre_value(self, value): + for strategy in self.strategies: + try: + return strategy.pre_value(value) + except TypeError as te: + warnings.warn( + f"serialization strategy '{strategy.name}' failed on `pre_value`; " + f"value: {value}; error: {te}." + ) + raise RuntimeError("all strategies failed to encode data") + + def post_value(self, value): + for strategy in self.strategies: + try: + return strategy.post_value(value) + except TypeError as te: + warnings.warn( + f"serialization strategy '{strategy.name}' failed on `post_value`; " + f"value: {value}; error: {te}." + ) + raise RuntimeError("all strategies failed to decode data") + + +class Strategy(Enum): + """ + available serialization strategies + """ + + ORJSON = _OrjsonStrategy + PICKLE = _PickleStrategy + FALLBACK = _FallbackStrategy + + +class JsonLmdb(Lmdb): + """ + custom `Lmdb` implementation with pre- and post-value strategy option + """ + + def __init__(self, strategy_type: Union[Strategy, Sequence[Strategy]], **kw): + self._strategy_map = {} + self.strategy = self.init_strategy(strategy_type) + super().__init__(**kw) + + def _pre_key(self, key): + return key.encode("utf-8") + + def _post_key(self, key): + return key.decode("utf-8") + + def _pre_value(self, value): + return self.strategy.pre_value(value) + + def _post_value(self, value): + return self.strategy.post_value(value) + + @property + def strategy_map(self): + """ + allows access to strategies by str name + """ + if not self._strategy_map: + self._strategy_map = {s.value.name: s.value for s in list(Strategy)} + return self._strategy_map + + def init_strategy(self, strategy_type) -> _Serializer: + """ + initialize an instance of the named strategy + """ + if isinstance(strategy_type, Strategy): + return self._init_single_strategy(strategy_type) + + # strategy with fallback + strategies = [self._init_single_strategy(typ) for typ in strategy_type] + return _FallbackStrategy(strategies) + + def _init_single_strategy(self, strategy_type) -> _Serializer: + if not isinstance(strategy_type, Strategy): + raise TypeError(f"expected Strategy, not {type(strategy_type.__class__.__name__)}") + + strategy_cls = self.strategy_map.get(strategy_type.value.name) + if strategy_cls is None: + raise ValueError(f"unknown database strategy '{strategy_type}'") + return strategy_cls() + + @classmethod + def open_with_strategy( + cls, + file, + flag="r", + mode=0o755, + map_size=2**20, + *, + strategy_name, + autogrow=True, + ): + """ + Custom open classmethod that takes a (new) strategy argument. Mostly + replicates original `Lmdb.open`, except passing `strategy_name` to initializer. + + Opens the database `file`. + `flag`: r (read only, existing), w (read and write, existing), + c (read, write, create if not exists), n (read, write, overwrite existing) + `map_size`: Initial database size. Defaults to 2**20 (1MB). + `autogrow`: Automatically grow the database size when `map_size` is exceeded. + WARNING: Set this to `False` for multi-process write access. + `strategy_name`: either 'orjson' or 'pickle' + """ + + if flag == "r": # Open existing database for reading only (default) + env = lmdb.open( + file, map_size=map_size, max_dbs=1, readonly=True, create=False, mode=mode + ) + elif flag == "w": # Open existing database for reading and writing + env = lmdb.open( + file, map_size=map_size, max_dbs=1, readonly=False, create=False, mode=mode + ) + elif flag == "c": # Open database for reading and writing, creating it if it doesn't exist + env = lmdb.open( + file, map_size=map_size, max_dbs=1, readonly=False, create=True, mode=mode + ) + elif flag == "n": # Always create a new, empty database, open for reading and writing + remove_lmdbm(file) + env = lmdb.open( + file, map_size=map_size, max_dbs=1, readonly=False, create=True, mode=mode + ) + else: + raise ValueError("Invalid flag") + + return cls(strategy_name, env=env, autogrow=autogrow) diff --git a/covalent/quantum/qserver/servers/__init__.py b/covalent/quantum/qserver/servers/__init__.py new file mode 100644 index 000000000..5391d2f13 --- /dev/null +++ b/covalent/quantum/qserver/servers/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +from .local import LocalQServer diff --git a/covalent/quantum/qserver/servers/local.py b/covalent/quantum/qserver/servers/local.py new file mode 100644 index 000000000..9e08aef1f --- /dev/null +++ b/covalent/quantum/qserver/servers/local.py @@ -0,0 +1,23 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +from ..core import QServer + +LocalQServer = QServer diff --git a/covalent/quantum/qserver/utils.py b/covalent/quantum/qserver/utils.py new file mode 100644 index 000000000..1ae2f7fd0 --- /dev/null +++ b/covalent/quantum/qserver/utils.py @@ -0,0 +1,81 @@ +# Copyright 2023 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +import datetime +import importlib +from functools import lru_cache +from typing import Any, Dict, List, Optional, Union + +import orjson +from pydantic import BaseModel + +from ..._shared_files.qinfo import QNodeSpecs +from ...executor.qbase import BaseQExecutor + +BATCH_ID_SEPARATOR = "@" +MAX_DIFFERENT_EXECUTORS = 10 + + +class CircuitInfo(BaseModel): + electron_node_id: Optional[int] = None + dispatch_id: Optional[str] = None + circuit_name: Optional[str] = None + circuit_description: Optional[str] = None + circuit_diagram: Optional[str] = None + qnode_specs: Optional[Union[Dict[str, Any], QNodeSpecs]] = None + qexecutor: Optional[BaseQExecutor] = None + save_time: datetime.datetime + circuit_id: Optional[str] = None + qscript: Optional[str] = None + execution_time: Optional[float] = None + result: Optional[List[Any]] = None + result_metadata: Optional[List[Dict[str, Any]]] = None + + +@lru_cache +def get_cached_module(): + return importlib.import_module(".executor", package="covalent") + + +def executor_from_dict(executor_dict: Dict): + if "executors" in executor_dict: + executors = [executor_from_dict(ed) for ed in executor_dict["executors"]] + executor_dict["executors"] = executors + + name = executor_dict["name"] + executor_class = getattr(get_cached_module(), name) + return executor_class(**executor_dict) + + +@lru_cache(maxsize=MAX_DIFFERENT_EXECUTORS) +def get_cached_executor(**executor_dict): + if "executors" in executor_dict: + executors = tuple(orjson.loads(ex) for ex in executor_dict["executors"]) + executor_dict["executors"] = executors + + return executor_from_dict(executor_dict) + + +def reconstruct_executors(deconstructed_executors: List[Dict]): + return [executor_from_dict(de) for de in deconstructed_executors] + + +def get_circuit_id(batch_id, circuit_number): + return f"circuit_{circuit_number}{BATCH_ID_SEPARATOR}{batch_id}" diff --git a/covalent_migrations/versions/de0a6c0a3e3d_add_qelectron_data_exists_flag_to_db.py b/covalent_migrations/versions/de0a6c0a3e3d_add_qelectron_data_exists_flag_to_db.py new file mode 100644 index 000000000..3be777400 --- /dev/null +++ b/covalent_migrations/versions/de0a6c0a3e3d_add_qelectron_data_exists_flag_to_db.py @@ -0,0 +1,60 @@ +# Copyright 2021 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +"""add qelectron_data_exists flag to db + +Revision ID: de0a6c0a3e3d +Revises: f64ecaa040d5 +Create Date: 2023-05-29 15:53:25.621195 + +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +# pragma: allowlist nextline secret +revision = "de0a6c0a3e3d" +# pragma: allowlist nextline secret +down_revision = "f64ecaa040d5" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("electron_dependency", schema=None) as batch_op: + batch_op.create_foreign_key("electron_link", "electrons", ["parent_electron_id"], ["id"]) + + with op.batch_alter_table("electrons", schema=None) as batch_op: + batch_op.add_column(sa.Column("qelectron_data_exists", sa.Boolean(), nullable=False)) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("electrons", schema=None) as batch_op: + batch_op.drop_column("qelectron_data_exists") + + with op.batch_alter_table("electron_dependency", schema=None) as batch_op: + batch_op.drop_constraint("electron_link", type_="foreignkey") + + # ### end Alembic commands ### diff --git a/covalent_ui/webapp/src/assets/codeview.svg b/covalent_ui/webapp/src/assets/codeview.svg new file mode 100644 index 000000000..a6181c7b9 --- /dev/null +++ b/covalent_ui/webapp/src/assets/codeview.svg @@ -0,0 +1,3 @@ + + + diff --git a/covalent_ui/webapp/src/assets/qelectron/circuit-large.svg b/covalent_ui/webapp/src/assets/qelectron/circuit-large.svg new file mode 100644 index 000000000..44b551ad9 --- /dev/null +++ b/covalent_ui/webapp/src/assets/qelectron/circuit-large.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/covalent_ui/webapp/src/assets/qelectron/circuit.svg b/covalent_ui/webapp/src/assets/qelectron/circuit.svg new file mode 100644 index 000000000..d09364479 --- /dev/null +++ b/covalent_ui/webapp/src/assets/qelectron/circuit.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/covalent_ui/webapp/src/assets/qelectron/filter.svg b/covalent_ui/webapp/src/assets/qelectron/filter.svg new file mode 100644 index 000000000..3a258ffab --- /dev/null +++ b/covalent_ui/webapp/src/assets/qelectron/filter.svg @@ -0,0 +1,3 @@ + + + diff --git a/covalent_ui/webapp/src/assets/qelectron/qelectron.svg b/covalent_ui/webapp/src/assets/qelectron/qelectron.svg new file mode 100644 index 000000000..610787b8a --- /dev/null +++ b/covalent_ui/webapp/src/assets/qelectron/qelectron.svg @@ -0,0 +1,4 @@ + + + + diff --git a/covalent_ui/webapp/src/assets/qelectron/view.svg b/covalent_ui/webapp/src/assets/qelectron/view.svg new file mode 100644 index 000000000..21bf285a2 --- /dev/null +++ b/covalent_ui/webapp/src/assets/qelectron/view.svg @@ -0,0 +1,3 @@ + + + diff --git a/covalent_ui/webapp/src/assets/status/running.svg b/covalent_ui/webapp/src/assets/status/running.svg new file mode 100644 index 000000000..69c5aaeaf --- /dev/null +++ b/covalent_ui/webapp/src/assets/status/running.svg @@ -0,0 +1,3 @@ + + + diff --git a/covalent_ui/webapp/src/components/common/QElectronCard.js b/covalent_ui/webapp/src/components/common/QElectronCard.js new file mode 100644 index 000000000..9b635e867 --- /dev/null +++ b/covalent_ui/webapp/src/components/common/QElectronCard.js @@ -0,0 +1,152 @@ +/* eslint-disable react/jsx-no-comment-textnodes */ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ + +import { Button, Grid, Typography, SvgIcon, Chip } from '@mui/material' +import React from 'react' +import { ReactComponent as ViewSvg } from '../../assets/qelectron/view.svg' +import { ReactComponent as QelectronSvg } from '../../assets/qelectron/qelectron.svg' +import { formatQElectronTime } from '../../utils/misc' + +const QElectronCard = (props) => { + const { qElectronDetails, toggleQelectron, openQelectronDrawer } = props + + const handleButtonClick = (e) => { + e.stopPropagation() + toggleQelectron() + } + + return ( + //main container + theme.palette.background.default, + border: '1px solid', + borderRadius: '8px', + cursor: 'pointer', + borderColor: (theme) => theme.palette.primary.grey, + '&:hover': { + backgroundColor: (theme) => theme.palette.background.coveBlack02, + }, + }} + data-testid="QelectronCard-grid" + > + + + + qelectrons + + + + + + + + + + + + + Quantum Calls + + {qElectronDetails.total_quantum_calls} + + + + Avg Time Of Call + + {formatQElectronTime(qElectronDetails.avg_quantum_calls)} + + + + + ) +} + +export default QElectronCard diff --git a/covalent_ui/webapp/src/components/common/QElectronDrawer.js b/covalent_ui/webapp/src/components/common/QElectronDrawer.js new file mode 100644 index 000000000..c7f79dd39 --- /dev/null +++ b/covalent_ui/webapp/src/components/common/QElectronDrawer.js @@ -0,0 +1,213 @@ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ + +import React, { useEffect } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { + Grid, + Drawer, + Snackbar, + SvgIcon +} from '@mui/material' +import QElectronTopBar from './QElectronTopBar' +import { ReactComponent as closeIcon } from '../../assets/close.svg' +import QElelctronAccordion from './QElelctronAccordion' +import QElectronList from '../qelectron/QElectronList' +import { + qelectronJobs, + qelectronJobOverview +} from '../../redux/electronSlice' + +const nodeDrawerWidth = 1110 + +const QElectronDrawer = ({ toggleQelectron, openQelectronDrawer, dispatchId, electronId }) => { + const dispatch = useDispatch() + const [expanded, setExpanded] = React.useState(true) + const [currentJob, setCurrentJob] = React.useState(''); + const [defaultId, setDefaultId] = React.useState(''); + + const isErrorJobs = useSelector( + (state) => state.electronResults.qelectronJobsList.error + ); + + const isErrorOverview = useSelector( + (state) => state.electronResults.qelectronJobOverviewList.error + ); + const [openSnackbar, setOpenSnackbar] = React.useState(Boolean(isErrorOverview) || Boolean(isErrorJobs)); + const [snackbarMessage, setSnackbarMessage] = React.useState(null); + + // check if there are any API errors and show a sncakbar + useEffect(() => { + if (isErrorOverview) { + setOpenSnackbar(true) + if (isErrorOverview?.detail && isErrorOverview?.detail?.length > 0 && isErrorOverview?.detail[0] && isErrorOverview?.detail[0]?.msg) { + setSnackbarMessage(isErrorOverview?.detail[0]?.msg) + } + else { + setSnackbarMessage( + 'Something went wrong,please contact the administrator!' + ) + } + } + if (isErrorJobs) { + setOpenSnackbar(true) + if (isErrorJobs?.detail && isErrorJobs?.detail?.length > 0 && isErrorJobs?.detail[0] && isErrorJobs?.detail[0]?.msg) { + setSnackbarMessage(isErrorJobs?.detail[0]?.msg) + } + else { + setSnackbarMessage( + 'Something went wrong,please contact the administrator!' + ) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isErrorOverview, isErrorJobs]); + + const listData = useSelector( + (state) => state.electronResults.qelectronJobs + ); + + const overviewData = useSelector( + (state) => state.electronResults.qelectronJobOverview + ); + + const handleDrawerClose = () => { + toggleQelectron() + } + + const rowClickHandler = (job_id) => { + setCurrentJob(job_id) + if (job_id !== currentJob) dispatch(qelectronJobOverview({ dispatchId, electronId, jobId: job_id })) + } + + const details = { + title: overviewData?.overview?.job_name, + status: overviewData?.overview?.status, + id: currentJob + } + + useEffect(() => { + setDefaultId(''); + if (openQelectronDrawer) { + setExpanded(true); + } + if (!(electronId === null || electronId === undefined) && openQelectronDrawer) { + const bodyParams = { + sort_by: 'start_time', + direction: 'DESC', + offset: 0 + } + dispatch( + qelectronJobs({ + dispatchId, + electronId, + bodyParams + }) + ).then((res) => { + let job_id = '' + if (res?.payload && res?.payload.length > 0 && res?.payload[0]?.job_id) job_id = res?.payload[0]?.job_id + setCurrentJob(job_id) + setDefaultId(job_id) + dispatch(qelectronJobOverview({ dispatchId, electronId, jobId: job_id })) + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [openQelectronDrawer]) + + return ( + <> + setOpenSnackbar(false)} + action={ + setOpenSnackbar(false)} + /> + } + /> + ({ + position: 'relative', + width: nodeDrawerWidth, + '& .MuiDrawer-paper': { + width: nodeDrawerWidth, + boxSizing: 'border-box', + border: 'none', + p: 3, + marginRight: '10px', + marginTop: '22px', + maxHeight: '95vh', + bgcolor: (theme) => theme.palette.background.qelectronDrawerbg, + boxShadow: '0px 16px 50px rgba(0, 0, 0, 0.9)', + backdropFilter: 'blur(8px)', + borderRadius: '16px', + '@media (max-width: 1290px)': { + height: '92vh', + marginTop: '70px', + }, + }, + })} + anchor="right" + variant="persistent" + open={openQelectronDrawer} + onClose={handleDrawerClose} + data-testid="qElectronDrawer" + > + + + + + + + + + + ) +} + +export default QElectronDrawer diff --git a/covalent_ui/webapp/src/components/common/QElectronTab.js b/covalent_ui/webapp/src/components/common/QElectronTab.js new file mode 100644 index 000000000..3f807c62a --- /dev/null +++ b/covalent_ui/webapp/src/components/common/QElectronTab.js @@ -0,0 +1,99 @@ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ +import React from 'react' +import Box from '@mui/material/Box' +import Tab from '@mui/material/Tab' +import TabContext from '@mui/lab/TabContext' +import TabList from '@mui/lab/TabList' +import Divider from '@mui/material/Divider' + +const QElectronTab = (props) => { + const { handleChange, value } = props + return ( + + + theme.palette.text.tertiary, + '&.Mui-selected': { + color: (theme) => theme.palette.text.secondary, + }, + }, + }} + > + + + + + + + + + ) +} + +export default QElectronTab diff --git a/covalent_ui/webapp/src/components/common/QElectronTopBar.js b/covalent_ui/webapp/src/components/common/QElectronTopBar.js new file mode 100644 index 000000000..7d1530379 --- /dev/null +++ b/covalent_ui/webapp/src/components/common/QElectronTopBar.js @@ -0,0 +1,145 @@ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ + +import { Grid, IconButton, Typography, Box, Tooltip, Skeleton } from '@mui/material' +import React from 'react' +import { ChevronRight, } from '@mui/icons-material' +import { statusIcon, statusColor, statusLabel } from '../../utils/misc' +import CopyButton from './CopyButton' +import { useSelector } from 'react-redux'; + +const QElectronTopBar = (props) => { + const { details, toggleQelectron } = props + const qelectronJobOverviewIsFetching = useSelector( + (state) => state.electronResults.qelectronJobOverviewList.isFetching + ) + return ( + theme.palette.background.coveBlack02, + }} + data-testid="QelectronTopBar-grid" + > + + toggleQelectron()} + data-testid="backbtn" + sx={{ + color: 'text.disabled', + cursor: 'pointer', + mr: 1, + backgroundColor: (theme) => theme.palette.background.buttonBg, + borderRadius: '10px', + width: '32px', + height: '32px', + }} + > + + + {qelectronJobOverviewIsFetching && !details ? + : <> + + {details?.title} + + } + theme.palette.background.coveBlack02, + borderRadius: '8px 0px 0px 8px', + backgroundColor: (theme) => theme.palette.background.buttonBg, + }} + > + {qelectronJobOverviewIsFetching && !details ? + : <> + +
+ {details?.id} +
+
+ } +
+ theme.palette.background.coveBlack02, + borderRadius: '0px 8px 8px 0px', + backgroundColor: (theme) => theme.palette.background.buttonBg, + }} + > + + +
+ + {qelectronJobOverviewIsFetching && !details ? + : <> + + {statusIcon(details?.status)} +   + {statusLabel(details?.status)} + + } + +
+ ) +} + +export default QElectronTopBar diff --git a/covalent_ui/webapp/src/components/common/QElelctronAccordion.js b/covalent_ui/webapp/src/components/common/QElelctronAccordion.js new file mode 100644 index 000000000..101499a88 --- /dev/null +++ b/covalent_ui/webapp/src/components/common/QElelctronAccordion.js @@ -0,0 +1,97 @@ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ + +import React from 'react' +import Accordion from '@mui/material/Accordion' +import AccordionSummary from '@mui/material/AccordionSummary' +import AccordionDetails from '@mui/material/AccordionDetails' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import { Grid, IconButton } from '@mui/material' +import QElectronTab from './QElectronTab' +import Overview from '../qelectron/Overview' +import Circuit from '../qelectron/Circuit' +import Executor from '../qelectron/Executor' + +const QElelctronAccordion = (props) => { + const { expanded, setExpanded, overviewData, openQelectronDrawer } = props + const [value, setValue] = React.useState('1') + + React.useEffect(() => { + if (openQelectronDrawer) setValue('1') + }, [openQelectronDrawer]); + + const handleAccordChange = () => { + setExpanded(!expanded) + } + + const handleChange = (event, newValue) => { + setValue(newValue) + setExpanded(true) + } + return ( + + theme.palette.background.qelectronDrawerbg, + border: '2px solid', + borderRadius: '8px', + minHeight: expanded ? '19rem' : '2rem', + borderColor: (theme) => theme.palette.background.qelectronbg, + }} + > + + + + } + aria-controls="panel1a-content" + id="panel1a-header" + > + + + + {value === '1' && } + {value === '2' && } + {value === '3' && } + + + + ) +} + +export default QElelctronAccordion diff --git a/covalent_ui/webapp/src/components/common/__tests__/QElectronCard.test.js b/covalent_ui/webapp/src/components/common/__tests__/QElectronCard.test.js new file mode 100644 index 000000000..b238756b8 --- /dev/null +++ b/covalent_ui/webapp/src/components/common/__tests__/QElectronCard.test.js @@ -0,0 +1,57 @@ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ +import { fireEvent, screen, render } from '@testing-library/react' +import App from '../QElectronCard' +import { BrowserRouter } from 'react-router-dom' +import React, { useState } from 'react' +import { Provider } from 'react-redux' +import reducers from '../../../redux/reducers' +import { configureStore } from '@reduxjs/toolkit' +import theme from '../../../utils/theme' +import ThemeProvider from '@mui/system/ThemeProvider' + +function reduxRender(renderedComponent) { + const store = configureStore({ + reducer: reducers, + }) + + return render( + + + {renderedComponent} + + + ) +} + +describe('Qelectron card', () => { + const cardDetails = { + 'total_quantum_calls': 10, + 'avg_quantum_calls': 0.01 + } + test('Qelectron Card is rendered', () => { + reduxRender() + const linkElement = screen.getByTestId('QelectronCard-grid') + expect(linkElement).toBeInTheDocument() + }) + +}) diff --git a/covalent_ui/webapp/src/components/common/__tests__/QElectronDrawer.test.js b/covalent_ui/webapp/src/components/common/__tests__/QElectronDrawer.test.js new file mode 100644 index 000000000..e5e7f211c --- /dev/null +++ b/covalent_ui/webapp/src/components/common/__tests__/QElectronDrawer.test.js @@ -0,0 +1,104 @@ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ +import { fireEvent, screen, render } from '@testing-library/react' +import App from '../QElectronDrawer' +import { BrowserRouter } from 'react-router-dom' +import React, { useState } from 'react' +import { Provider } from 'react-redux' +import reducers from '../../../redux/reducers' +import { configureStore } from '@reduxjs/toolkit' +import theme from '../../../utils/theme' +import ThemeProvider from '@mui/system/ThemeProvider' + +function reduxRender(renderedComponent) { + const initialState = { + electronResults: { + qelectronJobsList: { + error: true + }, + qelectronJobOverviewList: { + error: true + } + } + } + const store = configureStore({ + reducer: reducers, + preloadedState: initialState, + }) + + return render( + + + {renderedComponent} + + + ) +} + + +function reduxRenderMock(renderedComponent) { + const initialState = { + electronResults: { + qelectronJobsList: { + error: { + detail: [{ 'msg': 'Something went wrong' }] + } + }, + qelectronJobOverviewList: { + error: { + detail: [{ 'msg': 'Something went wrong' }] + } + } + } + } + const store = configureStore({ + reducer: reducers, + preloadedState: initialState, + }) + + return render( + + + {renderedComponent} + + + ) +} + +describe('Qelectron drawer', () => { + + test('Qelectron Drawer is rendered', () => { + reduxRender() + const linkElement = screen.getByTestId('qElectronDrawer') + expect(linkElement).toBeInTheDocument() + }) + + test('Qelectron Drawer is rendered with error detail', () => { + reduxRenderMock() + const linkElement = screen.getByTestId('qElectronDrawer') + expect(linkElement).toBeInTheDocument() + const ele = screen.getByTestId('qElectronDrawerSnackbar') + expect(ele).toBeInTheDocument() + fireEvent.click(ele) + }) + +}) diff --git a/covalent_ui/webapp/src/components/common/__tests__/QElectronTab.test.js b/covalent_ui/webapp/src/components/common/__tests__/QElectronTab.test.js new file mode 100644 index 000000000..e272bd03a --- /dev/null +++ b/covalent_ui/webapp/src/components/common/__tests__/QElectronTab.test.js @@ -0,0 +1,54 @@ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ +import { fireEvent, screen, render } from '@testing-library/react' +import App from '../QElectronTab' +import { BrowserRouter } from 'react-router-dom' +import React, { useState } from 'react' +import { Provider } from 'react-redux' +import reducers from '../../../redux/reducers' +import { configureStore } from '@reduxjs/toolkit' +import theme from '../../../utils/theme' +import ThemeProvider from '@mui/system/ThemeProvider' + +function reduxRender(renderedComponent) { + const store = configureStore({ + reducer: reducers, + }) + + return render( + + + {renderedComponent} + + + ) +} + +describe('Qelectron Tab', () => { + + test('Qelectron Tab is rendered', () => { + reduxRender() + const linkElement = screen.getByTestId('QelectronTab-box') + expect(linkElement).toBeInTheDocument() + }) + +}) diff --git a/covalent_ui/webapp/src/components/common/__tests__/QElectronTopBar.test.js b/covalent_ui/webapp/src/components/common/__tests__/QElectronTopBar.test.js new file mode 100644 index 000000000..ceedcfab2 --- /dev/null +++ b/covalent_ui/webapp/src/components/common/__tests__/QElectronTopBar.test.js @@ -0,0 +1,54 @@ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ +import { fireEvent, screen, render } from '@testing-library/react' +import App from '../QElectronTopBar' +import { BrowserRouter } from 'react-router-dom' +import React, { useState } from 'react' +import { Provider } from 'react-redux' +import reducers from '../../../redux/reducers' +import { configureStore } from '@reduxjs/toolkit' +import theme from '../../../utils/theme' +import ThemeProvider from '@mui/system/ThemeProvider' + +function reduxRender(renderedComponent) { + const store = configureStore({ + reducer: reducers, + }) + + return render( + + + {renderedComponent} + + + ) +} + +describe('Qelectron Top Bar', () => { + + test('Qelectron TopBar is rendered', () => { + reduxRender() + const linkElement = screen.getByTestId('QelectronTopBar-grid') + expect(linkElement).toBeInTheDocument() + }) + +}) diff --git a/covalent_ui/webapp/src/components/common/__tests__/QElelctronAccordion.test.js b/covalent_ui/webapp/src/components/common/__tests__/QElelctronAccordion.test.js new file mode 100644 index 000000000..1a2e88a7c --- /dev/null +++ b/covalent_ui/webapp/src/components/common/__tests__/QElelctronAccordion.test.js @@ -0,0 +1,61 @@ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ +import { fireEvent, screen, render } from '@testing-library/react' +import App from '../QElelctronAccordion' +import { BrowserRouter } from 'react-router-dom' +import React, { useState } from 'react' +import { Provider } from 'react-redux' +import reducers from '../../../redux/reducers' +import { configureStore } from '@reduxjs/toolkit' +import theme from '../../../utils/theme' +import ThemeProvider from '@mui/system/ThemeProvider' + +function reduxRender(renderedComponent) { + const store = configureStore({ + reducer: reducers + }) + + return render( + + + {renderedComponent} + + + ) +} + +describe('Qelectron accordion', () => { + + test('Qelectron Accordion is rendered', () => { + reduxRender() + const linkElement = screen.getByTestId('Accordion-grid') + expect(linkElement).toBeInTheDocument() + }) + + test('Qelectron accordion click events', () => { + reduxRender() + const ele = screen.getByLabelText('Toggle accordion') + expect(ele).toBeInTheDocument() + fireEvent.click(ele) + }) + +}) diff --git a/covalent_ui/webapp/src/components/qelectron/Circuit.js b/covalent_ui/webapp/src/components/qelectron/Circuit.js new file mode 100644 index 000000000..970047597 --- /dev/null +++ b/covalent_ui/webapp/src/components/qelectron/Circuit.js @@ -0,0 +1,223 @@ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ + +import { Grid, Typography, SvgIcon, Box, Modal, Paper, Skeleton } from '@mui/material' +import React, { useState } from 'react' +import theme from '../../utils/theme' +import { ReactComponent as CircuitLarge } from '../../assets/qelectron/circuit-large.svg' +import { ReactComponent as CloseSvg } from '../../assets/close.svg' +import SyntaxHighlighter from '../common/SyntaxHighlighter' +import { useSelector } from 'react-redux'; + +const styles = { + outline: 'none', + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + p: 4, + width: ' 95%', + height: '95%', + bgcolor: '#0B0B11E5', + border: '2px solid transparent', + boxShadow: 24, +} + +const SingleGrid = ({ title, value, id }) => { + + const qelectronJobOverviewIsFetching = useSelector( + (state) => state.electronResults.qelectronJobOverviewList.isFetching + ); + + return ( + + theme.palette.text.tertiary, + }} + > + {title} + + {qelectronJobOverviewIsFetching && !value ? + : <> + theme.palette.text.primary, + }} + > + {value || value === 0 ? value : '-'} + + } + + ) +} + +const Circuit = ({ circuitDetails }) => { + const [openModal, setOpenModal] = useState(false) + const [circuitData, setCircuitData] = useState(circuitDetails); + + const handleClose = () => { + setOpenModal(false) + } + const qelectronJobOverviewIsFetching = useSelector( + (state) => state.electronResults.qelectronJobOverviewList.isFetching + ); + + React.useEffect(() => { + const details = { ...circuitData }; + const gatesArray = []; + Object?.keys(details)?.forEach((item, index) => { + const obj = {}; + if (/qbit[0-9]+_gates/.test(item)) { + obj['value'] = details[item]; + const i = item?.substring(4, item?.indexOf('_')); + obj['title'] = `No. ${i}-Qubit Gates`; + obj['id'] = item; + gatesArray?.push(obj); + } + }) + details['gates'] = [...gatesArray]; + setCircuitData({ ...details }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [circuitDetails]) + + const renderQubitgates = () => { + return circuitData?.gates?.map((detail, index) => ( + + )); + } + + return ( + + + + {renderQubitgates()} + + + + theme.palette.text.tertiary, + }} + > + Circuit + + + + ({ + bgcolor: theme.palette.background.outRunBg, + })} + > + {' '} + + + + + + + + + + + theme.palette.text.primary, + }} + component={CircuitLarge} + viewBox="0 0 900 320" // Specify the viewBox to match the desired container size + /> + + + + + + + + + + + + + + + ) +} + +export default Circuit diff --git a/covalent_ui/webapp/src/components/qelectron/Executor.js b/covalent_ui/webapp/src/components/qelectron/Executor.js new file mode 100644 index 000000000..64ae6000b --- /dev/null +++ b/covalent_ui/webapp/src/components/qelectron/Executor.js @@ -0,0 +1,48 @@ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ + +import { Grid, Paper } from '@mui/material' +import React from 'react' +import SyntaxHighlighter from '../common/SyntaxHighlighter' +import { useSelector } from 'react-redux'; + +const Executor = (props) => { + const { code } = props + const qelectronJobOverviewIsFetching = useSelector( + (state) => state.electronResults.qelectronJobOverviewList.isFetching + ) + return ( + + ({ + bgcolor: theme.palette.background.outRunBg, + })} + > + {' '} + + + + ) +} + +export default Executor diff --git a/covalent_ui/webapp/src/components/qelectron/Overview.js b/covalent_ui/webapp/src/components/qelectron/Overview.js new file mode 100644 index 000000000..a480d437d --- /dev/null +++ b/covalent_ui/webapp/src/components/qelectron/Overview.js @@ -0,0 +1,139 @@ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ + +import React from 'react' +import { Grid, Typography, Paper, Skeleton } from '@mui/material' +import theme from '../../utils/theme' +import SyntaxHighlighter from '../common/SyntaxHighlighter' +import { formatQElectronTime, getLocalStartTime, formatDate } from '../../utils/misc' +import { useSelector } from 'react-redux'; + +const Overview = (props) => { + const { details } = props + const code = details?.result; + + const qelectronJobOverviewIsFetching = useSelector( + (state) => state.electronResults.qelectronJobOverviewList.isFetching + ) + + return ( + <> + {' '} + theme.palette.text.primary, + fontSize: theme.typography.sidebarh2, + fontWeight: 'bold', + }} + > + Execution Details + + + + theme.palette.text.tertiary, + }} + > + Backend + + theme.palette.text.primary, + }} + > + {qelectronJobOverviewIsFetching && !details ? + + : <>{(details?.backend) ? (details?.backend) : '-'}} + + theme.palette.text.tertiary, + }} + > + Time Elapsed + + theme.palette.text.primary, + }} + > + {qelectronJobOverviewIsFetching && !details ? + : <> + {(details?.time_elapsed) ? (formatQElectronTime(details?.time_elapsed)) : '-'} + } + + theme.palette.text.tertiary, + }} + > + Start time - End time + + {details?.start_time && details?.end_time && + theme.palette.text.primary, + }} + > + {qelectronJobOverviewIsFetching && !details ? + : <> + {formatDate(getLocalStartTime(details?.start_time))} + {` - ${formatDate(getLocalStartTime(details?.end_time))}`} + } + } + + + ({ + bgcolor: theme.palette.background.outRunBg, + })} + > + + + + + + ) +} + +export default Overview diff --git a/covalent_ui/webapp/src/components/qelectron/QElectronList.js b/covalent_ui/webapp/src/components/qelectron/QElectronList.js new file mode 100644 index 000000000..004655423 --- /dev/null +++ b/covalent_ui/webapp/src/components/qelectron/QElectronList.js @@ -0,0 +1,575 @@ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ + +import _ from 'lodash' +import { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { + Table, + TableRow, + TableHead, + TableCell, + TableBody, + Typography, + TableContainer, + TableSortLabel, + Box, + styled, + tableCellClasses, + tableRowClasses, + tableBodyClasses, + tableSortLabelClasses, + linkClasses, + Grid, + SvgIcon, + Tooltip, + Skeleton +} from '@mui/material' + +import { statusIcon, getLocalStartTime, formatDate, truncateMiddle } from '../../utils/misc' +import { Table as RTable } from 'react-virtualized'; +import { ReactComponent as FilterSvg } from '../../assets/qelectron/filter.svg' +import CopyButton from '../common/CopyButton' +import useMediaQuery from '@mui/material/useMediaQuery' +import { + qelectronJobs, +} from '../../redux/electronSlice' + +const headers = [ + { + id: 'job_id', + getter: 'job_id', + label: 'Job Id / Status', + sortable: true, + }, + { + id: 'start_time', + getter: 'start_time', + label: 'Start Time', + sortable: true, + }, + { + id: 'executor', + getter: 'executor', + label: 'Executor', + sortable: true, + }, +] + +const ResultsTableHead = ({ order, orderBy, onSort }) => { + return ( + + + {_.map(headers, (header) => { + return ( + ({ + border: 'none', + borderColor: + theme.palette.background.coveBlack03 + '!important', + paddingLeft: header?.id === 'executor' ? '2.3rem' : header?.id === 'start_time' ? '0.5rem' : '' + })} + > + {header.sortable ? ( + onSort(header.id)} + sx={{ + fontSize: '12px', + width: '100%', + mr: header.id === 'job_id' ? 20 : null, + '.Mui-active': { + color: (theme) => theme.palette.text.secondary, + }, + }} + > + {header.id === 'job_id' && ( + + + + + + )} + {header.label} + + ) : ( + header.label + )} + + ) + })} + + + ) +} + +const StyledTable = styled(Table)(({ theme }) => ({ + // stripe every odd body row except on select and hover + // [`& .MuiTableBody-root .MuiTableRow-root:nth-of-type(odd):not(.Mui-selected):not(:hover)`]: + // { + // backgroundColor: theme.palette.background.paper, + // }, + + // customize text + [`& .${tableBodyClasses.root} .${tableCellClasses.root}, & .${tableCellClasses.head}`]: + { + fontSize: '1rem', + }, + + // subdue header text + [`& .${tableCellClasses.head}, & .${tableSortLabelClasses.active}`]: { + color: theme.palette.text.tertiary, + backgroundColor: 'transparent', + }, + + // copy btn on hover + [`& .${tableBodyClasses.root} .${tableRowClasses.root}`]: { + '& .copy-btn': { visibility: 'hidden' }, + '&:hover .copy-btn': { visibility: 'visible' }, + }, + + // customize hover + [`& .${tableBodyClasses.root} .${tableRowClasses.root}:hover`]: { + backgroundColor: theme.palette.background.coveBlack02, + + [`& .${tableCellClasses.root}`]: { + borderColor: 'transparent', + paddingTop: 4, + paddingBottom: 4, + }, + [`& .${linkClasses.root}`]: { + color: theme.palette.text.secondary, + }, + }, + + [`& .${tableBodyClasses.root} .${tableRowClasses.root}`]: { + backgroundColor: 'transparent', + cursor: 'pointer', + + [`& .${tableCellClasses.root}`]: { + borderColor: 'transparent', + paddingTop: 4, + paddingBottom: 4, + }, + // [`& .${linkClasses.root}`]: { + // color: theme.palette.text.secondary, + // }, + }, + + // customize selected + [`& .${tableBodyClasses.root} .${tableRowClasses.root}.Mui-selected`]: { + backgroundColor: theme.palette.background.coveBlack02, + }, + [`& .${tableBodyClasses.root} .${tableRowClasses.root}.Mui-selected:hover`]: { + backgroundColor: theme.palette.background.default, + }, + + // customize border + [`& .${tableCellClasses.root}`]: { + borderColor: 'transparent', + paddingTop: 4, + paddingBottom: 4, + }, + + [`& .${tableCellClasses.root}:first-of-type`]: { + borderTopLeftRadius: 8, + borderBottomLeftRadius: 8, + }, + [`& .${tableCellClasses.root}:last-of-type`]: { + borderTopRightRadius: 8, + borderBottomRightRadius: 8, + }, +})) + +const QElectronList = ({ expanded, data, rowClick, electronId, dispatchId, setExpanded, defaultId, setOpenSnackbar, setSnackbarMessage }) => { + const dispatch = useDispatch() + const [selected, setSelected] = useState([]) + const [selectedId, setSelectedId] = useState(defaultId) + const [sortColumn, setSortColumn] = useState('start_time') + const [sortOrder, setSortOrder] = useState('DESC') + const isHeightAbove850px = useMediaQuery('(min-height: 850px)') + const isHeight900920px = useMediaQuery('(min-height: 900px) and (max-height: 920px)') + const isHeight920940px = useMediaQuery('(min-height: 920px) and (max-height: 940px)') + const isHeightAbove940px = useMediaQuery('(min-height: 940px)') + const isHeightAbove945px = useMediaQuery('(min-height: 945px)') + const isHeightAbove1024px = useMediaQuery('(min-height: 1024px)') + const isHeightAbove1040px = useMediaQuery('(min-height: 1040px)') + const isFetching = useSelector( + (state) => state.electronResults.qelectronJobsList.isFetching + ) + + useEffect(() => { + setSelectedId(defaultId) + }, [defaultId]) + + const isError = useSelector( + (state) => state.electronResults.qelectronJobsList.error + ); + + // check if there are any API errors and show a sncakbar + useEffect(() => { + if (isError) { + setOpenSnackbar(true) + if (isError?.detail && isError?.detail?.length > 0 && isError?.detail[0] && isError?.detail[0]?.msg) { + setSnackbarMessage(isError?.detail[0]?.msg) + } + else { + setSnackbarMessage( + 'Something went wrong,please contact the administrator!' + ) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isError]); + + useEffect(() => { + if (electronId || electronId === 0) { + const bodyParams = { + sort_by: sortColumn, + direction: sortOrder, + offset: 0 + } + dispatch( + qelectronJobs({ + dispatchId, + electronId, + bodyParams + }) + ) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sortColumn, sortOrder]) + + const handleChangeSort = (column) => { + setSelected([]) + const isAsc = sortColumn === column && sortOrder === 'asc' + setSortOrder(isAsc ? 'desc' : 'asc') + setSortColumn(column) + } + + // const getHeight = () => { + // if (xlmatches) { + // return expanded ? '23rem' : '40rem' + // } else if (xxlmatches) { + // return expanded ? '63rem' : '40rem' + // } else if (slmatches) { + // return expanded ? '24rem' : '48rem' + // } else { + // return expanded ? '16rem' : '32rem' + // } + // } + + const renderHeader = () => { + return (<> + {!(_.isEmpty(data)) && + } + + ) + } + + const getReactVirHeight = () => { + let height = !expanded ? 450 : 200 + if (isHeightAbove850px) { + height = !expanded ? 550 : 310 + } + if (isHeight900920px) { + height = !expanded ? 583 : 360 + } + if (isHeight920940px) { + height = !expanded ? 610 : 360 + } + if (isHeightAbove940px) { + height = !expanded ? 600 : 400 + } + if (isHeightAbove945px) { + height = !expanded ? 660 : 410 + } + if (isHeightAbove1024px) { + height = !expanded ? 700 : 480 + } + if (isHeightAbove1040px) { + height = !expanded ? 750 : 500 + } + return height; + } + + const getReactVirCount = () => { + let count = expanded ? 3 : 5; + if (isHeightAbove940px) { + count = !expanded ? 5 : 8 + } + if (isHeightAbove1040px) { + count = !expanded ? 8 : 11; + } + return count; + } + + function renderRow({ index, key, style }) { + const result = data[index] + return ( +
+ theme.palette.background.coveBlack02 + }, + '&.MuiTableRow-root.Mui-selected': { + backgroundColor: (theme) => theme.palette.background.coveBlack02 + }, + '&.MuiTableRow-root.Mui-selected:hover': { + backgroundColor: (theme) => theme.palette.background.default, + }, + '& .MuiTableCell-root': { + borderColor: 'transparent', + paddingTop: 0.2, + paddingBottom: 0.1, + cursor: 'pointer' + }, + '& .MuiTableCell-root:first-of-type': { + borderTopLeftRadius: 8, + borderBottomLeftRadius: 8, + }, + '& .MuiTableCell-root:last-of-type': { + borderTopRightRadius: 8, + borderBottomRightRadius: 8, + } + }} + data-testid="copyMessage" + data-tip + data-for="logRow" + onClick={() => { + setExpanded(true); + setSelectedId(result?.job_id) + rowClick(result?.job_id) + }} + hover + selected={result?.job_id === selectedId} + key={key} + > + theme.typography.logsFont, + }} + > + + {statusIcon(result?.status)} + + theme.palette.text.secondary, + }} + width="12rem" + > + {truncateMiddle(result?.job_id, 8, 13)} + + + + + + + + {formatDate(getLocalStartTime(result?.start_time))} + + + + + {result.executor} + + + +
+ ); + } + + return ( + theme.palette.background.qListBg, + }} + data-testid="QelectronList-grid" + > + + {!isFetching && data && ( + + + {!_.isEmpty(data) && !isFetching && + data[index]} + />} + + {_.isEmpty(data) && !isFetching && ( + + No results found. + + )} + + ) + } + + {isFetching && _.isEmpty(data) && ( + <> + {/* */} + {/* */} + + + + {[...Array(3)].map(() => ( + + + + + + + + + + + + ))} + + + + + ) + } + + ) +} + +export default QElectronList diff --git a/covalent_ui/webapp/src/components/qelectron/__tests__/Circuit.test.js b/covalent_ui/webapp/src/components/qelectron/__tests__/Circuit.test.js new file mode 100644 index 000000000..1964a1257 --- /dev/null +++ b/covalent_ui/webapp/src/components/qelectron/__tests__/Circuit.test.js @@ -0,0 +1,66 @@ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ +import { screen, render } from '@testing-library/react' +import App from '../Circuit' +import { BrowserRouter } from 'react-router-dom' +import React from 'react' +import { Provider } from 'react-redux' +import reducers from '../../../redux/reducers' +import { configureStore } from '@reduxjs/toolkit' +import theme from '../../../utils/theme' +import ThemeProvider from '@mui/system/ThemeProvider' + +function reduxRender(renderedComponent) { + const store = configureStore({ + reducer: reducers, + }) + + return render( + + + {renderedComponent} + + + ) +} + +describe('Circuit Tab', () => { + const circuitDetails = { + "total_qbits": 2, + "qbit1_gates": 2, + "qbit2_gates": 1, + "depth": 2, + "circuit": "RX!0.7984036206686643![0]Hadamard[1]CNOT[0, 1]|||ObservableReturnTypes.Expectation!['PauliY', 'PauliX'][0, 1]" + } + test('circuit tab is rendered', () => { + reduxRender() + const linkElement = screen.getByTestId('Circuit-grid') + expect(linkElement).toBeInTheDocument() + }) + + const filterData = Object.keys(circuitDetails); + test.each(filterData)('checks rendering for qubit values', (arg) => { + reduxRender() + const linkElement = screen.getByTestId(arg) + expect(linkElement).toBeInTheDocument() + }) +}) diff --git a/covalent_ui/webapp/src/components/qelectron/__tests__/Executor.test.js b/covalent_ui/webapp/src/components/qelectron/__tests__/Executor.test.js new file mode 100644 index 000000000..d2f202638 --- /dev/null +++ b/covalent_ui/webapp/src/components/qelectron/__tests__/Executor.test.js @@ -0,0 +1,83 @@ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ +import { screen, render } from '@testing-library/react' +import App from '../Executor' +import { BrowserRouter } from 'react-router-dom' +import React from 'react' +import { Provider } from 'react-redux' +import reducers from '../../../redux/reducers' +import { configureStore } from '@reduxjs/toolkit' +import theme from '../../../utils/theme' +import ThemeProvider from '@mui/system/ThemeProvider' + +function reduxRender(renderedComponent) { + const store = configureStore({ + reducer: reducers, + }) + + return render( + + + {renderedComponent} + + + ) +} + +describe('Executor Tab', () => { + const code = { + "name": "Simulator", + "executor": { + "persist_data": true, + "qnode_device_import_path": "pennylane.devices.default_qubit:DefaultQubit", + "qnode_device_shots": null, + "qnode_device_wires": 4, + "pennylane_active_return": true, + "device": "default.qubit", + "parallel": "thread", + "workers": 10, + "shots": 0, + "name": "Simulator", + "_backend": { + "persist_data": true, + "qnode_device_import_path": "pennylane.devices.default_qubit:DefaultQubit", + "qnode_device_shots": null, + "qnode_device_wires": 4, + "pennylane_active_return": true, + "device": "default.qubit", + "num_threads": 10, + "name": "BaseThreadPoolQExecutor" + } + } + } + test('executor tab is rendered', () => { + reduxRender() + const linkElement = screen.getByTestId('Executor-grid') + expect(linkElement).toBeInTheDocument() + }) + + test('checks rendering for executor code block', () => { + reduxRender() + const linkElement = screen.getByTestId('syntax') + expect(linkElement).toBeInTheDocument() + }) +}) diff --git a/covalent_ui/webapp/src/components/qelectron/__tests__/Overview.test.js b/covalent_ui/webapp/src/components/qelectron/__tests__/Overview.test.js new file mode 100644 index 000000000..515598eb7 --- /dev/null +++ b/covalent_ui/webapp/src/components/qelectron/__tests__/Overview.test.js @@ -0,0 +1,52 @@ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ +import { screen, render } from '@testing-library/react' +import App from '../Overview' +import { BrowserRouter } from 'react-router-dom' +import React from 'react' +import { Provider } from 'react-redux' +import reducers from '../../../redux/reducers' +import { configureStore } from '@reduxjs/toolkit' +import theme from '../../../utils/theme' +import ThemeProvider from '@mui/system/ThemeProvider' + +function reduxRender(renderedComponent) { + const store = configureStore({ + reducer: reducers, + }) + + return render( + + + {renderedComponent} + + + ) +} + +describe('Overview Tab', () => { + test('overview tab is rendered', () => { + reduxRender() + const linkElement = screen.getByTestId('Overview-grid') + expect(linkElement).toBeInTheDocument() + }) +}) diff --git a/covalent_ui/webapp/src/components/qelectron/__tests__/QElectronList.test.js b/covalent_ui/webapp/src/components/qelectron/__tests__/QElectronList.test.js new file mode 100644 index 000000000..77b413919 --- /dev/null +++ b/covalent_ui/webapp/src/components/qelectron/__tests__/QElectronList.test.js @@ -0,0 +1,125 @@ +/** + * Copyright 2023 Agnostiq Inc. + * + * This file is part of Covalent. + * + * Licensed under the GNU Affero General Public License 3.0 (the "License"). + * A copy of the License may be obtained with this software package or at + * + * https://www.gnu.org/licenses/agpl-3.0.en.html + * + * Use of this file is prohibited except in compliance with the License. Any + * modifications or derivative works of this file must retain this copyright + * notice, and modified files must contain a notice indicating that they have + * been altered from the originals. + * + * Covalent is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. + * + * Relief from the License may be granted by purchasing a commercial license. + */ +import { fireEvent, screen, render } from '@testing-library/react' +import App from '../QElectronList' +import { BrowserRouter } from 'react-router-dom' +import React, { useState } from 'react' +import { Provider } from 'react-redux' +import reducers from '../../../redux/reducers' +import { configureStore } from '@reduxjs/toolkit' +import theme from '../../../utils/theme' +import ThemeProvider from '@mui/system/ThemeProvider' + +function reduxRender(renderedComponent) { + const store = configureStore({ + reducer: reducers, + }) + + return render( + + + {renderedComponent} + + + ) +} + +function reduxRenderMock(renderedComponent) { + const initialState = { + electronResults: { + qelectronJobsList: { + isFetching: true + } + } + } + const store = configureStore({ + reducer: reducers, + preloadedState: initialState, + }) + + return render( + + + {renderedComponent} + + + ) +} + +describe('Qelectron List', () => { + test('Qelectron List Grid is rendered', () => { + reduxRender() + const linkElement = screen.getByTestId('QelectronList-grid') + expect(linkElement).toBeInTheDocument() + }) + + const data = [ + { + "job_id": "circuit_0@6418e062-7892-4239-8734-39926c5558fc", + "start_time": "2023-06-13T21:19:27.057015", + "executor": "QiskitExecutor", + "status": "COMPLETED" + }, + { + "job_id": "circuit_0@a86bf847-84a3-4414-adbe-2bc1d6727371", + "start_time": "2023-06-13T21:19:27.033453", + "executor": "QiskitExecutor", + "status": "COMPLETED" + }, + { + "job_id": "circuit_0@25352acb-de2a-4195-99a8-afe60c8ff675", + "start_time": "2023-06-13T21:19:27.009380", + "executor": "QiskitExecutor", + "status": "COMPLETED" + }, + { + "job_id": "circuit_0@75a6a5e1-63f3-4231-beea-9374705cbfc8", + "start_time": "2023-06-13T21:19:26.960943", + "executor": "QiskitExecutor", + "status": "COMPLETED" + } + ]; + test('Qelectron List data is rendered', () => { + reduxRender() + const linkElement = screen.getByTestId('QelectronList-table') + expect(linkElement).toBeInTheDocument() + const ele = screen.queryAllByTestId('tableHeader'); + expect(ele[0]).toBeInTheDocument() + fireEvent.click(ele[0]) + const ele1 = screen.queryAllByTestId('copyMessage'); + expect(ele1[0]).toBeInTheDocument() + fireEvent.click(ele1[0]) + }) + + test('Qelectron List empty data is rendered', () => { + reduxRender() + const linkElement = screen.queryByText('No results found.') + expect(linkElement).toBeInTheDocument() + }) + + test('Qelectron List empty data with isFetching', () => { + reduxRenderMock() + const linkElement = screen.queryByText('No results found.') + expect(linkElement).not.toBeInTheDocument() + }) + +}) diff --git a/covalent_ui/webapp/src/utils/style.css b/covalent_ui/webapp/src/utils/style.css new file mode 100644 index 000000000..5d959bf2c --- /dev/null +++ b/covalent_ui/webapp/src/utils/style.css @@ -0,0 +1,13 @@ +@keyframes rotate { + from { + transform: rotateZ(-360deg); + } + to { + transform: rotateZ(360deg); + } +} + +.circleRunningStatus { + animation: rotate 7s linear; + animation-iteration-count: infinite; +} diff --git a/doc/source/api/executors/braketqubit.rst b/doc/source/api/executors/braketqubit.rst new file mode 100644 index 000000000..58c0e2201 --- /dev/null +++ b/doc/source/api/executors/braketqubit.rst @@ -0,0 +1,122 @@ + +AWS Braket Qubit Executor +""""""""""""" + +This quantum executor accesses quantum resources operating under the qubit model as +made available through AWS (:code:`"braket.aws.qubit"`). + +It utilizes the Pennylane plugin found `here `_. +:code:`BraketQubitExecutor` introduces thread-based parallelism for circuit execution on the :code:`"braket.aws.qubit"` device. + +=============== +1. Installation +=============== + +:code:`BraketQubitExecutor` is not included in base Covalent. +To use it, you will need to install the Covalent with: + +.. code:: console + + pip install covalent[braket] + +and have valid AWS credentials as specified `here `_. + +================ +2. Usage Example +================ + +Using :code:`BraketQubitExecutor` requires specifying an AWS Quantum backend through the :code:`device_arn` argument. + +.. code:: python + + # Statevector simulator + sv1 = ct.executor.BraketQubitExecutor( + device_arn="arn:aws:braket:::device/quantum-simulator/amazon/sv1", + shots=1024, + s3_destination_folder=(), + ) + # Tensor network simulator + tn1 = ct.executor.BraketQubitExecutor( + device_arn="arn:aws:braket:::device/quantum-simulator/amazon/tn1", + shots=1024, + s3_destination_folder=(), + ) + + @ct.qelectron(executors=[sv1, tn1]) + @qml.qnode(qml.device("default.qubit", wires=2, shots=1000)) + def circuit(x): + qml.IQPEmbedding(features=x, wires=[0, 1]) + qml.Hadamard(wires=1) + return [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))] + +As a QElectron, the circuit can be called either normally or asynchronously using :code:`circuit.run_later()`. With the default :code:`"cyclic"` selector, circuit calls will `alternate` between the executors, :code:`[sv1, tn1]`. + +Synchronous example output is below + +.. code:: python + + >>> print(circuit([0.5, 0.1])) # alternate between sv1 and tn1 + + [array(0.008), array(0.996)] + + +and asynchronously: + +.. code:: python + + >>> x = [0.6, -1.57] + + >>> # Queue jobs for all three circuit calls simultaneously on AWS Braket. + >>> # Uses same executor order as above (sv1, tn1, ...). + >>> futs = [circuit.run_later(x) for _ in range(3)] + + >>> # Wait for all circuits to finish. + >>> [fut.result() for fut in futs] + + [[array(-0.02), array(0.01)], + [array(0.014), array(-0.022)], + [array(-0.074), array(0.05)]] + +============================ +3. Overview of Configuration +============================ + +The :code:`BraketQubitExecutor` configuration is found under :code:`[qelectron.BraketQubitExecutor]` in the `Covalent configuration file `_. + +.. list-table:: + :widths: 2 1 2 3 + :header-rows: 1 + + * - Config Key + - Is Required + - Default + - Description + * - device_arn + - Yes + - "" (blank string) + - A unique identifier used to represent and reference AWS resources. Stands for "Amazon Resource Name". + * - poll_timeout_seconds + - No + - 432000 + - Number of seconds before a poll to remote device is considered timed-out. + * - poll_interval_seconds + - No + - 1 + - Number of seconds between polling of a remote device's status. + * - max_connections + - No + - 100 + - the maximum number of connections in the :code:`Boto3` connection pool. + * - max_retries + - No + - 3 + - The maximum number of times a job will be re-sent if it failed. +=========================== +4. Required Cloud Resources +=========================== + +Users must acquire AWS credentials and make them discoverable following the instructions `here `_. + +----- + +.. autopydantic_model:: covalent.executor.BraketQubitExecutor diff --git a/doc/source/api/executors/ibmq.rst b/doc/source/api/executors/ibmq.rst new file mode 100644 index 000000000..52ead7cb6 --- /dev/null +++ b/doc/source/api/executors/ibmq.rst @@ -0,0 +1,122 @@ + +IBMQ Executor +""""""""""""" + +This quantum executor accesses IBM Quantum backends through Pennylane's :code:`"qiskit.ibmq"` `device `_. :code:`IBMQExecutor` introduces thread-based parallelism for circuit execution on the `"qiskit.ibmq"` device. Note that the more efficient :code:`QiskitExecutor` is recommended over :code:`IBMQExecutor` for production use. + +=============== +1. Installation +=============== + +The IBMQ executor is not included with base Covalent. To install it, run + +.. code:: console + + pip install covalent[qiskit] + +================ +2. Usage Example +================ + +Using `IBMQExecutor` requires specifying an IBM Quantum backend through the :code:`backend` argument. The :code:`ibmqx_token` is required if not specified in the configuration (see next section). + +.. code:: python + + import covalent as ct + import pennylane as qml + + # IBMQ executor that uses "ibmq_qasm_simulator" (default). + ibmq_qasm = ct.executor.IBMQExecutor() + + # IBMQ executor that uses the "ibmq_lima" QPU. + ibmq_lima = ct.executor.IBMQExecutor( + backend="ibmq_lima", + ibmqx_token="", + ) + + @ct.qelectron(executors=[ibmq_qasm, ibmq_lima]) + @qml.qnode(qml.device("default.qubit", wires=2, shots=1024), interface="jax") + def circuit(x): + qml.IQPEmbedding(features=x, wires=[0, 1]) + qml.Hadamard(wires=1) + return qml.probs(wires=range(2)) + +As a QElectron, the circuit can be called either normally or asynchronously using :code:`circuit.run_later()`. With the default :code:`"cyclic"` selector, circuit calls will `alternate` between the executors, :code:`[ibmq_qasm, ibmq_lima]`. + +A synchronous example is shown below. + +.. code:: python + + >>> print(circuit([0.5, 0.1])) # ibmq_qasm_simulator + + DeviceArray([0.51660156, 0.00097656, 0.4814453 , 0.00097656], dtype=float32) + + >>> print(circuit([0.5, 0.1])) # ibmq_lima + + DeviceArray([0.5048828 , 0.00195312, 0.49316406, 0. ], dtype=float32) + + >>> print(circuit([0.5, 0.1])) # ibmq_qasm_simulator (again) + + DeviceArray([0.5097656 , 0.00292969, 0.4873047 , 0. ], dtype=float32) + +Doing this asynchronously: + +.. code:: python + + >>> x = [0.6, -1.57] + + >>> # Queue jobs for all three circuit calls simultaneously on IBM Quantum. + >>> # Uses same executor order as above (qasm, lima, qasm, ...). + >>> futs = [circuit.run_later(x) for _ in range(3)] + + >>> # Wait for all circuits to finish. + >>> [fut.result() for fut in futs] + + [DeviceArray([0.51660156, 0.00097656, 0.4814453 , 0.00097656], dtype=float32), + DeviceArray([0.5048828 , 0.00195312, 0.49316406, 0. ], dtype=float32), + DeviceArray([0.5097656 , 0.00292969, 0.4873047 , 0. ], dtype=float32)] + +============================ +3. Overview of Configuration +============================ + +The :code:`IBMQExecutor` configuration is found under :code:`[qelectron.IBMQExecutor]` in the `Covalent configuration file `_. + +.. list-table:: + :widths: 2 1 2 3 + :header-rows: 1 + + * - Config Key + - Is Required + - Default + - Description + * - backend + - Yes + - ibm_qasm_simulator + - The name of an IBM Quantum system or simulator. + * - ibmqx_token + - Yes/No + - + - An access token obtained from IBM Quantum. Required for non-local execution. + * - hub + - No + - ibm-q + - Hub name for IBM Quantum. + * - group + - No + - open + - Group name for IBM Quantum. + * - project + - No + - main + - Project name for IBM Quantum. + +=========================== +4. Required Cloud Resources +=========================== + +Users must acquire an access token from the `IBM Quantum Experience `_ in order to use IBM systems and simulators. + +----- + +.. autopydantic_model:: covalent.executor.IBMQExecutor diff --git a/doc/source/api/executors/localbraketqubit.rst b/doc/source/api/executors/localbraketqubit.rst new file mode 100644 index 000000000..5fefc10d1 --- /dev/null +++ b/doc/source/api/executors/localbraketqubit.rst @@ -0,0 +1,92 @@ + +Local Braket Qubit Executor +""""""""""""" + +This quantum executor accesses the local Braket quantum circuit simulator (:code:`"braket.local.qubit"`). + +It utilizes the Pennylane plugin found `here `_. +:code:`LocalBraketQubitExecutor` introduces thread-based parallelism for circuit execution on the :code:`"braket.local.qubit"` device. + +=============== +1. Installation +=============== + +:code:`LocalBraketQubitExecutor` is not included in base Covalent. +To use it, you will need to install the Covalent with: + +.. code:: console + + pip install covalent[braket] + +================ +2. Usage Example +================ + +Using :code:`LocalBraketQubitExecutor` is simple: + +.. code:: python + + # Local simulator + executor = ct.executor.LocalBraketQubitExecutor( + device="default", + shots=1024, + num_threads=2 + ) + + @ct.qelectron(executors=executor) + @qml.qnode(qml.device("default.qubit", wires=2, shots=1024)) + def circuit(x): + qml.IQPEmbedding(features=x, wires=[0, 1]) + qml.Hadamard(wires=1) + return [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))] + +As a QElectron, the circuit can be called either normally or asynchronously using :code:`circuit.run_later()`. + +Synchronous example output is below + +.. code:: python + + >>> print(circuit([0.5, 0.1])) + + [array(0.008), array(0.996)] + + +and asynchronously: + +.. code:: python + + >>> x = [0.6, -1.57] + + >>> # Queue jobs for all three circuit calls simultaneously on. + >>> futs = [circuit.run_later(x) for _ in range(3)] + + >>> # Wait for all circuits to finish. + >>> [fut.result() for fut in futs] + + [[array(-0.02), array(0.01)], + [array(0.014), array(-0.022)], + [array(-0.074), array(0.05)]] + +============================ +3. Overview of Configuration +============================ + +The :code:`LocalBraketQubitExecutor` configuration is found under :code:`[qelectron.LocalBraketQubitExecutor]` in the `Covalent configuration file `_. + +.. list-table:: + :widths: 2 1 2 3 + :header-rows: 1 + + * - Config Key + - Is Required + - Default + - Description + * - backend + - No + - "default" + - The type of simulator backend to be used. Choices are :code:`"default"`, :code:`"braket_sv"`, :code:`"braket_dm"` and :code:`"braket_ahs"`. + + +----- + +.. autopydantic_model:: covalent.executor.LocalBraketQubitExecutor diff --git a/doc/source/api/executors/qiskit.rst b/doc/source/api/executors/qiskit.rst new file mode 100644 index 000000000..e7b8eaece --- /dev/null +++ b/doc/source/api/executors/qiskit.rst @@ -0,0 +1,170 @@ +Qiskit Runtime Executor +""""""""""""""""""""""" + +This quantum executor provides efficient access to IBM Quantum backends by using runtime sessions for submitting jobs. :code:`QiskitExecutor` uses asyncio for scalable parallelization. + +=============== +1. Installation +=============== + +The Qiskit Runtime executor is not included with base Covalent. To install it, run + +.. code:: console + + pip install covalent[qiskit] + +================ +2. Usage Example +================ + +Typical usage involves specifying a runtime primitive via the :code:`device` argument and specifying an IBM backend via the :code:`backend` argument. An access token from IBM Quantum can be provided explicitly as :code:`ibmqx_token` or in the `Covalent configuration file `_. + +The following example shows several :code:`QiskitExecutor` instances being utilized as a Quantum Cluster. + +.. code:: python + + import covalent as ct + import pennylane as qml + + # Default local qiskit executor. + qiskit_local = ct.executor.QiskitExecutor() + + # Runtime qiskit executor that uses the "ibmq_qasm_simulator" backend. + qiskit_qasm = ct.executor.QiskitExecutor( + device="sampler", + backend="ibmq_qasm_simulator", + ibmqx_token="", # required if not in config file + ) + + # Runtime qiskit executor that uses the "ibmq_lima" QPU. + qiskit_lima = ct.executor.QiskitExecutor( + device="sampler", + backend="ibmq_lima", + ibmqx_token="", + instance="my-hub/my-group/my-project", + + # Backend settings (optional) + options={ + "optimization_level": 2, + "resilience_level": 1, + # ... + } + ) + + # Create quantum electron that uses a cluster of 3 qiskit executors. + @ct.qelectron(executors=[qiskit_local, qiskit_qasm, qiskit_lima]) + @qml.qnode(qml.device("default.qubit", wires=2, shots=1024), interface="tf") + def circuit(x): + qml.IQPEmbedding(features=x, wires=[0, 1]) + qml.Hadamard(wires=1) + return qml.probs(wires=range(2)) + + +One converted to a QElectron, the circuit can be called normally or asynchronously via :code:`circuit.run_later()`. Since the example uses a quantum cluster with the default :code:`"cyclic"` selector, circuit calls will repeatedly cycle through :code:`executors` in order. + +A synchronous example is shown below. + +.. code:: python + + >>> circuit([0.6, -1.57]) # local + + tf.Tensor([0.0546875 0.42773438 0.46777344 0.04980469], shape=(4,), dtype=float64) + + >>> circuit([0.6, -1.57]) # ibmq_qasm_simulator + + tf.Tensor([0.04589844 0.45507812 0.45898438 0.04003906], shape=(4,), dtype=float64) + + >>> circuit([0.6, -1.57]) # ibmq_lima + + tf.Tensor([0.04199219 0.44628906 0.46679688 0.04492188], shape=(4,), dtype=float64) + + >>> circuit([0.6, -1.57]) # local (again) + + tf.Tensor([0.04394531 0.4609375 0.43945312 0.05566406], shape=(4,), dtype=float64) + +If instead doing this asynchronously: + +.. code:: python + + >>> x = [0.6, -1.57] + + >>> # Queue jobs for all four circuit calls simultaneously on IBM Quantum. + >>> # Uses same executor order as above (local, qasm, lima, local, ...). + >>> futs = [circuit.run_later(x) for _ in range(4)] + + >>> # Wait for all circuits to finish. + >>> [fut.result() for fut in futs] + + [tf.Tensor([0.0546875 0.42773438 0.46777344 0.04980469], shape=(4,), dtype=float64), + tf.Tensor([0.04589844 0.45507812 0.45898438 0.04003906], shape=(4,), dtype=float64), + tf.Tensor([0.04199219 0.44628906 0.46679688 0.04492188], shape=(4,), dtype=float64), + tf.Tensor([0.04394531 0.4609375 0.43945312 0.05566406], shape=(4,), dtype=float64)] + + +============================ +3. Overview of Configuration +============================ + +The :code:`QiskitExecutor` configuration is found under :code:`[qelectron.QiskitExecutor]` in the `Covalent configuration file `_. + +.. list-table:: + :widths: 2 1 2 3 + :header-rows: 1 + + * - Config Key + - Is Required + - Default + - Description + * - device + - Yes + - local_sampler + - The qiskit (e.g. :code:`"local_sampler"`) or qiskit runtime (e.g. :code:`"sampler"`) primitive used for running circuits on an IBM backend. + * - backend + - Yes + - ibm_qasm_simulator + - The name of an IBM Quantum system or simulator. + * - ibmqx_token + - Yes/No + - + - An access token obtained from IBM Quantum. Required for non-local execution. + * - hub + - No + - ibm-q + - Hub name for IBM Quantum. + * - group + - No + - open + - Group name for IBM Quantum. + * - project + - No + - main + - Project name for IBM Quantum. + +The following backend settings are also set by default under :code:`[qelectron.QiskitExecutor.options]`. These represent maximum optimization/resilience levels for the :code:`Sampler` primitive. Users can append additional settings to this configuration or specify them directly when instantiating a :code:`QiskitExecutor`. See the `Qiskit Runtime Options `_ page for a complete list of valid fields. + +.. list-table:: + :widths: 2 1 2 3 + :header-rows: 1 + + * - Config Key + - Is Required + - Default + - Description + * - optimization_level + - No + - 3 + - How much optimization to perform on the circuits. + * - resilience_level + - No + - 1 + - How much resilience to build against errors. + +=========================== +4. Required Cloud Resources +=========================== + +In order to access IBM backends, users must acquire an access token from IBM Quantum. This can be done by creating a free account on the `IBM Quantum Experience `_. + +----- + +.. autopydantic_model:: covalent.executor.QiskitExecutor diff --git a/doc/source/api/executors/simulator.rst b/doc/source/api/executors/simulator.rst new file mode 100644 index 000000000..5905d6a3f --- /dev/null +++ b/doc/source/api/executors/simulator.rst @@ -0,0 +1,56 @@ +Simulator +""""""""" + +This quantum executor introduces thread- or process-based parallelism to Pennylane circuits that utilize simulation-based devices (like :code:`"default.qubit"` or :code:`"lightning.qubit"`). + +=============== +1. Installation +=============== + +No additional installation is required. + +================ +2. Usage Example +================ + +A thread-based :code:`Simulator` is the default quantum executor for QElectrons. + +.. code:: python + + import covalent as ct + import pennylane as qml + + @ct.qelectron + @qml.qnode(qml.device("lightning.qubit", wires=2), interface="torch") + def circuit(x): + qml.IQPEmbedding(features=x, wires=[0, 1]) + qml.Hadamard(wires=1) + return qml.probs(wires=range(2)) + +Once converted to a QElectron, the circuit can be called either normally or asynchronously via :code:`circuit.run_later()`. + +A synchronous example is show below. + +.. code:: python + + >>> circuit([1.3, -0.7]), circuit([1.3, -0.7]) + + (tensor([0.3169, 0.3169, 0.1831, 0.1831], dtype=torch.float64), + tensor([0.3169, 0.3169, 0.1831, 0.1831], dtype=torch.float64)) + +Alternatively, doing this asynchronously: + +.. code:: python + + >>> # Use separate threads to run two circuits simultaneously. + >>> futs = [circuit.run_later([1.3, -0.7]) for _ in range(2)] + + # Wait for all circuits to finish. + >>> [fut.result() for fut in futs] + + [tensor([0.3169, 0.3169, 0.1831, 0.1831], dtype=torch.float64), + tensor([0.3169, 0.3169, 0.1831, 0.1831], dtype=torch.float64)] + +----- + +.. autopydantic_model:: covalent.executor.Simulator diff --git a/doc/source/api/qclusters.rst b/doc/source/api/qclusters.rst new file mode 100644 index 000000000..3a866c3d5 --- /dev/null +++ b/doc/source/api/qclusters.rst @@ -0,0 +1,6 @@ +.. _qclusters_api: + +Quantum Clusters +""""""""""""""""""""""""""" + +.. autopydantic_model:: covalent.executor.QCluster diff --git a/doc/source/api/qelectrons.rst b/doc/source/api/qelectrons.rst new file mode 100644 index 000000000..c8e058958 --- /dev/null +++ b/doc/source/api/qelectrons.rst @@ -0,0 +1,6 @@ +.. _qelectrons_api: + +Quantum Electrons +""""""""""""""""""""""""""" + +.. autodecorator:: covalent.qelectron