diff --git a/cirq-aqt/cirq_aqt/aqt_device.py b/cirq-aqt/cirq_aqt/aqt_device.py index 866ff39f9c9..9209fedeaf4 100644 --- a/cirq-aqt/cirq_aqt/aqt_device.py +++ b/cirq-aqt/cirq_aqt/aqt_device.py @@ -25,6 +25,7 @@ """ import json +from enum import Enum from typing import Any, cast, Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union import networkx as nx @@ -36,11 +37,27 @@ gate_dict = {'X': cirq.X, 'Y': cirq.Y, 'Z': cirq.Z, 'MS': cirq.XX, 'R': cirq.PhasedXPowGate} +class OperationString(Enum): + """String representations of operations supported by AQT resources.""" + + MS = "MS" + """Cirq: XXPowGate, AQT: RXX gate.""" + + Z = "Z" + """Cirq: ZPowGate, AQT: RZ gate.""" + + R = "R" + """Cirq: PhasedXPowGate, AQT: R gate.""" + + MEASURE = "Meas" + """Measurement gate.""" + + def get_op_string(op_obj: cirq.Operation) -> str: """Find the string representation for a given gate or operation. Args: - op_obj: Gate or operation object. Gate must be one of: XXPowGate, XPowGate, YPowGate, + op_obj: Gate or operation object. Gate must be one of: XXPowGate, ZPowGate, PhasedXPowGate, or MeasurementGate. Returns: @@ -50,20 +67,16 @@ def get_op_string(op_obj: cirq.Operation) -> str: ValueError: If the gate is not one of the supported gates. """ if isinstance(op_obj.gate, cirq.XXPowGate): - op_str = 'MS' - elif isinstance(op_obj.gate, cirq.XPowGate): - op_str = 'X' - elif isinstance(op_obj.gate, cirq.YPowGate): - op_str = 'Y' + op_str = OperationString.MS.value elif isinstance(op_obj.gate, cirq.ZPowGate): - op_str = 'Z' + op_str = OperationString.Z.value elif isinstance(op_obj.gate, cirq.PhasedXPowGate): - op_str = 'R' + op_str = OperationString.R.value elif isinstance(op_obj.gate, cirq.MeasurementGate): - op_str = 'Meas' + op_str = OperationString.MEASURE.value else: raise ValueError(f'Got unknown gate on operation: {op_obj}.') - return op_str + return str(op_str) class AQTNoiseModel(cirq.NoiseModel): @@ -97,6 +110,7 @@ def noisy_moment( for qubit in op.qubits: noise_list.append(noise_op.on(qubit)) noise_list += self.get_crosstalk_operation(op, system_qubits) + return list(moment) + noise_list def get_crosstalk_operation( @@ -122,16 +136,18 @@ def get_crosstalk_operation( for neigh_idx in neighbors: if neigh_idx >= 0 and neigh_idx < num_qubits: xtlk_arr[neigh_idx] = self.noise_op_dict['crosstalk'] + for idx in idx_list: xtlk_arr[idx] = 0 xtlk_op_list = [] op_str = get_op_string(operation) gate = cast(cirq.EigenGate, gate_dict[op_str]) + if len(operation.qubits) == 1: for idx in xtlk_arr.nonzero()[0]: exponent = operation.gate.exponent # type:ignore exponent = exponent * xtlk_arr[idx] - xtlk_op = gate.on(system_qubits[idx]) ** exponent + xtlk_op = operation.gate.on(system_qubits[idx]) ** exponent # type:ignore xtlk_op_list.append(xtlk_op) elif len(operation.qubits) == 2: for op_qubit in operation.qubits: @@ -216,10 +232,14 @@ def simulate_samples(self, repetitions: int) -> cirq.Result: noise_model = cirq.NO_NOISE else: noise_model = AQTNoiseModel() + if self.circuit == cirq.Circuit(): raise RuntimeError('Simulate called without a valid circuit.') + sim = cirq.DensityMatrixSimulator(noise=noise_model) + result = sim.run(self.circuit, repetitions=repetitions) + return result @@ -342,10 +362,9 @@ def get_aqt_device(num_qubits: int) -> Tuple[AQTDevice, List[cirq.LineQubit]]: def get_default_noise_dict() -> Dict[str, Any]: """Returns the current noise parameters""" default_noise_dict = { - 'X': cirq.depolarize(1e-3), - 'Y': cirq.depolarize(1e-3), - 'Z': cirq.depolarize(1e-3), - 'MS': cirq.depolarize(1e-2), + OperationString.R.value: cirq.depolarize(1e-3), + OperationString.Z.value: cirq.depolarize(0), + OperationString.MS.value: cirq.depolarize(1e-2), 'crosstalk': 0.03, } return default_noise_dict diff --git a/cirq-aqt/cirq_aqt/aqt_device_metadata.py b/cirq-aqt/cirq_aqt/aqt_device_metadata.py index 676537aefb7..1aadab5071b 100644 --- a/cirq-aqt/cirq_aqt/aqt_device_metadata.py +++ b/cirq-aqt/cirq_aqt/aqt_device_metadata.py @@ -53,8 +53,6 @@ def __init__( self._gate_durations = { cirq.GateFamily(cirq.MeasurementGate): self._measurement_duration, cirq.GateFamily(cirq.XXPowGate): self._twoq_gates_duration, - cirq.GateFamily(cirq.XPowGate): self._oneq_gates_duration, - cirq.GateFamily(cirq.YPowGate): self._oneq_gates_duration, cirq.GateFamily(cirq.ZPowGate): self._oneq_gates_duration, cirq.GateFamily(cirq.PhasedXPowGate): self._oneq_gates_duration, } diff --git a/cirq-aqt/cirq_aqt/aqt_device_metadata_test.py b/cirq-aqt/cirq_aqt/aqt_device_metadata_test.py index 7381dbaebd9..45fb032c1cc 100644 --- a/cirq-aqt/cirq_aqt/aqt_device_metadata_test.py +++ b/cirq-aqt/cirq_aqt/aqt_device_metadata_test.py @@ -45,7 +45,7 @@ def test_aqtdevice_metadata(metadata, qubits): assert len(edges) == 10 assert all(q0 != q1 for q0, q1 in edges) assert AQTTargetGateset() == metadata.gateset - assert len(metadata.gate_durations) == 6 + assert len(metadata.gate_durations) == 4 def test_aqtdevice_duration_of(metadata, qubits): diff --git a/cirq-aqt/cirq_aqt/aqt_sampler.py b/cirq-aqt/cirq_aqt/aqt_sampler.py index 10c0d214181..0474a008fbc 100644 --- a/cirq-aqt/cirq_aqt/aqt_sampler.py +++ b/cirq-aqt/cirq_aqt/aqt_sampler.py @@ -25,13 +25,75 @@ import json import time import uuid -from typing import cast, Dict, List, Sequence, Tuple, Union +from typing import Callable, cast, Dict, List, Sequence, Tuple, Union, Literal, TypedDict +from urllib.parse import urljoin import numpy as np -from requests import put +from requests import post, get import cirq -from cirq_aqt.aqt_device import AQTSimulator, get_op_string +from cirq_aqt.aqt_device import AQTSimulator, get_op_string, OperationString + + +_DEFAULT_HOST = "https://arnica.aqt.eu/api/v1/" + + +class SingleQubitGate(TypedDict): + """Abstract single qubit rotation.""" + + qubit: int + + +class GateRZ(SingleQubitGate): + """A single-qubit rotation rotation around the Bloch sphere's z-axis.""" + + operation: Literal["RZ"] + phi: float + + +class GateR(SingleQubitGate): + """A single-qubit rotation around an arbitrary axis on the Bloch sphere's equatorial plane.""" + + operation: Literal["R"] + phi: float + theta: float + + +class GateRXX(TypedDict): + """A two-qubit entangling gate of Mølmer-Sørenson-type.""" + + operation: Literal["RXX"] + qubits: list[int] + theta: float + + +class Measure(TypedDict): + """Measurement operation. + + The MEASURE operation instructs the resource + to perform a projective measurement of all qubits. + """ + + operation: Literal["MEASURE"] + + +Gate = GateRZ | GateR | GateRXX +Operation = Gate | Measure + + +class Resource(TypedDict): + """A quantum computing device.""" + + id: str + name: str + type: Literal["device", "simulator"] + + +class Workspace(TypedDict): + """A user workspace.""" + + id: str + resources: list[Resource] class AQTSampler(cirq.Sampler): @@ -40,16 +102,127 @@ class AQTSampler(cirq.Sampler): runs a single circuit or an entire sweep remotely """ - def __init__(self, remote_host: str, access_token: str): + def __init__( + self, workspace: str, resource: str, access_token: str, remote_host: str = _DEFAULT_HOST + ): """Inits AQTSampler. Args: - remote_host: Address of the remote device. - access_token: Access token for the remote api. + workspace: the ID of the workspace you have access to. + resource: the ID of the resource to run the circuit on. + access_token: Access token for the AQT API. + remote_host: Address of the AQT API. """ + self.workspace = workspace + self.resource = resource self.remote_host = remote_host self.access_token = access_token + @staticmethod + def fetch_resources(access_token: str, remote_host: str = _DEFAULT_HOST) -> list[Workspace]: + """Lists the workspaces and resources that are accessible with access_token. + + Returns a list containing the workspaces and resources that the passed + access_token gives access to. The workspace and resource IDs in this list can be + used to submit jobs using the run and run_sweep methods. + + The printed table contains four columns: + - WORKSPACE ID: the ID of the workspace. Use this value to submit circuits. + - RESOURCE NAME: the human-readable name of the resource. + - RESOURCE ID: the ID of the resource. Use this value to submit circuits. + - D/S: whether the resource is a (D)evice or (S)imulator. + + Args: + access_token: Access token for the AQT API. + remote_host: Address of the AQT API. Defaults to "https://arnica.aqt.eu/api/v1/". + + Raises: + RuntimeError: If there was an unexpected response from the server. + """ + headers = {"Authorization": f"Bearer {access_token}", "SDK": "cirq"} + url = urljoin(remote_host if remote_host[-1] == "/" else remote_host + "/", "workspaces") + + response = get(url, headers=headers) + if response.status_code != 200: + raise RuntimeError('Got unexpected return data from server: \n' + str(response.json())) + + workspaces = [ + Workspace( + id=w['id'], + resources=[ + Resource(id=r['id'], name=r['name'], type=r['type']) for r in w['resources'] + ], + ) + for w in response.json() + ] + + return workspaces + + @staticmethod + def print_resources( + access_token: str, emit: Callable = print, remote_host: str = _DEFAULT_HOST + ) -> None: + """Displays the workspaces and resources that are accessible with access_token. + + Prints a table using the function passed as 'emit' containing the workspaces and + resources that the passed access_token gives access to. The IDs in this table + can be used to submit jobs using the run and run_sweep methods. + + The printed table contains four columns: + - WORKSPACE ID: the ID of the workspace. Use this value to submit circuits. + - RESOURCE NAME: the human-readable name of the resource. + - RESOURCE ID: the ID of the resource. Use this value to submit circuits. + - D/S: whether the resource is a (D)evice or (S)imulator. + + Args: + access_token: Access token for the AQT API. + emit (optional): A Callable which will be called once with a single string argument, + containing the table. Defaults to print from the standard library. + remote_host (optional): Address of the AQT API. Defaults to + "https://arnica.aqt.eu/api/v1/". + + Raises: + RuntimeError: If there was an unexpected response from the server. + """ + table_lines = [] + workspaces = AQTSampler.fetch_resources(access_token, remote_host) + + if len(workspaces) == 0: + return emit("No workspaces are accessible with this access token.") + if any(len(w['resources']) == 0 for w in workspaces): + return emit("No workspaces accessible with this access token contain resources.") + + col_widths = [ + max([len(w['id']) for w in workspaces]), + max([len(d['name']) for w in workspaces for d in w['resources']]), + max([len(d['id']) for w in workspaces for d in w['resources']]), + 3, + ] + SEPARATOR = "+-" + "-+-".join(col_width * "-" for col_width in col_widths) + "-+" + + table_lines.append(SEPARATOR) + table_lines.append( + f"| {'WORKSPACE ID'.ljust(col_widths[0])} |" + f" {'RESOURCE NAME'.ljust(col_widths[1])} |" + f" {'RESOURCE ID'.ljust(col_widths[2])} |" + f" {'D/S'.ljust(col_widths[3])} |" + ) + table_lines.append(SEPARATOR) + + for workspace in workspaces: + next_workspace = workspace['id'] + for resource in workspace["resources"]: + table_lines.append( + f"| {next_workspace.ljust(col_widths[0])} |" + f" {resource['name'].ljust(col_widths[1])} |" + f" {resource['id'].ljust(col_widths[2])} |" + f" {resource['type'][0].upper().ljust(col_widths[3])} |" + ) + next_workspace = "" + table_lines.append(SEPARATOR) + + emit("\n".join(table_lines)) + def _generate_json( self, circuit: cirq.AbstractCircuit, param_resolver: cirq.ParamResolverOrSimilarType ) -> str: @@ -62,7 +235,7 @@ def _generate_json( which is a list of sequential quantum operations, each operation defined by: - op_string: str that specifies the operation type: "X","Y","Z","MS" + op_string: str that specifies the operation type: "Z","MS","R","Meas" gate_exponent: float that specifies the gate_exponent of the operation qubits: list of qubits where the operation acts on. @@ -100,27 +273,72 @@ def _generate_json( json_str = json.dumps(seq_list) return json_str + def _parse_legacy_circuit_json(self, json_str: str) -> list[Operation]: + """Converts a legacy JSON circuit representation. + + Converts a JSON created for the legacy API into one that will work + with the Arnica v1 API. + + Raises: + ValueError: + * if there is not exactly one measurement operation at the end + of the circuit. + + * if an operation is found in json_str that is not in + OperationString. + + Args: + json_str: A JSON-formatted string that could be used as the + data parameter in the body of a request to the old AQT API. + """ + circuit = [] + number_of_measurements = 0 + instruction: Operation + + for legacy_op in json.loads(json_str): + if number_of_measurements > 0: + raise ValueError("Need exactly one `MEASURE` operation at the end of the circuit.") + + if legacy_op[0] == OperationString.Z.value: + instruction = GateRZ(operation="RZ", qubit=legacy_op[2][0], phi=legacy_op[1]) + + elif legacy_op[0] == OperationString.R.value: + instruction = GateR( + operation="R", qubit=legacy_op[3][0], theta=legacy_op[1], phi=legacy_op[2] + ) + + elif legacy_op[0] == OperationString.MS.value: + instruction = GateRXX(operation="RXX", qubits=legacy_op[2], theta=legacy_op[1]) + + elif legacy_op[0] == OperationString.MEASURE.value: + instruction = Measure(operation="MEASURE") + number_of_measurements += 1 + + else: + raise ValueError(f'Got unknown gate on operation: {legacy_op}.') + + circuit.append(instruction) + + if circuit[-1]["operation"] != "MEASURE": + circuit.append({"operation": "MEASURE"}) + + return circuit + def _send_json( - self, - *, - json_str: str, - id_str: Union[str, uuid.UUID], - repetitions: int = 1, - num_qubits: int = 1, + self, *, json_str: str, id_str: str, repetitions: int = 1, num_qubits: int = 1 ) -> np.ndarray: - """Sends the json string to the remote AQT device + """Sends the json string to the remote AQT device. - The interface is given by PUT requests to a single endpoint URL. - The first PUT will insert the circuit into the remote queue, - given a valid access key. - Every subsequent PUT will return a dictionary, where the key "status" - is either 'queued', if the circuit has not been processed yet or - 'finished' if the circuit has been processed. - The experimental data is returned via the key 'data' + Submits a pre-prepared JSON string representing a circuit to the AQT + API, then polls for the result, which is parsed and returned when + available. + + Please consider that due to the potential for long wait-times, there is + no timeout in the result polling. Args: json_str: Json representation of the circuit. - id_str: Unique id of the datapoint. + id_str: A label to help identify a circuit. repetitions: Number of repetitions. num_qubits: Number of qubits present in the device. @@ -130,49 +348,60 @@ def _send_json( Raises: RuntimeError: If there was an unexpected response from the server. """ - header = {"Ocp-Apim-Subscription-Key": self.access_token, "SDK": "cirq"} - response = put( - self.remote_host, - data={ - 'data': json_str, - 'access_token': self.access_token, - 'repetitions': repetitions, - 'no_qubits': num_qubits, + headers = {"Authorization": f"Bearer {self.access_token}", "SDK": "cirq"} + quantum_circuit = self._parse_legacy_circuit_json(json_str) + submission_data = { + "job_type": "quantum_circuit", + "label": id_str, + "payload": { + "circuits": [ + { + "repetitions": repetitions, + "quantum_circuit": quantum_circuit, + "number_of_qubits": num_qubits, + } + ] }, - headers=header, - ) + } + + submission_url = urljoin(self.remote_host, f"submit/{self.workspace}/{self.resource}") + + response = post(submission_url, json=submission_data, headers=headers) response = response.json() data = cast(Dict, response) - if 'status' not in data.keys(): + + if 'response' not in data.keys() or 'status' not in data['response'].keys(): raise RuntimeError('Got unexpected return data from server: \n' + str(data)) - if data['status'] == 'error': + if data['response']['status'] == 'error': raise RuntimeError('AQT server reported error: \n' + str(data)) - if 'id' not in data.keys(): + if 'job' not in data.keys() or 'job_id' not in data['job'].keys(): raise RuntimeError('Got unexpected return data from AQT server: \n' + str(data)) - id_str = data['id'] + job_id = data['job']['job_id'] + result_url = urljoin(self.remote_host, f"result/{job_id}") while True: - response = put( - self.remote_host, - data={'id': id_str, 'access_token': self.access_token}, - headers=header, - ) + response = get(result_url, headers=headers) response = response.json() data = cast(Dict, response) - if 'status' not in data.keys(): + + if 'response' not in data.keys() or 'status' not in data['response'].keys(): raise RuntimeError('Got unexpected return data from AQT server: \n' + str(data)) - if data['status'] == 'finished': + if data['response']['status'] == 'finished': break - elif data['status'] == 'error': + elif data['response']['status'] == 'error': raise RuntimeError('Got unexpected return data from AQT server: \n' + str(data)) time.sleep(1.0) - measurements_int = data['samples'] - measurements = np.zeros((len(measurements_int), num_qubits)) - for i, result_int in enumerate(measurements_int): - measurement_int_bin = format(result_int, f'0{num_qubits}b') + + if 'result' not in data['response'].keys(): + raise RuntimeError('Got unexpected return data from AQT server: \n' + str(data)) + + measurement_int = data['response']['result']['0'] + measurements = np.zeros((repetitions, num_qubits), dtype=int) + for i, repetition in enumerate(measurement_int): for j in range(num_qubits): - measurements[i, j] = int(measurement_int_bin[j]) + measurements[i, j] = repetition[j] + return measurements def run_sweep( @@ -198,7 +427,7 @@ def run_sweep( meas_name = 'm' trial_results: List[cirq.Result] = [] for param_resolver in cirq.to_resolvers(params): - id_str = uuid.uuid1() + id_str = str(uuid.uuid1()) num_qubits = len(program.all_qubits()) json_str = self._generate_json(circuit=program, param_resolver=param_resolver) results = self._send_json( @@ -223,10 +452,19 @@ class AQTSamplerLocalSimulator(AQTSampler): sampler.simulate_ideal=True """ - def __init__(self, remote_host: str = '', access_token: str = '', simulate_ideal: bool = False): + def __init__( + self, + workspace: str = "", + resource: str = "", + access_token: str = "", + remote_host: str = "", + simulate_ideal: bool = False, + ): """Args: - remote_host: Remote host is not used by the local simulator. + workspace: Workspace is not used by the local simulator. + resource: Resource is not used by the local simulator. access_token: Access token is not used by the local simulator. + remote_host: Remote host is not used by the local simulator. simulate_ideal: Boolean that determines whether a noisy or an ideal simulation is performed. """ @@ -235,18 +473,13 @@ def __init__(self, remote_host: str = '', access_token: str = '', simulate_ideal self.simulate_ideal = simulate_ideal def _send_json( - self, - *, - json_str: str, - id_str: Union[str, uuid.UUID], - repetitions: int = 1, - num_qubits: int = 1, + self, *, json_str: str, id_str: str, repetitions: int = 1, num_qubits: int = 1 ) -> np.ndarray: """Replaces the remote host with a local simulator Args: json_str: Json representation of the circuit. - id_str: Unique id of the datapoint. + id_str: A label to help identify a datapoint. repetitions: Number of repetitions. num_qubits: Number of qubits present in the device. diff --git a/cirq-aqt/cirq_aqt/aqt_sampler_test.py b/cirq-aqt/cirq_aqt/aqt_sampler_test.py index 83369884312..3f506300737 100644 --- a/cirq-aqt/cirq_aqt/aqt_sampler_test.py +++ b/cirq-aqt/cirq_aqt/aqt_sampler_test.py @@ -13,6 +13,7 @@ # limitations under the License. from unittest import mock +import json import numpy as np import pytest import sympy @@ -22,15 +23,11 @@ from cirq_aqt.aqt_device import get_aqt_device, get_op_string -class EngineReturn: +class GetResultReturn: """A put mock class for testing the REST interface""" def __init__(self): - self.test_dict = { - 'status': 'queued', - 'id': '2131da', - 'samples': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - } + self.test_dict = {'job': {'job_id': '2131da'}, 'response': {'status': 'queued'}} self.counter = 0 def json(self): @@ -38,75 +35,104 @@ def json(self): return self.test_dict def update(self, *args, **kwargs): - if self.counter >= 2: - self.test_dict['status'] = 'finished' return self -class EngineError(EngineReturn): +class GetResultError(GetResultReturn): """A put mock class for testing error responses""" def __init__(self): - self.test_dict = {'status': 'error', 'id': '2131da', 'samples': "Error message"} + self.test_dict = {'response': {}} + self.test_dict['response']['status'] = 'error' + self.test_dict['response']['message'] = "Error message" self.counter = 0 -class EngineNoid(EngineReturn): - """A put mock class for testing error responses - This will not return an id at the first call""" - - def __init__(self): - self.test_dict = {'status': 'queued'} - self.counter = 0 - - -class EngineNoStatus(EngineReturn): +class GetResultNoStatus(GetResultReturn): """A put mock class for testing error responses This will not return a status in the second call""" def update(self, *args, **kwargs): - del self.test_dict['status'] + del self.test_dict['response']['status'] return self -class EngineNoStatus2(EngineReturn): +class GetResultErrorSecond(GetResultReturn): """A put mock class for testing error responses - This will not return a status in the second call""" + This will return an error on the second put call""" def update(self, *args, **kwargs): if self.counter >= 1: - del self.test_dict['status'] + self.test_dict['response']['status'] = 'error' return self -class EngineErrorSecond(EngineReturn): +class SubmitGoodResponse: + def json(self): + return {"job": {"job_id": "test_job"}, "response": {"status": "queued"}} + + +class SubmitResultNoID: """A put mock class for testing error responses - This will return an error on the second put call""" + This will not return an id at the first call""" + + def json(self): + return {"job": {}, "response": {"status": "queued"}} + + +class SubmitResultNoStatus: + """A put mock class for testing error responses + This will not return an id at the first call""" + + def json(self): + return {"job": {"job_id": "test_job"}, "response": {}} - def update(self, *args, **kwargs): - if self.counter >= 1: - self.test_dict['status'] = 'error' - return self +class SubmitResultWithError: + """A put mock class for testing error responses + This will not return an id at the first call""" + + def json(self): + return {"job": {"job_id": "test_job"}, "response": {"status": "error"}} -def test_aqt_sampler_error_handling(): - for e_return in [ - EngineError(), - EngineErrorSecond(), - EngineNoStatus(), - EngineNoStatus2(), - EngineNoid(), - ]: - with mock.patch( - 'cirq_aqt.aqt_sampler.put', return_value=e_return, side_effect=e_return.update - ) as _mock_method: + +def test_aqt_sampler_submit_job_error_handling(): + for e_return in [SubmitResultNoID(), SubmitResultNoStatus(), SubmitResultWithError()]: + with ( + mock.patch('cirq_aqt.aqt_sampler.post', return_value=e_return), + mock.patch('cirq_aqt.aqt_sampler.get', return_value=GetResultReturn()), + ): theta = sympy.Symbol('theta') num_points = 1 max_angle = np.pi repetitions = 10 - sampler = AQTSampler(remote_host="http://localhost:5000", access_token='testkey') + sampler = AQTSampler(access_token='testkey', workspace="default", resource="test") _, qubits = get_aqt_device(1) - circuit = cirq.Circuit(cirq.X(qubits[0]) ** theta) + circuit = cirq.Circuit( + cirq.PhasedXPowGate(exponent=theta, phase_exponent=0.0).on(qubits[0]) + ) + sweep = cirq.Linspace(key='theta', start=0.1, stop=max_angle / np.pi, length=num_points) + with pytest.raises(RuntimeError): + _results = sampler.run_sweep(circuit, params=sweep, repetitions=repetitions) + + +def test_aqt_sampler_get_result_error_handling(): + for e_return in [GetResultError(), GetResultErrorSecond(), GetResultNoStatus()]: + with ( + mock.patch('cirq_aqt.aqt_sampler.post', return_value=SubmitGoodResponse()), + mock.patch( + 'cirq_aqt.aqt_sampler.get', return_value=e_return, side_effect=e_return.update + ), + ): + theta = sympy.Symbol('theta') + num_points = 1 + max_angle = np.pi + repetitions = 10 + sampler = AQTSampler(access_token='testkey', workspace="default", resource="test") + _, qubits = get_aqt_device(1) + circuit = cirq.Circuit( + cirq.PhasedXPowGate(exponent=theta, phase_exponent=0.0).on(qubits[0]) + ) sweep = cirq.Linspace(key='theta', start=0.1, stop=max_angle / np.pi, length=num_points) with pytest.raises(RuntimeError): _results = sampler.run_sweep(circuit, params=sweep, repetitions=repetitions) @@ -127,28 +153,48 @@ def test_aqt_sampler_empty_circuit(): def test_aqt_sampler(): - put_call_args0 = {'access_token': 'testkey', 'id': '2131da'} - - e_return = EngineReturn() - with mock.patch( - 'cirq_aqt.aqt_sampler.put', return_value=e_return, side_effect=e_return.update - ) as mock_method: + class ResultReturn: + def __init__(self): + self.request_counter = 0 + self.status = "queued" + + def json(self): + return {"response": {"status": self.status, "result": {"0": [[1, 1], [0, 0]]}}} + + def on_request(self, *args, **kwargs): + self.request_counter += 1 + if self.request_counter >= 3: + self.status = "finished" + return self + + result_return = ResultReturn() + + with ( + mock.patch('cirq_aqt.aqt_sampler.post', return_value=SubmitGoodResponse()) as submit_method, + mock.patch( + 'cirq_aqt.aqt_sampler.get', + return_value=result_return, + side_effect=result_return.on_request, + ) as result_method, + ): theta = sympy.Symbol('theta') num_points = 1 max_angle = np.pi repetitions = 10 - sampler = AQTSampler(remote_host="http://localhost:5000", access_token='testkey') + sampler = AQTSampler(access_token='testkey', workspace="default", resource="test") _, qubits = get_aqt_device(1) - circuit = cirq.Circuit(cirq.X(qubits[0]) ** theta) + circuit = cirq.Circuit( + cirq.PhasedXPowGate(exponent=theta, phase_exponent=0.0).on(qubits[0]) + ) sweep = cirq.Linspace(key='theta', start=0.1, stop=max_angle / np.pi, length=num_points) results = sampler.run_sweep(circuit, params=sweep, repetitions=repetitions) excited_state_probs = np.zeros(num_points) + for i in range(num_points): excited_state_probs[i] = np.mean(results[i].measurements['m']) - callargs = mock_method.call_args[1]['data'] - for keys in put_call_args0: - assert callargs[keys] == put_call_args0[keys] - assert mock_method.call_count == 3 + + assert submit_method.call_count == 1 + assert result_method.call_count == 3 def test_aqt_sampler_sim(): @@ -161,13 +207,13 @@ def test_aqt_sampler_sim(): sampler = AQTSamplerLocalSimulator() sampler.simulate_ideal = True circuit = cirq.Circuit( - cirq.X(qubits[3]) ** theta, - cirq.X(qubits[0]), - cirq.X(qubits[0]), - cirq.X(qubits[1]), - cirq.X(qubits[1]), - cirq.X(qubits[2]), - cirq.X(qubits[2]), + cirq.PhasedXPowGate(phase_exponent=0.0, exponent=theta).on(qubits[3]), + cirq.PhasedXPowGate(phase_exponent=0.0, exponent=1.0).on(qubits[0]), + cirq.PhasedXPowGate(phase_exponent=0.0, exponent=1.0).on(qubits[0]), + cirq.PhasedXPowGate(phase_exponent=0.0, exponent=1.0).on(qubits[1]), + cirq.PhasedXPowGate(phase_exponent=0.0, exponent=1.0).on(qubits[1]), + cirq.PhasedXPowGate(phase_exponent=0.0, exponent=1.0).on(qubits[2]), + cirq.PhasedXPowGate(phase_exponent=0.0, exponent=1.0).on(qubits[2]), ) circuit.append(cirq.PhasedXPowGate(phase_exponent=0.5, exponent=-0.5).on(qubits[0])) circuit.append(cirq.PhasedXPowGate(phase_exponent=0.5, exponent=0.5).on(qubits[0])) @@ -188,11 +234,13 @@ def test_aqt_sampler_sim_xtalk(): sampler = AQTSamplerLocalSimulator() sampler.simulate_ideal = False circuit = cirq.Circuit( - cirq.X(qubits[0]), - cirq.X(qubits[1]), - cirq.X(qubits[1]), - cirq.X(qubits[3]), - cirq.X(qubits[2]), + cirq.PhasedXPowGate(phase_exponent=0.0, exponent=1.0).on(qubits[0]), + cirq.PhasedXPowGate(phase_exponent=0.0, exponent=1.0).on(qubits[1]), + cirq.PhasedXPowGate(phase_exponent=0.0, exponent=1.0).on(qubits[1]), + cirq.PhasedXPowGate(phase_exponent=0.0, exponent=1.0).on(qubits[3]), + cirq.PhasedXPowGate(phase_exponent=0.0, exponent=1.0).on(qubits[2]), + cirq.XX(qubits[0], qubits[1]) ** 0.5, + cirq.Z.on_each(*qubits), ) sweep = cirq.Linspace(key='theta', start=0.1, stop=max_angle / np.pi, length=num_points) _results = sampler.run_sweep(circuit, params=sweep, repetitions=repetitions) @@ -220,3 +268,190 @@ def test_aqt_device_wrong_op_str(): for op in circuit.all_operations(): with pytest.raises(ValueError): _result = get_op_string(op) + + +def test_aqt_sampler_parses_legacy_json_correctly() -> None: + legacy_json = json.dumps( + [ + ["R", 1.0, 0.0, [0]], + ["MS", 0.5, [0, 1]], + ["Z", -0.5, [0]], + ["R", 0.5, 1.0, [0]], + ["R", 0.5, 1.0, [1]], + ] + ) + + sampler = AQTSampler("default", "test", "testkey") + quantum_circuit = sampler._parse_legacy_circuit_json(legacy_json) + + assert quantum_circuit == [ + {"operation": "R", "phi": 0.0, "theta": 1.0, "qubit": 0}, + {"operation": "RXX", "qubits": [0, 1], "theta": 0.5}, + {"operation": "RZ", "qubit": 0, "phi": -0.5}, + {"operation": "R", "qubit": 0, "theta": 0.5, "phi": 1.0}, + {"operation": "R", "qubit": 1, "theta": 0.5, "phi": 1.0}, + {"operation": "MEASURE"}, + ] + + +def test_aqt_sampler_submits_jobs_correctly() -> None: + legacy_json = json.dumps( + [ + ["R", 1.0, 0.0, [0]], + ["MS", 0.5, [0, 1]], + ["Z", -0.5, [0]], + ["R", 0.5, 1.0, [0]], + ["R", 0.5, 1.0, [1]], + ] + ) + + result = [[1, 1], [0, 0]] + + class ResultReturn: + def json(self): + return {"response": {"status": "finished", "result": {"0": result}}} + + sampler = AQTSampler("default", "test", "testkey", "http://localhost:7777/api/v1/") + + with ( + mock.patch('cirq_aqt.aqt_sampler.post', return_value=SubmitGoodResponse()) as submit_method, + mock.patch('cirq_aqt.aqt_sampler.get', return_value=ResultReturn()) as result_method, + ): + measurements = sampler._send_json( + json_str=legacy_json, id_str="test", repetitions=2, num_qubits=2 + ) + + assert submit_method.call_count == 1 + assert submit_method.call_args[0][0] == "http://localhost:7777/api/v1/submit/default/test" + + assert result_method.call_count == 1 + assert result_method.call_args[0][0] == "http://localhost:7777/api/v1/result/test_job" + + for i, rep in enumerate(measurements): + for j, sample in enumerate(rep): + assert sample == result[i][j] + + +def test_measurement_not_at_end_is_not_allowed() -> None: + legacy_json = json.dumps([["R", 1.0, 0.0, [0]], ["Meas"], ["MS", 0.5, [0, 1]]]) + + sampler = AQTSampler("default", "dummy_resource", "test") + with pytest.raises(ValueError): + sampler._send_json(json_str=legacy_json, id_str="test") + + +def test_multiple_measurements_are_not_allowed() -> None: + legacy_json = json.dumps([["R", 1.0, 0.0, [0]], ["Meas"], ["Meas"]]) + + sampler = AQTSampler("default", "dummy_resource", "test") + with pytest.raises(ValueError): + sampler._send_json(json_str=legacy_json, id_str="test") + + +def test_unknown_gate_in_json() -> None: + legacy_json = json.dumps([["A", 1.0, 0.0, [0]], ["Meas"]]) + + sampler = AQTSampler("default", "dummy_resource", "test") + with pytest.raises( + ValueError, match=r"Got unknown gate on operation: \['A', 1\.0, 0\.0, \[0\]\]\." + ): + sampler._send_json(json_str=legacy_json, id_str="test") + + +def test_aqt_sampler_raises_exception_on_bad_result_response() -> None: + legacy_json = json.dumps([["R", 1.0, 0.0, [0]]]) + + class ResultReturn: + def json(self): + return {"response": {"status": "finished"}} + + sampler = AQTSampler("default", "test", "testkey", "http://localhost:7777/api/v1/") + + with ( + mock.patch('cirq_aqt.aqt_sampler.post', return_value=SubmitGoodResponse()), + mock.patch('cirq_aqt.aqt_sampler.get', return_value=ResultReturn()), + pytest.raises(RuntimeError), + ): + sampler._send_json(json_str=legacy_json, id_str="test", repetitions=2, num_qubits=2) + + +def test_aqt_sampler_print_resources_shows_hint_if_no_workspaces() -> None: + output = [] + + def intercept(values): + output.append(str(values)) + + with mock.patch('cirq_aqt.aqt_sampler.AQTSampler.fetch_resources', return_value=[]): + AQTSampler.print_resources(access_token="test", emit=intercept) + + assert output[0] == "No workspaces are accessible with this access token." + + +def test_aqt_sampler_print_resources_shows_hint_if_no_resources() -> None: + output = [] + + def intercept(values): + output.append(str(values)) + + empty_workspace_list = [{"id": "test_ws", "resources": []}] + + with mock.patch( + 'cirq_aqt.aqt_sampler.AQTSampler.fetch_resources', return_value=empty_workspace_list + ): + AQTSampler.print_resources("test", emit=intercept) + + assert output[0] == "No workspaces accessible with this access token contain resources." + + +def test_aqt_sampler_print_resources_includes_received_resources_in_table() -> None: + output = [] + + def intercept(values): + output.append(str(values)) + + workspace_list = [ + {"id": "test_ws", "resources": [{"id": "resource", "name": "Resource", "type": "device"}]} + ] + + with mock.patch('cirq_aqt.aqt_sampler.AQTSampler.fetch_resources', return_value=workspace_list): + AQTSampler.print_resources("test", emit=intercept) + + assert any("test_ws" in o and "resource" in o and "Resource" in o and "D" in o for o in output) + + +def test_aqt_sampler_fetch_resources_raises_exception_if_non_200_status_code() -> None: + class ResourceResponse: + def __init__(self): + self.status_code = 403 + + def json(self): + return "error" + + sampler = AQTSampler("default", "test", "testkey", "http://localhost:7777/api/v1/") + + with ( + mock.patch('cirq_aqt.aqt_sampler.get', return_value=ResourceResponse()), + pytest.raises(RuntimeError), + ): + sampler.fetch_resources("token") + + +def test_aqt_sampler_fetch_resources_returns_retrieved_resources() -> None: + class ResourceResponse: + def __init__(self): + self.status_code = 200 + + def json(self): + return [ + {"id": "wid", "resources": [{"id": "rid", "name": "Resource", "type": "device"}]} + ] + + sampler = AQTSampler("default", "test", "testkey", "http://localhost:7777/api/v1/") + + with mock.patch('cirq_aqt.aqt_sampler.get', return_value=ResourceResponse()): + workspaces = sampler.fetch_resources("token") + + assert workspaces[0]["id"] == "wid" + assert workspaces[0]["resources"][0]["id"] == "rid" + assert workspaces[0]["resources"][0]["name"] == "Resource" + assert workspaces[0]["resources"][0]["type"] == "device" diff --git a/cirq-aqt/cirq_aqt/aqt_simulator_test.py b/cirq-aqt/cirq_aqt/aqt_simulator_test.py index 9b075be2679..afad7db19b3 100644 --- a/cirq-aqt/cirq_aqt/aqt_simulator_test.py +++ b/cirq-aqt/cirq_aqt/aqt_simulator_test.py @@ -44,21 +44,3 @@ def test_ms_crosstalk_n_noise(): (cirq.XX**0.015).on(cirq.LineQubit(2), cirq.LineQubit(0)), (cirq.XX**0.015).on(cirq.LineQubit(2), cirq.LineQubit(3)), ] - - -def test_x_crosstalk_n_noise(): - num_qubits = 4 - noise_mod = AQTNoiseModel() - _, qubits = get_aqt_device(num_qubits) - circuit = cirq.Circuit() - circuit.append(cirq.Y(qubits[1]) ** 0.5) - circuit.append(cirq.Z(qubits[1]) ** 0.5) - circuit.append(cirq.X(qubits[1]) ** 0.5) - for moment in circuit.moments: - noisy_moment = noise_mod.noisy_moment(moment, qubits) - assert noisy_moment == [ - (cirq.X**0.5).on(cirq.LineQubit(1)), - cirq.depolarize(p=0.001).on(cirq.LineQubit(1)), - (cirq.X**0.015).on(cirq.LineQubit(0)), - (cirq.X**0.015).on(cirq.LineQubit(2)), - ] diff --git a/cirq-aqt/cirq_aqt/aqt_target_gateset.py b/cirq-aqt/cirq_aqt/aqt_target_gateset.py index bfce02a2b7e..81c6a39fa1f 100644 --- a/cirq-aqt/cirq_aqt/aqt_target_gateset.py +++ b/cirq-aqt/cirq_aqt/aqt_target_gateset.py @@ -30,8 +30,7 @@ class AQTTargetGateset(cirq.TwoQubitCompilationTargetGateset): gates to the following universal target gateset: - `cirq.XXPowGate`: The two qubit entangling gate. - - `cirq.XPowGate`, `cirq.YPowGate`, `cirq.ZPowGate`, - `cirq.PhasedXPowGate`: Single qubit rotations. + - `cirq.ZPowGate`, `cirq.PhasedXPowGate`: Single qubit rotations. - `cirq.MeasurementGate`: Measurements. """ @@ -39,8 +38,6 @@ def __init__(self): super().__init__( cirq.XXPowGate, cirq.MeasurementGate, - cirq.XPowGate, - cirq.YPowGate, cirq.ZPowGate, cirq.PhasedXPowGate, unroll_circuit_op=False, diff --git a/cirq-aqt/cirq_aqt/aqt_target_gateset_test.py b/cirq-aqt/cirq_aqt/aqt_target_gateset_test.py index 719da5edc70..47e61d8cb9f 100644 --- a/cirq-aqt/cirq_aqt/aqt_target_gateset_test.py +++ b/cirq-aqt/cirq_aqt/aqt_target_gateset_test.py @@ -31,8 +31,8 @@ (cirq.HPowGate(exponent=0.5)(Q), False), (cirq.XX(Q, Q2), True), (cirq.measure(Q), True), - (cirq.XPowGate(exponent=0.5)(Q), True), - (cirq.YPowGate(exponent=0.25)(Q), True), + (cirq.XPowGate(exponent=0.5)(Q), False), + (cirq.YPowGate(exponent=0.25)(Q), False), (cirq.ZPowGate(exponent=0.125)(Q), True), (cirq.PhasedXPowGate(exponent=0.25, phase_exponent=0.125)(Q), True), (cirq.CZPowGate(exponent=0.5)(Q, Q2), False), diff --git a/docs/hardware/aqt/access.md b/docs/hardware/aqt/access.md index 74869b3b621..5cc61de6aa2 100644 --- a/docs/hardware/aqt/access.md +++ b/docs/hardware/aqt/access.md @@ -1,44 +1,42 @@ # Access and Authentication -AQT offers access to several quantum computing devices, called backends, +AQT offers access to several quantum computing devices, called quantum resources, ranging from real-hardware ion traps with various number of ions to quantum computing simulators including different noise models. -To get an overview of available devices visit -[www.aqt.eu](https://www.aqt.eu){:.external} and get direct access to the devices via the -[AQT gateway portal](https://gateway-portal.aqt.eu){:.external}. +To get an overview of available resources and information on how to get +access to them, visit [www.aqt.eu](https://www.aqt.eu/qc-systems/){:.external}. ## Tokens -The AQT API to access backends uses token-based authentication. In order to be -able to submit quantum circuits via quantum programming software development -kits, you need to supply these tokens. Once you have successfully subscribed -to an AQT backend, you can retrieve the token on the -[AQT gateway portal](https://gateway-portal.aqt.eu){:.external} -and use it in your quantum programs or Jupyter notebook tutorials. +The AQT API to access quantum resources uses token-based authentication. In order to be +able to submit quantum circuits you need to supply your token. You can request a +token from AQT and once you have it, use it in your quantum programs +or Jupyter notebook tutorials. -## Backend URLs +## Workspaces and Resources -Accessing the AQT backends is done using a URL for each backend. -E.g. the AQT simulators which are capable of running ideal simulations -(without a noise model) and real simulations (with a noise model) of a -quantum circuit have different URLs. For running a simulation without noise model use: +To submit circuits to an AQT backend you need to specify a workspace and resource. +E.g. to send a circuit to one of the hosted AQT simulators, which are capable of +running ideal simulations (without a noise model) and real simulations (with a +noise model) of a quantum circuit, you might use the workspace `aqt-simulators` +and the resource `simulator_noise`. -```python -url = 'https://gateway.aqt.eu/marmot/sim/' -``` +Which workspaces and resources you have access to, can be retrieved using your access +token. The resource type helps distinguishing between +- device (real hardware) +- simulator (hosted simulators) +- offline_simulator (offline simulators) -whereas for a simulation with noise model use: +## Offline Simulators -```python -url = 'https://gateway.aqt.eu/marmot/sim/noise-model-1' -``` +The Cirq simulator with AQT specific noise model can be used to simulate circuits +even without a token on your machine. -Real-hardware backends have similar URLs which can be retrieved together -with the token on the -[AQT gateway portal](https://gateway-portal.aqt.eu){:.external}. +## REST API + +It is also possible to access the documentation of the underlying REST API at +[AQT Public API](https://arnica.aqt.eu/api/v1/docs){:.external}. ## Next Steps -At this point, you should now have access to the AQT service. -You can now try out our -[Getting Started Guide](./getting_started.ipynb). +You can now try out our [Getting Started Guide](./getting_started.ipynb). diff --git a/docs/hardware/aqt/getting_started.ipynb b/docs/hardware/aqt/getting_started.ipynb index a37156a9464..0991eea61e5 100644 --- a/docs/hardware/aqt/getting_started.ipynb +++ b/docs/hardware/aqt/getting_started.ipynb @@ -84,9 +84,9 @@ "id": "b42eeeef4398" }, "source": [ - "[AQT](https://www.aqt.eu) supports Cirq as a third party software development kit and offers access to various quantum computing devices and simulators in the backend. Login to the [AQT Gateway Portal](https://gateway-portal.aqt.eu) to get a list of available devices.\n", + "[AQT](https://www.aqt.eu) supports Cirq as a third party software development kit and offers access to quantum computing devices and simulators in the backend. Visit [www.aqt.eu](https://www.aqt.eu/qc-systems/) to find available resources and information on how to get access to them.\n", "\n", - "After the Cirq installation has finished successfully, you are ready to use different backends by the use of a token and the corresponding backend URL like in the following getting started tutorial.\n", + "After the Cirq installation has finished successfully, you are ready to use the offline simulator or different backends through the use of an access token and the corresponding parameters, as in the following getting started tutorial.\n", "\n", "## Use your AQT credentials" ] @@ -111,7 +111,23 @@ "id": "63a64281ca4e" }, "source": [ - "Where `MY_TOKEN` is your access token for a specific AQT device. You need to subscribe to an AQT backend at the [AQT Gateway Portal](https://gateway-portal.aqt.eu) and retrieve the access token. Then you can access the AQT device by:" + "Where `MY_TOKEN` is your access token for the AQT Arnica API. Then you can retrieve the information which workspaces and quantum resources are available for you:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "AQTSampler.fetch_resources(access_token)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then you can specify the workspace and resource you want to send your quantum circuits to." ] }, { @@ -130,6 +146,10 @@ } ], "source": [ + "workspace = 'WORKSPACE_NAME'\n", + "resource = 'RESOURCE_NAME'\n", + "aqt_sampler = AQTSampler(workspace=workspace, resource=resource, access_token=access_token)\n", + "\n", "device, qubits = get_aqt_device(2)\n", "print(device)" ] @@ -167,8 +187,7 @@ } ], "source": [ - "circuit = cirq.Circuit(device=device)\n", - "circuit.append([cirq.XX(qubits[0], qubits[1])**0.5])\n", + "circuit = cirq.Circuit(cirq.XX(qubits[0], qubits[1])**0.5)\n", "device.validate_circuit(circuit)\n", "print(circuit, qubits)" ] @@ -198,8 +217,6 @@ } ], "source": [ - "url = 'BACKEND_URL'\n", - "aqt_sampler = AQTSampler(url, access_token=access_token)\n", "aqt_sweep = aqt_sampler.run(circuit, repetitions=100)\n", "print(aqt_sweep)" ] @@ -210,9 +227,7 @@ "id": "a6c7fd3190fe" }, "source": [ - "Where `BACKEND_URL` is the API URL of the AQT backend as specified in your subscription.\n", - "\n", - "**Note:** At the moment, the ```run()``` method of the AQTSampler implicitly performs measurements on all qubits at the end of the circuit, so explicit measurement operations aren't required. In fact, using explicit measurements will cause the AQTSampler to fail. More fine-grained measuement operations will be added to the AQT API in the future." + "**Note:** At the moment, the ```run()``` method of the AQTSampler implicitly performs measurements on all qubits at the end of the circuit, so explicit measurement operations aren't _required_. In fact, using explicit measurements apart from _exactly one at the end_ will cause the AQTSampler to fail. More fine-grained measurement operations will be added to the AQT Arnica API in the future." ] }, { @@ -221,9 +236,9 @@ "id": "c469db551c46" }, "source": [ - "## AQT Simulators\n", + "## Offline simulation of AQT devices\n", "\n", - "The AQT simulators are capable of running ideal simulations (without a noise model) and real simulations (with a noise model) of a quantum circuit. Using a simulator with noise model allows you to estimate the performance of running a circuit on the real hardware. Switching between the two simulation types is done by using the respective `BACKEND_URL` in above example.\n", + "The AQT simulators are capable of running ideal simulations (without a noise model) and real simulations (with a noise model) of a quantum circuit. Using a simulator with noise model allows you to estimate the performance of running a circuit on the real hardware. Switching between the two simulation types is done by setting the simulate_ideal flag, as in the example below.\n", "\n", "For running a simulation without noise model use" ] @@ -236,7 +251,9 @@ }, "outputs": [], "source": [ - "url = 'https://gateway.aqt.eu/marmot/sim/'" + "from cirq.aqt.aqt_sampler import AQTSamplerLocalSimulator\n", + "\n", + "aqt_sampler = AQTSamplerLocalSimulator(simulate_ideal=True)" ] }, { @@ -256,16 +273,26 @@ }, "outputs": [], "source": [ - "url = 'https://gateway.aqt.eu/marmot/sim/noise-model-1'" + "from cirq.aqt.aqt_sampler import AQTSamplerLocalSimulator\n", + "\n", + "aqt_sampler = AQTSamplerLocalSimulator(simulate_ideal=False)" ] }, { "cell_type": "markdown", - "metadata": { - "id": "13dd925bd48d" - }, + "metadata": {}, "source": [ - "We will provide different noise models in the future, which will be listed on the subscriptions page at the [AQT Gateway Portal](https://gateway-portal.aqt.eu)." + "Then you can use the Sampler Simulator as you would the regular one, for example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "aqt_sweep = aqt_sampler.run(circuit, repetitions=100)\n", + "print(aqt_sweep)" ] } ],