Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Comp 420 cotton #4

Merged
merged 4 commits into from
Jul 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
Changelog
=========

Version 0.2
=============

- Added circuit commands ``cortex circuit validate`` and ``cortex circuit run``

Version 0.1
=============

Expand Down
50 changes: 50 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 3 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions src/cortex_cli/circuit.py
Original file line number Diff line number Diff line change
@@ -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
177 changes: 141 additions & 36 deletions src/cortex_cli/cortex_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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']

Expand All @@ -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
Expand Down Expand Up @@ -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']

Expand Down Expand Up @@ -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.

Expand All @@ -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__':
Expand Down
Loading