Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enabling qelectron tests and making qelectron opt-in only #1916

Merged
merged 16 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion .github/workflows/boilerplate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,26 @@ jobs:
# See the License for the specific language governing permissions and
# limitations under the License.

boilerplate2024: |-
# Copyright 2024 Agnostiq Inc.
#
# This file is part of Covalent.
#
# Licensed under the Apache License 2.0 (the "License"). A copy of the
# License may be obtained with this software package or at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Use of this file is prohibited except in compliance with the License.
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

run: |
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
if [[ ! $( cat $file | tr -d '\r' ) =~ "$boilerplate2021" && ! $( cat $file | tr -d '\r' ) =~ "$boilerplate2022" && ! $( cat $file | tr -d '\r' ) =~ "$boilerplate2023" ]] ; then
if [[ ! $( cat $file | tr -d '\r' ) =~ "$boilerplate2021" && ! $( cat $file | tr -d '\r' ) =~ "$boilerplate2022" && ! $( cat $file | tr -d '\r' ) =~ "$boilerplate2023" && ! $( cat $file | tr -d '\r' ) =~ "$boilerplate2024" ]] ; then
printf "Boilerplate is missing from $file.\n"
printf "The first 15 lines of $file are\n\n"
cat $file | tr -d '\r' | cat -ET | head -n 15
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/requirements.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,14 @@ jobs:
--ignore-module=pkg_resources
--ignore-module=covalent/_dispatcher_plugins
--ignore-module=covalent/_shared_files
--ignore-file=covalent/quantum/**
--ignore-file=covalent/_workflow/q*
--ignore-file=covalent/_shared_files/q*
--ignore-file=covalent/_results_manager/q*
--ignore-file=covalent/_shared_files/pickling.py
--ignore-file=covalent/executor/**
--ignore-file=covalent/triggers/**
--ignore-file=covalent/cloud_resource_manager/**
--ignore-file=covalent/quantum/qserver/**
--ignore-file=covalent/_programmatic/**
covalent

Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ jobs:
sdk:
- 'covalent/**'
- 'tests/covalent_tests/**'
qelectron:
- 'covalent/executor/quantum_plugins/**'
- 'covalent/executor/qbase.py'
- 'covalent/quantum/**'
- 'tests/qelectron_tests/**'
dispatcher:
- 'covalent_dispatcher/**'
- 'tests/covalent_dispatcher_tests/**'
Expand Down Expand Up @@ -134,6 +139,7 @@ jobs:
echo "NEED_PYTHON=$NEED_PYTHON" >> $GITHUB_ENV
echo "NEED_FRONTEND=$NEED_FRONTEND" >> $GITHUB_ENV
echo "BUILD_AND_RUN_ALL=$BUILD_AND_RUN_ALL" >> $GITHUB_ENV
echo "COVALENT_DISABLE_QELECTRON_TESTS=true" >> $GITHUB_ENV

- name: Set up Python
if: >
Expand All @@ -159,6 +165,7 @@ jobs:
run: |
pip install --no-cache-dir -r ./requirements.txt
pip install --no-cache-dir -r ./tests/requirements.txt
pip install --no-cache-dir -r ./requirements-qelectron.txt

- name: Set up Node
if: env.NEED_FRONTEND || env.BUILD_AND_RUN_ALL
Expand Down Expand Up @@ -252,6 +259,18 @@ jobs:
if: steps.sdk-tests.outcome == 'success'
run: coverage xml -o sdk_coverage.xml

- name: Run Qelectron tests and measure coverage
id: qelectron-tests
if: >
(steps.modified-files.outputs.qelectron == 'true'
|| env.BUILD_AND_RUN_ALL) && env.COVALENT_DISABLE_QELECTRON_TESTS != 'true'
run: PYTHONPATH=$PWD/ pytest -vvs --reruns=5 tests/qelectron_tests/core_tests --cov=covalent_qelectron --cov-config=.coveragerc

- name: Generate Qelectron coverage report
id: qelectron-coverage
if: steps.qelectron-tests.outcome == 'success' && env.COVALENT_DISABLE_QELECTRON_TESTS != 'true'
run: coverage xml -o qelectron_coverage.xml

- name: Run dispatcher tests and measure coverage
id: dispatcher-tests
if: >
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
!pyproject.toml
!requirements.txt
!requirements-client.txt
!requirements-qelectron.txt
!setup.py

# Allow markdown etc
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [UNRELEASED]

### Added

- Added `pennylane` as a requirement in tests due to the tutorials using it

### Changed

- Updated RTD notebooks to fix their behavior
- Changed the error being shown when drawing the transport graph of a lattice to a debug message instead
- Revamped README
- Reorganized `qelectron` tests
- Made qelectron an opt-in feature using `covalent[quantum]` extra

### Removed

Expand All @@ -26,8 +32,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Fixed the scenario where any deploy commands would fail if the user had a non deploy compatible plugin installed
- Fixed the SQLAlchemy warning that used to show up at every fresh server start
- Fixed deploy commands' default value of plugins not being propagated to the tfvars file

### Operations

- Added qelectron tests to the `tests` workflow

## [0.233.0-rc.0] - 2024-01-07

### Authors
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
include VERSION
include requirements.txt
include requirements-client.txt
include requirements-qelectron.txt
include covalent/py.typed
recursive-include covalent/executor/ *
recursive-include covalent_dispatcher/_service/ *
Expand Down
7 changes: 5 additions & 2 deletions covalent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

"""Main Covalent public functionality."""

import contextlib
from importlib import metadata

from . import _file_transfer as fs # nopycln: import
Expand Down Expand Up @@ -48,9 +49,11 @@
lattice,
)
from ._workflow.electron import wait # nopycln: import
from ._workflow.qelectron import qelectron # nopycln: import
from .executor.utils import get_context # nopycln: import
from .quantum import QCluster # nopycln: import

with contextlib.suppress(ImportError):
from ._workflow.qelectron import qelectron # nopycln: import
from .quantum import QCluster # nopycln: import

__all__ = [s for s in dir() if not s.startswith("_")]

Expand Down
58 changes: 57 additions & 1 deletion covalent/_shared_files/qelectron_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import importlib
import inspect
from typing import Any, Tuple

from covalent.quantum.qserver.database import Database
import cloudpickle
from pennylane._device import Device

from .logger import app_log
from .pickling import _qml_mods_pickle

_IMPORT_PATH_SEPARATOR = ":"


def get_qelectron_db_path(dispatch_id: str, task_id: int):
Expand All @@ -28,6 +35,8 @@
AS WHERE THE USER'S TASK FUNCTION IS BEING RUN.
"""

from covalent.quantum.qserver.database import Database

database = Database()

db_path = database.get_db_path(dispatch_id=dispatch_id, node_id=task_id)
Expand All @@ -38,3 +47,50 @@
else:
app_log.debug(f"Qelectron database not found for task {task_id}")
return None


@_qml_mods_pickle
def cloudpickle_serialize(obj):
return cloudpickle.dumps(obj)

Check warning on line 54 in covalent/_shared_files/qelectron_utils.py

View check run for this annotation

Codecov / codecov/patch

covalent/_shared_files/qelectron_utils.py#L54

Added line #L54 was not covered by tests


def cloudpickle_deserialize(obj):
return cloudpickle.loads(obj)

Check warning on line 58 in covalent/_shared_files/qelectron_utils.py

View check run for this annotation

Codecov / codecov/patch

covalent/_shared_files/qelectron_utils.py#L58

Added line #L58 was not covered by tests


def select_first_executor(qnode, executors):
"""Selects the first executor to run the qnode"""
return executors[0]

Check warning on line 63 in covalent/_shared_files/qelectron_utils.py

View check run for this annotation

Codecov / codecov/patch

covalent/_shared_files/qelectron_utils.py#L63

Added line #L63 was not covered by tests


def get_import_path(obj) -> Tuple[str, str]:
"""
Determine the import path of an object.
"""
if module := inspect.getmodule(obj):
module_path = module.__name__
class_name = obj.__name__
return f"{module_path}{_IMPORT_PATH_SEPARATOR}{class_name}"
raise RuntimeError(f"Unable to determine import path for {obj}.")

Check warning on line 74 in covalent/_shared_files/qelectron_utils.py

View check run for this annotation

Codecov / codecov/patch

covalent/_shared_files/qelectron_utils.py#L70-L74

Added lines #L70 - L74 were not covered by tests


def import_from_path(path: str) -> Any:
"""
Import a class from a path.
"""
module_path, class_name = path.split(_IMPORT_PATH_SEPARATOR)
module = importlib.import_module(module_path)
return getattr(module, class_name)

Check warning on line 83 in covalent/_shared_files/qelectron_utils.py

View check run for this annotation

Codecov / codecov/patch

covalent/_shared_files/qelectron_utils.py#L81-L83

Added lines #L81 - L83 were not covered by tests


def get_original_shots(dev: Device):
"""
Recreate vector of shots if device has a shot vector.
"""
if not dev.shot_vector:
return dev.shots

Check warning on line 91 in covalent/_shared_files/qelectron_utils.py

View check run for this annotation

Codecov / codecov/patch

covalent/_shared_files/qelectron_utils.py#L90-L91

Added lines #L90 - L91 were not covered by tests

shot_sequence = []
for shots in dev.shot_vector:
shot_sequence.extend([shots.shots] * shots.copies)
return type(dev.shot_vector)(shot_sequence)

Check warning on line 96 in covalent/_shared_files/qelectron_utils.py

View check run for this annotation

Codecov / codecov/patch

covalent/_shared_files/qelectron_utils.py#L93-L96

Added lines #L93 - L96 were not covered by tests
2 changes: 1 addition & 1 deletion covalent/_shared_files/qresult_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from pennylane.tape import QuantumTape

from .._workflow.qdevice import QEDevice
from .utils import get_original_shots
from .qelectron_utils import get_original_shots


def re_execute(
Expand Down
59 changes: 9 additions & 50 deletions covalent/_shared_files/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,14 @@

"""General utils for Covalent."""

import importlib
import inspect
import shutil
import socket
from datetime import timedelta
from typing import Any, Callable, Dict, List, Tuple

import cloudpickle
from pennylane._device import Device
from typing import Callable, Dict, List, Tuple

from . import logger
from .config import get_config
from .pickling import _qml_mods_pickle

app_log = logger.app_log
log_stack_info = logger.log_stack_info
Expand All @@ -37,9 +32,6 @@
DEFAULT_UI_PORT = get_config("user_interface.port")


_IMPORT_PATH_SEPARATOR = ":"


def get_ui_url(path):
baseUrl = f"http://{DEFAULT_UI_ADDRESS}:{DEFAULT_UI_PORT}"
return f"{baseUrl}{path}"
Expand Down Expand Up @@ -264,49 +256,16 @@
shutil.copyfile(src_path, dest_path)


@_qml_mods_pickle
def cloudpickle_serialize(obj):
return cloudpickle.dumps(obj)


def cloudpickle_deserialize(obj):
return cloudpickle.loads(obj)


def select_first_executor(qnode, executors):
"""Selects the first executor to run the qnode"""
return executors[0]


def get_import_path(obj) -> Tuple[str, str]:
"""
Determine the import path of an object.
def get_qelectron_db_path(dispatch_id: str, task_id: int):
"""
module = inspect.getmodule(obj)
if module:
module_path = module.__name__
class_name = obj.__name__
return f"{module_path}{_IMPORT_PATH_SEPARATOR}{class_name}"
raise RuntimeError(f"Unable to determine import path for {obj}.")
Return the path to the Qelectron database for a given dispatch_id and task_id.


def import_from_path(path: str) -> Any:
"""
Import a class from a path.
This is a proxy to qelectron_utils.get_qelectron_db_path() for removing qelectron dependency.
"""
module_path, class_name = path.split(_IMPORT_PATH_SEPARATOR)
module = importlib.import_module(module_path)
return getattr(module, class_name)

try:
from .qelectron_utils import get_qelectron_db_path

def get_original_shots(dev: Device):
"""
Recreate vector of shots if device has a shot vector.
"""
if not dev.shot_vector:
return dev.shots

shot_sequence = []
for shots in dev.shot_vector:
shot_sequence.extend([shots.shots] * shots.copies)
return type(dev.shot_vector)(shot_sequence)
return get_qelectron_db_path(dispatch_id, task_id)
except ImportError:
return None

Check warning on line 271 in covalent/_shared_files/utils.py

View check run for this annotation

Codecov / codecov/patch

covalent/_shared_files/utils.py#L270-L271

Added lines #L270 - L271 were not covered by tests
2 changes: 1 addition & 1 deletion covalent/_workflow/qelectron.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import pennylane as qml

from .._shared_files.utils import get_import_path, get_original_shots
from .._shared_files.qelectron_utils import get_import_path, get_original_shots
from ..quantum.qcluster import QCluster
from ..quantum.qcluster.base import AsyncBaseQCluster, BaseQExecutor
from ..quantum.qcluster.simulator import Simulator
Expand Down
2 changes: 1 addition & 1 deletion covalent/_workflow/qnode.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@

from .._results_manager.qresult import QNodeFutureResult
from .._shared_files import logger
from .._shared_files.qelectron_utils import get_original_shots
from .._shared_files.qinfo import QElectronInfo, QNodeSpecs
from .._shared_files.qresult_utils import re_execute
from .._shared_files.utils import get_original_shots
from ..executor.qbase import BaseQExecutor
from .qdevice import QEDevice

Expand Down
12 changes: 7 additions & 5 deletions covalent/executor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@

from .._shared_files import logger
from .._shared_files.config import get_config, update_config
from ..quantum import QCluster, Simulator
from .base import BaseExecutor

app_log = logger.app_log
Expand Down Expand Up @@ -284,6 +283,8 @@ class _QExecutorManager:
"""

def __init__(self):
from ..quantum import QCluster, Simulator

# Dictionary mapping executor name to executor class
self.executor_plugins_map: Dict[str, Any] = {
"QCluster": QCluster,
Expand Down Expand Up @@ -370,11 +371,12 @@ def validate_module(self, module_obj) -> None:


_executor_manager = _ExecutorManager()
_qexecutor_manager = _QExecutorManager()

for name in _executor_manager.executor_plugins_map:
plugin_class = _executor_manager.executor_plugins_map[name]
globals()[plugin_class.__name__] = plugin_class

for qexecutor_cls in _qexecutor_manager.executor_plugins_map.values():
globals()[qexecutor_cls.__name__] = qexecutor_cls
# Only creating the qexecutor manager if its requirements are installed
with contextlib.suppress(ImportError):
_qexecutor_manager = _QExecutorManager()
for qexecutor_cls in _qexecutor_manager.executor_plugins_map.values():
globals()[qexecutor_cls.__name__] = qexecutor_cls
Loading
Loading