From 919f8b7bee2b12c10faf0f409f0da2821a640ec5 Mon Sep 17 00:00:00 2001 From: QFer Date: Tue, 29 Oct 2024 10:59:04 +0100 Subject: [PATCH 1/3] [QNEBE-901] Remote hardware backend support --- README.md | 22 +- .../experiment_complete_example.json | 2 +- .../experiment_meta_example.json | 2 +- src/adk/api/local_api.py | 52 ++- src/adk/api/qne_client.py | 11 +- src/adk/api/remote_api.py | 62 ++- src/adk/command_list.py | 4 +- src/adk/command_processor.py | 16 +- src/adk/schema/experiments/experiment.json | 3 - src/adk/settings.py | 1 + src/tests/api/test_local_api.py | 125 ++++-- src/tests/api/test_remote_api.py | 364 +++++++++++++++++- src/tests/test_command_list.py | 10 +- src/tests/test_command_processor.py | 14 +- 14 files changed, 571 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index 7b3554e..47a5b7c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ # Quantum Network Explorer ADK -The QNE-ADK is a Quantum Network Explorer - Application Development Kit that allows you to create your own applications and experiments and run them on a simulator. +The QNE-ADK is a Quantum Network Explorer - Application Development Kit that allows you to create your own applications +and experiments and run them on a simulator. ## Local development With the ADK you can create your own application from scratch using the ``qne application create`` command -(see section 'Commands' below for more information about the individual commands). An application directory is created with all the necessary files for you to configure. +(see section 'Commands' below for more information about the individual commands). An application directory is created +with all the necessary files for you to configure. When configuring an application, you specify the different roles and what types of inputs your application uses. In addition, you write the functionality of your application using the NetQASM library. @@ -34,10 +36,13 @@ A copy of a working application is made to your application directory and can be for application development. ## Prerequisites -- A modern Linux or macOS (10 or 11) 64-bit (x86_64) operating system. If you don’t have Linux or macOS you could run it via virtualization, e.g. using VirtualBox. If you have Windows 10 or 11 you can also use the [Bash on Ubuntu](https://docs.microsoft.com/en-us/windows/wsl/) subsystem. +- A modern Linux or macOS (10 or 11) 64-bit (x86_64) operating system. If you don’t have Linux or macOS you could run + it via virtualization, e.g. using VirtualBox. If you have Windows 10 or 11 you can also use the [Bash on Ubuntu](https://docs.microsoft.com/en-us/windows/wsl/) + subsystem. - A [virtual environment](https://docs.python.org/3/library/venv.html) should be created and activated before creating an application. - Python version 3.8 or higher. -- NetQASM makes use of SquidASM for which you need credentials in order to use it. These credentials can be obtained by registering on the forum of [NetSquid](https://forum.netsquid.org/). +- NetQASM makes use of SquidASM for which you need credentials in order to use it. These credentials can be obtained by + registering on the forum of [NetSquid](https://forum.netsquid.org/). ## Installation To install all the required packages, execute the following command: @@ -46,16 +51,19 @@ To install all the required packages, execute the following command: pip install qne-adk ``` -After installing the qne-adk, you can install SquidASM. Replace '{netsquid-user-name}' and '{netsquid-password}' with the credentials you registered on [NetSquid](https://forum.netsquid.org/): +After installing the qne-adk, you can install SquidASM. Replace '{netsquid-user-name}' and '{netsquid-password}' with +the credentials you registered on [NetSquid](https://forum.netsquid.org/): ``` pip install squidasm --extra-index-url=https://{netsquid-user-name}:{netsquid-password}@pypi.netsquid.org ``` -Now everything should be setup and ready in order to create your own applications and experiments and run them on the simulator! +Now everything should be setup and ready in order to create your own applications and experiments and run them on the +simulator! ## Commands -The QNE-ADK uses various commands to create and run your applications and experiments. All the commands are listed below: +The QNE-ADK uses various commands to create and run your applications and experiments. All the commands are listed +below:
diff --git a/docs/json_examples/experiment_complete_example.json b/docs/json_examples/experiment_complete_example.json index 0eff0c9..f537f3a 100644 --- a/docs/json_examples/experiment_complete_example.json +++ b/docs/json_examples/experiment_complete_example.json @@ -7,7 +7,7 @@ }, "backend": { "location": "local", - "type": "local_netsquid" + "type": "NetSquid simulator" }, "description": "teleport experiment description", "number_of_rounds": 1, diff --git a/docs/json_examples/experiment_meta_example.json b/docs/json_examples/experiment_meta_example.json index ac1fef9..743e0a0 100644 --- a/docs/json_examples/experiment_meta_example.json +++ b/docs/json_examples/experiment_meta_example.json @@ -7,7 +7,7 @@ }, "backend": { "location": "local", - "type": "local_netsquid" + "type": "NetSquid simulator" }, "description": "teleport experiment description", "number_of_rounds": 1, diff --git a/src/adk/api/local_api.py b/src/adk/api/local_api.py index 16cee64..8e92a73 100644 --- a/src/adk/api/local_api.py +++ b/src/adk/api/local_api.py @@ -15,7 +15,7 @@ from adk.managers.roundset_manager import RoundSetManager from adk.generators.network_generator import FullyConnectedNetworkGenerator from adk.parsers.output_converter import OutputConverter -from adk.settings import BASE_DIR +from adk.settings import BASE_DIR, DEFAULT_BACKEND_TYPE from adk.type_aliases import (AppConfigType, AppResultType, ApplicationType, ApplicationDataType, app_configNetworkType, app_configApplicationType, AssetType, assetApplicationType, assetNetworkType, ErrorDictType, ExperimentDataType, ResultType, @@ -591,7 +591,7 @@ def validate_application(self, application_name: str, application_path: Path) -> application_path: Path of where the application is located returns: - Returns empty list when all validations passes + Returns empty list when all validations pass Returns dict containing error messages of the validations that failed """ error_dict: ErrorDictType = utils.get_empty_errordict() @@ -990,7 +990,7 @@ def __create_experiment(self, experiment_name: str, application_name: str, local }, "backend": { "location": "local" if local else "remote", - "type": "local_netsquid" if local else "remote_netsquid" + "type": DEFAULT_BACKEND_TYPE }, "description": f"description of {experiment_name} here", "number_of_rounds": 1, @@ -1298,9 +1298,9 @@ def get_experiment_round_set(self, experiment_path: Path) -> Optional[str]: The round set from the experiment.json """ if not self.is_experiment_local(experiment_path): - experiment_data = self.get_experiment_data(experiment_path) - if "round_set" in experiment_data["meta"]: - return cast(str, experiment_data["meta"]["round_set"]) + experiment_meta = self.get_experiment_meta(experiment_path) + if "round_set" in experiment_meta: + return cast(str, experiment_meta["round_set"]) return None @staticmethod @@ -1565,7 +1565,7 @@ def get_results(experiment_path: Path) -> List[ResultType]: result = output_converter.convert(round_number=1) return [result] - def validate_experiment(self, experiment_path: Path) -> ErrorDictType: + def validate_experiment(self, experiment_path: Path, error_dict: ErrorDictType) -> None: """ Validates the experiment by checking: - if the structure is correct and consists of an experiment.json @@ -1577,19 +1577,14 @@ def validate_experiment(self, experiment_path: Path) -> ErrorDictType: Args: experiment_path: The location of the experiment - - Returns: - Dictionary containing lists of error, warning and info messages of the validations that failed + error_dict: Dictionary containing error and warning messages of the validations that failed """ local = self.is_experiment_local(experiment_path=experiment_path) - error_dict: ErrorDictType = utils.get_empty_errordict() self._validate_experiment_json(experiment_path=experiment_path, error_dict=error_dict) if local: self._validate_experiment_input(experiment_path=experiment_path, error_dict=error_dict) - return error_dict - def _validate_experiment_json(self, experiment_path: Path, error_dict: ErrorDictType) -> None: """ This function validates if experiment.json contains valid json and if it passes schema validation. @@ -1606,13 +1601,8 @@ def _validate_experiment_json(self, experiment_path: Path, error_dict: ErrorDict if not valid: error_dict["error"].append(message) else: - # Check if experiment is local or remote experiment_data = self.get_experiment_data(experiment_path) - # location is required field in schema - location = experiment_data["meta"]["backend"]["location"].strip().lower() - if location not in ["local", "remote"]: - error_dict["warning"].append(f"In file '{experiment_json_file}': only 'local' or 'remote' is supported " - f"for property 'location'") + self._validate_experiment_backend(experiment_json_file, experiment_data, error_dict) # slug is now also a required field experiment_network_slug = experiment_data["asset"]["network"]["slug"] # Check if the chosen network exists @@ -1690,6 +1680,28 @@ def _validate_application_roles(self, experiment_path: Path, error_dict: ErrorDi error_dict["error"].append(f"In file '{experiment_path / 'experiment.json'}': role '{role}' is not" f" valid for the application") + def _validate_experiment_backend(self, experiment_file_path: Path, experiment_data: Dict[str, Any], error_dict: + ErrorDictType) -> None: + """ + Validate if the backend properties are valid. Backend property location must be local or remote. + + Args: + experiment_file_path: The location of the experiment.json file + experiment_data: contents of the experiment.json file + error_dict: Dictionary containing error and warning messages of the validations that failed + """ + # Check if experiment backend location is local or remote + # location is required field in schema + location = experiment_data["meta"]["backend"]["location"].strip().lower() + if location not in ["local", "remote"]: + error_dict["warning"].append(f"In file '{experiment_file_path}': only 'local' or 'remote' is supported " + f"for backend property 'location'") + if location == "local": + # local backend must be default backend + if experiment_data["meta"]["backend"]["type"].strip().lower() != DEFAULT_BACKEND_TYPE.strip().lower(): + error_dict["warning"].append(f"In file '{experiment_file_path}': local backend must " + f"be {DEFAULT_BACKEND_TYPE}") + def _validate_experiment_nodes(self, experiment_file_path: Path, experiment_data: Dict[str, Any], error_dict: ErrorDictType) -> None: """ @@ -1704,7 +1716,7 @@ def _validate_experiment_nodes(self, experiment_file_path: Path, experiment_data """ experiment_network_slug = experiment_data["asset"]["network"]["slug"] - # Check if the amount of nodes are valid for this network + # Check if the amount of nodes is valid for this network experiment_nodes = experiment_data["asset"]["network"]["nodes"] all_network_nodes = self._get_network_nodes() diff --git a/src/adk/api/qne_client.py b/src/adk/api/qne_client.py index 3ce0198..95c6fe9 100644 --- a/src/adk/api/qne_client.py +++ b/src/adk/api/qne_client.py @@ -1,4 +1,5 @@ from datetime import datetime, timezone +from functools import lru_cache import re from pathlib import Path from typing import Any, cast, Dict, List, Optional, Tuple, Union @@ -432,10 +433,11 @@ def partial_update_asset(self, asset_url: str, asset: AssetType) -> AssetType: response = self._action('partialUpdateAsset', id=asset_id, asset=params) return cast(AssetType, response) - def list_default_backendtypes(self) -> List[BackendTypeType]: + def list_default_backendtypes(self) -> BackendTypeType: response = self._action('listDefaultBackendTypes') - return cast(List[BackendTypeType], response) + return cast(BackendTypeType, response) + @lru_cache def list_backendtypes(self) -> List[BackendTypeType]: response = self._action('listBackendTypes') return cast(List[BackendTypeType], response) @@ -454,6 +456,11 @@ def retrieve_backend(self, backend_url: str) -> BackendType: response = self._action('retrieveBackend', id=backend_id) return cast(BackendType, response) + def list_backends_networks(self) -> List[Any]: + response = self._action('listBackendsNetworks') + return cast(List[Any], response) + + def list_experiments(self) -> List[ExperimentType]: response = self._action('listExperiments') return cast(List[ExperimentType], response) diff --git a/src/adk/api/remote_api.py b/src/adk/api/remote_api.py index 5acb031..e7f4555 100644 --- a/src/adk/api/remote_api.py +++ b/src/adk/api/remote_api.py @@ -16,7 +16,7 @@ ApplicationType, ApplicationDataType, AssetType, assetNetworkType, ErrorDictType, ExperimentType, FinalResultType, GenericNetworkData, ExperimentDataType, ResultType, RoundSetType, round_resultType, cumulative_resultType, instructionsType, ChannelType, - parametersType, coordinatesType, listValuesType, app_ownerType) + parametersType, coordinatesType, listValuesType, app_ownerType, BackendTypeType) from adk.settings import BASE_DIR from adk.utils import get_default_remote_data @@ -683,7 +683,7 @@ def validate_application(self, application_slug: str) -> ErrorDictType: application_slug: Slug of the application returns: - Returns empty list when all validations passes + Returns empty list when all validations pass Returns dict containing error messages of the validations that failed """ error_dict: ErrorDictType = utils.get_empty_errordict() @@ -724,6 +724,41 @@ def experiments_list(self) -> List[ExperimentType]: return experiment_list + def __get_backend_type(self, backend_type: str) -> List[BackendTypeType]: + """ + Get the backend info for the remote backend in the experiment data. + + Returns: + 0 or 1 backend types with this type + """ + backend_types = self.__qne_client.list_backendtypes() + # "type" is required + backends = [backend for backend in backend_types if backend["name"].lower() == backend_type.lower()] + if len(backends) > 1: + # QNE configuration error, should be solved in QNE + raise ExperimentValueError("More backend types with the same name configured in QNE") + return backends + + def validate_experiment(self, experiment_data: ExperimentDataType, error_dict: ErrorDictType) -> None: + """ + Validate if the remote backend settings are valid before experiment is created + """ + backend_data = experiment_data["meta"]["backend"] + # "location" is required + if backend_data["location"].strip().lower() != "remote": + error_dict["error"].append("Backend location in experiment is not remote") + # "type" is required + backend_types = self.__get_backend_type(experiment_data["meta"]["backend"]["type"]) + if len(backend_types) == 0: + error_dict["error"].append("Backend type in experiment is not a valid remote backend type " + "(names must match)") + else: + if not backend_types[0]["is_allowed"]: + error_dict["error"].append("The requested remote backend is not available for your current account " + "type, select a different backend.") + if backend_types[0]["status"] == "OFFLINE": + error_dict["warning"].append("The requested remote backend is OFFLINE") + def __create_experiment(self, application_slug: str, app_version_url: str) -> ExperimentType: """ Create and send an experiment object to api-router @@ -774,7 +809,7 @@ def __translate_asset(asset_to_create: AssetType, experiment_url: str) -> AssetT """ Because of differences in channel definition for api-router networks and asset networks we need a fix to translate these (local) channels to a format that the backend expects. - Also the asset needs an experiment entry with the experiment url + Also, the asset needs an experiment entry with the experiment url """ asset_network = cast(assetNetworkType, asset_to_create["network"]) experiment_channels = asset_network["channels"] @@ -789,23 +824,24 @@ def __translate_asset(asset_to_create: AssetType, experiment_url: str) -> AssetT asset_to_create["experiment"] = experiment_url return asset_to_create - def __create_round_set(self, asset_url: str, number_of_rounds: int) -> RoundSetType: + def __create_round_set(self, asset_url: str, backend_type_url: str, number_of_rounds: int) -> RoundSetType: """ Create and send a round set object to api-router """ - round_set_to_create = self.__create_round_set_type(asset_url, number_of_rounds) + round_set_to_create = self.__create_round_set_type(asset_url, backend_type_url, number_of_rounds) round_set = self.__qne_client.create_roundset(round_set_to_create) return round_set @staticmethod - def __create_round_set_type(asset_url: str, number_of_rounds: int) -> RoundSetType: + def __create_round_set_type(asset_url: str, backend_type_url: str, number_of_rounds: int) -> RoundSetType: """ Create and return a round set object for sending to api-router """ round_set: RoundSetType = { "number_of_rounds": number_of_rounds, "status": "NEW", + "backend_type": backend_type_url, "input": asset_url } return round_set @@ -822,11 +858,15 @@ def run_experiment(self, experiment_data: ExperimentDataType) -> Tuple[str, str] """ application_slug = experiment_data["meta"]["application"]["slug"] app_version_url = experiment_data["meta"]["application"]["app_version"] + backend_type = self.__get_backend_type(experiment_data["meta"]["backend"]["type"]) + if len(backend_type) == 0: + raise ExperimentValueError("Backend type in experiment is not a valid remote backend type " + "(names must match)") experiment = self.__create_experiment(application_slug, app_version_url) experiment_asset = experiment_data["asset"] asset = self.__create_asset(experiment_asset, str(experiment["url"])) number_of_rounds = experiment_data["meta"]["number_of_rounds"] - round_set = self.__create_round_set(str(asset["url"]), number_of_rounds) + round_set = self.__create_round_set(str(asset["url"]), backend_type[0]["url"], number_of_rounds) return str(round_set["url"]), str(experiment["id"]) @@ -948,7 +988,7 @@ def __update_networks_networks(self, overwrite: bool) -> None: Get the remote networks and update the local network definitions Args: - overwrite: When True, replace the local files. Otherwise try to merge (keeping the new local network + overwrite: When True, replace the local files. Otherwise, try to merge (keeping the new local network entities) """ entity = "networks" @@ -991,7 +1031,7 @@ def __update_networks_channels(self, overwrite: bool) -> None: Get the remote channels and update the local channel definitions Args: - overwrite: When True, replace the local files. Otherwise try to merge (keeping the new local network + overwrite: When True, replace the local files. Otherwise, try to merge (keeping the new local network entities) """ entity = "channels" @@ -1020,7 +1060,7 @@ def __update_networks_nodes(self, overwrite: bool) -> None: Get the remote nodes and update the local node definitions Args: - overwrite: When True, replace the local files. Otherwise try to merge (keeping the new local network + overwrite: When True, replace the local files. Otherwise, try to merge (keeping the new local network entities) """ entity = "nodes" @@ -1053,7 +1093,7 @@ def __update_networks_templates(self, overwrite: bool) -> None: Get the remote templates and update the local template definitions Args: - overwrite: When True, replace the local files. Otherwise try to merge (keeping the new local network + overwrite: When True, replace the local files. Otherwise, try to merge (keeping the new local network entities) """ entity = "templates" diff --git a/src/adk/command_list.py b/src/adk/command_list.py index 8840c78..d3276e8 100644 --- a/src/adk/command_list.py +++ b/src/adk/command_list.py @@ -533,15 +533,15 @@ def experiments_run( fetched at a later moment. """ experiment_path, _ = retrieve_experiment_name_and_path(experiment_name=experiment_name) + local = local_api.is_experiment_local(experiment_path=experiment_path) # Validate the experiment before executing the run command - validate_dict = processor.experiments_validate(experiment_path=experiment_path) + validate_dict = processor.experiments_validate(experiment_path=experiment_path, local=local) if validate_dict["error"] or validate_dict["warning"]: typer.echo("Experiment did not run") validation_messages = format_validation_messages(validate_dict) raise ExperimentFailedValidation(validation_messages) - local = local_api.is_experiment_local(experiment_path=experiment_path) if local: block = True if update: diff --git a/src/adk/command_processor.py b/src/adk/command_processor.py index f37d7a9..ccc6de1 100644 --- a/src/adk/command_processor.py +++ b/src/adk/command_processor.py @@ -291,16 +291,24 @@ def experiments_list(self) -> List[ExperimentType]: return self.__remote.experiments_list() @log_function - def experiments_validate(self, experiment_path: Path) -> ErrorDictType: - """ Validate the local experiment files + def experiments_validate(self, experiment_path: Path, local: bool) -> ErrorDictType: + """ + Validate the experiment files. For remote also check if the backend is a valid backend and the requested + backend is available. Args: experiment_path: directory where experiment resides + local: Boolean flag specifying whether experiment is local or remote Returns: Dictionary with errors, warnings found """ - return self.__local.validate_experiment(experiment_path) + error_dict: ErrorDictType = utils.get_empty_errordict() + if not local: + experiment_data = self.__local.get_experiment_data(experiment_path) + self.__remote.validate_experiment(experiment_data, error_dict) + self.__local.validate_experiment(experiment_path, error_dict) + return error_dict @log_function def experiments_delete_remote_only(self, experiment_id: str) -> bool: @@ -319,7 +327,7 @@ def experiments_delete_remote_only(self, experiment_id: str) -> bool: @log_function def experiments_delete(self, experiment_name: str, experiment_path: Path) -> bool: """ - Get the remote experiment id registered for this experiment when it run remote, delete this remote experiment. + Get the remote experiment id registered for this experiment when it ran remote, delete this remote experiment. Then delete the local experiment. Args: diff --git a/src/adk/schema/experiments/experiment.json b/src/adk/schema/experiments/experiment.json index c7694dc..32eac5c 100644 --- a/src/adk/schema/experiments/experiment.json +++ b/src/adk/schema/experiments/experiment.json @@ -39,9 +39,6 @@ }, "type": { "type": "string" - }, - "experiment_id": { - "type": "integer" } }, "required": [ diff --git a/src/adk/settings.py b/src/adk/settings.py index b9957d1..5ec2e97 100644 --- a/src/adk/settings.py +++ b/src/adk/settings.py @@ -3,6 +3,7 @@ from pydantic_settings import BaseSettings BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +DEFAULT_BACKEND_TYPE = 'NetSquid simulator' class Settings(BaseSettings): diff --git a/src/tests/api/test_local_api.py b/src/tests/api/test_local_api.py index dd478fd..b98f85a 100644 --- a/src/tests/api/test_local_api.py +++ b/src/tests/api/test_local_api.py @@ -5,10 +5,12 @@ from unittest.mock import call, patch, MagicMock, mock_open from adk.api.local_api import LocalApi -from adk.exceptions import (ApplicationAlreadyExists, ApplicationDoesNotExist, ExperimentDirectoryNotValid, +from adk.exceptions import (ApplicationDoesNotExist, ExperimentDirectoryNotValid, JsonFileNotFound, NoNetworkAvailable, PackageNotComplete) from adk.managers.roundset_manager import RoundSetManager from adk.parsers.output_converter import OutputConverter +from adk.settings import DEFAULT_BACKEND_TYPE +from adk.utils import get_empty_errordict # pylint: disable=R0902 @@ -33,7 +35,7 @@ def setUp(self) -> None: }, "backend": { "location": "local", - "type": "local_netsquid" + "type": DEFAULT_BACKEND_TYPE }, "description": "", "number_of_rounds": 1, @@ -311,7 +313,7 @@ def setUp(self) -> None: }, "backend": { "location": "local", - "type": "local_netsquid" + "type": DEFAULT_BACKEND_TYPE }, "description": "exptest3: experiment description", "number_of_rounds": 1, @@ -469,7 +471,7 @@ def test_clone_application(self): get_application_data_mock.return_value = self.mock_app_manifest self.local_api.clone_application(self.application, "New_App", self.path) - get_application_path_mock.called_once_with("new_app") + get_application_path_mock.assert_called_once_with("test_application") copy_files_calls = [call(self.path / 'old', self.path, files_list=['manifest.json']), call(self.path / 'old' / 'config', self.path / 'config', files_list=['application.json', 'network.json', 'result.json']), @@ -1318,7 +1320,9 @@ def test_validate_experiment(self): patch.object(LocalApi, "is_experiment_local", return_value=True), \ patch.object(LocalApi, "_validate_experiment_input") as validate_experiment_input: - self.assertEqual(self.local_api.validate_experiment(self.path), self.error_dict) + error_dict = {'error': [], 'warning': [], 'info': []} + self.local_api.validate_experiment(self.path, error_dict) + self.assertEqual(error_dict, self.error_dict) validate_experiment_json_mock.assert_called_once_with(experiment_path=self.path, error_dict=self.error_dict) validate_experiment_input.assert_called_once_with(experiment_path=self.path, error_dict=self.error_dict) @@ -1337,10 +1341,9 @@ def test_validate_experiment_json_all_ok(self): patch.object(LocalApi, "_validate_experiment_application") as validate_experiment_application_mock: path_join_mock.return_value = self.path - validate_experiment_input_mock.return_value = self.error_dict validate_json_schema_mock.return_value = True, None get_network_info_mock.return_value = "slug" - self.local_api.validate_experiment(self.path) + self.local_api.validate_experiment(self.path, self.error_dict) validate_experiment_input_mock.assert_called_once_with(experiment_path=self.path, error_dict=self.error_dict) path_join_mock.assert_called_once() @@ -1364,14 +1367,14 @@ def test_validate_experiment_json_experiment_not_valid(self): path_join_mock.return_value = self.path validate_json_schema_mock.return_value = False, "message" - self.local_api.validate_experiment(self.path) + self.local_api.validate_experiment(self.path, self.error_dict) validate_experiment_input_mock.assert_called_once_with(experiment_path=self.path, error_dict={'error': ['message'], 'warning': [], 'info': []}) path_join_mock.assert_called_once() validate_json_schema_mock.assert_called_once_with(self.experiment_file_path, self.path) - def test_validate_experiment_json_location_not_local(self): + def test_validate_experiment_json_location_not_valid(self): with patch("adk.api.local_api.validate_json_schema") as validate_json_schema_mock, \ patch("adk.api.local_api.utils.read_json_file"), \ patch("adk.api.local_api.os.path.join") as path_join_mock, \ @@ -1388,7 +1391,7 @@ def test_validate_experiment_json_location_not_local(self): "meta": { "backend": { "location": "something_else", - "type": "local_netsquid" + "type": DEFAULT_BACKEND_TYPE }, "number_of_rounds": 1, "description": "exptest3: experiment description" @@ -1401,15 +1404,57 @@ def test_validate_experiment_json_location_not_local(self): } } warning_message = f"In file '{self.experiment_file_path}': only 'local' or 'remote' is supported for " \ - f"property 'location'" + f"backend property 'location'" error_dict = {'error': [], 'warning': [warning_message], 'info': []} path_join_mock.return_value = self.path - validate_experiment_input_mock.return_value = self.error_dict validate_json_schema_mock.return_value = True, None get_experiment_data_mock.return_value = experiment_data get_network_info_mock.return_value = "slug" - self.local_api.validate_experiment(self.path) + self.local_api.validate_experiment(self.path, self.error_dict) + validate_experiment_input_mock.assert_called_once_with(experiment_path=self.path, + error_dict=error_dict) + path_join_mock.assert_called_once() + validate_json_schema_mock.assert_called_once_with(self.experiment_file_path, self.path) + get_experiment_data_mock.assert_called_once_with(self.path) + + def test_validate_experiment_json_local_backend_type_not_valid(self): + with patch("adk.api.local_api.validate_json_schema") as validate_json_schema_mock, \ + patch("adk.api.local_api.utils.read_json_file"), \ + patch("adk.api.local_api.os.path.join") as path_join_mock, \ + patch.object(LocalApi, "_validate_experiment_input") as validate_experiment_input_mock, \ + patch.object(LocalApi, "get_experiment_data") as get_experiment_data_mock, \ + patch.object(LocalApi, "is_experiment_local", return_value=True), \ + patch.object(LocalApi, "_get_network_info") as get_network_info_mock, \ + patch.object(LocalApi, "_validate_experiment_nodes"), \ + patch.object(LocalApi, "_validate_experiment_channels"), \ + patch.object(LocalApi, "_validate_experiment_roles"), \ + patch.object(LocalApi, "_validate_experiment_application"): + + experiment_data = { + "meta": { + "backend": { + "location": "local", + "type": "not the default backend type" + }, + "number_of_rounds": 1, + "description": "exptest3: experiment description" + }, + "asset": { + "network": { + "name": "Randstad", + "slug": "randstad", + } + } + } + warning_message = f"In file '{self.experiment_file_path}': local backend must be {DEFAULT_BACKEND_TYPE}" + error_dict = {'error': [], 'warning': [warning_message], 'info': []} + + path_join_mock.return_value = self.path + validate_json_schema_mock.return_value = True, None + get_experiment_data_mock.return_value = experiment_data + get_network_info_mock.return_value = "slug" + self.local_api.validate_experiment(self.path, self.error_dict) validate_experiment_input_mock.assert_called_once_with(experiment_path=self.path, error_dict=error_dict) path_join_mock.assert_called_once() @@ -1431,11 +1476,10 @@ def test_validate_experiment_json_network_slug_does_not_exist(self): error_dict = {'error': [], 'warning': [warning_message], 'info': []} path_join_mock.return_value = self.path - validate_experiment_input_mock.return_value = self.error_dict validate_json_schema_mock.return_value = True, None get_experiment_data_mock.return_value = self.mock_experiment_data get_network_info_mock.return_value = None - self.local_api.validate_experiment(self.path) + self.local_api.validate_experiment(self.path, self.error_dict) validate_experiment_input_mock.assert_called_once_with(experiment_path=self.path, error_dict=error_dict) path_join_mock.assert_called_once() @@ -1481,7 +1525,7 @@ def test_validate_experiment_input_all_ok(self): patch.object(LocalApi, "_LocalApi__check_all_experiment_input_files_exist") as exp_input_files_mock: is_dir_mock.return_value = True - self.local_api.validate_experiment(self.path) + self.local_api.validate_experiment(self.path, self.error_dict) validate_experiment_json_mock.assert_called_once_with(experiment_path=self.path, error_dict=self.error_dict) exp_input_files_mock.assert_called_once_with(self.path / 'input', self.error_dict) is_dir_mock.assert_called_once() @@ -1495,7 +1539,7 @@ def test_validate_experiment_input_no_dir_exist(self): error_dict = {'error': [error_message], 'warning': [], 'info': []} is_dir_mock.return_value = False - self.local_api.validate_experiment(self.path) + self.local_api.validate_experiment(self.path, self.error_dict) is_dir_mock.assert_called_once() validate_experiment_json_mock.assert_called_once_with(experiment_path=self.path, error_dict=error_dict) @@ -1516,7 +1560,7 @@ def test_validate_experiment_input_file_missing(self): get_config_file_names_mock.return_value = self.config_files is_file_mock.return_value = False - self.local_api.validate_experiment(self.path) + self.local_api.validate_experiment(self.path, self.error_dict) validate_application_roles_mock.assert_called_once() is_dir_mock.assert_called_once() get_config_file_names_mock.assert_called_once() @@ -1539,7 +1583,7 @@ def test_validate_experiment_input_experiment_data_invalid(self): is_file_mock.return_value = True validate_json_schema_mock.return_value = False, "message" - self.local_api.validate_experiment(self.path) + self.local_api.validate_experiment(self.path, self.error_dict) validate_application_roles_mock.assert_called_once() is_dir_mock.assert_called_once() get_config_file_names_mock.assert_called_once() @@ -1567,7 +1611,7 @@ def test_validate_experiment_input_missing_role_file_names(self): validate_json_schema_mock.return_value = True, None get_role_file_names_mock.return_value = self.roles - self.local_api.validate_experiment(self.path) + self.local_api.validate_experiment(self.path, self.error_dict) validate_application_roles_mock.assert_called_once() is_dir_mock.assert_called_once() get_config_file_names_mock.assert_called_once() @@ -1595,9 +1639,9 @@ def test_validate_experiment_input_missing_application_roles(self): mock_exp_data_for_roles["asset"]["network"]["roles"] = {"role1": "n1", "role2": "n2"} get_experiment_data_mock.return_value = mock_exp_data_for_roles - error_dict_actual = self.local_api.validate_experiment(self.path) + self.local_api.validate_experiment(self.path, self.error_dict) - self.assertDictEqual(error_dict_actual, error_dict_expected) + self.assertDictEqual(self.error_dict, error_dict_expected) validate_experiment_json_mock.assert_called_once() check_all_input_files_mock.assert_called_once() is_dir_mock.assert_called_once() @@ -1620,7 +1664,7 @@ def test_validate_experiment_nodes(self): "meta": { "backend": { "location": "local", - "type": "local_netsquid" + "type": DEFAULT_BACKEND_TYPE }, "number_of_rounds": 1, "description": "exptest3: experiment description" @@ -1640,11 +1684,10 @@ def test_validate_experiment_nodes(self): } path_join_mock.return_value = self.path - validate_experiment_input_mock.return_value = self.error_dict validate_json_schema_mock.return_value = True, None get_experiment_data_mock.return_value = self.mock_experiment_data get_network_nodes_mock.return_value = self.all_network_nodes - self.local_api.validate_experiment(self.path) + self.local_api.validate_experiment(self.path, self.error_dict) get_network_nodes_mock.assert_called_once() validate_experiment_input_mock.assert_called_once_with(experiment_path=self.path, error_dict=self.error_dict) @@ -1663,7 +1706,7 @@ def test_validate_experiment_nodes(self): get_experiment_data_mock.reset_mock() validate_experiment_input_mock.reset_mock() get_experiment_data_mock.return_value = experiment_data_too_many_nodes - self.local_api.validate_experiment(self.path) + self.local_api.validate_experiment(self.path, self.error_dict) get_network_nodes_mock.assert_called_once() validate_experiment_input_mock.assert_called_once_with(experiment_path=self.path, error_dict=error_dict) @@ -1687,7 +1730,7 @@ def test_validate_experiment_channels(self): "meta": { "backend": { "location": "local", - "type": "local_netsquid" + "type": DEFAULT_BACKEND_TYPE }, "number_of_rounds": 1, "description": "exptest3: experiment description" @@ -1706,7 +1749,6 @@ def test_validate_experiment_channels(self): } path_join_mock.return_value = self.path - validate_experiment_input_mock.return_value = self.error_dict validate_json_schema_mock.return_value = True, None get_experiment_data_mock.return_value = self.mock_experiment_data get_channels_for_network_mock.return_value = self.all_network_channels @@ -1715,7 +1757,7 @@ def test_validate_experiment_channels(self): {"slug": "n4-n3", "node1": "n4", "node2": "n3"}, {"slug": "n4-n5", "node1": "n4", "node2": "n5"}] get_channel_info_mock.side_effect = channel_info_list - self.local_api.validate_experiment(self.path) + self.local_api.validate_experiment(self.path, self.error_dict) get_channels_for_network_mock.assert_called_once_with(network_slug='randstad') validate_experiment_input_mock.assert_called_once_with(experiment_path=self.path, error_dict=self.error_dict) @@ -1744,7 +1786,8 @@ def test_validate_experiment_channels(self): {"slug": "n4-n5", "node1": "n4", "node2": "n5"}, {"slug": "n5-n6", "node1": "n5", "node2": "n6"}] get_channel_info_mock.side_effect = channel_info_list - self.local_api.validate_experiment(self.path) + self.error_dict = get_empty_errordict() + self.local_api.validate_experiment(self.path, self.error_dict) get_channels_for_network_mock.assert_called_once_with(network_slug='randstad') validate_experiment_input_mock.assert_called_once_with(experiment_path=self.path, error_dict=error_dict) @@ -1754,7 +1797,8 @@ def test_validate_experiment_channels(self): get_channels_for_network_mock.reset_mock() validate_experiment_input_mock.reset_mock() get_channels_for_network_mock.return_value = None - self.local_api.validate_experiment(self.path) + self.error_dict = get_empty_errordict() + self.local_api.validate_experiment(self.path, self.error_dict) get_channels_for_network_mock.assert_called_once_with(network_slug='randstad') validate_experiment_input_mock.assert_called_once_with(experiment_path=self.path, error_dict=error_dict) @@ -1769,7 +1813,7 @@ def test_validate_experiment_channels(self): "meta": { "backend": { "location": "local", - "type": "local_netsquid" + "type": DEFAULT_BACKEND_TYPE }, "number_of_rounds": 1, "description": "exptest3: experiment description" @@ -1789,7 +1833,8 @@ def test_validate_experiment_channels(self): error_2 = f"In file '{self.path / 'experiment.json'}': value 'n' of node 'node2' in channel 'n1-n2' " \ "does not exist or is not a valid node for the channel" error_dict = {'error': [error_1, error_2], 'warning': [], 'info': []} - self.local_api.validate_experiment(self.path) + self.error_dict = get_empty_errordict() + self.local_api.validate_experiment(self.path, self.error_dict) validate_experiment_input_mock.assert_called_once_with(experiment_path=self.path, error_dict=error_dict) def test_validate_experiment_roles(self): @@ -1808,12 +1853,11 @@ def test_validate_experiment_roles(self): patch.object(LocalApi, "_get_channels_for_network"): path_join_mock.return_value = self.path - validate_experiment_input_mock.return_value = self.error_dict validate_json_schema_mock.return_value = True, None get_experiment_data_mock.return_value = self.mock_experiment_data get_roles_mock.return_value = ['role1', 'role2'] get_nodes_mock.return_value = {"randstad": ["n1", "n2", "n3"]} - self.local_api.validate_experiment(self.path) + self.local_api.validate_experiment(self.path, self.error_dict) validate_experiment_input_mock.assert_called_once_with(experiment_path=self.path, error_dict=self.error_dict) @@ -1832,7 +1876,8 @@ def test_validate_experiment_roles(self): "for the application" error_3 = f"In file '{self.path / 'experiment.json'}': node 'n1' is used for multiple roles" error_dict = {'error': [error_1, error_2, error_3], 'warning': [], 'info': []} - self.local_api.validate_experiment(self.path) + self.error_dict = get_empty_errordict() + self.local_api.validate_experiment(self.path, self.error_dict) validate_experiment_input_mock.assert_called_once_with(experiment_path=self.path, error_dict=error_dict) def test_validate_template_parameters(self): # pylint: disable=R0914 too-many-locals @@ -1849,7 +1894,6 @@ def test_validate_template_parameters(self): # pylint: disable=R0914 too-many-lo patch.object(LocalApi, "_get_channels_for_network") as get_channels_for_network_mock: path_join_mock.return_value = self.path - validate_experiment_input_mock.return_value = self.error_dict validate_json_schema_mock.return_value = True, None dummy_channel_params_1 = [ { @@ -1938,7 +1982,7 @@ def test_validate_template_parameters(self): # pylint: disable=R0914 too-many-lo error_4 = f"In file '{self.path / 'experiment.json'}': 'incorrect_name' is not valid for param " \ "'relaxation-time' in channel 'n2-n3'" error_dict = {'error': [error_1, error_2, error_3, error_4], 'warning': [], 'info': []} - self.local_api.validate_experiment(self.path) + self.local_api.validate_experiment(self.path, self.error_dict) get_channels_for_network_mock.assert_called_once_with(network_slug='randstad') validate_experiment_input_mock.assert_called_once_with(experiment_path=self.path, error_dict=error_dict) @@ -1959,7 +2003,7 @@ def test_validate_experiment_application(self): "meta": { "backend": { "location": "local", - "type": "local_netsquid" + "type": DEFAULT_BACKEND_TYPE }, "number_of_rounds": 1, "description": "exptest3: experiment description" @@ -2001,13 +2045,12 @@ def test_validate_experiment_application(self): ] path_join_mock.return_value = self.path - validate_experiment_input_mock.return_value = self.error_dict validate_json_schema_mock.return_value = True, None is_file_mock.return_value = True read_json_file_mock.return_value = {'networks': ['randstad', 'europe', 'the-netherlands'], 'roles': ['sender', 'receiver']} - self.local_api.validate_experiment(self.path) + self.local_api.validate_experiment(self.path, self.error_dict) is_file_mock.assert_called_once() read_json_file_mock.assert_called_with(self.path / "input/network.json") self.assertEqual(read_json_file_mock.call_count, 1) diff --git a/src/tests/api/test_remote_api.py b/src/tests/api/test_remote_api.py index 4166d8f..cd1037f 100644 --- a/src/tests/api/test_remote_api.py +++ b/src/tests/api/test_remote_api.py @@ -1,11 +1,13 @@ import copy import unittest +from apistar.exceptions import ErrorResponse from pathlib import Path from unittest.mock import call, patch, MagicMock, mock_open from adk.api.remote_api import RemoteApi -from adk.exceptions import ApiClientError, ApplicationError, ApplicationNotFound +from adk.exceptions import ApiClientError, ApplicationError, ApplicationNotFound, ExperimentValueError +from adk.utils import get_empty_errordict class TestRemoteApi(unittest.TestCase): @@ -16,7 +18,7 @@ def setUp(self): self.email = 'test@email.com' self.password = 'password' self.token = 'token' - self.user = {"id": 123456} + self.user = {"id": 123456, "url": f'{self.host}users/1/'} self.application_data = { "application": { "name": "test_app", @@ -83,10 +85,10 @@ def setUp(self): 'app_version': { 'enabled': True, 'version': 1, - 'app_version': 'http://unittest_server/app_version/42', - 'app_config': 'http://unittest_server/app_config/43', - 'app_result': 'http://unittest_server/app_result/44', - 'app_source': 'http://unittest_server/app_source/45' + 'app_version': f'{self.host}app_version/42', + 'app_config': f'{self.host}app_config/43', + 'app_result': f'{self.host}app_result/44', + 'app_source': f'{self.host}app_source/45' } } } @@ -99,14 +101,6 @@ def setUp(self): "description": self.application_data["application"]["description"], "owner": self.application_data["application"]["owner"] } - - self.app_version = { - "id": 42, - "url": f"{self.host}app_version/42", - "application": self.application["url"], - "version": 1, - "is_disabled": True - } app_version_url = f"{self.host}app_version/42" self.app_config = { "id": 43, @@ -158,6 +152,210 @@ def setUp(self): } self.app_versions_owner = [self.app_version_disabled, self.app_version] self.app_versions_not_owner = [self.app_version] + self.retrieve_application = self.application + self.list_applications = [self.application] + self.retrieve_appversion = self.app_version + self.dummy_qubit = [{ + "qubit_id": 0, + "qubit_parameters": [ + { + "slug": "relaxation-time", + "values": [ + { + "name": "t1", + "value": 0, + "scale_value": 1.0 + } + ] + } + ] + } + ] + + self.dummy_channel_params = [ + { + "slug": "elementary-link-fidelity", + "values": [ + { + "name": "fidelity", + "value": 12344.0, + "scale_value": 1.0 + } + ] + } + ] + self.experiment_data = { + "meta": { + "application": { + "slug": "app_slug", + "app_version": f"{self.host}app_version/42", + "multi_round": "False" + }, + "backend": { + "location": "remote", + "type": 'NV center' + }, + "description": "experiment description", + "number_of_rounds": 1, + "name": "experiment3" + }, + "asset": { + "network": { + "name": "Randstad", + "slug": "randstad", + "nodes": [{"slug": "n1", "node_parameters": None, "qubits": self.dummy_qubit}, + {"slug": "n2", "node_parameters": None, "qubits": self.dummy_qubit}, + {"slug": "n3", "node_parameters": None, "qubits": self.dummy_qubit}, + {"slug": "n4", "node_parameters": None, "qubits": self.dummy_qubit}, + {"slug": "n5", "node_parameters": None, "qubits": self.dummy_qubit}], + "channels": [{"slug": "n1-n2", "parameters": self.dummy_channel_params, + "node1": "n1", "node2": "n2"}, + {"slug": "n2-n3", "parameters": self.dummy_channel_params, + "node1": "n2", "node2": "n3"}, + {"slug": "n4-n3", "parameters": self.dummy_channel_params, + "node1": "n4", "node2": "n3"}, + {"slug": "n4-n5", "parameters": self.dummy_channel_params, + "node1": "n4", "node2": "n5"}], + "roles": {"role1": "n1", "role2": "n2"} + }, + "application": [ + {'roles': ['role2'], + 'values': [{'name': 'phi', 'scale_value': 'pi', 'value': 0.0}, + {'name': 'theta', 'scale_value': 'pi', 'value': 0.0}]} + ] + } + } + self.experiment_1 = { + 'id': 2, + 'url': f'{self.host}experiments/1/', + 'app_version': f'{self.host}app-versions/42/', + 'personal_note': 'Experiment created by qne-adk', + 'is_marked': False + } + self.experiment_2 = { + 'id': 3, + 'url': f'{self.host}experiments/2/', + 'app_version': f'{self.host}app-versions/42/', + 'personal_note': 'Test', + 'is_marked': False + } + self.list_experiments = [self.experiment_1, self.experiment_2] + self.backend_nv_center = { + 'id': 2, + 'url': f'{self.host}backendtypes/2/', + 'name': 'NV center', + 'is_hardware_backend': True, + 'required_permission': 'can_execute', + 'description': 'Hardware network using NV center technology located at the QuTech Delft lab.', + 'is_allowed': True, + 'status': 'ONLINE', + 'status_message': 'This backend is online.', + 'max_number_of_simultaneous_experiments': 1 + } + self.backend_netsquid_simulator = { + 'id': 1, + 'url': f'{self.host}backendtypes/1/', + 'name': 'NetSquid simulator', + 'is_hardware_backend': False, + 'required_permission': 'can_simulate', + 'description': 'The Network Simulator for Quantum Information using discrete events running on a Hetzner VPS.', + 'is_allowed': True, + 'status': 'ONLINE', + 'status_message': 'This backend is online.', + 'max_number_of_simultaneous_experiments': 0 + } + self.list_backendtypes = [self.backend_nv_center, self.backend_netsquid_simulator] + self.list_backends_networks = [ + { + "backend_type": { + 'id': 1, + 'url': f'{self.host}backendtypes/1/', + 'name': 'NetSquid simulator', + 'is_hardware_backend': False, + 'required_permission': 'can_simulate', + 'description': 'The Network Simulator for Quantum Information using discrete events running on a Hetzner VPS.', + 'is_allowed': True, + 'status': 'ONLINE', + 'status_message': 'This backend is online.', + 'max_number_of_simultaneous_experiments': 0 + }, + "networks": [ + {'id': 1, 'url': 'http://127.0.0.1:8000/networks/1/', 'name': 'Randstad', 'slug': 'randstad'}, + {'id': 2, 'url': 'http://127.0.0.1:8000/networks/2/', 'name': 'The Netherlands', 'slug': 'the-netherlands'}, + {'id': 3, 'url': 'http://127.0.0.1:8000/networks/3/', 'name': 'Europe', 'slug': 'europe'}, + {'id': 4, 'url': 'http://127.0.0.1:8000/networks/4/', 'name': 'Basement', 'slug': 'basement'} + ] + }, + { + "backend_type": { + 'id': 2, + 'url': f'{self.host}backendtypes/2/', + 'name': 'NV center', + 'is_hardware_backend': True, + 'required_permission': 'can_execute', + 'description': 'Hardware network using NV center technology located at the QuTech Delft lab.', + 'is_allowed': True, + 'status': 'ONLINE', + 'status_message': 'This backend is online.', + 'max_number_of_simultaneous_experiments': 1 + }, + "networks": [ + {'id': 4, 'url': 'http://127.0.0.1:8000/networks/4/', 'name': 'Basement', 'slug': 'basement'} + ] + }, + ] + self.experiment = { + 'app_version': f'{self.host}app-versions/42/', + 'created_at': '2024-10-27T14:08:54.127099Z', + 'id': 18, + 'is_marked': False, + 'personal_note': 'Experiment created by qne-adk', + 'url': f'{self.host}experiments/18/' + } + self.asset = { + 'id': 18, + 'url': f'{self.host}assets/18/', + 'network': { + 'name': 'Randstad', + 'slug': 'randstad', + 'channels': [], + 'nodes': [], + 'roles': {'role1': 'n1', + 'role2': 'n2'}}, + 'application': [{'roles': ['role2'], 'values': [{'name': 'phi', 'value': 0.0, 'scale_value': 'pi'}, + {'name': 'theta', 'value': 0.0, 'scale_value': 'pi'}]}], + 'experiment': f'{self.host}experiments/18/'} + self.roundset_new = { + 'id': 11, + 'url': f'{self.host}round-sets/11/', + 'number_of_rounds': 1, + 'status': 'NEW', + 'backend_type': f'{self.host}backendtypes/2/', + 'backend': None, + 'queued_at': '2024-10-27T14:21:12.292070Z', + 'input': f'{self.host}assets/18/', + 'progress': 0.0, + 'description': '' + } + self.roundset_complete = { + 'id': 11, + 'url': f'{self.host}round-sets/11/', + 'number_of_rounds': 1, + 'status': 'COMPLETE', + 'backend_type': f'{self.host}backendtypes/2/', + 'backend': f'{self.host}backend/1/', + 'queued_at': '2024-10-27T14:21:12.292070Z', + 'input': f'{self.host}assets/18/', + 'progress': 100.0, + 'description': '' + } + self.results_roundset = { + 'round_number': 1, + 'round_set': f'{self.host}round-sets/11/', + 'round_result': {}, + 'instructions': [], + 'cumulative_result': {} + } with patch('adk.api.remote_api.AuthManager'), \ patch('adk.api.remote_api.QneFrontendClient'), \ patch('adk.api.remote_api.ResourceManager'): @@ -186,8 +384,25 @@ def test_logout_with_host(self): class TestRemoteApiApplication(TestRemoteApi): - def test_list_application(self): - pass + def test_list_applications(self): + self.remote_api._RemoteApi__qne_client.list_applications.return_value = self.list_applications + application_list = self.remote_api.list_applications() + for application in application_list: + self.assertEqual(application["slug"], self.application["slug"]) + + def test_delete_application(self): + self.remote_api._RemoteApi__qne_client.destroy_application.side_effect = None + deleted = self.remote_api.delete_application(None) + self.assertFalse(deleted) + deleted = self.remote_api.delete_application("not_a_number") + self.assertFalse(deleted) + deleted = self.remote_api.delete_application("124") + self.assertTrue(deleted) + self.remote_api._RemoteApi__qne_client.destroy_application.side_effect = ErrorResponse(title="title", + status_code=3, + content=None) + deleted = self.remote_api.delete_application("124") + self.assertFalse(deleted) def test_get_application_config(self): pass @@ -538,5 +753,118 @@ def test_publish_not_existing_application_fails(self): class TestRemoteApiExperiment(TestRemoteApi): - def test_create_experiment(self): - pass + def test_delete_experiment(self): + self.remote_api._RemoteApi__qne_client.destroy_experiment.side_effect = None + deleted = self.remote_api.delete_experiment(None) + self.assertFalse(deleted) + deleted = self.remote_api.delete_experiment("not_a_number") + self.assertFalse(deleted) + deleted = self.remote_api.delete_experiment("124") + self.assertTrue(deleted) + self.remote_api._RemoteApi__qne_client.destroy_experiment.side_effect = ErrorResponse(title="title", + status_code=3, + content=None) + deleted = self.remote_api.delete_experiment("124") + self.assertFalse(deleted) + + def test_experiment_list(self): + self.remote_api._RemoteApi__qne_client.list_experiments.return_value = self.list_experiments + self.remote_api._RemoteApi__qne_client.retrieve_appversion.return_value = self.retrieve_appversion + self.remote_api._RemoteApi__qne_client.retrieve_application.return_value = self.retrieve_application + experiment_list = self.remote_api.experiments_list() + for experiment in experiment_list: + self.assertEqual(experiment["name"], str(self.retrieve_application["slug"])) + + def test_validate_experiment_succeeds(self): + self.remote_api._RemoteApi__qne_client.list_backendtypes.return_value = self.list_backendtypes + + error_dict = get_empty_errordict() + self.remote_api.validate_experiment(self.experiment_data, error_dict) + self.assertEqual(error_dict, {'error': [], 'info': [], 'warning': []}) + + def test_validate_experiment_fails(self): + self.remote_api._RemoteApi__qne_client.list_backendtypes.return_value = self.list_backendtypes + + error_dict = get_empty_errordict() + self.remote_api._RemoteApi__qne_client.list_backendtypes.return_value = [] + self.remote_api.validate_experiment(self.experiment_data, error_dict) + self.assertIn("Backend type in experiment is not a valid remote backend type " + "(names must match)", error_dict['error']) + + error_dict = get_empty_errordict() + self.remote_api._RemoteApi__qne_client.list_backendtypes.return_value = self.list_backendtypes + self.experiment_data["meta"]["backend"]["type"] = "non-existent backendtype" + self.remote_api.validate_experiment(self.experiment_data, error_dict) + self.assertIn("Backend type in experiment is not a valid remote backend type " + "(names must match)", error_dict['error']) + + error_dict = get_empty_errordict() + self.remote_api._RemoteApi__qne_client.list_backendtypes.return_value = self.list_backendtypes + self.experiment_data["meta"]["backend"]["location"] = "local" + self.experiment_data["meta"]["backend"]["type"] = "NV center" + self.list_backendtypes[0]["is_allowed"] = False + self.list_backendtypes[0]["status"] = "OFFLINE" + self.remote_api.validate_experiment(self.experiment_data, error_dict) + + self.assertIn("Backend location in experiment is not remote", error_dict['error']) + self.assertIn("The requested remote backend is OFFLINE", error_dict['warning']) + self.assertIn('The requested remote backend is not available for your current account type, select a ' + 'different backend.', error_dict['error']) + + def test_run_experiment_success(self): + with patch.object(RemoteApi, "_RemoteApi__get_application_by_slug") as get_application_by_slug_mock: + self.remote_api._RemoteApi__qne_client.list_backendtypes.return_value = self.list_backendtypes + self.remote_api._RemoteApi__qne_client.retrieve_user.return_value = self.user + self.remote_api._RemoteApi__qne_client.app_config_appversion.return_value = self.app_config + self.remote_api._RemoteApi__qne_client.app_versions_application.return_value = self.app_versions_not_owner + self.remote_api._RemoteApi__qne_client.create_experiment.return_value = self.experiment + self.remote_api._RemoteApi__qne_client.create_asset.return_value = self.asset + self.remote_api._RemoteApi__qne_client.create_roundset.return_value = self.roundset_new + + get_application_by_slug_mock.return_value = self.application + return_value = self.remote_api.run_experiment(self.experiment_data) + + self.assertEqual(return_value, (str(self.roundset_new["url"]), str(self.experiment["id"]))) + + experiment_to_create = { + 'app_version': f'{self.host}app_version/42', + 'personal_note': 'Experiment created by qne-adk', + 'is_marked': False, + 'owner': f'{self.host}users/1/' + } + self.remote_api._RemoteApi__qne_client.create_experiment.assert_called_once_with(experiment_to_create) + + roundset_to_create = { + 'backend_type': f'{self.host}backendtypes/2/', + 'input': f'{self.host}assets/18/', + 'number_of_rounds': 1, + 'status': 'NEW' + } + self.remote_api._RemoteApi__qne_client.create_roundset.assert_called_once_with(roundset_to_create) + self.remote_api._RemoteApi__qne_client.app_config_appversion.assert_called_once_with(self.app_config["app_version"]) + self.remote_api._RemoteApi__qne_client.app_versions_application.assert_called_once_with( + self.application["url"]) + + def test_run_experiment_fails(self): + self.remote_api._RemoteApi__qne_client.list_backendtypes.return_value = self.list_backendtypes + + self.experiment_data["meta"]["backend"]["type"] = "non-existent backendtype" + with self.assertRaises(ExperimentValueError) as cm: + return_value = self.remote_api.run_experiment(self.experiment_data) + self.assertEqual("Backend type in experiment is not a valid remote backend type (names must match)", + str(cm.exception)) + + def test_get_results_succeeds(self): + with patch("adk.api.remote_api.time"): + self.remote_api._RemoteApi__qne_client.retrieve_roundset.side_effect = [self.roundset_new, self.roundset_complete] + self.remote_api._RemoteApi__qne_client.results_roundset.return_value = [self.results_roundset] + roundset_url = self.roundset_new["url"] + + result_list = self.remote_api.get_results(roundset_url, block=True) + self.assertEqual(len(result_list), 1) + for result in result_list: + self.assertEqual(result["round_number"], self.results_roundset["round_number"]) + self.assertEqual(result["round_result"], self.results_roundset["round_result"]) + self.assertEqual(result["instructions"], self.results_roundset["instructions"]) + self.assertEqual(result["cumulative_result"], self.results_roundset["cumulative_result"]) + self.assertEqual(result["round_result"], self.results_roundset["round_result"]) diff --git a/src/tests/test_command_list.py b/src/tests/test_command_list.py index 7862268..8f84e99 100644 --- a/src/tests/test_command_list.py +++ b/src/tests/test_command_list.py @@ -843,7 +843,7 @@ def test_experiment_run_succeeds(self): exp_local_mock.return_value = True exp_validate_mock.return_value = {"error": [], "warning": [], "info": []} exp_run_output = self.runner.invoke(experiments_app, ['run']) - exp_validate_mock.assert_called_once_with(experiment_path=self.path) + exp_validate_mock.assert_called_once_with(experiment_path=self.path, local=True) retrieve_expname_and_path_mock.assert_called_once() exp_run_mock.assert_called_once_with(experiment_path=self.path, block=True, update=False, timeout=None) self.assertEqual(exp_run_output.exit_code, 0) @@ -892,20 +892,22 @@ def test_experiment_run_fails(self): exp_run_output = self.runner.invoke(experiments_app, ['run']) format_validation_messages_mock.assert_called_once() - exp_local_mock.assert_not_called() - exp_validate_mock.assert_called_once_with(experiment_path=self.path) + exp_local_mock.assert_called_once() + exp_validate_mock.assert_called_once_with(experiment_path=self.path, local=True) retrieve_expname_and_path_mock.assert_called_once_with(experiment_name=None) exp_run_mock.assert_not_called() self.assertEqual(exp_run_output.exit_code, 1) self.assertIn("Experiment failed validation.", exp_run_output.stdout) + exp_validate_mock.reset_mock() exp_validate_mock.return_value = {"error": [], "warning": [], "info": []} exp_local_mock.return_value = False retrieve_expname_and_path_mock.reset_mock() exp_run_mock.reset_mock() exp_run_mock.return_value = [{"round_result": {"error": "Just an error"}}] exp_run_output = self.runner.invoke(experiments_app, ['run', '--timeout=30']) + exp_validate_mock.assert_called_once_with(experiment_path=self.path, local=False) exp_run_mock.assert_called_once_with(experiment_path=self.path, block=False, update=False, timeout=30) retrieve_expname_and_path_mock.assert_called_once() self.assertEqual(exp_run_output.exit_code, 1) @@ -931,7 +933,7 @@ def test_experiment_run_update_succeeds(self): app_validate_mock.return_value = {"error": [], "warning": [], "info": []} exp_run_output = self.runner.invoke(experiments_app, ['run', '--update']) - exp_validate_mock.assert_called_once_with(experiment_path=self.path) + exp_validate_mock.assert_called_once_with(experiment_path=self.path, local=True) retrieve_expname_and_path_mock.assert_called_once() exp_run_mock.assert_called_once_with(experiment_path=self.path, block=True, update=True, timeout=None) self.assertEqual(exp_run_output.exit_code, 0) diff --git a/src/tests/test_command_processor.py b/src/tests/test_command_processor.py index 8d7610a..9d98730 100644 --- a/src/tests/test_command_processor.py +++ b/src/tests/test_command_processor.py @@ -331,10 +331,18 @@ def test_experiments_delete_remote(self): self.remote_api.delete_experiment.assert_called_once() self.assertFalse(return_value) - def test_experiments_validate(self): + def test_experiments_validate_local(self): self.local_api.validate_experiment.return_value = self.error_dict - self.assertEqual(self.processor.experiments_validate(experiment_path=self.path), self.error_dict) - self.local_api.validate_experiment.assert_called_once_with(self.path) + self.assertEqual(self.processor.experiments_validate(experiment_path=self.path, local=True), self.error_dict) + self.local_api.validate_experiment.assert_called_once_with(self.path, self.error_dict) + + def test_experiments_validate_remote(self): + self.local_api.validate_experiment.return_value = self.error_dict + experiment_data = {"test": 1} + self.local_api.get_experiment_data.return_value = experiment_data + self.assertEqual(self.processor.experiments_validate(experiment_path=self.path, local=False), self.error_dict) + self.remote_api.validate_experiment.assert_called_once_with(experiment_data, self.error_dict) + self.local_api.validate_experiment.assert_called_once_with(self.path, self.error_dict) def test_experiments_run(self): with patch('adk.command_processor.Path.mkdir') as mkdir_mock, \ From 9458cdf628dde2edfa4ed6095efc05d2853d42c9 Mon Sep 17 00:00:00 2001 From: QFer Date: Tue, 29 Oct 2024 13:45:12 +0100 Subject: [PATCH 2/3] linting --- .coveragerc | 2 +- LICENSE | 2 +- docs/conf.py | 2 +- setup.py | 2 +- src/adk/api/remote_api.py | 4 ++-- src/adk/command_list.py | 2 +- src/adk/parsers/output_converter.py | 2 +- src/adk/version.py | 2 +- src/tests/test_command_list.py | 8 ++++---- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.coveragerc b/.coveragerc index dc3fe9f..577adbb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,6 +4,6 @@ command_line = -m pytest -s --junitxml=report.xml [report] show_missing = True -; fail_under = 100 +fail_under = 75 exclude_lines = if __name__ == .__main__.: diff --git a/LICENSE b/LICENSE index 585dd58..e45a64a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 QuTech +Copyright (c) 2024 QuTech Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/docs/conf.py b/docs/conf.py index 666c20f..1981742 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,7 +21,7 @@ # -- Project information ----------------------------------------------------- project = 'Quantum Network Explorer Application Development Kit' -copyright = '2022, QuTech' +copyright = '2024, QuTech' author = 'QuTech' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index 60535c3..f329f89 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ """ Quantum Network Explorer ADK -Copyright (c) 2022 QuTech +Copyright (c) 2024 QuTech Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/adk/api/remote_api.py b/src/adk/api/remote_api.py index e7f4555..d5df7ff 100644 --- a/src/adk/api/remote_api.py +++ b/src/adk/api/remote_api.py @@ -733,7 +733,7 @@ def __get_backend_type(self, backend_type: str) -> List[BackendTypeType]: """ backend_types = self.__qne_client.list_backendtypes() # "type" is required - backends = [backend for backend in backend_types if backend["name"].lower() == backend_type.lower()] + backends = [backend for backend in backend_types if str(backend["name"]).lower() == backend_type.lower()] if len(backends) > 1: # QNE configuration error, should be solved in QNE raise ExperimentValueError("More backend types with the same name configured in QNE") @@ -866,7 +866,7 @@ def run_experiment(self, experiment_data: ExperimentDataType) -> Tuple[str, str] experiment_asset = experiment_data["asset"] asset = self.__create_asset(experiment_asset, str(experiment["url"])) number_of_rounds = experiment_data["meta"]["number_of_rounds"] - round_set = self.__create_round_set(str(asset["url"]), backend_type[0]["url"], number_of_rounds) + round_set = self.__create_round_set(str(asset["url"]), str(backend_type[0]["url"]), number_of_rounds) return str(round_set["url"]), str(experiment["id"]) diff --git a/src/adk/command_list.py b/src/adk/command_list.py index d3276e8..17c1e7c 100644 --- a/src/adk/command_list.py +++ b/src/adk/command_list.py @@ -588,7 +588,7 @@ def experiments_validate( """ experiment_path, experiment_name = retrieve_experiment_name_and_path(experiment_name=experiment_name) - validate_dict = processor.experiments_validate(experiment_path=experiment_path) + validate_dict = processor.experiments_validate(experiment_path=experiment_path, local=True) validation_messages = format_validation_messages(validate_dict) if validate_dict["error"] or validate_dict["warning"]: diff --git a/src/adk/parsers/output_converter.py b/src/adk/parsers/output_converter.py index e8a3fe0..1a32580 100644 --- a/src/adk/parsers/output_converter.py +++ b/src/adk/parsers/output_converter.py @@ -115,7 +115,7 @@ def __combine_log_files(self, log_files: List[Dict[str, Optional[str]]]) -> List log_record['INS'] = 'apply_gate' logs.append(log_record) - return sorted(logs, key=lambda l: l['WCT']) + return sorted(logs, key=lambda l: cast(str, l['WCT'])) def convert(self, round_number: int) -> ResultType: """Convert result and log files for one round number into a Result format compatible with the QNE diff --git a/src/adk/version.py b/src/adk/version.py index abeeedb..f0ede3d 100644 --- a/src/adk/version.py +++ b/src/adk/version.py @@ -1 +1 @@ -__version__ = '0.4.0' +__version__ = '0.4.1' diff --git a/src/tests/test_command_list.py b/src/tests/test_command_list.py index 8f84e99..1d23b1a 100644 --- a/src/tests/test_command_list.py +++ b/src/tests/test_command_list.py @@ -746,7 +746,7 @@ def test_experiment_validate(self): experiment_validate_output = self.runner.invoke(experiments_app, ['validate']) retrieve_experiment_name_and_path_mock.assert_called_once_with(experiment_name=None) - experiments_validate_mock.assert_called_once_with(experiment_path=self.path) + experiments_validate_mock.assert_called_once_with(experiment_path=self.path, local=True) format_validation_messages_mock.assert_called_once() self.assertIn("Experiment failed validation", experiment_validate_output.stdout) @@ -758,7 +758,7 @@ def test_experiment_validate(self): experiment_validate_output = self.runner.invoke(experiments_app, ['validate']) retrieve_experiment_name_and_path_mock.assert_called_once_with(experiment_name=None) - experiments_validate_mock.assert_called_once_with(experiment_path=self.path) + experiments_validate_mock.assert_called_once_with(experiment_path=self.path, local=True) format_validation_messages_mock.assert_called_once() self.assertIn("Experiment failed validation", experiment_validate_output.stdout) @@ -770,7 +770,7 @@ def test_experiment_validate(self): experiment_validate_output = self.runner.invoke(experiments_app, ['validate']) retrieve_experiment_name_and_path_mock.assert_called_once_with(experiment_name=None) - experiments_validate_mock.assert_called_once_with(experiment_path=self.path) + experiments_validate_mock.assert_called_once_with(experiment_path=self.path, local=True) self.assertIn("Experiment is valid", experiment_validate_output.stdout) # When application is valid with item in in 'info' @@ -781,7 +781,7 @@ def test_experiment_validate(self): experiment_validate_output = self.runner.invoke(experiments_app, ['validate']) retrieve_experiment_name_and_path_mock.assert_called_once_with(experiment_name=None) - experiments_validate_mock.assert_called_once_with(experiment_path=self.path) + experiments_validate_mock.assert_called_once_with(experiment_path=self.path, local=True) self.assertIn("Experiment is valid", experiment_validate_output.stdout) def test_experiment_delete_no_experiment_dir(self): From 640501598ac7955d842dbc2483892bb2a447551e Mon Sep 17 00:00:00 2001 From: QFer Date: Fri, 1 Nov 2024 12:01:26 +0100 Subject: [PATCH 3/3] review comments and qnebe-902 --- README.md | 2 +- src/adk/api/local_api.py | 7 +- src/adk/api/qne_client.py | 9 +-- src/adk/api/remote_api.py | 28 +++++--- src/adk/networks/channels.json | 10 ++- src/adk/networks/networks.json | 11 +++- src/adk/networks/nodes.json | 38 ++++++++++- src/adk/networks/templates.json | 1 - src/adk/type_aliases.py | 7 +- src/tests/api/test_remote_api.py | 106 ++++++++++++------------------- 10 files changed, 120 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 47a5b7c..af38968 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Quantum Network Explorer ADK The QNE-ADK is a Quantum Network Explorer - Application Development Kit that allows you to create your own applications -and experiments and run them on a simulator. +and experiments and run them on a simulator or real quantum network hardware. ## Local development diff --git a/src/adk/api/local_api.py b/src/adk/api/local_api.py index 8e92a73..bbac406 100644 --- a/src/adk/api/local_api.py +++ b/src/adk/api/local_api.py @@ -212,7 +212,7 @@ def _get_template_params_max_min_range(self) -> Dict[str, Dict[str, Dict[str, An def _get_network_nodes(self) -> Dict[str, List[str]]: """ - Loops trough all the networks in networks/networks.json and gets all the nodes within this network. + Loops through all the networks in networks/networks.json and gets all the nodes within this network. returns: Returns a dict of networks, each having their own list including the nodes: @@ -903,7 +903,7 @@ def experiments_create(self, experiment_name: str, application_name: str, networ Create all the necessary resources for experiment creation - 1. Get the network data for the specified network_name - 2. Create the asset - - 3. Create experiment.json containing the meta data and asset information + - 3. Create experiment.json containing the metadata and asset information Args: experiment_name: Name of the experiment @@ -963,7 +963,8 @@ def get_network_data(self, network_name: str) -> assetNetworkType: def __create_experiment(self, experiment_name: str, application_name: str, local: bool, path: Path, app_config: AppConfigType, asset_network: assetNetworkType) -> None: """ - Create experiment.json with meta and asset information + Create experiment.json with meta and asset information. We don't need to check if the network is compatible + with the default backend type because the default backend type can run all networks. Args: experiment_name: Name of the directory where experiment.json will be created diff --git a/src/adk/api/qne_client.py b/src/adk/api/qne_client.py index 95c6fe9..a61f414 100644 --- a/src/adk/api/qne_client.py +++ b/src/adk/api/qne_client.py @@ -456,11 +456,6 @@ def retrieve_backend(self, backend_url: str) -> BackendType: response = self._action('retrieveBackend', id=backend_id) return cast(BackendType, response) - def list_backends_networks(self) -> List[Any]: - response = self._action('listBackendsNetworks') - return cast(List[Any], response) - - def list_experiments(self) -> List[ExperimentType]: response = self._action('listExperiments') return cast(List[ExperimentType], response) @@ -561,9 +556,9 @@ def retrieve_template(self, template_url: str) -> TemplateType: response = self._action('retrieveTemplate', id=template_id) return cast(TemplateType, response) - def list_networks(self) -> List[NetworkListType]: + def list_networks(self) -> NetworkListType: response = self._action('listNetworks') - return cast(List[NetworkListType], response) + return cast(NetworkListType, response) def retrieve_network(self, network_url: str) -> NetworkType: _, network_id = QneClient.parse_url(network_url) diff --git a/src/adk/api/remote_api.py b/src/adk/api/remote_api.py index d5df7ff..63f9167 100644 --- a/src/adk/api/remote_api.py +++ b/src/adk/api/remote_api.py @@ -16,7 +16,8 @@ ApplicationType, ApplicationDataType, AssetType, assetNetworkType, ErrorDictType, ExperimentType, FinalResultType, GenericNetworkData, ExperimentDataType, ResultType, RoundSetType, round_resultType, cumulative_resultType, instructionsType, ChannelType, - parametersType, coordinatesType, listValuesType, app_ownerType, BackendTypeType) + parametersType, coordinatesType, listValuesType, app_ownerType, BackendTypeType, + NetworkListType) from adk.settings import BASE_DIR from adk.utils import get_default_remote_data @@ -724,20 +725,21 @@ def experiments_list(self) -> List[ExperimentType]: return experiment_list - def __get_backend_type(self, backend_type: str) -> List[BackendTypeType]: + def __get_backend_type(self, backend_type_name: str) -> List[BackendTypeType]: """ - Get the backend info for the remote backend in the experiment data. + Get the backend type info for the (remote) backend type in the experiment data. Returns: 0 or 1 backend types with this type """ backend_types = self.__qne_client.list_backendtypes() # "type" is required - backends = [backend for backend in backend_types if str(backend["name"]).lower() == backend_type.lower()] - if len(backends) > 1: + backend_types = [backend_type for backend_type in backend_types if str(backend_type["name"]).lower() == + backend_type_name.lower()] + if len(backend_types) > 1: # QNE configuration error, should be solved in QNE raise ExperimentValueError("More backend types with the same name configured in QNE") - return backends + return backend_types def validate_experiment(self, experiment_data: ExperimentDataType, error_dict: ErrorDictType) -> None: """ @@ -750,12 +752,18 @@ def validate_experiment(self, experiment_data: ExperimentDataType, error_dict: E # "type" is required backend_types = self.__get_backend_type(experiment_data["meta"]["backend"]["type"]) if len(backend_types) == 0: - error_dict["error"].append("Backend type in experiment is not a valid remote backend type " + error_dict["error"].append("Backend in experiment is not a valid remote backend " "(names must match)") else: + networks = cast(NetworkListType, backend_types[0]["networks"]) + networks = [network for network in networks if network["slug"] == + experiment_data["asset"]["network"]["slug"]] + if len(networks) == 0: + error_dict["error"].append("The requested remote backend is not able to run an experiment on the " + "selected network, select a different backend or change the network") if not backend_types[0]["is_allowed"]: error_dict["error"].append("The requested remote backend is not available for your current account " - "type, select a different backend.") + "type, select a different backend") if backend_types[0]["status"] == "OFFLINE": error_dict["warning"].append("The requested remote backend is OFFLINE") @@ -860,7 +868,7 @@ def run_experiment(self, experiment_data: ExperimentDataType) -> Tuple[str, str] app_version_url = experiment_data["meta"]["application"]["app_version"] backend_type = self.__get_backend_type(experiment_data["meta"]["backend"]["type"]) if len(backend_type) == 0: - raise ExperimentValueError("Backend type in experiment is not a valid remote backend type " + raise ExperimentValueError("Backend in experiment is not a valid remote backend " "(names must match)") experiment = self.__create_experiment(application_slug, app_version_url) experiment_asset = experiment_data["asset"] @@ -998,7 +1006,7 @@ def __update_networks_networks(self, overwrite: bool) -> None: networks = self.__qne_client.list_networks() for network in networks: network_type_json: Dict[str, Union[str, List[str]]] = {} - network_type = self.__qne_client.retrieve_network(str(network["url"])) + network_type = self.__qne_client.retrieve_network(cast(str, network["url"])) network_type_json["name"] = str(network_type["name"]) network_type_json["slug"] = str(network_type["slug"]) network_type_channels: List[str] = [] diff --git a/src/adk/networks/channels.json b/src/adk/networks/channels.json index 8acdfdf..f9458ff 100644 --- a/src/adk/networks/channels.json +++ b/src/adk/networks/channels.json @@ -151,6 +151,14 @@ "parameters": [ "elementary-link-fidelity" ] + }, + { + "slug": "node-1-node-2", + "node1": "node-1", + "node2": "node-2", + "parameters": [ + "elementary-link-fidelity" + ] } ] -} +} \ No newline at end of file diff --git a/src/adk/networks/networks.json b/src/adk/networks/networks.json index 8e40c13..864564d 100644 --- a/src/adk/networks/networks.json +++ b/src/adk/networks/networks.json @@ -36,8 +36,13 @@ "eindhoven-enschede", "enschede-amsterdam" ] + }, + "qutech-delft-lab-1": { + "name": "QuTech Delft lab 1", + "slug": "qutech-delft-lab-1", + "channels": [ + "node-1-node-2" + ] } } -} - - +} \ No newline at end of file diff --git a/src/adk/networks/nodes.json b/src/adk/networks/nodes.json index 07abb24..684bba8 100644 --- a/src/adk/networks/nodes.json +++ b/src/adk/networks/nodes.json @@ -129,8 +129,8 @@ ] }, { - "name":"Paris", - "slug":"paris", + "name": "Paris", + "slug": "paris", "coordinates": { "latitude": 48.8302, "longitude": 2.3816 @@ -175,6 +175,38 @@ "relaxation-time", "dephasing-time" ] + }, + { + "name": "Node 1", + "slug": "node-1", + "coordinates": { + "latitude": 52.0019, + "longitude": 4.37509 + }, + "node_parameters": [ + "gate-fidelity" + ], + "number_of_qubits": 1, + "qubit_parameters": [ + "relaxation-time", + "dephasing-time" + ] + }, + { + "name": "Node 2", + "slug": "node-2", + "coordinates": { + "latitude": 52.00092, + "longitude": 4.37579 + }, + "node_parameters": [ + "gate-fidelity" + ], + "number_of_qubits": 1, + "qubit_parameters": [ + "relaxation-time", + "dephasing-time" + ] } ] -} +} \ No newline at end of file diff --git a/src/adk/networks/templates.json b/src/adk/networks/templates.json index bf86922..16d125a 100644 --- a/src/adk/networks/templates.json +++ b/src/adk/networks/templates.json @@ -62,4 +62,3 @@ } ] } - diff --git a/src/adk/type_aliases.py b/src/adk/type_aliases.py index 46625fd..ea7672d 100644 --- a/src/adk/type_aliases.py +++ b/src/adk/type_aliases.py @@ -27,7 +27,8 @@ NodeType = Dict[str, Union[DefaultPayloadType, coordinatesType, parametersType]] ChannelType = Dict[str, Union[DefaultPayloadType, parametersType]] NetworkType = Dict[str, Union[DefaultPayloadType, List[ChannelType], List[NodeType]]] -NetworkListType = Dict[str, Union[DefaultPayloadType]] +NetworkNameType = Dict[str, DefaultPayloadType] +NetworkListType = List[NetworkNameType] TemplatesType = Dict[str, List[Dict[str, Any]]] AssetChannelListType = List[Dict[str, Any]] AssetNodeListType = List[Dict[str, Any]] @@ -42,7 +43,7 @@ TokenType = Dict[str, str] MetaType = Dict[str, Any] -UserType = Dict[str, Union[DefaultPayloadType]] +UserType = Dict[str, DefaultPayloadType] RoundSetType = Dict[str, Union[DefaultPayloadType, float]] AssetType = Dict[str, Union[DefaultPayloadType, assetNetworkType, assetApplicationType]] ExperimentType = Dict[str, Union[DefaultPayloadType, bool]] @@ -56,7 +57,7 @@ InstructionType = Dict[str, Any] LogEntryType = Dict[str, Any] BackendType = Dict[str, DefaultPayloadType] -BackendTypeType = Dict[str, Union[DefaultPayloadType, bool]] +BackendTypeType = Dict[str, Union[DefaultPayloadType, bool, NetworkListType]] ActionsType = Union[str, List[str]] ParametersType = Union[str, Dict[str, str], Any] diff --git a/src/tests/api/test_remote_api.py b/src/tests/api/test_remote_api.py index cd1037f..5038be1 100644 --- a/src/tests/api/test_remote_api.py +++ b/src/tests/api/test_remote_api.py @@ -32,7 +32,8 @@ def setUp(self): "networks": [ "randstad", "europe", - "the-netherlands" + "the-netherlands", + 'qutech-delft-lab-1' ], "roles": [ "sender", @@ -193,7 +194,7 @@ def setUp(self): }, "backend": { "location": "remote", - "type": 'NV center' + "type": 'NV center hardware' }, "description": "experiment description", "number_of_rounds": 1, @@ -201,22 +202,13 @@ def setUp(self): }, "asset": { "network": { - "name": "Randstad", - "slug": "randstad", - "nodes": [{"slug": "n1", "node_parameters": None, "qubits": self.dummy_qubit}, - {"slug": "n2", "node_parameters": None, "qubits": self.dummy_qubit}, - {"slug": "n3", "node_parameters": None, "qubits": self.dummy_qubit}, - {"slug": "n4", "node_parameters": None, "qubits": self.dummy_qubit}, - {"slug": "n5", "node_parameters": None, "qubits": self.dummy_qubit}], - "channels": [{"slug": "n1-n2", "parameters": self.dummy_channel_params, - "node1": "n1", "node2": "n2"}, - {"slug": "n2-n3", "parameters": self.dummy_channel_params, - "node1": "n2", "node2": "n3"}, - {"slug": "n4-n3", "parameters": self.dummy_channel_params, - "node1": "n4", "node2": "n3"}, - {"slug": "n4-n5", "parameters": self.dummy_channel_params, - "node1": "n4", "node2": "n5"}], - "roles": {"role1": "n1", "role2": "n2"} + "name": "QuTech Delft lab 1", + "slug": "qutech-delft-lab-1", + "nodes": [{"slug": "node-1", "node_parameters": None, "qubits": self.dummy_qubit}, + {"slug": "node-2", "node_parameters": None, "qubits": self.dummy_qubit}], + "channels": [{"slug": "node-1-node-2", "parameters": self.dummy_channel_params, + "node1": "node-1", "node2": "node-2"}], + "roles": {"role1": "node-1", "role2": "node-2"} }, "application": [ {'roles': ['role2'], @@ -243,14 +235,16 @@ def setUp(self): self.backend_nv_center = { 'id': 2, 'url': f'{self.host}backendtypes/2/', - 'name': 'NV center', + 'name': 'NV center hardware', 'is_hardware_backend': True, 'required_permission': 'can_execute', - 'description': 'Hardware network using NV center technology located at the QuTech Delft lab.', + 'description': 'NV center quantum computing backend for specialized experiments.', 'is_allowed': True, 'status': 'ONLINE', 'status_message': 'This backend is online.', - 'max_number_of_simultaneous_experiments': 1 + 'max_number_of_simultaneous_experiments': 1, + 'networks': [{'id': 4, 'url': f'{self.host}networks/4/', 'name': 'QuTech Delft lab 1', + 'slug': 'qutech-delft-lab-1'}] } self.backend_netsquid_simulator = { 'id': 1, @@ -262,48 +256,15 @@ def setUp(self): 'is_allowed': True, 'status': 'ONLINE', 'status_message': 'This backend is online.', - 'max_number_of_simultaneous_experiments': 0 + 'max_number_of_simultaneous_experiments': 0, + 'networks': [{'id': 1, 'url': f'{self.host}networks/1/', 'name': 'Randstad', 'slug': 'randstad'}, + {'id': 2, 'url': f'{self.host}networks/2/', 'name': 'The Netherlands', + 'slug': 'the-netherlands'}, + {'id': 3, 'url': f'{self.host}networks/3/', 'name': 'Europe', 'slug': 'europe'}, + {'id': 4, 'url': f'{self.host}networks/4/', 'name': 'QuTech Delft lab 1', + 'slug': 'qutech-delft-lab-1'}] } self.list_backendtypes = [self.backend_nv_center, self.backend_netsquid_simulator] - self.list_backends_networks = [ - { - "backend_type": { - 'id': 1, - 'url': f'{self.host}backendtypes/1/', - 'name': 'NetSquid simulator', - 'is_hardware_backend': False, - 'required_permission': 'can_simulate', - 'description': 'The Network Simulator for Quantum Information using discrete events running on a Hetzner VPS.', - 'is_allowed': True, - 'status': 'ONLINE', - 'status_message': 'This backend is online.', - 'max_number_of_simultaneous_experiments': 0 - }, - "networks": [ - {'id': 1, 'url': 'http://127.0.0.1:8000/networks/1/', 'name': 'Randstad', 'slug': 'randstad'}, - {'id': 2, 'url': 'http://127.0.0.1:8000/networks/2/', 'name': 'The Netherlands', 'slug': 'the-netherlands'}, - {'id': 3, 'url': 'http://127.0.0.1:8000/networks/3/', 'name': 'Europe', 'slug': 'europe'}, - {'id': 4, 'url': 'http://127.0.0.1:8000/networks/4/', 'name': 'Basement', 'slug': 'basement'} - ] - }, - { - "backend_type": { - 'id': 2, - 'url': f'{self.host}backendtypes/2/', - 'name': 'NV center', - 'is_hardware_backend': True, - 'required_permission': 'can_execute', - 'description': 'Hardware network using NV center technology located at the QuTech Delft lab.', - 'is_allowed': True, - 'status': 'ONLINE', - 'status_message': 'This backend is online.', - 'max_number_of_simultaneous_experiments': 1 - }, - "networks": [ - {'id': 4, 'url': 'http://127.0.0.1:8000/networks/4/', 'name': 'Basement', 'slug': 'basement'} - ] - }, - ] self.experiment = { 'app_version': f'{self.host}app-versions/42/', 'created_at': '2024-10-27T14:08:54.127099Z', @@ -788,28 +749,39 @@ def test_validate_experiment_fails(self): error_dict = get_empty_errordict() self.remote_api._RemoteApi__qne_client.list_backendtypes.return_value = [] self.remote_api.validate_experiment(self.experiment_data, error_dict) - self.assertIn("Backend type in experiment is not a valid remote backend type " + self.assertIn("Backend in experiment is not a valid remote backend " "(names must match)", error_dict['error']) error_dict = get_empty_errordict() self.remote_api._RemoteApi__qne_client.list_backendtypes.return_value = self.list_backendtypes self.experiment_data["meta"]["backend"]["type"] = "non-existent backendtype" self.remote_api.validate_experiment(self.experiment_data, error_dict) - self.assertIn("Backend type in experiment is not a valid remote backend type " + self.assertIn("Backend in experiment is not a valid remote backend " "(names must match)", error_dict['error']) error_dict = get_empty_errordict() self.remote_api._RemoteApi__qne_client.list_backendtypes.return_value = self.list_backendtypes self.experiment_data["meta"]["backend"]["location"] = "local" - self.experiment_data["meta"]["backend"]["type"] = "NV center" + self.experiment_data["meta"]["backend"]["type"] = "NV center hardware" self.list_backendtypes[0]["is_allowed"] = False self.list_backendtypes[0]["status"] = "OFFLINE" self.remote_api.validate_experiment(self.experiment_data, error_dict) - self.assertIn("Backend location in experiment is not remote", error_dict['error']) self.assertIn("The requested remote backend is OFFLINE", error_dict['warning']) self.assertIn('The requested remote backend is not available for your current account type, select a ' - 'different backend.', error_dict['error']) + 'different backend', error_dict['error']) + + error_dict = get_empty_errordict() + self.remote_api._RemoteApi__qne_client.list_backendtypes.return_value = self.list_backendtypes + self.experiment_data["asset"]["network"]["slug"] = "randstad" + self.experiment_data["asset"]["network"]["name"] = "Randstad" + self.experiment_data["meta"]["backend"]["location"] = "remote" + self.experiment_data["meta"]["backend"]["type"] = "NV center hardware" + self.list_backendtypes[0]["is_allowed"] = True + self.list_backendtypes[0]["status"] = "ONLINE" + self.remote_api.validate_experiment(self.experiment_data, error_dict) + self.assertIn("The requested remote backend is not able to run an experiment on the " + "selected network, select a different backend or change the network", error_dict['error']) def test_run_experiment_success(self): with patch.object(RemoteApi, "_RemoteApi__get_application_by_slug") as get_application_by_slug_mock: @@ -851,7 +823,7 @@ def test_run_experiment_fails(self): self.experiment_data["meta"]["backend"]["type"] = "non-existent backendtype" with self.assertRaises(ExperimentValueError) as cm: return_value = self.remote_api.run_experiment(self.experiment_data) - self.assertEqual("Backend type in experiment is not a valid remote backend type (names must match)", + self.assertEqual("Backend in experiment is not a valid remote backend (names must match)", str(cm.exception)) def test_get_results_succeeds(self):