diff --git a/src/braket/circuits/__init__.py b/src/braket/circuits/__init__.py index a5fb52980..a554cd4a0 100644 --- a/src/braket/circuits/__init__.py +++ b/src/braket/circuits/__init__.py @@ -22,6 +22,7 @@ from braket.circuits.angled_gate import AngledGate, DoubleAngledGate # noqa: F401 from braket.circuits.circuit import Circuit # noqa: F401 from braket.circuits.circuit_diagram import CircuitDiagram # noqa: F401 +from braket.circuits.circuit_pulse_sequence import CircuitPulseSequenceBuilder # noqa: F401 from braket.circuits.compiler_directive import CompilerDirective # noqa: F401 from braket.circuits.free_parameter import FreeParameter # noqa: F401 from braket.circuits.free_parameter_expression import FreeParameterExpression # noqa: F401 diff --git a/src/braket/circuits/circuit.py b/src/braket/circuits/circuit.py index 3f4918a1f..956a01b87 100644 --- a/src/braket/circuits/circuit.py +++ b/src/braket/circuits/circuit.py @@ -15,13 +15,14 @@ from collections.abc import Callable, Iterable from numbers import Number -from typing import Any, Optional, TypeVar, Union +from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union import numpy as np import oqpy from sympy import Expr from braket.circuits import compiler_directives +from braket.circuits.circuit_pulse_sequence import CircuitPulseSequenceBuilder from braket.circuits.free_parameter import FreeParameter from braket.circuits.free_parameter_expression import FreeParameterExpression from braket.circuits.gate import Gate @@ -63,6 +64,9 @@ from braket.registers.qubit import QubitInput from braket.registers.qubit_set import QubitSet, QubitSetInput +if TYPE_CHECKING: # pragma: no cover + from braket.aws import AwsDevice + SubroutineReturn = TypeVar( "SubroutineReturn", Iterable[Instruction], Instruction, ResultType, Iterable[ResultType] ) @@ -1098,6 +1102,26 @@ def diagram(self, circuit_diagram_class: type = UnicodeCircuitDiagram) -> str: """ return circuit_diagram_class.build_diagram(self) + def pulse_sequence( + self, + device: AwsDevice, + gate_definitions: dict[tuple[Gate, QubitSet], PulseSequence] | None = None, + pulse_sequence_builder_class: type = CircuitPulseSequenceBuilder, + ) -> PulseSequence: + """Get the associated pulse sequence for the current circuit. + + Args: + device (AwsDevice): an AWS device. + gate_definitions (dict[tuple[Gate, QubitSet], PulseSequence] | None): Additional + gate definitions. + pulse_sequence_builder_class (type): A `CircuitPulseSequenceBuilder` class that builds + the pulse sequence for this circuit. Default = `CircuitPulseSequenceBuilder`. + + Returns: + PulseSequence: A PulseSequence corresponding to the full circuit. + """ + return pulse_sequence_builder_class(device, gate_definitions).build_pulse_sequence(self) + def to_ir( self, ir_type: IRType = IRType.JAQCD, diff --git a/src/braket/circuits/circuit_pulse_sequence.py b/src/braket/circuits/circuit_pulse_sequence.py new file mode 100644 index 000000000..8ea2424f5 --- /dev/null +++ b/src/braket/circuits/circuit_pulse_sequence.py @@ -0,0 +1,168 @@ +# Copyright 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 __future__ import annotations + +import warnings +from typing import TYPE_CHECKING + +if TYPE_CHECKING: # pragma: no cover + from braket.aws.aws_device import AwsDevice + +import braket.circuits.circuit as cir +from braket.circuits.gate import Gate +from braket.circuits.gate_calibrations import GateCalibrations +from braket.circuits.qubit_set import QubitSet +from braket.circuits.result_type import ResultType +from braket.parametric.free_parameter import FreeParameter +from braket.pulse.frame import Frame +from braket.pulse.pulse_sequence import PulseSequence + + +class CircuitPulseSequenceBuilder: + """Builds a pulse sequence from circuits.""" + + def __init__( + self, + device: AwsDevice, + gate_definitions: dict[tuple[Gate, QubitSet], PulseSequence] | None = None, + ) -> None: + _validate_device(device) + gate_definitions = gate_definitions or {} + + self._device = device + self._gate_calibrations = ( + device.gate_calibrations + if device.gate_calibrations is not None + else GateCalibrations({}) + ) + self._gate_calibrations.pulse_sequences.update(gate_definitions) + + def build_pulse_sequence(self, circuit: cir.Circuit) -> PulseSequence: + """ + Build a PulseSequence corresponding to the full circuit. + + Args: + circuit (Circuit): Circuit for which to build a diagram. + + Returns: + PulseSequence: a pulse sequence created from all gates. + """ + + pulse_sequence = PulseSequence() + if not circuit.instructions: + return pulse_sequence + + if circuit.parameters: + raise ValueError("All parameters must be assigned to draw the pulse sequence.") + + for instruction in circuit.instructions: + gate = instruction.operator + qubits = instruction.target + + gate_pulse_sequence = self._get_pulse_sequence(gate, qubits) + + # FIXME: this creates a single cal block, "barrier;" in defcal could be either + # global or restricted to the defcal context + # Right now they are global + pulse_sequence += gate_pulse_sequence + + # Result type columns + target_result_types = CircuitPulseSequenceBuilder._categorize_result_types( + circuit.result_types + ) + + for result_type in target_result_types: + pulse_sequence += result_type._to_pulse_sequence() + + for qubit in circuit.qubits: + pulse_sequence.capture_v0(self._readout_frame(qubit)) + + return pulse_sequence + + def _get_pulse_sequence(self, gate: Gate, qubit: QubitSet) -> PulseSequence: + parameters = gate.parameters if hasattr(gate, "parameters") else [] + if isinstance(gate, Gate) and gate.name == "PulseGate": + gate_pulse_sequence = gate.pulse_sequence + elif ( + gate_pulse_sequence := self._gate_calibrations.pulse_sequences.get((gate, qubit), None) + ) is None: + if ( + not hasattr(gate, "parameters") + or ( + gate_pulse_sequence := self._find_parametric_gate_calibration( + gate, qubit, len(gate.parameters) + ) + ) + is None + ): + parameter_str = ", ".join(str(p) for p in parameters) + qubit_str = ", ".join(str(int(q)) for q in qubit) + raise ValueError( + f"No pulse sequence for {gate.name}({parameter_str}) on qubit {qubit_str} was" + " provided in the gate calibration set." + ) + + return gate_pulse_sequence( + **{p.name: v for p, v in zip(gate_pulse_sequence.parameters, parameters)} + ) + + def _find_parametric_gate_calibration( + self, gate: Gate, qubitset: QubitSet, number_assigned_values: int + ) -> PulseSequence | None: + for key in self._gate_calibrations.pulse_sequences: + if ( + key[0].name == gate.name + and key[1] == qubitset + and sum(isinstance(param, FreeParameter) for param in key[0].parameters) + == number_assigned_values + ): + return self._gate_calibrations.pulse_sequences[key] + + def _readout_frame(self, qubit: QubitSet) -> Frame: + readout_frame_names = { + "Rigetti": f"q{int(qubit)}_ro_rx_frame", + "Oxford": f"r{int(qubit)}_measure", + } + frame_name = readout_frame_names[self._device.provider_name] + return self._device.frames[frame_name] + + @staticmethod + def _categorize_result_types( + result_types: list[ResultType], + ) -> list[ResultType]: + """ + Categorize result types into result types with target and those without. + + Args: + result_types (list[ResultType]): list of result types + + Returns: + list[ResultType]: a list of result types with `target` attribute + """ + target_result_types = [] + for result_type in result_types: + if hasattr(result_type, "target"): + target_result_types.append(result_type) + else: + warnings.warn( + f"{result_type} does not have have a pulse representation" " and it is ignored." + ) + return target_result_types + + +def _validate_device(device: AwsDevice | None) -> None: + if device is None: + raise ValueError("Device must be set before building pulse sequences.") + elif device.provider_name not in ("Rigetti", "Oxford"): + raise ValueError(f"Device {device.name} is not supported.") diff --git a/src/braket/circuits/observable.py b/src/braket/circuits/observable.py index d3f3fc862..d674e669a 100644 --- a/src/braket/circuits/observable.py +++ b/src/braket/circuits/observable.py @@ -27,6 +27,7 @@ OpenQASMSerializationProperties, SerializationProperties, ) +from braket.pulse.pulse_sequence import PulseSequence from braket.registers.qubit_set import QubitSet @@ -105,6 +106,17 @@ def _to_openqasm( """ raise NotImplementedError("to_openqasm has not been implemented yet.") + def _to_pulse_sequence(self, target: QubitSet | None = None) -> PulseSequence: + """Returns the pulse sequence of the result type. + + Args: + target (QubitSet | None): target qubit(s). Defaults to None. + + Returns: + PulseSequence: A PulseSequence of the basis rotation for the corresponding observable. + """ + raise NotImplementedError("_to_pulse_sequence has not been implemented yet.") + @property def coefficient(self) -> int: """The coefficient of the observable. diff --git a/src/braket/circuits/observables.py b/src/braket/circuits/observables.py index 2d5ccdb68..b95db2200 100644 --- a/src/braket/circuits/observables.py +++ b/src/braket/circuits/observables.py @@ -30,6 +30,7 @@ verify_quantum_operator_matrix_dimensions, ) from braket.circuits.serialization import IRType, OpenQASMSerializationProperties +from braket.pulse.pulse_sequence import PulseSequence from braket.registers.qubit_set import QubitSet @@ -65,6 +66,9 @@ def to_matrix(self) -> np.ndarray: 1.0 / np.sqrt(2.0) * np.array([[1.0, 1.0], [1.0, -1.0]], dtype=complex) ) + def _to_pulse_sequence(self, target: QubitSet | None = None) -> PulseSequence: + raise NotImplementedError("_to_pulse_sequence has not been implemented yet.") + @property def basis_rotation_gates(self) -> tuple[Gate, ...]: return tuple([Gate.Ry(-math.pi / 4)]) # noqa: C409 @@ -103,6 +107,9 @@ def _to_openqasm( def to_matrix(self) -> np.ndarray: return self.coefficient * np.eye(2, dtype=complex) + def _to_pulse_sequence(self, target: QubitSet | None = None) -> PulseSequence: + raise NotImplementedError("_to_pulse_sequence has not been implemented yet.") + @property def basis_rotation_gates(self) -> tuple[Gate, ...]: return () @@ -150,6 +157,9 @@ def _to_openqasm( else: return f"{coef_prefix}x all" + def _to_pulse_sequence(self, target: QubitSet | None = None) -> PulseSequence: + raise NotImplementedError("_to_pulse_sequence has not been implemented yet.") + def to_matrix(self) -> np.ndarray: return self.coefficient * np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex) @@ -191,6 +201,9 @@ def _to_openqasm( def to_matrix(self) -> np.ndarray: return self.coefficient * np.array([[0.0, -1.0j], [1.0j, 0.0]], dtype=complex) + def _to_pulse_sequence(self, target: QubitSet | None = None) -> PulseSequence: + raise NotImplementedError("_to_pulse_sequence has not been implemented yet.") + @property def basis_rotation_gates(self) -> tuple[Gate, ...]: return tuple([Gate.Z(), Gate.S(), Gate.H()]) # noqa: C409 @@ -229,6 +242,9 @@ def _to_openqasm( def to_matrix(self) -> np.ndarray: return self.coefficient * np.array([[1.0, 0.0], [0.0, -1.0]], dtype=complex) + def _to_pulse_sequence(self, target: QubitSet | None = None) -> PulseSequence: + raise NotImplementedError("_to_pulse_sequence has not been implemented yet.") + @property def basis_rotation_gates(self) -> tuple[Gate, ...]: return () diff --git a/src/braket/circuits/result_type.py b/src/braket/circuits/result_type.py index b66d4da67..805a63ded 100644 --- a/src/braket/circuits/result_type.py +++ b/src/braket/circuits/result_type.py @@ -23,6 +23,7 @@ OpenQASMSerializationProperties, SerializationProperties, ) +from braket.pulse.pulse_sequence import PulseSequence from braket.registers.qubit import QubitInput from braket.registers.qubit_set import QubitSet, QubitSetInput @@ -117,6 +118,14 @@ def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties """ raise NotImplementedError("to_openqasm has not been implemented yet.") + def _to_pulse_sequence(self) -> PulseSequence: + """Returns the pulse sequence of the result type. + + Returns: + PulseSequence: A PulseSequence corresponding to the result type. + """ + raise NotImplementedError("_to_pulse_sequence has not been implemented yet.") + def copy( self, target_mapping: dict[QubitInput, QubitInput] | None = None, diff --git a/src/braket/circuits/result_types.py b/src/braket/circuits/result_types.py index 0b73c5e7b..796aa1113 100644 --- a/src/braket/circuits/result_types.py +++ b/src/braket/circuits/result_types.py @@ -28,6 +28,7 @@ ResultType, ) from braket.circuits.serialization import IRType, OpenQASMSerializationProperties +from braket.pulse.pulse_sequence import PulseSequence from braket.registers.qubit_set import QubitSet, QubitSetInput """ @@ -492,6 +493,9 @@ def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties ) return f"#pragma braket result expectation {observable_ir}" + def _to_pulse_sequence(self) -> PulseSequence: + return self.observable._to_pulse_sequence(target=self.target) + @staticmethod @circuit.subroutine(register=True) def expectation(observable: Observable, target: QubitSetInput | None = None) -> ResultType: diff --git a/src/braket/pulse/pulse_sequence.py b/src/braket/pulse/pulse_sequence.py index a788b38c0..dfcf6e98c 100644 --- a/src/braket/pulse/pulse_sequence.py +++ b/src/braket/pulse/pulse_sequence.py @@ -426,6 +426,37 @@ def __call__( param_values[str(key)] = val return self.make_bound_pulse_sequence(param_values) + def __iadd__(self, other: PulseSequence) -> PulseSequence: + """In-place concatenation of pulse sequence.""" + if self._capture_v0_count: + raise ValueError( + "Cannot add a pulse sequence to another one that has already measurement" + " instructions." + ) + for frameId, frame in other._frames.items(): + _validate_uniqueness(self._frames, frame) + self._frames[frameId] = frame + for waveformId, waveform in other._waveforms.items(): + _validate_uniqueness(self._waveforms, waveform) + self._waveforms[waveformId] = waveform + for fp in other._free_parameters: + if fp in self._free_parameters: + raise ValueError( + f"A free parameter with the name {fp.name} already exists in the pulse" + " sequence. Please rename this free parameter." + ) + self._free_parameters.add(fp) + + self._program += other._program + return self + + def __add__(self, other: PulseSequence) -> PulseSequence: + """Return concatenation of two programs.""" + assert isinstance(other, PulseSequence) + self_copy = deepcopy(self) + self_copy += other + return self_copy + def __eq__(self, other: PulseSequence): sort_input_parameters = True return ( diff --git a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py new file mode 100644 index 000000000..e8dfa75cd --- /dev/null +++ b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py @@ -0,0 +1,715 @@ +# Copyright 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 io +import json +from unittest.mock import Mock, patch + +import numpy as np +import pytest + +from braket.aws import AwsDevice +from braket.circuits import Circuit, Gate, Observable, QubitSet +from braket.circuits.circuit_pulse_sequence import CircuitPulseSequenceBuilder +from braket.circuits.gate_calibrations import GateCalibrations +from braket.device_schema.ionq import IonqDeviceCapabilities +from braket.device_schema.oqc import OqcDeviceCapabilities +from braket.device_schema.rigetti import RigettiDeviceCapabilities +from braket.parametric.free_parameter import FreeParameter +from braket.pulse.frame import Frame +from braket.pulse.port import Port +from braket.pulse.pulse_sequence import PulseSequence +from braket.pulse.waveforms import DragGaussianWaveform + +RIGETTI_ARN = "arn:aws:braket:::device/qpu/rigetti/Aspen-M-3" +OQC_ARN = "arn:aws:braket:::device/qpu/oqc/Lucy" +IONQ_ARN = "arn:aws:braket:::device/qpu/ionq/Harmony" + +MOCK_RIGETTI_PULSE_CAPABILITIES_JSON = { + "braketSchemaHeader": { + "name": "braket.device_schema.pulse.pulse_device_action_properties", + "version": "1", + }, + "supportedQhpTemplateWaveforms": {}, + "supportedFunctions": {}, + "ports": { + "q0_ff": { + "portId": "q0_ff", + "direction": "tx", + "portType": "ff", + "dt": 1e-09, + "qubitMappings": None, + "centerFrequencies": [375000000.0], + "qhpSpecificProperties": None, + }, + "q0_rf": { + "portId": "q0_rf", + "direction": "tx", + "portType": "rf", + "dt": 1e-09, + "qubitMappings": None, + "centerFrequencies": [4875000000.0], + "qhpSpecificProperties": None, + }, + "q0_ro_rx": { + "portId": "q0_ro_rx", + "direction": "rx", + "portType": "ro_rx", + "dt": 5e-10, + "qubitMappings": None, + "centerFrequencies": [], + "qhpSpecificProperties": None, + }, + "q1_ro_rx": { + "portId": "q1_ro_rx", + "direction": "rx", + "portType": "ro_rx", + "dt": 5e-10, + "qubitMappings": None, + "centerFrequencies": [], + "qhpSpecificProperties": None, + }, + }, + "frames": { + "q0_q1_cphase_frame": { + "frameId": "q0_q1_cphase_frame", + "portId": "q0_ff", + "frequency": 4276236.85736918, + "centerFrequency": 375000000.0, + "phase": 1.0, + "associatedGate": "cphase", + "qubitMappings": [0, 1], + "qhpSpecificProperties": None, + }, + "q0_rf_frame": { + "frameId": "q0_rf_frame", + "portId": "q0_rf", + "frequency": 5041233612.58213, + "centerFrequency": 4875000000.0, + "phase": 0.0, + "associatedGate": None, + "qubitMappings": [0], + "qhpSpecificProperties": None, + }, + "q0_ro_rx_frame": { + "frameId": "q0_ro_rx_frame", + "portId": "q0_ro_rx", + "frequency": 7359200001.83969, + "centerFrequency": None, + "phase": 0.0, + "associatedGate": None, + "qubitMappings": [0], + "qhpSpecificProperties": None, + }, + "q1_ro_rx_frame": { + "frameId": "q1_ro_rx_frame", + "portId": "q1_ro_rx", + "frequency": 7228259838.38781, + "centerFrequency": None, + "phase": 0.0, + "associatedGate": None, + "qubitMappings": [1], + "qhpSpecificProperties": None, + }, + }, + "nativeGateCalibrationsRef": "file://hostname/foo/bar", +} + +MOCK_OQC_PULSE_CAPABILITIES_JSON = { + "braketSchemaHeader": { + "name": "braket.device_schema.pulse.pulse_device_action_properties", + "version": "1", + }, + "supportedQhpTemplateWaveforms": {}, + "supportedFunctions": {}, + "ports": { + "channel_14": { + "portId": "channel_14", + "direction": "rx", + "portType": "port_type_1", + "dt": 5e-10, + "qubitMappings": [1], + "centerFrequencies": None, + "qhpSpecificProperties": None, + } + }, + "frames": { + "r1_measure": { + "frameId": "r1_measure", + "portId": "channel_14", + "frequency": 9624889855.38831, + "centerFrequency": 9000000000.0, + "phase": 0.0, + "associatedGate": None, + "qubitMappings": [1], + "qhpSpecificProperties": None, + } + }, +} + + +def get_rigetti_pulse_model(capabilities_json): + device_json = { + "braketSchemaHeader": { + "name": "braket.device_schema.rigetti.rigetti_device_capabilities", + "version": "1", + }, + "service": { + "executionWindows": [], + "shotsRange": [1, 10], + }, + "provider": {"specs": {}}, + "action": { + "braket.ir.jaqcd.program": { + "actionType": "braket.ir.jaqcd.program", + "version": ["1"], + "supportedOperations": [], + } + }, + "paradigm": { + "qubitCount": 1, + "nativeGateSet": [], + "connectivity": {"fullyConnected": False, "connectivityGraph": {}}, + }, + "deviceParameters": {}, + "pulse": capabilities_json, + } + device_obj = RigettiDeviceCapabilities.parse_obj(device_json) + return { + "deviceName": "Aspen-M-3", + "deviceType": "QPU", + "providerName": "Rigetti", + "deviceStatus": "OFFLINE", + "deviceCapabilities": device_obj.json(), + } + + +def get_oqc_pulse_model(capabilities_json): + device_json = { + "braketSchemaHeader": { + "name": "braket.device_schema.oqc.oqc_device_capabilities", + "version": "1", + }, + "service": { + "executionWindows": [], + "shotsRange": [1, 10], + }, + "action": { + "braket.ir.jaqcd.program": { + "actionType": "braket.ir.jaqcd.program", + "version": ["1"], + "supportedOperations": [], + } + }, + "paradigm": { + "qubitCount": 30, + "nativeGateSet": [], + "connectivity": {"fullyConnected": False, "connectivityGraph": {}}, + }, + "deviceParameters": {}, + "pulse": capabilities_json, + } + device_obj = OqcDeviceCapabilities.parse_obj(device_json) + return { + "deviceName": "Lucy", + "deviceType": "QPU", + "providerName": "Oxford", + "deviceStatus": "OFFLINE", + "deviceCapabilities": device_obj.json(), + } + + +def get_ionq_model(): + device_json = { + "braketSchemaHeader": { + "name": "braket.device_schema.ionq.ionq_device_capabilities", + "version": "1", + }, + "service": { + "executionWindows": [], + "shotsRange": [1, 10], + }, + "action": { + "braket.ir.jaqcd.program": { + "actionType": "braket.ir.jaqcd.program", + "version": ["1"], + "supportedOperations": [], + } + }, + "paradigm": { + "qubitCount": 30, + "nativeGateSet": [], + "connectivity": {"fullyConnected": True, "connectivityGraph": {}}, + }, + "deviceParameters": {}, + } + device_obj = IonqDeviceCapabilities.parse_obj(device_json) + return { + "deviceName": "IonqDevice", + "deviceType": "QPU", + "providerName": "IonQ", + "deviceStatus": "OFFLINE", + "deviceCapabilities": device_obj.json(), + } + + +@pytest.fixture +def rigetti_device(): + response_data_content = { + "gates": { + "0_1": { + "cphaseshift": [ + { + "name": "cphaseshift", + "qubits": ["0", "1"], + "arguments": ["theta"], + "calibrations": [ + { + "name": "play", + "arguments": [ + { + "name": "waveform", + "value": "q0_q1_cphase_sqrtCPHASE", + "type": "waveform", + }, + { + "name": "frame", + "value": "q0_q1_cphase_frame", + "type": "frame", + }, + ], + }, + { + "name": "shift_phase", + "arguments": [ + { + "name": "frame", + "value": "q0_rf_frame", + "type": "frame", + "optional": False, + }, + { + "name": "phase", + "value": "-1.0*theta", + "type": "expr", + "optional": False, + }, + ], + }, + ], + } + ], + }, + "0": { + "rx": [ + { + "name": "rx", + "qubits": ["0"], + "arguments": ["1.5707963267948966"], + "calibrations": [ + { + "name": "barrier", + "arguments": [ + { + "name": "qubit", + "value": "0", + "type": "string", + "optional": False, + } + ], + }, + { + "name": "shift_frequency", + "arguments": [ + { + "name": "frame", + "value": "q0_rf_frame", + "type": "frame", + "optional": False, + }, + { + "name": "frequency", + "value": -321047.14178613486, + "type": "float", + "optional": False, + }, + ], + }, + { + "name": "play", + "arguments": [ + { + "name": "frame", + "value": "q0_rf_frame", + "type": "frame", + "optional": False, + }, + { + "name": "waveform", + "value": "wf_drag_gaussian_1", + "type": "waveform", + "optional": False, + }, + ], + }, + { + "name": "shift_frequency", + "arguments": [ + { + "name": "frame", + "value": "q0_rf_frame", + "type": "frame", + "optional": False, + }, + { + "name": "frequency", + "value": 321047.14178613486, + "type": "float", + "optional": False, + }, + ], + }, + { + "name": "barrier", + "arguments": [ + { + "name": "qubit", + "value": "0", + "type": "string", + "optional": False, + } + ], + }, + ], + }, + ], + "rz": [ + { + "name": "rz", + "qubits": ["0"], + "arguments": ["theta"], + "calibrations": [ + { + "name": "shift_phase", + "arguments": [ + { + "name": "frame", + "value": "q0_rf_frame", + "type": "frame", + "optional": False, + }, + { + "name": "phase", + "value": "-1.0*theta", + "type": "expr", + "optional": False, + }, + ], + }, + ], + }, + ], + }, + }, + "waveforms": { + "wf_drag_gaussian_1": { + "waveformId": "wf_drag_gaussian_1", + "name": "drag_gaussian", + "arguments": [ + {"name": "length", "value": 6.000000000000001e-8, "type": "float"}, + {"name": "sigma", "value": 6.369913502160144e-9, "type": "float"}, + {"name": "amplitude", "value": -0.4549282253548838, "type": "float"}, + {"name": "beta", "value": 7.494904522022295e-10, "type": "float"}, + ], + }, + "q0_q1_cphase_sqrtCPHASE": { + "waveformId": "q0_q1_cphase_sqrtCPHASE", + "amplitudes": [ + [0.0, 0.0], + [0.0, 0.0], + [0.0, 0.0], + [0.0, 0.0], + ], + }, + }, + } + + response_data_stream = io.BytesIO(json.dumps(response_data_content).encode("utf-8")) + with patch("urllib.request.urlopen") as mock_url_request: + mock_url_request.return_value.__enter__.return_value = response_data_stream + mock_session = Mock() + mock_session.get_device.return_value = get_rigetti_pulse_model( + MOCK_RIGETTI_PULSE_CAPABILITIES_JSON + ) + yield AwsDevice(RIGETTI_ARN, mock_session) + + +@pytest.fixture +def oqc_device(): + response_data_content = {"gates": {}, "waveforms": {}} + response_data_stream = io.BytesIO(json.dumps(response_data_content).encode("utf-8")) + with patch("urllib.request.urlopen") as mock_url_request: + mock_url_request.return_value.__enter__.return_value = response_data_stream + mock_session = Mock() + mock_session.get_device.return_value = get_oqc_pulse_model(MOCK_OQC_PULSE_CAPABILITIES_JSON) + yield AwsDevice(OQC_ARN, mock_session) + + +@pytest.fixture +def not_supported_device(): + response_data_content = {"gates": {}, "waveforms": {}} + response_data_stream = io.BytesIO(json.dumps(response_data_content).encode("utf-8")) + with patch("urllib.request.urlopen") as mock_url_request: + mock_url_request.return_value.__enter__.return_value = response_data_stream + mock_session = Mock() + mock_session.get_device.return_value = get_ionq_model() + yield AwsDevice(IONQ_ARN, mock_session) + + +@pytest.fixture +def port(): + return Port(port_id="device_port_x0", dt=1e-9, properties={}) + + +@pytest.fixture +def frame(port): + return Frame(frame_id="frame", frequency=2e9, port=port, phase=0, is_predefined=False) + + +@pytest.fixture +def pulse_sequence(frame): + return ( + PulseSequence() + .set_frequency( + frame, + 6e6, + ) + .play( + frame, + DragGaussianWaveform(length=3e-3, sigma=0.4, beta=0.2, id="drag_gauss_wf"), + ) + ) + + +@pytest.fixture() +def user_defined_gate_calibrations(pulse_sequence): + calibration_key = (Gate.Z(), QubitSet([1])) + return GateCalibrations( + { + calibration_key: pulse_sequence, + } + ) + + +def test_empty_circuit(rigetti_device): + assert ( + CircuitPulseSequenceBuilder(rigetti_device).build_pulse_sequence(Circuit()) + == PulseSequence() + ) + + +def test_no_device(): + with pytest.raises(ValueError, match="Device must be set before building pulse sequences."): + CircuitPulseSequenceBuilder(None) + + +def test_not_supported_device(not_supported_device): + with pytest.raises(ValueError, match=f"Device {not_supported_device.name} is not supported."): + CircuitPulseSequenceBuilder(not_supported_device) + + +def test_unbound_circuit(rigetti_device): + circuit = Circuit().rz(0, FreeParameter("theta")) + with pytest.raises( + ValueError, match="All parameters must be assigned to draw the pulse sequence." + ): + circuit.pulse_sequence(rigetti_device) + + +def test_non_parametric_defcal(rigetti_device): + circ = Circuit().rx(0, np.pi / 2) + expected = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + " bit[1] psb;", + " waveform wf_drag_gaussian_1 = drag_gaussian(60.0ns, 6.36991350216ns, " + "7.494904522022295e-10, -0.4549282253548838, false);", + " barrier $0;", + " shift_frequency(q0_rf_frame, -321047.14178613486);", + " play(q0_rf_frame, wf_drag_gaussian_1);", + " shift_frequency(q0_rf_frame, 321047.14178613486);", + " barrier $0;", + " psb[0] = capture_v0(q0_ro_rx_frame);", + "}", + ] + ) + assert circ.pulse_sequence(rigetti_device).to_ir() == expected + + +def test_user_defined_gate_calibrations_extension(rigetti_device, user_defined_gate_calibrations): + circ = Circuit().z(1) + expected = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + " bit[1] psb;", + " frame frame = newframe(device_port_x0, 2000000000.0, 0);", + " waveform drag_gauss_wf = drag_gaussian(3.0ms, 400.0ms, 0.2, 1, false);", + " set_frequency(frame, 6000000.0);", + " play(frame, drag_gauss_wf);", + " psb[0] = capture_v0(q1_ro_rx_frame);", + "}", + ] + ) + assert ( + circ.pulse_sequence(rigetti_device, user_defined_gate_calibrations.pulse_sequences).to_ir() + == expected + ) + + +def test_with_oxford_device(oqc_device, user_defined_gate_calibrations): + circ = Circuit().z(1) + expected = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + " bit[1] psb;", + " frame frame = newframe(device_port_x0, 2000000000.0, 0);", + " waveform drag_gauss_wf = drag_gaussian(3.0ms, 400.0ms, 0.2, 1, false);", + " set_frequency(frame, 6000000.0);", + " play(frame, drag_gauss_wf);", + " psb[0] = capture_v0(r1_measure);", + "}", + ] + ) + assert ( + circ.pulse_sequence(oqc_device, user_defined_gate_calibrations.pulse_sequences).to_ir() + == expected + ) + + +def test_parametric_defcal(rigetti_device): + circ = Circuit().cphaseshift(0, 1, 0.1) + expected = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + " bit[2] psb;", + " waveform q0_q1_cphase_sqrtCPHASE = {0.0, 0.0, 0.0, 0.0};", + " play(q0_q1_cphase_frame, q0_q1_cphase_sqrtCPHASE);", + " shift_phase(q0_rf_frame, -0.1);", + " psb[0] = capture_v0(q0_ro_rx_frame);", + " psb[1] = capture_v0(q1_ro_rx_frame);", + "}", + ] + ) + + assert circ.pulse_sequence(rigetti_device).to_ir() == expected + + +def test_pulse_gate(rigetti_device): + pulse_sequence = PulseSequence().set_frequency(rigetti_device.frames["q0_rf_frame"], 1e6) + circ = Circuit().pulse_gate(0, pulse_sequence) + expected = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + " bit[1] psb;", + " set_frequency(q0_rf_frame, 1000000.0);", + " psb[0] = capture_v0(q0_ro_rx_frame);", + "}", + ] + ) + + assert circ.pulse_sequence(rigetti_device).to_ir() == expected + + +def test_missing_calibration(rigetti_device): + circuit = Circuit().rz(1, 0.1) + with pytest.raises( + ValueError, + match=r"No pulse sequence for Rz\(0.1\) on qubit 1 was provided" + " in the gate calibration set.", + ): + circuit.pulse_sequence(rigetti_device) + + +def test_expectation_value_result_type_on_one_qubit(rigetti_device): + circ = Circuit().cphaseshift(0, 1, 0.1).expectation(observable=Observable.X(), target=[1]) + expected = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + " bit[2] psb;", + " waveform q0_q1_cphase_sqrtCPHASE = {0.0, 0.0, 0.0, 0.0};", + " play(q0_q1_cphase_frame, q0_q1_cphase_sqrtCPHASE);", + " shift_phase(q0_rf_frame, -0.1);", + " rx(pi/2) $1;", # FIXME: this needs the right basis rotation + " psb[0] = capture_v0(q0_ro_rx_frame);", + " psb[1] = capture_v0(q1_ro_rx_frame);", + "}", + ] + ) + with pytest.raises( + NotImplementedError, match="_to_pulse_sequence has not been implemented yet." + ): + assert circ.pulse_sequence(rigetti_device).to_ir() == expected + + +def test_expectation_value_result_type_on_all_qubits(rigetti_device): + circ = Circuit().cphaseshift(0, 1, 0.1).expectation(observable=Observable.X()) + expected = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + " bit[2] psb;", + " waveform q0_q1_cphase_sqrtCPHASE = {0.0, 0.0, 0.0, 0.0};", + " play(q0_q1_cphase_frame, q0_q1_cphase_sqrtCPHASE);", + " shift_phase(q0_rf_frame, -0.1);", + " rx(pi/2) $0;", # FIXME: this needs the right basis rotation + " rx(pi/2) $1;", # FIXME: this needs the right basis rotation + " psb[0] = capture_v0(q0_ro_rx_frame);", + " psb[1] = capture_v0(q1_ro_rx_frame);", + "}", + ] + ) + with pytest.raises( + NotImplementedError, match="_to_pulse_sequence has not been implemented yet." + ): + assert circ.pulse_sequence(rigetti_device).to_ir() == expected + + +def test_no_target_result_type(rigetti_device): + circ = Circuit().rx(0, np.pi / 2).state_vector() + expected = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + " bit[1] psb;", + " waveform wf_drag_gaussian_1 = drag_gaussian(60.0ns, 6.36991350216ns, " + "7.494904522022295e-10, -0.4549282253548838, false);", + " barrier $0;", + " shift_frequency(q0_rf_frame, -321047.14178613486);", + " play(q0_rf_frame, wf_drag_gaussian_1);", + " shift_frequency(q0_rf_frame, 321047.14178613486);", + " barrier $0;", + " psb[0] = capture_v0(q0_ro_rx_frame);", + "}", + ] + ) + with pytest.warns( + UserWarning, + match=r"StateVector\(\) does not have have a pulse representation and it is ignored.", + ): + assert circ.pulse_sequence(rigetti_device).to_ir() == expected diff --git a/test/unit_tests/braket/pulse/test_pulse_sequence.py b/test/unit_tests/braket/pulse/test_pulse_sequence.py index da8e0a8a3..d5429aef2 100644 --- a/test/unit_tests/braket/pulse/test_pulse_sequence.py +++ b/test/unit_tests/braket/pulse/test_pulse_sequence.py @@ -416,3 +416,50 @@ def test_parse_from_calibration_schema(predefined_frame_1, predefined_frame_2): PulseSequence._parse_from_calibration_schema(calibration_instrs, waveforms, frames) == expected_pulse_sequence ) + + +def test_add_pulse_sequences_with_different_free_parameters(predefined_frame_1): + theta1 = FreeParameter("theta1") + theta2 = FreeParameter("theta2") + pulse_sequence1 = PulseSequence().shift_phase(predefined_frame_1, theta1) + pulse_sequence2 = PulseSequence().shift_phase(predefined_frame_1, theta2) + pulse_sequence = pulse_sequence1 + pulse_sequence2 + + expected_str = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + *[ + f" input float {parameter};" + for parameter in reversed(list(pulse_sequence.parameters)) + ], + " shift_phase(predefined_frame_1, theta1);", + " shift_phase(predefined_frame_1, theta2);", + "}", + ] + ) + + assert pulse_sequence.to_ir() == expected_str + assert pulse_sequence.parameters == {theta1, theta2} + + +def test_add_pulse_sequences_with_capture_v0(predefined_frame_1): + pulse_sequence1 = PulseSequence().capture_v0(predefined_frame_1) + pulse_sequence2 = PulseSequence() + with pytest.raises( + ValueError, + match="Cannot add a pulse sequence to another" + " one that has already measurement instructions.", + ): + pulse_sequence1 += pulse_sequence2 + + +def test_add_pulse_sequences_with_same_free_parameter(predefined_frame_1): + theta = FreeParameter("theta") + pulse_sequence1 = PulseSequence().shift_phase(predefined_frame_1, theta) + with pytest.raises( + ValueError, + match="A free parameter with the name theta already exists in the pulse" + " sequence. Please rename this free parameter.", + ): + pulse_sequence1 + pulse_sequence1