diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cc13aca2..bb91584a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,11 @@ Changelog ========= +Version 0.2 +============= + +- Added circuit commands ``cortex circuit validate`` and ``cortex circuit run`` + Version 0.1 ============= diff --git a/README.rst b/README.rst index c22d7f17..fbc0f40f 100644 --- a/README.rst +++ b/README.rst @@ -98,3 +98,53 @@ By default, all Cortex CLI commands read the configuration file from the default $ cortex auth status --config-file /home/joe/config.json $ cortex auth login --config-file /home/joe/config.json $ cortex auth logout --config-file /home/joe/config.json + +Circuit validation +^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + $ cortex circuit validate my_circuit.qasm + +validates the quantum circuit in file `my_circuit.qasm`, and reports errors if the circuit is not valid OpenQASM 2.0. The exit code is 0 if and only if the circuit is valid. + +Executing circuits on a quantum computer +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You can execute a quantum circuit on an IQM quantum computer with + +.. code-block:: bash + + $ export IQM_SERVER_URL="https://example.com/iqm-server" + $ cortex circuit run --settings "path/to/settings.json" --shots 100 --qubit-mapping my_qubit_mapping.json my_circuit.qasm + +The server URL and settings path can be set either with command-line options or as environment variables. + +By default, authentication is handled the same way as with other Cortex CLI commands. You can override this and provide your own server url, username and password by setting environment variables IQM_AUTH_SERVER, IQM_AUTH_USERNAME and IQM_AUTH_PASSWORD. + +Note that the circuit needs to be transpiled so that it only contains operations natively supported by the IQM quantum +computer you are using. + +Run the following command: + +.. code-block:: bash + + $ cortex circuit run --help + +for information on all the parameters and their usage. + +The results of the measurements in the circuit are returned in JSON format: + +.. code-block:: json + + {"measurement_0": + [ + [1, 0, 1, 1], + [1, 0, 1, 1], + [1, 0, 1, 1] + ] + } + +The dictionary keys are measurement keys from the circuit. The value for each measurement is a 2-D array of binary +integers. The first index goes over the shots, and the second over the qubits in the measurement. For example, in the +example above, "measurement_0" is a 4-qubit measurement, and the number of shots is three. diff --git a/setup.cfg b/setup.cfg index 95a74643..85bede43 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,8 +36,10 @@ install_requires = click >= 7.1.2 python-daemon >= 2.3.0 jsonschema >= 4.6.0 - requests == 2.28.0 + requests >= 2.26.0 pydantic >= 1.8.2, < 2.0 + cirq-iqm >= 6.0, < 7.0 + iqm-client >= 4.1, < 5.0 # Require a specific Python version, e.g. Python 2.7 or >= 3.4 python_requires = ~= 3.9 diff --git a/src/cortex_cli/circuit.py b/src/cortex_cli/circuit.py new file mode 100644 index 00000000..08341daf --- /dev/null +++ b/src/cortex_cli/circuit.py @@ -0,0 +1,36 @@ +# Copyright 2021-2022 IQM client developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Submit circuit jobs to IQM quantum computers via Cortex CLI. +""" +import cirq_iqm +import click +from cirq.contrib.qasm_import.exception import QasmException + +from cortex_cli.utils import read_file + + +def validate_circuit(filename: str) -> None: + """Validates the given OpenQASM 2.0 file. + + Args: + filename: name of the QASM file + Raises: + ClickException: if circuit is invalid or not found + """ + try: + cirq_iqm.circuit_from_qasm(read_file(filename)) + except QasmException as ex: + message = f'Invalid quantum circuit in {filename}\n{ex.message}' + raise click.ClickException(message) from ex diff --git a/src/cortex_cli/cortex_cli.py b/src/cortex_cli/cortex_cli.py index 8382e1db..5c890fd5 100644 --- a/src/cortex_cli/cortex_cli.py +++ b/src/cortex_cli/cortex_cli.py @@ -20,16 +20,23 @@ import os import sys import time +from io import TextIOWrapper from pathlib import Path +from typing import Optional +import cirq_iqm import click +from cirq_iqm.iqm_sampler import serialize_circuit, serialize_qubit_mapping +from iqm_client.iqm_client import Circuit, IQMClient from cortex_cli import __version__ from cortex_cli.auth import (ClientAuthenticationError, login_request, logout_request, refresh_request, time_left_seconds) +from cortex_cli.circuit import validate_circuit from cortex_cli.token_manager import (check_daemon, daemonize_token_manager, kill_by_pid) +from cortex_cli.utils import read_file, read_json HOME_PATH = str(Path.home()) CONFIG_PATH = f'{HOME_PATH}/.config/iqm-cortex-cli/config.json' @@ -141,7 +148,15 @@ def cortex_cli() -> None: default=USERNAME, help='Username. If not provided, it will be asked for at login.') @click.option('-v', '--verbose', is_flag=True, help='Print extra information.') -def init(config_file, tokens_file, base_url, realm, client_id, username, verbose) -> None: #pylint: disable=too-many-arguments +def init( #pylint: disable=too-many-arguments + config_file: str, + tokens_file: str, + base_url: str, + realm: str, + client_id: str, + username: str, + verbose: bool +) -> None: """Initialize configuration and authentication.""" _setLogLevelByVerbosity(verbose) @@ -189,12 +204,12 @@ def status(config_file, verbose) -> None: _setLogLevelByVerbosity(verbose) logger.debug('Using configuration file: %s', config_file) - config = _read_json(config_file) + config = read_json(config_file) tokens_file = config['tokens_file'] if not Path(tokens_file).is_file(): raise click.ClickException(f'Tokens file not found: {tokens_file}') - tokens_data = _read_json(tokens_file) + tokens_data = read_json(tokens_file) click.echo(f'Tokens file: {tokens_file}') if not 'pid' in tokens_data: @@ -225,11 +240,18 @@ def status(config_file, verbose) -> None: @click.option('--refresh-period', default=REFRESH_PERIOD, help='How often to reresh tokens (in seconds).') @click.option('--no-daemon', is_flag=True, default=False, help='Do not start token manager to refresh tokens.') @click.option('-v', '--verbose', is_flag=True, help='Print extra information.') -def login(config_file, username, password, refresh_period, no_daemon, verbose) -> None: #pylint: disable=too-many-arguments +def login( #pylint: disable=too-many-arguments + config_file:str, + username:str, + password:str, + refresh_period:int, + no_daemon: bool, + verbose:bool +) -> None: """Authenticate on the IQM server.""" _setLogLevelByVerbosity(verbose) - config = _read_json(config_file) + config = read_json(config_file) base_url, realm, client_id = config['base_url'], config['realm'], config['client_id'] tokens_file = config['tokens_file'] @@ -239,7 +261,7 @@ def login(config_file, username, password, refresh_period, no_daemon, verbose) - return # Tokens file exists; Refresh tokens without username/password - refresh_token = _read_json(tokens_file)['refresh_token'] + refresh_token = read_json(tokens_file)['refresh_token'] logger.debug('Attempting to refresh tokens by using existing refresh token from file: %s', tokens_file) new_tokens = None @@ -291,13 +313,13 @@ def login(config_file, username, password, refresh_period, no_daemon, verbose) - is_flag=True, default=False, help="Don't delete tokens file, but kill token manager daemon.") @click.option('-f', '--force', is_flag=True, default=False, help="Don't ask for confirmation.") -def logout(config_file, keep_tokens, force) -> None: +def logout(config_file: str, keep_tokens: str, force: bool) -> None: """Either logout completely, or just stop token manager while keeping tokens file.""" - config = _read_json(config_file) + config = read_json(config_file) base_url, realm, client_id = config['base_url'], config['realm'], config['client_id'] tokens_file = config['tokens_file'] - tokens = _read_json(tokens_file) + tokens = read_json(tokens_file) pid = tokens['pid'] if 'pid' in tokens else None refresh_token = tokens['refresh_token'] @@ -344,6 +366,21 @@ def logout(config_file, keep_tokens, force) -> None: logger.info('Logout aborted.') + + +@cortex_cli.group() +def circuit() -> None: + """Execute your quantum circuits with Cortex CLI.""" + return + +@circuit.command() +@click.argument('filename') +def validate(filename:str) -> None: + """Check if a quantum circuit is valid.""" + validate_circuit(filename) + logger.info('File %s contains a valid quantum circuit', filename) + + def save_tokens_file(path: str, tokens: dict[str, str], auth_server_url: str) -> None: """Saves tokens as JSON file at given path. @@ -370,37 +407,105 @@ def save_tokens_file(path: str, tokens: dict[str, str], auth_server_url: str) -> raise click.ClickException(f'Error writing tokens file, {error}') from error -def _read(filename: str) -> str: - """Opens and reads the given file. - - Args: - filename (str): name of the file to read - Returns: - str: contents of the file - Raises: - ClickException: if file is not found +@circuit.command() +@click.option('-v', '--verbose', is_flag=True, help='Print extra information.') +@click.option('--shots', default=1, type=int, help='Number of times to sample the circuit.') +@click.option('--settings', default=None, type=click.File(), envvar='IQM_SETTINGS_PATH', + help='Path to the settings file containing calibration data for the backend. Must be JSON formatted. ' + 'Can also be set using the IQM_SETTINGS_PATH environment variable:\n' + '`export IQM_SETTINGS_PATH=\"/path/to/settings/file.json\"`\n' + 'If not set, the default (latest) calibration data will be used.') +@click.option('--qubit-mapping', default=None, type=click.File(), envvar='IQM_QUBIT_MAPPING_PATH', + help='Path to the qubit mapping JSON file. Must consist of a single JSON object, with logical ' + 'qubit names ("Alice", "Bob", ...) as keys, and physical qubit names (appearing in ' + 'the settings file) as values. For example: {"Alice": "QB1", "Bob": "QB2"}. ' + 'Can also be set using the IQM_QUBIT_MAPPING_PATH environment variable:\n' + '`export IQM_QUBIT_MAPPING_PATH=\"/path/to/qubit/mapping.json\"`\n' + 'If not set, the qubit names are assumed to be physical names.') +@click.option('--url', envvar='IQM_SERVER_URL', type=str, required=True, + help='URL of the IQM server interface for running circuits. Must start with http or https. ' + 'Can also be set using the IQM_SERVER_URL environment variable:\n' + '`export IQM_SERVER_URL=\"https://example.com\"`') +@click.option('-i', '--iqm-json', is_flag=True, + help='Set this flag if FILENAME is already in IQM JSON format (instead of being an OpenQASM file).') +@click.option('--config-file', + default=CONFIG_PATH, + type=click.Path(exists=True, dir_okay=False), + help='Location of the configuration file to be used.') +@click.option('--no-auth', is_flag=True, default=False, + help="Do not use Cortex CLI's auth functionality. " + 'If True, then --config-file option is ignored. ' + 'When submitting a circuit job, Cortex CLI will use IQM Client without passing any auth tokens. ' + 'Auth data can still be set using environment variables for IQM Client.') +@click.argument('filename', type=click.Path()) +def run( #pylint: disable=too-many-arguments, too-many-locals + verbose: bool, + shots: int, + settings: Optional[TextIOWrapper], + qubit_mapping: Optional[TextIOWrapper], + url: str, + filename: str, + iqm_json: bool, + config_file: str, + no_auth: bool +) -> None: + """Execute a quantum circuit. + + The circuit is provided in the OpenQASM 2.0 file FILENAME. The circuit must only contain operations that are + natively supported by the quantum computer the execution happens on. You can use the separate IQM + Quantum Circuit Optimizer (QCO) to convert your circuit to a supported format. + + Returns a JSON object whose keys correspond to the measurement operations in the circuit. + The value for each key is a 2-D array of integers containing the corresponding measurement + results. The first index of the array goes over the shots, and the second over the qubits + included in the measurement. """ - try: - with open(filename, 'r', encoding='utf-8') as file: - return file.read() - except FileNotFoundError as error: - raise click.ClickException(f'File {filename} not found') from error + _setLogLevelByVerbosity(verbose) -def _read_json(filename: str) -> dict: - """Opens and parses the given JSON file. + raw_input = read_file(filename) + + if not no_auth: + config = read_json(config_file) + tokens_file = config['tokens_file'] + if not Path(tokens_file).is_file(): + raise click.ClickException(f'Tokens file not found: {tokens_file}') - Args: - filename (str): name of the file to read - Returns: - dict: object derived from JSON file - Raises: - JSONDecodeError: if parsing fails - """ try: - json_data = json.loads(_read(filename)) - except json.decoder.JSONDecodeError as error: - raise click.ClickException(f'Decoding JSON has failed, {error}') from error - return json_data + # serialize the circuit and the qubit mapping + if iqm_json: + input_circuit = Circuit.parse_raw(raw_input) + else: + validate_circuit(filename) + input_circuit = cirq_iqm.circuit_from_qasm(raw_input) + input_circuit = serialize_circuit(input_circuit) + + logger.debug('\nInput circuit:\n%s', input_circuit) + + if qubit_mapping is not None: + qubit_mapping = serialize_qubit_mapping(json.load(qubit_mapping)) + + if settings is not None: + settings = json.load(settings) + + # run the circuit on the backend + if no_auth: + iqm_client = IQMClient(url, settings) + else: + iqm_client = IQMClient(url, settings, tokens_file = tokens_file) + job_id = iqm_client.submit_circuits([input_circuit], qubit_mapping, shots=shots) + results = iqm_client.wait_for_results(job_id) + iqm_client.close() + except Exception as ex: + # just show the error message, not a stack trace + raise click.ClickException(str(ex)) from ex + + if results.measurements is None: + raise click.ClickException( + f'No measurements obtained from backend. Job status is ${results.status}' + ) + + logger.debug('\nResults:') + logger.info(json.dumps(results.measurements[0])) if __name__ == '__main__': diff --git a/src/cortex_cli/utils.py b/src/cortex_cli/utils.py new file mode 100644 index 00000000..c821e5c7 --- /dev/null +++ b/src/cortex_cli/utils.py @@ -0,0 +1,52 @@ +# Copyright 2021-2022 IQM client developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Utility functions for Cortex CLI. +""" +import json + +import click + + +def read_file(filename: str) -> str: + """Opens and reads the given file. + + Args: + filename (str): name of the file to read + Returns: + str: contents of the file + Raises: + ClickException: if file is not found + """ + try: + with open(filename, 'r', encoding='utf-8') as file: + return file.read() + except FileNotFoundError as error: + raise click.ClickException(f'File {filename} not found') from error + +def read_json(filename: str) -> dict: + """Opens and parses the given JSON file. + + Args: + filename (str): name of the file to read + Returns: + dict: object derived from JSON file + Raises: + JSONDecodeError: if parsing fails + """ + try: + json_data = json.loads(read_file(filename)) + except json.decoder.JSONDecodeError as error: + raise click.ClickException(f'Decoding JSON has failed, {error}') from error + return json_data diff --git a/tests/auth_test.py b/tests/auth_test.py index 8378c28d..04c717d0 100644 --- a/tests/auth_test.py +++ b/tests/auth_test.py @@ -49,6 +49,7 @@ def test_refresh_request(credentials, config_dict, tokens_dict): assert result == expected_tokens unstub() + def test_refresh_request_handles_expired_token(config_dict, tokens_dict): """ Tests that refresh request is not made when token is expired. @@ -87,6 +88,7 @@ def test_raises_client_authentication_error_if_login_fails(credentials, config_d login_request(base_url, realm, client_id, username, password) unstub() + def test_raises_client_authentication_error_if_refresh_fails(credentials, config_dict, tokens_dict): """ Tests that authentication failure at refresh raises ClientAuthenticationError @@ -100,6 +102,7 @@ def test_raises_client_authentication_error_if_refresh_fails(credentials, config refresh_request(base_url, realm, client_id, refresh_token) unstub() + def test_raises_client_authentication_error_if_logout_fails(config_dict, tokens_dict): """ Tests that authentication failure at logout raises ClientAuthenticationError @@ -112,6 +115,7 @@ def test_raises_client_authentication_error_if_logout_fails(config_dict, tokens_ logout_request(base_url, realm, client_id, refresh_token) unstub() + def test_token_is_valid(credentials): """ Test that valid refreshed token is recognized as valid. diff --git a/tests/circuit_test.py b/tests/circuit_test.py new file mode 100644 index 00000000..96e69a32 --- /dev/null +++ b/tests/circuit_test.py @@ -0,0 +1,155 @@ +# Copyright 2021-2022 IQM client developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Tests for Cortex CLI's circuit commands +""" +import os + +from click.testing import CliRunner +from mockito import unstub + +from cortex_cli.cortex_cli import cortex_cli +from tests.conftest import expect_jobs_requests, resources_path + +valid_circuit_qasm = os.path.join(resources_path(), 'valid_circuit.qasm') +qubit_mapping_path = os.path.join(resources_path(), 'qubit_mapping.json') +settings_path = os.path.join(resources_path(), 'settings.json') + + +def test_circuit_validate_no_argument_fails(): + """ + Tests that ``circuit validate`` fails without argument. + """ + result = CliRunner().invoke(cortex_cli, ['circuit', 'validate']) + assert result.exit_code != 0 + assert 'Missing' in result.output + assert 'FILENAME' in result.output + + +def test_circuit_validate_no_file_fails(): + """ + Tests that ``circuit validate`` fails with non-existing file. + """ + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(cortex_cli, ['circuit', 'validate', 'nope.qasm']) # this file does not exist + assert result.exit_code != 0 + assert 'File' in result.output + assert 'not found' in result.output + + +def test_circuit_validate_obviously_invalid_fails(): + """ + Tests that ``circuit validate`` fails with obviously invalid file. + """ + runner = CliRunner() + with runner.isolated_filesystem(): + with open('my_circuit.qasm', 'w', encoding='utf-8') as circuit_file: + circuit_file.write('foo baz') + result = runner.invoke(cortex_cli, ['circuit', 'validate', 'my_circuit.qasm']) + assert result.exit_code != 0 + assert 'Invalid quantum circuit in my_circuit.qasm' in result.output + + +def test_circuit_validate_slightly_invalid_fails(): + """ + Tests that ``circuit validate`` fails with slightly invalid file. + """ + slightly_invalid_circuit = """OPENQASM 2.0; + include "qelib1.inc"; + qreg q[2]; + cx q[1], q[2]; + """ + runner = CliRunner() + with runner.isolated_filesystem(): + with open('my_circuit.qasm', 'w', encoding='utf-8') as circuit_file: + circuit_file.write(slightly_invalid_circuit) + result = runner.invoke(cortex_cli, ['circuit', 'validate', 'my_circuit.qasm']) + assert result.exit_code != 0 + assert 'Invalid quantum circuit in my_circuit.qasm' in result.output + + +def test_circuit_validate_valid_circuit(): + """ + Tests that ``circuit validate`` validates a valid circuit validly. + """ + result = CliRunner().invoke(cortex_cli, ['circuit', 'validate', valid_circuit_qasm]) + assert result.exit_code == 0 + assert f'File {valid_circuit_qasm} contains a valid quantum circuit' in result.output + + +def test_circuit_run_invalid_circuit(mock_environment_vars_for_backend): # pylint: disable=unused-argument + """ + Tests that ``circuit run`` fails with an invalid circuit. + """ + runner = CliRunner() + with runner.isolated_filesystem(): + with open('my_circuit.qasm', 'w', encoding='utf-8') as circuit_file: + circuit_file.write('foo bar') + with open('my_qubits.json', 'w', encoding='utf-8') as qubit_mapping_file: + qubit_mapping_file.write('{}') + result = CliRunner().invoke(cortex_cli, + ['circuit', 'run', 'my_circuit.qasm', + '--qubit-mapping', 'my_qubits.json']) + + assert result.exit_code != 0 + assert 'Invalid quantum circuit in my_circuit.qasm' in result.output + + +def test_circuit_run_valid_qasm_circuit(credentials): + """ + Tests that ``circuit run`` succeeds with valid QASM circuit. + """ + base_url = credentials['base_url'] + expect_jobs_requests(base_url) + result = CliRunner().invoke(cortex_cli, + ['circuit', 'run', valid_circuit_qasm, + '--qubit-mapping', qubit_mapping_path, + '--settings', settings_path, + '--url', base_url, + '--no-auth']) + assert 'result' in result.output + assert result.exit_code == 0 + unstub() + + +def test_circuit_run_valid_json_circuit(credentials): + """ + Tests that ``circuit run`` succeeds with valid JSON circuit. + """ + base_url = credentials['base_url'] + expect_jobs_requests(base_url) + result = CliRunner().invoke(cortex_cli, + ['circuit', 'run', os.path.join(resources_path(), 'valid_circuit.json'), '--iqm-json', + '--qubit-mapping', qubit_mapping_path, + '--settings', settings_path, '--url', base_url, + '--no-auth']) + assert 'result' in result.output + assert result.exit_code == 0 + unstub() + + +def test_circuit_run_valid_json_circuit_with_default_settings_and_without_qubit_mapping(credentials): + """ + Tests that ``circuit run`` succeeds with valid qasm circuit and no qubit mapping. + """ + base_url = credentials['base_url'] + expect_jobs_requests(base_url) + result = CliRunner().invoke(cortex_cli, + ['circuit', 'run', os.path.join(resources_path(), 'valid_circuit.json'), '--iqm-json', + '--url', base_url, + '--no-auth']) + assert 'result' in result.output + assert result.exit_code == 0 + unstub() diff --git a/tests/conftest.py b/tests/conftest.py index 71b03db5..4deaa434 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,6 +22,8 @@ import time from base64 import b64encode from typing import Optional +from unittest import mock as umock +from uuid import UUID import pytest import requests @@ -32,6 +34,11 @@ from cortex_cli.auth import AuthRequest, GrantType from cortex_cli.cortex_cli import CLIENT_ID, REALM_NAME +existing_run = UUID('3c3fcda3-e860-46bf-92a4-bcc59fa76ce9') + +def resources_path(): + """Get path to tests/resources directory from current location""" + return os.path.join(os.path.dirname(os.path.realpath(__file__)), 'resources') @pytest.fixture() def credentials(): @@ -45,17 +52,27 @@ def credentials(): @pytest.fixture def config_dict(): """Reads and parses config file into a dictionary""" - config_file = os.path.dirname(os.path.realpath(__file__)) + '/resources/config.json' + config_file = os.path.join(resources_path(), 'config.json') with open(config_file, 'r', encoding='utf-8') as file: return json.loads(file.read()) @pytest.fixture def tokens_dict(): """Reads and parses tokens file into a dictionary""" - tokens_file = os.path.dirname(os.path.realpath(__file__)) + '/resources/tokens.json' + tokens_file = os.path.join(resources_path(), 'tokens.json') with open(tokens_file, 'r', encoding='utf-8') as file: return json.loads(file.read()) +@pytest.fixture() +def mock_environment_vars_for_backend(credentials): + """ + Mocks environment variables + """ + settings_path = os.path.join(resources_path(), 'settings.json') + with (umock.patch.dict(os.environ, {'IQM_SERVER_URL': credentials['base_url']}), + umock.patch.dict(os.environ, {'IQM_SETTINGS_PATH': settings_path})): + yield + class MockJsonResponse: def __init__(self, status_code: int, json_data: dict): @@ -134,6 +151,7 @@ def prepare_tokens( return tokens + def expect_logout( base_url: str, realm: str, @@ -156,6 +174,38 @@ def expect_logout( mock({'status_code': status_code, 'text': '{}'}) ) + +def expect_jobs_requests(base_url): + """ + Prepare for job submission requests. + """ + success_submit_result = {'id': str(existing_run)} + success_submit_response = mock({'status_code': 201, 'text': json.dumps(success_submit_result)}) + when(success_submit_response).json().thenReturn(success_submit_result) + when(requests).post(f'{base_url}/jobs', ...).thenReturn(success_submit_response) + + running_result = {'status': 'pending'} + running_response = mock({'status_code': 200, 'text': json.dumps(running_result)}) + when(running_response).json().thenReturn(running_result) + + success_get_result = { + 'status': 'ready', + 'measurements': [{ + 'result': [[1, 0, 1, 1], [1, 0, 0, 1], [1, 0, 1, 1], [1, 0, 1, 1]] + }] + } + success_get_response = mock({'status_code': 200, 'text': json.dumps(success_get_result)}) + when(success_get_response).json().thenReturn(success_get_result) + + when(requests).get( + f'{base_url}/jobs/{existing_run}', ... + ).thenReturn( + running_response + ).thenReturn( + success_get_response + ) + + def expect_token_is_valid(token:str, result:bool = True): """ Prepare for token_is_valid call diff --git a/tests/resources/qubit_mapping.json b/tests/resources/qubit_mapping.json new file mode 100644 index 00000000..2cd3e2b2 --- /dev/null +++ b/tests/resources/qubit_mapping.json @@ -0,0 +1,7 @@ +{ +"QB1": "QB1", +"QB2": "QB2", +"QB3": "QB3", +"QB4": "QB4", +"QB5": "QB5" +} diff --git a/tests/resources/settings.json b/tests/resources/settings.json new file mode 100644 index 00000000..68698535 --- /dev/null +++ b/tests/resources/settings.json @@ -0,0 +1,8 @@ +{ + "name": "root", + "settings": {}, + "subtrees": { + "QB1": {}, + "QB2": {} + } +} diff --git a/tests/resources/valid_circuit.json b/tests/resources/valid_circuit.json new file mode 100644 index 00000000..995b41a4 --- /dev/null +++ b/tests/resources/valid_circuit.json @@ -0,0 +1,36 @@ +{ + "name": "Serialized from Cirq", + "instructions": [ + { + "name": "cz", + "qubits": [ + "QB1", + "QB3" + ], + "args": {} + }, + { + "name": "phased_rx", + "qubits": [ + "QB5" + ], + "args": { + "angle_t": -0.25, + "phase_t": 0 + } + }, + { + "name": "measurement", + "qubits": [ + "QB2", + "QB4", + "QB1", + "QB5", + "QB3" + ], + "args": { + "key": "a measurement" + } + } + ] +} diff --git a/tests/resources/valid_circuit.qasm b/tests/resources/valid_circuit.qasm new file mode 100644 index 00000000..68cb8a88 --- /dev/null +++ b/tests/resources/valid_circuit.qasm @@ -0,0 +1,5 @@ +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +x q[0]; +cz q[0], q[1]; \ No newline at end of file