diff --git a/examples/bell.py b/examples/bell.py index c18dcac18..400550b5e 100644 --- a/examples/bell.py +++ b/examples/bell.py @@ -9,4 +9,4 @@ # https://wikipedia.org/wiki/Bell_state bell = Circuit().h(0).cnot(0, 1) -print(device.run(bell, s3_folder).result().measurement_counts) +print(device.run(bell, s3_folder, shots=100).result().measurement_counts) diff --git a/src/braket/aws/aws_qpu.py b/src/braket/aws/aws_qpu.py index c00b5246a..dff68c943 100644 --- a/src/braket/aws/aws_qpu.py +++ b/src/braket/aws/aws_qpu.py @@ -11,7 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Union import boto3 from braket.annealing.problem import Problem @@ -64,7 +64,7 @@ def run( self, task_specification: Union[Circuit, Problem], s3_destination_folder: AwsSession.S3DestinationFolder, - shots: Optional[int] = None, + shots: int = 1000, *aws_quantum_task_args, **aws_quantum_task_kwargs, ) -> AwsQuantumTask: @@ -76,7 +76,8 @@ def run( task_specification (Union[Circuit, Problem]): Specification of task (circuit or annealing problem) to run on device. s3_destination_folder: The S3 location to save the task's results - shots (Optional[int]): The number of times to run the circuit or annealing problem + shots (int, optional): The number of times to run the circuit or annealing problem. + Default is 1000. *aws_quantum_task_args: Variable length positional arguments for `braket.aws.aws_quantum_task.AwsQuantumTask.create()`. **aws_quantum_task_kwargs: Variable length keyword arguments for diff --git a/src/braket/aws/aws_quantum_simulator.py b/src/braket/aws/aws_quantum_simulator.py index 9fcbaf6d7..3deee3c53 100644 --- a/src/braket/aws/aws_quantum_simulator.py +++ b/src/braket/aws/aws_quantum_simulator.py @@ -11,7 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Union from braket.annealing.problem import Problem from braket.aws.aws_quantum_task import AwsQuantumTask @@ -31,7 +31,7 @@ def __init__(self, arn: str, aws_session=None): """ Args: arn (str): The ARN of the simulator, for example, - "arn:aws:aqx:::quantum-simulator:aqx:qs1". + "arn:aws:aqx:::quantum-simulator:aqx:qs1". aws_session (AwsSession, optional) aws_session: An AWS session object. Default = None. """ super().__init__(name=None, status=None, status_reason=None) @@ -44,7 +44,7 @@ def run( self, task_specification: Union[Circuit, Problem], s3_destination_folder: AwsSession.S3DestinationFolder, - shots: Optional[int] = None, + shots: int = 0, *aws_quantum_task_args, **aws_quantum_task_kwargs, ) -> AwsQuantumTask: @@ -55,7 +55,13 @@ def run( task_specification (Union[Circuit, Problem]): Specification of task (circuit or annealing problem) to run on device. s3_destination_folder: The S3 location to save the task's results - shots (Optional[int]): The number of times to run the circuit or annealing problem + shots (int, optional): The number of times to run the circuit or annealing problem. + Default is 0. + For circuits, when `shots=0`, the simulator will support simulator-only + result types, compute the exact results based on the task specification, + and sampling is not supported. + `shots>0` means that the simulator will be treated like a QPU and + only support result types available for a QPU. *aws_quantum_task_args: Variable length positional arguments for `braket.aws.aws_quantum_task.AwsQuantumTask.create()`. **aws_quantum_task_kwargs: Variable length keyword arguments for diff --git a/src/braket/aws/aws_quantum_task.py b/src/braket/aws/aws_quantum_task.py index a456b09ec..06224d759 100644 --- a/src/braket/aws/aws_quantum_task.py +++ b/src/braket/aws/aws_quantum_task.py @@ -17,12 +17,13 @@ import time from functools import singledispatch from logging import Logger, getLogger -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Union import boto3 from braket.annealing.problem import Problem from braket.aws.aws_session import AwsSession from braket.circuits.circuit import Circuit +from braket.circuits.circuit_helpers import validate_circuit_and_shots from braket.tasks import AnnealingQuantumTaskResult, GateModelQuantumTaskResult, QuantumTask @@ -46,7 +47,7 @@ def create( device_arn: str, task_specification: Union[Circuit, Problem], s3_destination_folder: AwsSession.S3DestinationFolder, - shots: Optional[int] = None, + shots: int, backend_parameters: Dict[str, Any] = None, *args, **kwargs, @@ -68,8 +69,9 @@ def create( to store task results in. shots (int): The number of times to run the task on the device. If the device is a - simulator, this implies the state is sampled N times, where N = `shots`. Default - shots = 1_000. + simulator, this implies the state is sampled N times, where N = `shots`. + `shots=0` is only available on simulators and means that the simulator + will compute the exact results based on the task specification. backend_parameters (Dict[str, Any]): Additional parameters to send to the device. For example, for D-Wave: @@ -77,11 +79,16 @@ def create( Returns: AwsQuantumTask: AwsQuantumTask tracking the task execution on the device. + Note: The following arguments are typically defined via clients of Device. - `task_specification` - `s3_destination_folder` - `shots` + + See Also: + `braket.aws.aws_quantum_simulator.AwsQuantumSimulator.run()` + `braket.aws.aws_qpu.AwsQpu.run()` """ if len(s3_destination_folder) != 2: raise ValueError( @@ -351,6 +358,7 @@ def _( *args, **kwargs, ) -> AwsQuantumTask: + validate_circuit_and_shots(circuit, create_task_kwargs["shots"]) create_task_kwargs.update( { "ir": circuit.to_ir().json(), diff --git a/src/braket/circuits/circuit_helpers.py b/src/braket/circuits/circuit_helpers.py new file mode 100644 index 000000000..b6c134cd9 --- /dev/null +++ b/src/braket/circuits/circuit_helpers.py @@ -0,0 +1,37 @@ +# Copyright 2019-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file 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. + +from braket.circuits import Circuit, ResultType + + +def validate_circuit_and_shots(circuit: Circuit, shots: int) -> None: + """ + Validates if circuit and shots are correct before running on a device + + Args: + circuit (Circuit): circuit to validate + shots (int): shots to validate + + Raises: + ValueError: If no result types specified for circuit and shots=0. + See `braket.circuit.result_types. Or, if `StateVector` or `Amplitude` + are specified as result types when shots > 0. + """ + if not shots and not circuit.result_types: + raise ValueError( + "No result types specified for circuit and shots=0. See `braket.circuit.result_types`" + ) + elif shots and circuit.result_types: + for rt in circuit.result_types: + if isinstance(rt, ResultType.StateVector) or isinstance(rt, ResultType.Amplitude): + raise ValueError("StateVector or Amplitude cannot be specified when shots>0") diff --git a/src/braket/circuits/result_types.py b/src/braket/circuits/result_types.py index 2b3a6ddf4..457601c30 100644 --- a/src/braket/circuits/result_types.py +++ b/src/braket/circuits/result_types.py @@ -34,7 +34,10 @@ class StateVector(ResultType): - """The full state vector as a requested result type.""" + """ + The full state vector as a requested result type. + This is only available when `shots=0` for simulators. + """ def __init__(self): super().__init__(ascii_symbol=["StateVector"]) @@ -68,7 +71,10 @@ def __copy__(self) -> StateVector: class Amplitude(ResultType): - """The amplitude of specified quantum states as a requested result type.""" + """ + The amplitude of specified quantum states as a requested result type. + This is only available when `shots=0` for simulators. + """ def __init__(self, state: List[str]): """ @@ -129,10 +135,14 @@ def __copy__(self): class Probability(ResultType): - """Probability as the requested result type. + """Probability in the computational basis as the requested result type. It can be the probability of all states if no targets are specified or the marginal probability - of a restricted set of states if only a subset of all qubits are specified as target.""" + of a restricted set of states if only a subset of all qubits are specified as target. + + For `shots>0`, this is calculated by measurements. For `shots=0`, this is supported + only by simulators and represents the exact result. + """ def __init__(self, target: QubitSetInput = None): """ @@ -260,6 +270,9 @@ class Expectation(ObservableResultType): will be applied to all qubits in parallel. Otherwise, the number of specified targets must be equivalent to the number of qubits the observable can be applied to. + For `shots>0`, this is calculated by measurements. For `shots=0`, this is supported + only by simulators and represents the exact result. + See :mod:`braket.circuits.observables` module for all of the supported observables. """ @@ -321,6 +334,8 @@ class Sample(ObservableResultType): will be applied to all qubits in parallel. Otherwise, the number of specified targets must be equivalent to the number of qubits the observable can be applied to. + This is only available for `shots>0`. + See :mod:`braket.circuits.observables` module for all of the supported observables. """ @@ -382,6 +397,9 @@ class Variance(ObservableResultType): will be applied to all qubits in parallel. Otherwise, the number of specified targets must be equivalent to the number of qubits the observable can be applied to. + For `shots>0`, this is calculated by measurements. For `shots=0`, this is supported + only by simulators and represents the exact result. + See :mod:`braket.circuits.observables` module for all of the supported observables. """ diff --git a/src/braket/devices/local_simulator.py b/src/braket/devices/local_simulator.py index 73fb4bde3..b380800ac 100644 --- a/src/braket/devices/local_simulator.py +++ b/src/braket/devices/local_simulator.py @@ -18,6 +18,7 @@ from braket.annealing.problem import Problem from braket.circuits import Circuit +from braket.circuits.circuit_helpers import validate_circuit_and_shots from braket.devices.braket_simulator import BraketSimulator from braket.devices.device import Device from braket.tasks import AnnealingQuantumTaskResult, GateModelQuantumTaskResult @@ -51,12 +52,16 @@ def __init__(self, backend: Union[str, BraketSimulator] = "default"): self._delegate = delegate def run( - self, task_specification: Union[Circuit, Problem], *args, **kwargs, + self, task_specification: Union[Circuit, Problem], shots: int = 0, *args, **kwargs, ) -> LocalQuantumTask: """ Runs the given task with the wrapped local simulator. Args: task_specification (Union[Circuit, Problem]): + shots (int, optional): The number of times to run the circuit or annealing problem. + Default is 0, which means that the simulator will compute the exact + results based on the task specification. + Sampling is not supported for shots=0. *args: Positional args to pass to the IR simulator **kwargs: Keyword arguments to pass to the IR simulator @@ -73,7 +78,7 @@ def run( >>> device = LocalSimulator("default") >>> device.run(circuit, shots=1000) """ - result = _run_internal(task_specification, self._delegate, *args, **kwargs) + result = _run_internal(task_specification, self._delegate, shots, *args, **kwargs) return LocalQuantumTask(result) @staticmethod @@ -114,16 +119,16 @@ def _run_internal( @_run_internal.register -def _(circuit: Circuit, simulator: BraketSimulator, shots: Optional[int] = None, *args, **kwargs): +def _(circuit: Circuit, simulator: BraketSimulator, shots, *args, **kwargs): + validate_circuit_and_shots(circuit, shots) program = circuit.to_ir() qubits = circuit.qubit_count - shots_count = shots if shots else LocalQuantumTask.DEFAULT_SHOTS - results_dict = simulator.run(program, qubits, shots=shots_count, *args, **kwargs) + results_dict = simulator.run(program, qubits, shots, *args, **kwargs) return GateModelQuantumTaskResult.from_dict(results_dict) @_run_internal.register -def _(problem: Problem, simulator: BraketSimulator, shots: Optional[int] = None, *args, **kwargs): +def _(problem: Problem, simulator: BraketSimulator, shots, *args, **kwargs): ir = problem.to_ir() - results_dict = simulator.run(ir, *args, *kwargs) + results_dict = simulator.run(ir, shots, *args, *kwargs) return AnnealingQuantumTaskResult.from_dict(results_dict) diff --git a/src/braket/tasks/gate_model_quantum_task_result.py b/src/braket/tasks/gate_model_quantum_task_result.py index 3dbf62ecf..e548f1b73 100644 --- a/src/braket/tasks/gate_model_quantum_task_result.py +++ b/src/braket/tasks/gate_model_quantum_task_result.py @@ -15,9 +15,10 @@ import json from dataclasses import dataclass -from typing import Any, Counter, Dict +from typing import Any, Counter, Dict, List import numpy as np +from braket.circuits import ResultType @dataclass @@ -27,40 +28,78 @@ class GateModelQuantumTaskResult: to be initialized by a QuantumTask class. Args: - measurements (numpy.ndarray): 2d array - row is shot, column is qubit. - measurement_counts (Counter): A Counter of measurements. Key is the measurements + task_metadata (Dict[str, Any]): Dictionary of task metadata. task_metadata must have + keys 'Id', 'Shots', 'Ir', and 'IrType'. + result_types (List[Dict[str, Any]], optional): List of dictionaries where each dictionary + has two keys: 'Type' (the result type in IR JSON form) and + 'Value' (the result value for this result type). + Default is None. Currently only available when shots = 0. TODO: update + values (List[Any], optional): The values for result types requested in the circuit. + Default is None. Currently only available when shots = 0. TODO: update + measurements (numpy.ndarray, optional): 2d array - row is shot, column is qubit. + Default is None. Only available when shots > 0. The qubits in `measurements` + are the ones in `GateModelQuantumTaskResult.measured_qubits`. + measured_qubits (List[int], optional): The indices of the measured qubits. Default + is None. Only available when shots > 0. Indicates which qubits are in + `measurements`. + measurement_counts (Counter, optional): A Counter of measurements. Key is the measurements in a big endian binary string. Value is the number of times that measurement occurred. - measurement_probabilities (Dict[str, float]): A dictionary of probabilistic results. + Default is None. Only available when shots > 0. + measurement_probabilities (Dict[str, float], optional): + A dictionary of probabilistic results. Key is the measurements in a big endian binary string. Value is the probability the measurement occurred. - measurements_copied_from_device (bool): flag whether `measurements` were copied from device. - If false, `measurements` are calculated from device data. - measurement_counts_copied_from_device (bool): flag whether `measurement_counts` were copied - from device. If false, `measurement_counts` are calculated from device data. - measurement_probabilities_copied_from_device (bool): flag whether + Default is None. Only available when shots > 0. + measurements_copied_from_device (bool, optional): flag whether `measurements` + were copied from device. If false, `measurements` are calculated from device data. + Default is None. Only available when shots > 0. + measurement_counts_copied_from_device (bool, optional): flag whether `measurement_counts` + were copied from device. If False, `measurement_counts` are calculated from device data. + Default is None. Only available when shots > 0. + measurement_probabilities_copied_from_device (bool, optional): flag whether `measurement_probabilities` were copied from device. If false, - `measurement_probabilities` are calculated from device data. - task_metadata (Dict[str, Any]): Dictionary of task metadata. - state_vector (Dict[str, complex]): Dictionary where Key is state and Value is amplitude. + `measurement_probabilities` are calculated from device data. Default is None. + Only available when shots > 0. """ - measurements: np.ndarray - measurement_counts: Counter - measurement_probabilities: Dict[str, float] - measurements_copied_from_device: bool - measurement_counts_copied_from_device: bool - measurement_probabilities_copied_from_device: bool task_metadata: Dict[str, Any] - state_vector: Dict[str, complex] = None + result_types: List[Dict[str, str]] = None + values: List[Any] = None + measurements: np.ndarray = None + measured_qubits: List[int] = None + measurement_counts: Counter = None + measurement_probabilities: Dict[str, float] = None + measurements_copied_from_device: bool = None + measurement_counts_copied_from_device: bool = None + measurement_probabilities_copied_from_device: bool = None + + def get_value_by_result_type(self, result_type: ResultType) -> Any: + """ + Get value by result type. The result type must have already been + requested in the circuit sent to the device for this task result. + + Args: + result_type (ResultType): result type requested + + Returns: + Any: value of the result corresponding to the result type + + Raises: + ValueError: If result type not found in result. + Result types must be added to circuit before circuit is run on device. + """ + rt_json = result_type.to_ir().json() + for rt in self.result_types: + if rt_json == json.dumps(rt["Type"]): + return rt["Value"] + raise ValueError( + "Result type not found in result. " + + "Result types must be added to circuit before circuit is run on device." + ) def __eq__(self, other) -> bool: - if isinstance(other, GateModelQuantumTaskResult): - # __eq__ on numpy arrays results in an array of booleans and therefore can't use - # the default dataclass __eq__ implementation. Override equals to check if all - # elements in the array are equal. - self_fields = (self.task_metadata, self.state_vector) - other_fields = (other.task_metadata, other.state_vector) - return (self.measurements == other.measurements).all() and self_fields == other_fields + if isinstance(other, GateModelQuantumTaskResult) and self.task_metadata.get("Id"): + return self.task_metadata["Id"] == other.task_metadata["Id"] return NotImplemented @staticmethod @@ -143,11 +182,7 @@ def from_dict(result: Dict[str, Any]): Raises: ValueError: If neither "Measurements" nor "MeasurementProbabilities" is a key - in the result dict - - Note: - For the "StateVector" key, the value should be of type Dict[str, complex]; - each bitstring's amplitude is a Python complex number. + in the result dict """ return GateModelQuantumTaskResult._from_dict_internal(result) @@ -164,24 +199,33 @@ def from_string(result: str) -> GateModelQuantumTaskResult: Raises: ValueError: If neither "Measurements" nor "MeasurementProbabilities" is a key - in the result dict - - Note: - For the "StateVector" key, the value should be of type Dict[str, List[float, float]]; - each bitstring's amplitude is represented by a two-element list, with first element - the real component and second element the imaginary component. + in the result dict """ json_obj = json.loads(result) - state_vector = json_obj.get("StateVector", None) - if state_vector: - for state in state_vector: - state_vector[state] = complex(*state_vector[state]) + for result_type in json_obj.get("ResultTypes", []): + type = result_type["Type"]["type"] + if type == "probability": + result_type["Value"] = np.array(result_type["Value"]) + elif type == "statevector": + result_type["Value"] = np.array([complex(*value) for value in result_type["Value"]]) + elif type == "amplitude": + for state in result_type["Value"]: + result_type["Value"][state] = complex(*result_type["Value"][state]) return GateModelQuantumTaskResult._from_dict_internal(json_obj) @classmethod def _from_dict_internal(cls, result: Dict[str, Any]): + if result["TaskMetadata"]["Shots"] > 0: + return GateModelQuantumTaskResult._from_dict_internal_computational_basis_sampling( + result + ) + else: + return GateModelQuantumTaskResult._from_dict_internal_simulator_only(result) + + @classmethod + def _from_dict_internal_computational_basis_sampling(cls, result: Dict[str, Any]): task_metadata = result["TaskMetadata"] - state_vector = result.get("StateVector", None) + measured_qubits = result.get("MeasuredQubits") if "Measurements" in result: measurements = np.asarray(result["Measurements"], dtype=int) m_counts = GateModelQuantumTaskResult.measurement_counts_from_measurements(measurements) @@ -203,15 +247,23 @@ def _from_dict_internal(cls, result: Dict[str, Any]): m_probabilities_copied_from_device = True else: raise ValueError( - 'One of "Measurements" or "MeasurementProbabilities" must be in the results dict' + 'One of "Measurements" or "MeasurementProbabilities" must be in the result dict' ) + # TODO: add result types for shots > 0 after basis rotation instructions are added to IR return cls( - state_vector=state_vector, task_metadata=task_metadata, measurements=measurements, + measured_qubits=measured_qubits, measurement_counts=m_counts, measurement_probabilities=m_probs, measurements_copied_from_device=measurements_copied_from_device, measurement_counts_copied_from_device=m_counts_copied_from_device, measurement_probabilities_copied_from_device=m_probabilities_copied_from_device, ) + + @classmethod + def _from_dict_internal_simulator_only(cls, result: Dict[str, Any]): + task_metadata = result["TaskMetadata"] + result_types = result["ResultTypes"] + values = [rt["Value"] for rt in result_types] + return cls(task_metadata=task_metadata, result_types=result_types, values=values) diff --git a/src/braket/tasks/local_quantum_task.py b/src/braket/tasks/local_quantum_task.py index 2b6f2c6fd..653cd1541 100644 --- a/src/braket/tasks/local_quantum_task.py +++ b/src/braket/tasks/local_quantum_task.py @@ -12,7 +12,6 @@ # language governing permissions and limitations under the License. import asyncio -import uuid from typing import Union from braket.tasks import AnnealingQuantumTaskResult, GateModelQuantumTaskResult, QuantumTask @@ -25,7 +24,7 @@ class LocalQuantumTask(QuantumTask): """ def __init__(self, result: Union[GateModelQuantumTaskResult, AnnealingQuantumTaskResult]): - self._id = uuid.uuid4() + self._id = result.task_metadata["Id"] self._result = result @property diff --git a/src/braket/tasks/quantum_task.py b/src/braket/tasks/quantum_task.py index e1e8d7878..99b037519 100644 --- a/src/braket/tasks/quantum_task.py +++ b/src/braket/tasks/quantum_task.py @@ -22,8 +22,6 @@ class QuantumTask(ABC): """An abstraction over a quantum task on a quantum device.""" - DEFAULT_SHOTS = 1_000 - @property @abstractmethod def id(self) -> str: diff --git a/test/unit_tests/braket/aws/common_test_utils.py b/test/unit_tests/braket/aws/common_test_utils.py index 6530fea84..f3a197d24 100644 --- a/test/unit_tests/braket/aws/common_test_utils.py +++ b/test/unit_tests/braket/aws/common_test_utils.py @@ -181,8 +181,8 @@ class MockS3: "Id": "UUID_blah_1", "Status": "COMPLETED", "BackendArn": AwsQpuArns.RIGETTI, - "CwLogGroupArn": "blah", - "Program": "....", + "Shots": 1000, + "Ir": "{}", }, } ) @@ -195,8 +195,8 @@ class MockS3: "Id": "UUID_blah_2", "Status": "COMPLETED", "BackendArn": AwsQpuArns.RIGETTI, - "CwLogGroupArn": "blah", - "Program": "....", + "Shots": 1000, + "Ir": "{}", }, } ) @@ -212,6 +212,7 @@ class MockS3: "Modified": 1574140388.6908717, "Shots": 100, "GateModelConfig": {"QubitCount": 6}, + "Ir": "{}", }, "MeasurementProbabilities": {"011000": 0.9999999999999982}, } diff --git a/test/unit_tests/braket/aws/test_aws_qpu.py b/test/unit_tests/braket/aws/test_aws_qpu.py index be26496c2..1378bcb71 100644 --- a/test/unit_tests/braket/aws/test_aws_qpu.py +++ b/test/unit_tests/braket/aws/test_aws_qpu.py @@ -194,8 +194,13 @@ def test_run_with_kwargs(aws_quantum_task_mock, qpu, circuit, s3_destination_fol @patch("braket.aws.aws_quantum_task.AwsQuantumTask.create") def test_run_with_kwargs_no_shots(aws_quantum_task_mock, qpu, circuit, s3_destination_folder): - _run_and_assert( - aws_quantum_task_mock, qpu, circuit, s3_destination_folder, None, [], {"bar": 1, "baz": 2} + task_mock = Mock() + aws_quantum_task_mock.return_value = task_mock + qpu = qpu(AwsQpuArns.RIGETTI) + task = qpu.run(circuit, s3_destination_folder) + assert task == task_mock + aws_quantum_task_mock.assert_called_with( + qpu._aws_session, qpu.arn, circuit, s3_destination_folder, 1000 ) diff --git a/test/unit_tests/braket/aws/test_aws_quantum_simulator.py b/test/unit_tests/braket/aws/test_aws_quantum_simulator.py index ccd4f1924..45859b551 100644 --- a/test/unit_tests/braket/aws/test_aws_quantum_simulator.py +++ b/test/unit_tests/braket/aws/test_aws_quantum_simulator.py @@ -104,19 +104,6 @@ def test_run_with_positional_args(aws_quantum_task_mock, simulator, circuit, s3_ @patch("braket.aws.aws_quantum_task.AwsQuantumTask.create") def test_run_with_kwargs(aws_quantum_task_mock, simulator, circuit, s3_destination_folder): - _run_and_assert( - aws_quantum_task_mock, - simulator, - circuit, - s3_destination_folder, - None, - [], - {"bar": 1, "baz": 2}, - ) - - -@patch("braket.aws.aws_quantum_task.AwsQuantumTask.create") -def test_run_with_kwargs_no_shots(aws_quantum_task_mock, simulator, circuit, s3_destination_folder): _run_and_assert( aws_quantum_task_mock, simulator, diff --git a/test/unit_tests/braket/aws/test_aws_quantum_task.py b/test/unit_tests/braket/aws/test_aws_quantum_task.py index c7640d612..623b72cb1 100644 --- a/test/unit_tests/braket/aws/test_aws_quantum_task.py +++ b/test/unit_tests/braket/aws/test_aws_quantum_task.py @@ -237,21 +237,22 @@ def test_timeout_no_result_terminal_state(aws_session): @pytest.mark.xfail(raises=ValueError) def test_create_invalid_s3_folder(aws_session, arn, circuit): - AwsQuantumTask.create(aws_session, arn, circuit, ("bucket",)) + AwsQuantumTask.create(aws_session, arn, circuit, ("bucket",), 1000) @pytest.mark.xfail(raises=TypeError) -def test_create_invalid_task_specification(aws_session, arn, circuit): +def test_create_invalid_task_specification(aws_session, arn): mocked_task_arn = "task-arn-1" aws_session.create_quantum_task.return_value = mocked_task_arn - AwsQuantumTask.create(aws_session, arn, "foo", S3_TARGET) + AwsQuantumTask.create(aws_session, arn, "foo", S3_TARGET, 1000) -def test_from_circuit_default_shots(aws_session, arn, circuit): +def test_from_circuit_with_shots(aws_session, arn, circuit): mocked_task_arn = "task-arn-1" aws_session.create_quantum_task.return_value = mocked_task_arn + shots = 53 - task = AwsQuantumTask.create(aws_session, arn, circuit, S3_TARGET) + task = AwsQuantumTask.create(aws_session, arn, circuit, S3_TARGET, shots) assert task == AwsQuantumTask( mocked_task_arn, aws_session, GateModelQuantumTaskResult.from_string ) @@ -262,30 +263,16 @@ def test_from_circuit_default_shots(aws_session, arn, circuit): circuit, AwsQuantumTask.GATE_IR_TYPE, S3_TARGET, - AwsQuantumTask.DEFAULT_SHOTS, + shots, {"gateModelParameters": {"qubitCount": circuit.qubit_count}}, ) -def test_from_circuit_with_shots(aws_session, arn, circuit): +@pytest.mark.xfail(raises=ValueError) +def test_from_circuit_with_shots_value_error(aws_session, arn, circuit): mocked_task_arn = "task-arn-1" aws_session.create_quantum_task.return_value = mocked_task_arn - shots = 53 - - task = AwsQuantumTask.create(aws_session, arn, circuit, S3_TARGET, shots=shots) - assert task == AwsQuantumTask( - mocked_task_arn, aws_session, GateModelQuantumTaskResult.from_string - ) - - _assert_create_quantum_task_called_with( - aws_session, - arn, - circuit, - AwsQuantumTask.GATE_IR_TYPE, - S3_TARGET, - shots, - {"gateModelParameters": {"qubitCount": circuit.qubit_count}}, - ) + AwsQuantumTask.create(aws_session, arn, circuit, S3_TARGET, 0) def test_from_annealing(aws_session, arn, problem): @@ -297,6 +284,7 @@ def test_from_annealing(aws_session, arn, problem): arn, problem, S3_TARGET, + 1000, backend_parameters={"dWaveParameters": {"postprocessingType": "OPTIMIZATION"}}, ) assert task == AwsQuantumTask( @@ -309,7 +297,7 @@ def test_from_annealing(aws_session, arn, problem): problem, AwsQuantumTask.ANNEALING_IR_TYPE, S3_TARGET, - AwsQuantumTask.DEFAULT_SHOTS, + 1000, {"annealingModelParameters": {"dWaveParameters": {"postprocessingType": "OPTIMIZATION"}}}, ) diff --git a/test/unit_tests/braket/circuits/test_circuit_helpers.py b/test/unit_tests/braket/circuits/test_circuit_helpers.py new file mode 100644 index 000000000..2ff39be53 --- /dev/null +++ b/test/unit_tests/braket/circuits/test_circuit_helpers.py @@ -0,0 +1,43 @@ +# Copyright 2019-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file 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. + +import pytest +from braket.circuits import Circuit +from braket.circuits.circuit_helpers import validate_circuit_and_shots + + +@pytest.mark.xfail(raises=ValueError) +def test_validate_circuit_and_shots_0_no_results(): + validate_circuit_and_shots(Circuit().h(0), 0) + + +def test_validate_circuit_and_shots_100_no_results(): + assert validate_circuit_and_shots(Circuit().h(0), 100) is None + + +def test_validate_circuit_and_shots_0_results(): + assert validate_circuit_and_shots(Circuit().h(0).state_vector(), 0) is None + + +def test_validate_circuit_and_shots_100_results(): + assert validate_circuit_and_shots(Circuit().h(0).probability(), 100) is None + + +@pytest.mark.xfail(raises=ValueError) +def test_validate_circuit_and_shots_100_result_state_vector(): + validate_circuit_and_shots(Circuit().h(0).state_vector(), 100) + + +@pytest.mark.xfail(raises=ValueError) +def test_validate_circuit_and_shots_100_result_amplitude(): + validate_circuit_and_shots(Circuit().h(0).amplitude(state=["0"]), 100) diff --git a/test/unit_tests/braket/devices/test_local_simulator.py b/test/unit_tests/braket/devices/test_local_simulator.py index cd1c83dbe..adb6113a7 100644 --- a/test/unit_tests/braket/devices/test_local_simulator.py +++ b/test/unit_tests/braket/devices/test_local_simulator.py @@ -11,6 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +import json from typing import Any, Dict, Optional from unittest.mock import Mock @@ -23,14 +24,13 @@ from braket.tasks import AnnealingQuantumTaskResult, GateModelQuantumTaskResult GATE_MODEL_RESULT = { - "StateVector": { - "00": complex(0.2, 0.2), - "01": complex(0.3, 0.1), - "10": complex(0.1, 0.3), - "11": complex(0.2, 0.2), - }, "Measurements": [[0, 0], [0, 1], [0, 1], [0, 1]], - "TaskMetadata": {"Id": "UUID_blah_1", "Status": "COMPLETED"}, + "TaskMetadata": { + "Id": "UUID_blah_1", + "Status": "COMPLETED", + "Shots": 1000, + "Ir": json.dumps({"results": []}), + }, } ANNEALING_RESULT = { @@ -40,7 +40,7 @@ "SolutionCounts": None, "ProblemType": "ising", "Foo": {"Bar": "Baz"}, - "TaskMetadata": {"Id": "UUID_blah_1", "Status": "COMPLETED", "Shots": 5,}, + "TaskMetadata": {"Id": "UUID_blah_1", "Status": "COMPLETED", "Shots": 5}, } @@ -84,6 +84,13 @@ def test_run_gate_model(): assert task.result() == GateModelQuantumTaskResult.from_dict(GATE_MODEL_RESULT) +@pytest.mark.xfail(raises=ValueError) +def test_run_gate_model_value_error(): + dummy = DummyCircuitSimulator() + sim = LocalSimulator(dummy) + sim.run(Circuit().h(0).cnot(0, 1)) + + def test_run_annealing(): sim = LocalSimulator(DummyAnnealingSimulator()) task = sim.run(Problem(ProblemType.ISING)) diff --git a/test/unit_tests/braket/tasks/test_gate_model_quantum_task_result.py b/test/unit_tests/braket/tasks/test_gate_model_quantum_task_result.py index 98032a70e..0651696c1 100644 --- a/test/unit_tests/braket/tasks/test_gate_model_quantum_task_result.py +++ b/test/unit_tests/braket/tasks/test_gate_model_quantum_task_result.py @@ -11,58 +11,49 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +import copy import json from typing import Any, Counter, Dict import numpy as np import pytest from braket.aws.aws_qpu_arns import AwsQpuArns +from braket.circuits import Observable, ResultType from braket.tasks import GateModelQuantumTaskResult @pytest.fixture def result_dict_1(): return { - "StateVector": { - "00": complex(0.2, 0.2), - "01": complex(0.3, 0.1), - "10": complex(0.1, 0.3), - "11": complex(0.2, 0.2), - }, "Measurements": [[0, 0], [0, 1], [0, 1], [0, 1]], + "MeasuredQubits": [0, 1], "TaskMetadata": { "Id": "UUID_blah_1", "Status": "COMPLETED", "BackendArn": AwsQpuArns.RIGETTI, + "Shots": 1000, + "Ir": json.dumps({"results": []}), }, } @pytest.fixture -def result_str_1(): - return json.dumps( - { - "StateVector": {"00": [0.2, 0.2], "01": [0.3, 0.1], "10": [0.1, 0.3], "11": [0.2, 0.2]}, - "Measurements": [[0, 0], [0, 1], [0, 1], [0, 1]], - "TaskMetadata": { - "Id": "UUID_blah_1", - "Status": "COMPLETED", - "BackendArn": AwsQpuArns.RIGETTI, - }, - } - ) +def result_str_1(result_dict_1): + return json.dumps(result_dict_1) @pytest.fixture def result_str_2(): return json.dumps( { - "StateVector": {"00": [0.2, 0.2], "01": [0.3, 0.1], "10": [0.1, 0.3], "11": [0.2, 0.2]}, "Measurements": [[0, 0], [0, 0], [0, 0], [1, 1]], + "MeasuredQubits": [0, 1], "TaskMetadata": { "Id": "UUID_blah_2", "Status": "COMPLETED", "BackendArn": AwsQpuArns.RIGETTI, + "Shots": 1000, + "Ir": json.dumps({"results": []}), }, } ) @@ -81,22 +72,55 @@ def result_str_3(): "Modified": 1574140388.6908717, "Shots": 100, "GateModelConfig": {"QubitCount": 6}, + "Ir": json.dumps({"results": []}), }, "MeasurementProbabilities": {"011000": 0.9999999999999982}, + "MeasuredQubits": list(range(6)), } ) @pytest.fixture -def parsed_state_vector(): +def result_dict_4(): return { - "00": complex(0.2, 0.2), - "01": complex(0.3, 0.1), - "10": complex(0.1, 0.3), - "11": complex(0.2, 0.2), + "TaskMetadata": { + "Id": "1231231", + "Shots": 0, + "GateModelConfig": {"QubitCount": 2}, + "Ir": json.dumps({"results": []}), + }, + "ResultTypes": [ + {"Type": {"targets": [0], "type": "probability"}, "Value": np.array([0.5, 0.5])}, + { + "Type": {"type": "statevector"}, + "Value": np.array([complex(0.70710678, 0), 0, 0, complex(0.70710678, 0)]), + }, + {"Type": {"targets": [0], "type": "expectation", "observable": ["y"]}, "Value": 0.0}, + {"Type": {"targets": [0], "type": "variance", "observable": ["y"]}, "Value": 0.1}, + { + "Type": {"type": "amplitude", "states": ["00"]}, + "Value": {"00": complex(0.70710678, 0)}, + }, + ], } +@pytest.fixture +def result_str_4(result_dict_4): + result = copy.deepcopy(result_dict_4) + result["ResultTypes"] = [ + {"Type": {"targets": [0], "type": "probability"}, "Value": [0.5, 0.5]}, + { + "Type": {"type": "statevector"}, + "Value": [(0.70710678, 0), (0, 0), (0, 0), (0.70710678, 0)], + }, + {"Type": {"targets": [0], "type": "expectation", "observable": ["y"]}, "Value": 0.0}, + {"Type": {"targets": [0], "type": "variance", "observable": ["y"]}, "Value": 0.1}, + {"Type": {"type": "amplitude", "states": ["00"]}, "Value": {"00": (0.70710678, 0)}}, + ] + return json.dumps(result) + + @pytest.fixture def malformatted_results(): return { @@ -104,6 +128,8 @@ def malformatted_results(): "Id": "UUID_blah_1", "Status": "COMPLETED", "BackendArn": AwsQpuArns.RIGETTI, + "Shots": 1000, + "Ir": json.dumps({"results": []}), }, } @@ -143,20 +169,6 @@ def test_measurements_from_measurement_probabilities(): assert np.allclose(measurements, expected_results) -def test_state_vector(parsed_state_vector): - result: GateModelQuantumTaskResult = GateModelQuantumTaskResult( - measurements=None, - task_metadata=None, - state_vector=parsed_state_vector, - measurement_counts=None, - measurement_probabilities=None, - measurements_copied_from_device=False, - measurement_counts_copied_from_device=False, - measurement_probabilities_copied_from_device=False, - ) - assert result.state_vector == parsed_state_vector - - def test_task_metadata(): task_metadata: Dict[str, Any] = { "Id": "UUID_blah", @@ -179,15 +191,15 @@ def test_task_metadata(): assert result.task_metadata == task_metadata -def test_from_dict_measurements_state_vector(result_dict_1): +def test_from_dict_measurements(result_dict_1): task_result = GateModelQuantumTaskResult.from_dict(result_dict_1) expected_measurements = np.asarray(result_dict_1["Measurements"], dtype=int) assert task_result.task_metadata == result_dict_1["TaskMetadata"] - assert task_result.state_vector == result_dict_1["StateVector"] assert np.array2string(task_result.measurements) == np.array2string(expected_measurements) assert not task_result.measurement_counts_copied_from_device assert not task_result.measurement_probabilities_copied_from_device assert task_result.measurements_copied_from_device + assert task_result.measured_qubits == result_dict_1["MeasuredQubits"] def test_from_dict_measurement_probabilities(result_str_3): @@ -195,7 +207,6 @@ def test_from_dict_measurement_probabilities(result_str_3): task_result = GateModelQuantumTaskResult.from_dict(result_obj) assert task_result.measurement_probabilities == result_obj["MeasurementProbabilities"] assert task_result.task_metadata == result_obj["TaskMetadata"] - assert task_result.state_vector is None shots = 100 measurement_list = [list("011000") for x in range(shots)] expected_measurements = np.asarray(measurement_list, dtype=int) @@ -204,18 +215,19 @@ def test_from_dict_measurement_probabilities(result_str_3): assert not task_result.measurement_counts_copied_from_device assert task_result.measurement_probabilities_copied_from_device assert not task_result.measurements_copied_from_device + assert task_result.measured_qubits == result_obj["MeasuredQubits"] -def test_from_string_measurements_state_vector(result_str_1, parsed_state_vector): +def test_from_string_measurements(result_str_1): result_obj = json.loads(result_str_1) task_result = GateModelQuantumTaskResult.from_string(result_str_1) expected_measurements = np.asarray(result_obj["Measurements"], dtype=int) assert task_result.task_metadata == result_obj["TaskMetadata"] - assert task_result.state_vector == parsed_state_vector assert np.array2string(task_result.measurements) == np.array2string(expected_measurements) assert not task_result.measurement_counts_copied_from_device assert not task_result.measurement_probabilities_copied_from_device assert task_result.measurements_copied_from_device + assert task_result.measured_qubits == result_obj["MeasuredQubits"] def test_from_string_measurement_probabilities(result_str_3): @@ -223,7 +235,6 @@ def test_from_string_measurement_probabilities(result_str_3): task_result = GateModelQuantumTaskResult.from_string(result_str_3) assert task_result.measurement_probabilities == result_obj["MeasurementProbabilities"] assert task_result.task_metadata == result_obj["TaskMetadata"] - assert task_result.state_vector is None shots = 100 measurement_list = [list("011000") for x in range(shots)] expected_measurements = np.asarray(measurement_list, dtype=int) @@ -255,6 +266,43 @@ def test_equality(result_str_1, result_str_2): assert result_1 != non_result +def test_from_string_simulator_only(result_dict_4, result_str_4): + result = GateModelQuantumTaskResult.from_string(result_str_4) + assert len(result.result_types) == len(result_dict_4["ResultTypes"]) + for i in range(len(result.result_types)): + rt = result.result_types[i] + expected_rt = result_dict_4["ResultTypes"][i] + assert rt["Type"] == expected_rt["Type"] + if isinstance(rt["Value"], np.ndarray): + assert np.allclose(rt["Value"], expected_rt["Value"]) + else: + assert rt["Value"] == expected_rt["Value"] + assert result.task_metadata == result.task_metadata + + +def test_get_value_by_result_type(result_dict_4): + result = GateModelQuantumTaskResult.from_dict(result_dict_4) + assert np.allclose( + result.get_value_by_result_type(ResultType.Probability(target=0)), result.values[0] + ) + assert np.allclose(result.get_value_by_result_type(ResultType.StateVector()), result.values[1]) + assert ( + result.get_value_by_result_type(ResultType.Expectation(observable=Observable.Y(), target=0)) + == result.values[2] + ) + assert ( + result.get_value_by_result_type(ResultType.Variance(observable=Observable.Y(), target=0)) + == result.values[3] + ) + assert result.get_value_by_result_type(ResultType.Amplitude(state=["00"])) == result.values[4] + + +@pytest.mark.xfail(raises=ValueError) +def test_get_value_by_result_type_value_error(result_dict_4): + result = GateModelQuantumTaskResult.from_dict(result_dict_4) + result.get_value_by_result_type(ResultType.Probability(target=[0, 1])) + + @pytest.mark.xfail(raises=ValueError) def test_bad_dict(malformatted_results): GateModelQuantumTaskResult.from_dict(malformatted_results) diff --git a/test/unit_tests/braket/tasks/test_local_quantum_task.py b/test/unit_tests/braket/tasks/test_local_quantum_task.py index 58a171578..6af0b4c85 100644 --- a/test/unit_tests/braket/tasks/test_local_quantum_task.py +++ b/test/unit_tests/braket/tasks/test_local_quantum_task.py @@ -11,6 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +import json import uuid import pytest @@ -19,14 +20,13 @@ RESULT = GateModelQuantumTaskResult.from_dict( { - "StateVector": { - "00": complex(0.2, 0.2), - "01": complex(0.3, 0.1), - "10": complex(0.1, 0.3), - "11": complex(0.2, 0.2), - }, "Measurements": [[0, 0], [0, 1], [0, 1], [0, 1]], - "TaskMetadata": {"Id": "UUID_blah_1", "Status": "COMPLETED"}, + "TaskMetadata": { + "Id": str(uuid.uuid4()), + "Status": "COMPLETED", + "Shots": 2, + "Ir": json.dumps({"results": []}), + }, } ) @@ -43,6 +43,7 @@ def test_state(): def test_result(): + assert RESULT.task_metadata["Id"] == TASK.id assert TASK.result() == RESULT