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/README.md b/README.md index 7b3554e..af38968 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 or real quantum network hardware. ## 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/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/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/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/local_api.py b/src/adk/api/local_api.py index 16cee64..bbac406 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, @@ -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: @@ -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() @@ -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 @@ -990,7 +991,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 +1299,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 +1566,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 +1578,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 +1602,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 +1681,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 +1717,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..a61f414 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) @@ -554,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 5acb031..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) + parametersType, coordinatesType, listValuesType, app_ownerType, BackendTypeType, + NetworkListType) from adk.settings import BASE_DIR from adk.utils import get_default_remote_data @@ -683,7 +684,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 +725,48 @@ def experiments_list(self) -> List[ExperimentType]: return experiment_list + def __get_backend_type(self, backend_type_name: str) -> List[BackendTypeType]: + """ + 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 + 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 backend_types + + 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 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") + 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 +817,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 +832,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 +866,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 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"] 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"]), str(backend_type[0]["url"]), number_of_rounds) return str(round_set["url"]), str(experiment["id"]) @@ -948,7 +996,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" @@ -958,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] = [] @@ -991,7 +1039,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 +1068,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 +1101,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..17c1e7c 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: @@ -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/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/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/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/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/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/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/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..5038be1 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", @@ -30,7 +32,8 @@ def setUp(self): "networks": [ "randstad", "europe", - "the-netherlands" + "the-netherlands", + 'qutech-delft-lab-1' ], "roles": [ "sender", @@ -83,10 +86,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 +102,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 +153,170 @@ 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 hardware' + }, + "description": "experiment description", + "number_of_rounds": 1, + "name": "experiment3" + }, + "asset": { + "network": { + "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'], + '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 hardware', + 'is_hardware_backend': True, + 'required_permission': 'can_execute', + '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, + '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, + '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': 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.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 +345,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 +714,129 @@ 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 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 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 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']) + + 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: + 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 in experiment is not a valid remote backend (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..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): @@ -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, \