diff --git a/src/braket/circuits/ascii_circuit_diagram.py b/src/braket/circuits/ascii_circuit_diagram.py index daef01793..2c7024574 100644 --- a/src/braket/circuits/ascii_circuit_diagram.py +++ b/src/braket/circuits/ascii_circuit_diagram.py @@ -21,8 +21,10 @@ from braket.circuits.compiler_directive import CompilerDirective from braket.circuits.gate import Gate from braket.circuits.instruction import Instruction +from braket.circuits.moments import MomentType from braket.circuits.noise import Noise from braket.circuits.result_type import ResultType +from braket.registers.qubit import Qubit from braket.registers.qubit_set import QubitSet @@ -44,23 +46,26 @@ def build_diagram(circuit: cir.Circuit) -> str: if not circuit.instructions: return "" + if all(m.moment_type == MomentType.GLOBAL_PHASE for m in circuit._moments): + return f"Global phase: {circuit.global_phase}" + circuit_qubits = circuit.qubits circuit_qubits.sort() - # Y Axis Column - y_axis_width = len(str(int(max(circuit_qubits)))) - y_axis_str = "{0:{width}} : |\n".format("T", width=y_axis_width + 1) - for qubit in circuit_qubits: - y_axis_str += "{0:{width}}\n".format(" ", width=y_axis_width + 5) - y_axis_str += "q{0:{width}} : -\n".format(str(int(qubit)), width=y_axis_width) + y_axis_str, global_phase = AsciiCircuitDiagram._prepare_diagram_vars( + circuit, circuit_qubits + ) time_slices = circuit.moments.time_slices() column_strs = [] # Moment columns for time, instructions in time_slices.items(): + global_phase = AsciiCircuitDiagram._compute_moment_global_phase( + global_phase, instructions + ) moment_str = AsciiCircuitDiagram._ascii_diagram_column_set( - str(time), circuit_qubits, instructions + str(time), circuit_qubits, instructions, global_phase ) column_strs.append(moment_str) @@ -71,7 +76,7 @@ def build_diagram(circuit: cir.Circuit) -> str: if target_result_types: column_strs.append( AsciiCircuitDiagram._ascii_diagram_column_set( - "Result Types", circuit_qubits, target_result_types + "Result Types", circuit_qubits, target_result_types, global_phase ) ) @@ -84,6 +89,9 @@ def build_diagram(circuit: cir.Circuit) -> str: # Time on top and bottom lines.append(lines[0]) + if global_phase: + lines.append(f"\nGlobal phase: {global_phase}") + # Additional result types line on bottom if additional_result_types: lines.append(f"\nAdditional result types: {', '.join(additional_result_types)}") @@ -97,6 +105,49 @@ def build_diagram(circuit: cir.Circuit) -> str: return "\n".join(lines) + @staticmethod + def _prepare_diagram_vars( + circuit: cir.Circuit, circuit_qubits: QubitSet + ) -> tuple[str, float | None]: + # Y Axis Column + y_axis_width = len(str(int(max(circuit_qubits)))) + y_axis_str = "{0:{width}} : |\n".format("T", width=y_axis_width + 1) + + global_phase = None + if any(m.moment_type == MomentType.GLOBAL_PHASE for m in circuit._moments): + y_axis_str += "{0:{width}} : |\n".format("GP", width=y_axis_width) + global_phase = 0 + + for qubit in circuit_qubits: + y_axis_str += "{0:{width}}\n".format(" ", width=y_axis_width + 5) + y_axis_str += "q{0:{width}} : -\n".format(str(int(qubit)), width=y_axis_width) + + return y_axis_str, global_phase + + @staticmethod + def _compute_moment_global_phase( + global_phase: float | None, items: list[Instruction] + ) -> float | None: + """ + Compute the integrated phase at a certain moment. + + Args: + global_phase (float | None): The integrated phase up to the computed moment + items (list[Instruction]): list of instructions + + Returns: + float | None: The updated integrated phase. + """ + moment_phase = 0 + for item in items: + if ( + isinstance(item, Instruction) + and isinstance(item.operator, Gate) + and item.operator.name == "GPhase" + ): + moment_phase += item.operator.angle + return global_phase + moment_phase if global_phase is not None else None + @staticmethod def _ascii_group_items( circuit_qubits: QubitSet, @@ -120,7 +171,15 @@ def _ascii_group_items( ): continue - if (isinstance(item, ResultType) and not item.target) or ( + # As a zero-qubit gate, GPhase can be grouped with anything. We set qubit_range + # to an empty list and we just add it to the first group below. + if ( + isinstance(item, Instruction) + and isinstance(item.operator, Gate) + and item.operator.name == "GPhase" + ): + qubit_range = QubitSet() + elif (isinstance(item, ResultType) and not item.target) or ( isinstance(item, Instruction) and isinstance(item.operator, CompilerDirective) ): qubit_range = circuit_qubits @@ -175,7 +234,10 @@ def _categorize_result_types( @staticmethod def _ascii_diagram_column_set( - col_title: str, circuit_qubits: QubitSet, items: list[Union[Instruction, ResultType]] + col_title: str, + circuit_qubits: QubitSet, + items: list[Union[Instruction, ResultType]], + global_phase: float | None, ) -> str: """ Return a set of columns in the ASCII string diagram of the circuit for a list of items. @@ -184,6 +246,7 @@ def _ascii_diagram_column_set( col_title (str): title of column set circuit_qubits (QubitSet): qubits in circuit items (list[Union[Instruction, ResultType]]): list of instructions or result types + global_phase (float | None): the integrated global phase up to this set Returns: str: An ASCII string diagram for the column set. @@ -193,7 +256,7 @@ def _ascii_diagram_column_set( groupings = AsciiCircuitDiagram._ascii_group_items(circuit_qubits, items) column_strs = [ - AsciiCircuitDiagram._ascii_diagram_column(circuit_qubits, grouping[1]) + AsciiCircuitDiagram._ascii_diagram_column(circuit_qubits, grouping[1], global_phase) for grouping in groupings ] @@ -220,7 +283,9 @@ def _ascii_diagram_column_set( @staticmethod def _ascii_diagram_column( - circuit_qubits: QubitSet, items: list[Union[Instruction, ResultType]] + circuit_qubits: QubitSet, + items: list[Union[Instruction, ResultType]], + global_phase: float | None = None, ) -> str: """ Return a column in the ASCII string diagram of the circuit for a given list of items. @@ -228,9 +293,10 @@ def _ascii_diagram_column( Args: circuit_qubits (QubitSet): qubits in circuit items (list[Union[Instruction, ResultType]]): list of instructions or result types + global_phase (float | None): the integrated global phase up to this column Returns: - str: An ASCII string diagram for the specified moment in time for a column. + str: an ASCII string diagram for the specified moment in time for a column. """ symbols = {qubit: "-" for qubit in circuit_qubits} margins = {qubit: " " for qubit in circuit_qubits} @@ -252,12 +318,26 @@ def _ascii_diagram_column( num_after = len(circuit_qubits) - 1 after = ["|"] * (num_after - 1) + ([marker] if num_after else []) ascii_symbols = [ascii_symbol] + after + elif ( + isinstance(item, Instruction) + and isinstance(item.operator, Gate) + and item.operator.name == "GPhase" + ): + target_qubits = circuit_qubits + control_qubits = QubitSet() + target_and_control = QubitSet() + qubits = circuit_qubits + ascii_symbols = "-" * len(circuit_qubits) else: if isinstance(item.target, list): target_qubits = reduce(QubitSet.union, map(QubitSet, item.target), QubitSet()) else: target_qubits = item.target control_qubits = getattr(item, "control", QubitSet()) + map_control_qubit_states = AsciiCircuitDiagram._build_map_control_qubits( + item, control_qubits + ) + target_and_control = target_qubits.union(control_qubits) qubits = QubitSet(range(min(target_and_control), max(target_and_control) + 1)) @@ -288,20 +368,54 @@ def _ascii_diagram_column( else ascii_symbols[item_qubit_index] ) elif qubit in control_qubits: - symbols[qubit] = "C" + symbols[qubit] = "C" if map_control_qubit_states[qubit] else "N" else: symbols[qubit] = "|" # Set the margin to be a connector if not on the first qubit - if qubit != min(target_and_control): + if target_and_control and qubit != min(target_and_control): margins[qubit] = "|" - symbols_width = max([len(symbol) for symbol in symbols.values()]) + output = AsciiCircuitDiagram._create_output(symbols, margins, circuit_qubits, global_phase) + return output + @staticmethod + def _create_output( + symbols: dict[Qubit, str], + margins: dict[Qubit, str], + qubits: QubitSet, + global_phase: float | None, + ) -> str: + symbols_width = max([len(symbol) for symbol in symbols.values()]) output = "" - for qubit in circuit_qubits: + + if global_phase is not None: + global_phase_str = ( + f"{global_phase:.2f}" if isinstance(global_phase, float) else str(global_phase) + ) + symbols_width = max([symbols_width, len(global_phase_str)]) + output += "{0:{fill}{align}{width}}|\n".format( + global_phase_str, + fill=" ", + align="^", + width=symbols_width, + ) + + for qubit in qubits: output += "{0:{width}}\n".format(margins[qubit], width=symbols_width + 1) output += "{0:{fill}{align}{width}}\n".format( symbols[qubit], fill="-", align="<", width=symbols_width + 1 ) return output + + @staticmethod + def _build_map_control_qubits(item: Instruction, control_qubits: QubitSet) -> dict(Qubit, int): + control_state = getattr(item, "control_state", None) + if control_state is not None: + map_control_qubit_states = { + qubit: state for qubit, state in zip(control_qubits, control_state) + } + else: + map_control_qubit_states = {qubit: 1 for qubit in control_qubits} + + return map_control_qubit_states diff --git a/src/braket/circuits/braket_program_context.py b/src/braket/circuits/braket_program_context.py index 19e2c986f..863513565 100644 --- a/src/braket/circuits/braket_program_context.py +++ b/src/braket/circuits/braket_program_context.py @@ -56,8 +56,15 @@ def is_builtin_gate(self, name: str) -> bool: user_defined_gate = self.is_user_defined_gate(name) return name in BRAKET_GATES and not user_defined_gate - def add_phase_instruction(self, target: tuple[int], phase_value: int) -> None: - raise NotImplementedError + def add_phase_instruction(self, target: tuple[int], phase_value: float) -> None: + """Add a global phase to the circuit. + + Args: + target (tuple[int]): Unused + phase_value (float): The phase value to be applied + """ + instruction = Instruction(BRAKET_GATES["gphase"](phase_value)) + self._circuit.add_instruction(instruction) def add_gate_instruction( self, gate_name: str, target: tuple[int], *params, ctrl_modifiers: list[int], power: float diff --git a/src/braket/circuits/circuit.py b/src/braket/circuits/circuit.py index 76e986087..aeae8585d 100644 --- a/src/braket/circuits/circuit.py +++ b/src/braket/circuits/circuit.py @@ -26,7 +26,7 @@ from braket.circuits.free_parameter_expression import FreeParameterExpression from braket.circuits.gate import Gate from braket.circuits.instruction import Instruction -from braket.circuits.moments import Moments +from braket.circuits.moments import Moments, MomentType from braket.circuits.noise import Noise from braket.circuits.noise_helpers import ( apply_noise_to_gates, @@ -156,6 +156,17 @@ def depth(self) -> int: """int: Get the circuit depth.""" return self._moments.depth + @property + def global_phase(self) -> float: + """float: Get the global phase of the circuit.""" + return sum( + [ + instr.operator.angle + for moment, instr in self._moments.items() + if moment.moment_type == MomentType.GLOBAL_PHASE + ] + ) + @property def instructions(self) -> list[Instruction]: """Iterable[Instruction]: Get an `iterable` of instructions in the circuit.""" diff --git a/src/braket/circuits/gate.py b/src/braket/circuits/gate.py index 3c59409e5..2183a2329 100644 --- a/src/braket/circuits/gate.py +++ b/src/braket/circuits/gate.py @@ -198,7 +198,7 @@ def _to_openqasm( return ( f"{inv_prefix}{power_prefix}{control_prefix}" - f"{self._qasm_name}{param_string} {', '.join(qubits)};" + f"{self._qasm_name}{param_string}{','.join([f' {qubit}' for qubit in qubits])};" ) @property diff --git a/src/braket/circuits/gates.py b/src/braket/circuits/gates.py index 9a4214c37..e955c4bb0 100644 --- a/src/braket/circuits/gates.py +++ b/src/braket/circuits/gates.py @@ -30,7 +30,7 @@ angled_ascii_characters, get_angle, ) -from braket.circuits.basis_state import BasisStateInput +from braket.circuits.basis_state import BasisState, BasisStateInput from braket.circuits.free_parameter import FreeParameter from braket.circuits.free_parameter_expression import FreeParameterExpression from braket.circuits.gate import Gate @@ -215,6 +215,118 @@ def i( Gate.register_gate(I) +class GPhase(AngledGate): + r"""Global phase gate. + + Unitary matrix: + + .. math:: \mathtt{gphase}(\gamma) = e^(i \gamma) I_1. + + Args: + angle (Union[FreeParameterExpression, float]): angle in radians. + """ + + def __init__(self, angle: Union[FreeParameterExpression, float]): + # Avoid parent constructor because _qubit_count must be zero + self._qubit_count = self.fixed_qubit_count() + self._ascii_symbols = [] + + if angle is None: + raise ValueError("angle must not be None") + if isinstance(angle, FreeParameterExpression): + self._parameters = [angle] + else: + self._parameters = [float(angle)] # explicit casting in case angle is e.g. np.float32 + + @property + def _qasm_name(self) -> str: + return "gphase" + + def adjoint(self) -> list[Gate]: + return [GPhase(-self.angle)] + + def to_matrix(self) -> np.ndarray: + return np.exp(1j * self.angle) * np.eye(1, dtype=complex) + + def bind_values(self, **kwargs) -> AngledGate: + return get_angle(self, **kwargs) + + @staticmethod + def fixed_qubit_count() -> int: + return 0 + + @staticmethod + @circuit.subroutine(register=True) + def gphase( + angle: Union[FreeParameterExpression, float], + *, + control: Optional[QubitSetInput] = None, + control_state: Optional[BasisStateInput] = None, + power: float = 1, + ) -> Instruction | Iterable[Instruction]: + r"""Global phase gate. + + If the gate is applied with control/negative control modifiers, it is translated in an + equivalent gate using the following definition: `phaseshift(λ) = ctrl @ gphase(λ)`. + The rightmost control qubit is used for the translation. If the polarity of the rightmost + control modifier is negative, the following identity is used: + `negctrl @ gphase(λ) q = x q; ctrl @ gphase(λ) q; x q`. + + Unitary matrix: + + .. math:: \mathtt{gphase}(\gamma) = e^(i \gamma) I_1. + + Args: + angle (Union[FreeParameterExpression, float]): Phase in radians. + control (Optional[QubitSetInput]): Control qubit(s). Default None. + control_state (Optional[BasisStateInput]): Quantum state on which to control the + operation. Must be a binary sequence of same length as number of qubits in + `control`. Will be ignored if `control` is not present. May be represented as a + string, list, or int. For example "0101", [0, 1, 0, 1], 5 all represent + controlling on qubits 0 and 2 being in the \\|0⟩ state and qubits 1 and 3 being + in the \\|1⟩ state. Default "1" * len(control). + power (float): Integer or fractional power to raise the gate to. Negative + powers will be split into an inverse, accompanied by the positive power. + Default 1. + + Returns: + Instruction | Iterable[Instruction]: GPhase instruction. + + Examples: + >>> circ = Circuit().gphase(0.45) + """ + if control is not None: + control_qubits = QubitSet(control) + + control_state = ( + control_state if control_state is not None else (1,) * len(control_qubits) + ) + control_basis_state = BasisState(control_state, len(control_qubits)) + + phaseshift_target = control_qubits[-1] + phaseshift_instruction = PhaseShift.phaseshift( + phaseshift_target, + angle, + control=control_qubits[:-1], + control_state=control_basis_state[:-1], + power=power, + ) + return ( + phaseshift_instruction + if control_basis_state[-1] + else [ + X.x(phaseshift_target), + phaseshift_instruction, + X.x(phaseshift_target), + ] + ) + + return Instruction(GPhase(angle), power=power) + + +Gate.register_gate(GPhase) + + class X(Gate): r"""Pauli-X gate. @@ -1287,6 +1399,129 @@ def phaseshift( Gate.register_gate(PhaseShift) +class U(TripleAngledGate): + r"""Generalized single-qubit rotation gate. + + Unitary matrix: + + .. math:: \mathtt{U}(\theta, \phi, \lambda) = \begin{bmatrix} + \cos{(\theta/2)} & -e^{i \lambda} \sin{(\theta/2)} \\ + e^{i \phi} \sin{(\theta/2)} & -e^{i (\phi + \lambda)} \cos{(\theta/2)} + \end{bmatrix}. + + Args: + angle_1 (Union[FreeParameterExpression, float]): theta angle in radians. + angle_2 (Union[FreeParameterExpression, float]): phi angle in radians. + angle_3 (Union[FreeParameterExpression, float]): lambda angle in radians. + """ + + def __init__( + self, + angle_1: Union[FreeParameterExpression, float], + angle_2: Union[FreeParameterExpression, float], + angle_3: Union[FreeParameterExpression, float], + ): + super().__init__( + angle_1=angle_1, + angle_2=angle_2, + angle_3=angle_3, + qubit_count=None, + ascii_symbols=[_multi_angled_ascii_characters("U", angle_1, angle_2, angle_3)], + ) + + @property + def _qasm_name(self) -> str: + return "U" + + def to_matrix(self) -> np.ndarray: + r"""Returns a matrix representation of this gate. + Returns: + ndarray: The matrix representation of this gate. + """ + _theta = self.angle_1 + _phi = self.angle_2 + _lambda = self.angle_3 + return np.array( + [ + [ + np.cos(_theta / 2), + -np.exp(1j * _lambda) * np.sin(_theta / 2), + ], + [ + np.exp(1j * _phi) * np.sin(_theta / 2), + np.exp(1j * (_phi + _lambda)) * np.cos(_theta / 2), + ], + ] + ) + + def adjoint(self) -> list[Gate]: + return [U(-self.angle_1, -self.angle_3, -self.angle_2)] + + @staticmethod + def fixed_qubit_count() -> int: + return 1 + + def bind_values(self, **kwargs) -> TripleAngledGate: + return _get_angles(self, **kwargs) + + @staticmethod + @circuit.subroutine(register=True) + def u( + target: QubitSetInput, + angle_1: Union[FreeParameterExpression, float], + angle_2: Union[FreeParameterExpression, float], + angle_3: Union[FreeParameterExpression, float], + *, + control: Optional[QubitSetInput] = None, + control_state: Optional[BasisStateInput] = None, + power: float = 1, + ) -> Iterable[Instruction]: + r"""Generalized single-qubit rotation gate. + + Unitary matrix: + + .. math:: \mathtt{U}(\theta, \phi, \lambda) = \begin{bmatrix} + \cos{(\theta/2)} & -e^{i \lambda} \sin{(\theta/2)} \\ + e^{i \phi} \sin{(\theta/2)} & -e^{i (\phi + \lambda)} \cos{(\theta/2)} + \end{bmatrix}. + + Args: + target (QubitSetInput): Target qubit(s) + angle_1 (Union[FreeParameterExpression, float]): theta angle in radians. + angle_2 (Union[FreeParameterExpression, float]): phi angle in radians. + angle_3 (Union[FreeParameterExpression, float]): lambda angle in radians. + control (Optional[QubitSetInput]): Control qubit(s). Default None. + control_state (Optional[BasisStateInput]): Quantum state on which to control the + operation. Must be a binary sequence of same length as number of qubits in + `control`. Will be ignored if `control` is not present. May be represented as a + string, list, or int. For example "0101", [0, 1, 0, 1], 5 all represent + controlling on qubits 0 and 2 being in the \\|0⟩ state and qubits 1 and 3 being + in the \\|1⟩ state. Default "1" * len(control). + power (float): Integer or fractional power to raise the gate to. Negative + powers will be split into an inverse, accompanied by the positive power. + Default 1. + + Returns: + Iterable[Instruction]: U instruction. + + Examples: + >>> circ = Circuit().u(0, 0.15, 0.34, 0.52) + """ + return [ + Instruction( + U(angle_1, angle_2, angle_3), + target=qubit, + control=control, + control_state=control_state, + power=power, + ) + for qubit in QubitSet(target) + ] + + +Gate.register_gate(U) + + # Two qubit gates # diff --git a/src/braket/circuits/moments.py b/src/braket/circuits/moments.py index 7cd8e0a9d..6e87db78d 100644 --- a/src/braket/circuits/moments.py +++ b/src/braket/circuits/moments.py @@ -19,6 +19,7 @@ from typing import Any, NamedTuple, Union from braket.circuits.compiler_directive import CompilerDirective +from braket.circuits.gate import Gate from braket.circuits.instruction import Instruction from braket.circuits.noise import Noise from braket.registers.qubit import Qubit @@ -42,6 +43,7 @@ class MomentType(str, Enum): INITIALIZATION_NOISE = "initialization_noise" READOUT_NOISE = "readout_noise" COMPILER_DIRECTIVE = "compiler_directive" + GLOBAL_PHASE = "global_phase" class MomentsKey(NamedTuple): @@ -59,6 +61,7 @@ class MomentsKey(NamedTuple): qubits: QubitSet moment_type: MomentType noise_index: int + subindex: int = 0 class Moments(Mapping[MomentsKey, Instruction]): @@ -106,6 +109,7 @@ def __init__(self, instructions: Iterable[Instruction] | None = None): self._qubits = QubitSet() self._depth = 0 self._time_all_qubits = -1 + self._number_gphase_in_current_moment = 0 self.add(instructions or []) @@ -181,6 +185,17 @@ def _add(self, instruction: Instruction, noise_index: int = 0) -> None: self._time_all_qubits = time elif isinstance(operator, Noise): self.add_noise(instruction) + elif isinstance(operator, Gate) and operator.name == "GPhase": + time = self._get_qubit_times(self._max_times.keys()) + 1 + self._number_gphase_in_current_moment += 1 + key = MomentsKey( + time, + QubitSet([]), + MomentType.GLOBAL_PHASE, + 0, + self._number_gphase_in_current_moment, + ) + self._moments[key] = instruction else: qubit_range = instruction.target.union(instruction.control) time = self._update_qubit_times(qubit_range) @@ -188,14 +203,15 @@ def _add(self, instruction: Instruction, noise_index: int = 0) -> None: self._qubits.update(qubit_range) self._depth = max(self._depth, time + 1) + def _get_qubit_times(self, qubits: QubitSet) -> int: + return max([self._max_time_for_qubit(qubit) for qubit in qubits] + [self._time_all_qubits]) + def _update_qubit_times(self, qubits: QubitSet) -> int: - qubit_max_times = [self._max_time_for_qubit(qubit) for qubit in qubits] + [ - self._time_all_qubits - ] - time = max(qubit_max_times) + 1 + time = self._get_qubit_times(qubits) + 1 # Update time for all specified qubits for qubit in qubits: self._max_times[qubit] = time + self._number_gphase_in_current_moment = 0 return time def add_noise( diff --git a/src/braket/circuits/translations.py b/src/braket/circuits/translations.py index ba536594f..bbb194be3 100644 --- a/src/braket/circuits/translations.py +++ b/src/braket/circuits/translations.py @@ -31,6 +31,7 @@ from braket.ir.jaqcd.program_v1 import Results BRAKET_GATES = { + "gphase": braket_gates.GPhase, "i": braket_gates.I, "h": braket_gates.H, "x": braket_gates.X, @@ -55,6 +56,7 @@ "rx": braket_gates.Rx, "ry": braket_gates.Ry, "rz": braket_gates.Rz, + "U": braket_gates.U, "swap": braket_gates.Swap, "iswap": braket_gates.ISwap, "pswap": braket_gates.PSwap, diff --git a/test/unit_tests/braket/circuits/test_angled_gate.py b/test/unit_tests/braket/circuits/test_angled_gate.py index e76756e29..4e093e5b4 100644 --- a/test/unit_tests/braket/circuits/test_angled_gate.py +++ b/test/unit_tests/braket/circuits/test_angled_gate.py @@ -31,7 +31,7 @@ def test_is_operator(angled_gate): def test_angle_is_none(): - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="angle must not be None"): AngledGate(qubit_count=1, ascii_symbols=["foo"], angle=None) diff --git a/test/unit_tests/braket/circuits/test_ascii_circuit_diagram.py b/test/unit_tests/braket/circuits/test_ascii_circuit_diagram.py index 3f3268f83..916bfb050 100644 --- a/test/unit_tests/braket/circuits/test_ascii_circuit_diagram.py +++ b/test/unit_tests/braket/circuits/test_ascii_circuit_diagram.py @@ -12,6 +12,7 @@ # language governing permissions and limitations under the License. import numpy as np +import pytest from braket.circuits import ( AsciiCircuitDiagram, @@ -29,6 +30,10 @@ def test_empty_circuit(): assert AsciiCircuitDiagram.build_diagram(Circuit()) == "" +def test_only_gphase_circuit(): + assert AsciiCircuitDiagram.build_diagram(Circuit().gphase(0.1)) == "Global phase: 0.1" + + def test_one_gate_one_qubit(): circ = Circuit().h(0) expected = ("T : |0|", " ", "q0 : -H-", "", "T : |0|") @@ -58,6 +63,35 @@ def test_one_gate_one_qubit_rotation_with_parameter(): _assert_correct_diagram(circ, expected) +@pytest.mark.parametrize("target", [0, 1]) +def test_one_gate_with_global_phase(target): + circ = Circuit().x(target=target).gphase(0.15) + expected = ( + "T : |0| 1 |", + "GP : |0|0.15|", + " ", + f"q{target} : -X------", + "", + "T : |0| 1 |", + "", + "Global phase: 0.15", + ) + _assert_correct_diagram(circ, expected) + + +def test_one_gate_with_zero_global_phase(): + circ = Circuit().gphase(-0.15).x(target=0).gphase(0.15) + expected = ( + "T : | 0 | 1 |", + "GP : |-0.15|0.00|", + " ", + "q0 : -X----------", + "", + "T : | 0 | 1 |", + ) + _assert_correct_diagram(circ, expected) + + def test_one_gate_one_qubit_rotation_with_unicode(): theta = FreeParameter("\u03B8") circ = Circuit().rx(angle=theta, target=0) @@ -74,6 +108,24 @@ def test_one_gate_one_qubit_rotation_with_unicode(): _assert_correct_diagram(circ, expected) +def test_one_gate_with_parametric_expression_global_phase_(): + theta = FreeParameter("\u03B8") + circ = Circuit().x(target=0).gphase(2 * theta).x(0).gphase(1) + expected = ( + "T : |0| 1 | 2 |", + "GP : |0|2*θ|2*θ + 1.0|", + " ", + "q0 : -X-X-------------", + "", + "T : |0| 1 | 2 |", + "", + "Global phase: 2*θ + 1.0", + "", + "Unassigned parameters: [θ].", + ) + _assert_correct_diagram(circ, expected) + + def test_one_gate_one_qubit_rotation_with_parameter_assigned(): theta = FreeParameter("theta") circ = Circuit().rx(angle=theta, target=0) @@ -187,6 +239,38 @@ def test_connector_across_two_qubits(): _assert_correct_diagram(circ, expected) +def test_neg_control_qubits(): + circ = Circuit().x(2, control=[0, 1], control_state=[0, 1]) + expected = ( + "T : |0|", + " ", + "q0 : -N-", + " | ", + "q1 : -C-", + " | ", + "q2 : -X-", + "", + "T : |0|", + ) + _assert_correct_diagram(circ, expected) + + +def test_only_neg_control_qubits(): + circ = Circuit().x(2, control=[0, 1], control_state=0) + expected = ( + "T : |0|", + " ", + "q0 : -N-", + " | ", + "q1 : -N-", + " | ", + "q2 : -X-", + "", + "T : |0|", + ) + _assert_correct_diagram(circ, expected) + + def test_connector_across_three_qubits(): circ = Circuit().x(control=(3, 4), target=5).h(range(2, 6)) expected = ( diff --git a/test/unit_tests/braket/circuits/test_circuit.py b/test/unit_tests/braket/circuits/test_circuit.py index 71cff0de7..928cff757 100644 --- a/test/unit_tests/braket/circuits/test_circuit.py +++ b/test/unit_tests/braket/circuits/test_circuit.py @@ -1748,6 +1748,22 @@ def foo( inputs={}, ), ), + ( + Circuit().gphase(0.15).x(0), + OpenQasmProgram( + source="\n".join( + [ + "OPENQASM 3.0;", + "bit[1] b;", + "qubit[1] q;", + "gphase(0.15);", + "x q[0];", + "b[0] = measure q[0];", + ] + ), + inputs={}, + ), + ), ], ) def test_from_ir(expected_circuit, ir): @@ -2027,6 +2043,14 @@ def test_to_unitary_with_compiler_directives_returns_expected_unitary(): ) +def test_to_unitary_with_global_phase(): + circuit = Circuit().x(0) + circuit_unitary = np.array([[0, 1], [1, 0]]) + assert np.allclose(circuit.to_unitary(), circuit_unitary) + circuit = circuit.gphase(np.pi / 2) + assert np.allclose(circuit.to_unitary(), 1j * circuit_unitary) + + @pytest.mark.parametrize( "circuit,expected_unitary", [ @@ -2046,6 +2070,8 @@ def test_to_unitary_with_compiler_directives_returns_expected_unitary(): (Circuit().rx(0, 0.15), gates.Rx(0.15).to_matrix()), (Circuit().ry(0, 0.15), gates.Ry(0.15).to_matrix()), (Circuit().rz(0, 0.15), gates.Rz(0.15).to_matrix()), + (Circuit().u(0, 0.15, 0.16, 0.17), gates.U(0.15, 0.16, 0.17).to_matrix()), + (Circuit().gphase(0.15), gates.GPhase(0.15).to_matrix()), (Circuit().phaseshift(0, 0.15), gates.PhaseShift(0.15).to_matrix()), (Circuit().cnot(0, 1), gates.CNot().to_matrix()), (Circuit().cnot(0, 1).add_result_type(ResultType.StateVector()), gates.CNot().to_matrix()), @@ -3134,3 +3160,23 @@ def test_parametrized_pulse_circuit(user_defined_frame): def test_free_param_float_mix(): Circuit().ms(0, 1, 0.1, FreeParameter("theta")) + + +def test_circuit_with_global_phase(): + circuit = Circuit().gphase(0.15).x(0) + assert circuit.global_phase == 0.15 + + assert circuit.to_ir( + ir_type=IRType.OPENQASM, + serialization_properties=OpenQASMSerializationProperties( + qubit_reference_type=QubitReferenceType.PHYSICAL + ), + ).source == "\n".join( + [ + "OPENQASM 3.0;", + "bit[1] b;", + "gphase(0.15);", + "x $0;", + "b[0] = measure $0;", + ] + ) diff --git a/test/unit_tests/braket/circuits/test_gates.py b/test/unit_tests/braket/circuits/test_gates.py index 05b98ade4..fc8fe7787 100644 --- a/test/unit_tests/braket/circuits/test_gates.py +++ b/test/unit_tests/braket/circuits/test_gates.py @@ -35,12 +35,21 @@ from braket.pulse import ArbitraryWaveform, Frame, Port, PulseSequence +class NoTarget: + pass + + class TripleAngle: pass +class SingleNegControlModifier: + pass + + testdata = [ (Gate.H, "h", ir.H, [SingleTarget], {}), + (Gate.GPhase, "gphase", None, [NoTarget, Angle], {}), (Gate.I, "i", ir.I, [SingleTarget], {}), (Gate.X, "x", ir.X, [SingleTarget], {}), (Gate.Y, "y", ir.Y, [SingleTarget], {}), @@ -54,6 +63,7 @@ class TripleAngle: (Gate.Rx, "rx", ir.Rx, [SingleTarget, Angle], {}), (Gate.Ry, "ry", ir.Ry, [SingleTarget, Angle], {}), (Gate.Rz, "rz", ir.Rz, [SingleTarget, Angle], {}), + (Gate.U, "u", None, [SingleTarget, TripleAngle], {}), (Gate.CNot, "cnot", ir.CNot, [SingleTarget, SingleControl], {}), (Gate.CV, "cv", ir.CV, [SingleTarget, SingleControl], {}), (Gate.CCNot, "ccnot", ir.CCNot, [SingleTarget, DoubleControl], {}), @@ -118,9 +128,11 @@ class TripleAngle: ] parameterizable_gates = [ + Gate.GPhase, Gate.Rx, Gate.Ry, Gate.Rz, + Gate.U, Gate.PhaseShift, Gate.PSwap, Gate.XX, @@ -147,6 +159,10 @@ class TripleAngle: ] +def no_target_valid_input(**kwargs): + return {} + + def single_target_valid_input(**kwargs): return {"target": 2} @@ -171,6 +187,10 @@ def single_control_valid_input(**kwargs): return {"control": 0} +def single_neg_control_valid_input(**kwargs): + return {"control": [0], "control_state": [0]} + + def double_control_valid_ir_input(**kwargs): return {"controls": [0, 1]} @@ -193,11 +213,13 @@ def two_dimensional_matrix_valid_input(**kwargs): valid_ir_switcher = { + "NoTarget": no_target_valid_input, "SingleTarget": single_target_valid_input, "DoubleTarget": double_target_valid_ir_input, "Angle": angle_valid_input, "TripleAngle": triple_angle_valid_input, "SingleControl": single_control_valid_input, + "SingleNegControlModifier": single_neg_control_valid_input, "DoubleControl": double_control_valid_ir_input, "MultiTarget": multi_target_valid_input, "TwoDimensionalMatrix": two_dimensional_matrix_valid_ir_input, @@ -232,9 +254,13 @@ def create_valid_subroutine_input(irsubclasses, **kwargs): def create_valid_target_input(irsubclasses): input = {} qubit_set = [] + control_qubit_set = [] + control_state = None # based on the concept that control goes first in target input for subclass in irsubclasses: - if subclass == SingleTarget: + if subclass == NoTarget: + qubit_set.extend(list(no_target_valid_input().values())) + elif subclass == SingleTarget: qubit_set.extend(list(single_target_valid_input().values())) elif subclass == DoubleTarget: qubit_set.extend(list(double_target_valid_ir_input().values())) @@ -242,6 +268,9 @@ def create_valid_target_input(irsubclasses): qubit_set.extend(list(multi_target_valid_input().values())) elif subclass == SingleControl: qubit_set = list(single_control_valid_input().values()) + qubit_set + elif subclass == SingleNegControlModifier: + control_qubit_set = list(single_neg_control_valid_input()["control"]) + control_state = list(single_neg_control_valid_input()["control_state"]) elif subclass == DoubleControl: qubit_set = list(double_control_valid_ir_input().values()) + qubit_set elif subclass in (Angle, TwoDimensionalMatrix, TripleAngle): @@ -249,6 +278,8 @@ def create_valid_target_input(irsubclasses): else: raise ValueError("Invalid subclass") input["target"] = QubitSet(qubit_set) + input["control"] = QubitSet(control_qubit_set) + input["control_state"] = control_state return input @@ -282,7 +313,7 @@ def calculate_qubit_count(irsubclasses): qubit_count += 2 elif subclass == MultiTarget: qubit_count += 3 - elif subclass in (Angle, TwoDimensionalMatrix, TripleAngle): + elif subclass in (NoTarget, Angle, TwoDimensionalMatrix, TripleAngle): pass else: raise ValueError("Invalid subclass") @@ -434,6 +465,18 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), "rz(0.17) $4;", ), + ( + Gate.U(angle_1=0.17, angle_2=3.45, angle_3=5.21), + [4], + OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), + "U(0.17, 3.45, 5.21) q[4];", + ), + ( + Gate.U(angle_1=0.17, angle_2=3.45, angle_3=5.21), + [4], + OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), + "U(0.17, 3.45, 5.21) $4;", + ), ( Gate.XX(angle=0.17), [4, 5], @@ -859,9 +902,53 @@ def test_gate_subroutine(testclass, subroutine_name, irclass, irsubclasses, kwar subroutine_input = {"target": multi_targets} if Angle in irsubclasses: subroutine_input.update(angle_valid_input()) + if TripleAngle in irsubclasses: + subroutine_input.update(triple_angle_valid_input()) assert subroutine(**subroutine_input) == Circuit(instruction_list) +@pytest.mark.parametrize( + "control, control_state, instruction_set", + [ + ( + 2, + None, + Instruction(**create_valid_instruction_input(Gate.PhaseShift, [SingleTarget, Angle])), + ), + ( + 2, + [0], + [ + Instruction(operator=Gate.X(), target=2), + Instruction( + **create_valid_instruction_input(Gate.PhaseShift, [SingleTarget, Angle]) + ), + Instruction(operator=Gate.X(), target=2), + ], + ), + ( + [0, 2], + [0, 1], + Instruction( + **create_valid_instruction_input( + Gate.PhaseShift, [SingleTarget, SingleNegControlModifier, Angle] + ) + ), + ), + ], +) +def test_control_gphase_subroutine(control, control_state, instruction_set): + subroutine = getattr(Circuit(), "gphase") + assert subroutine(angle=0.123, control=control, control_state=control_state) == Circuit( + instruction_set + ) + + +def test_angle_gphase_is_none(): + with pytest.raises(ValueError, match="angle must not be None"): + Gate.GPhase(angle=None) + + @pytest.mark.parametrize("testclass,subroutine_name,irclass,irsubclasses,kwargs", testdata) def test_gate_adjoint_expansion_correct(testclass, subroutine_name, irclass, irsubclasses, kwargs): gate = testclass(**create_valid_gate_class_input(irsubclasses, **kwargs)) @@ -925,7 +1012,7 @@ def test_large_unitary(): @pytest.mark.parametrize("gate", parameterizable_gates) def test_bind_values(gate): - triple_angled = gate.__name__ in ("MS",) + triple_angled = gate.__name__ in ("MS", "U") num_params = 3 if triple_angled else 1 thetas = [FreeParameter(f"theta_{i}") for i in range(num_params)] mapping = {f"theta_{i}": i for i in range(num_params)} @@ -1070,6 +1157,13 @@ def test_pulse_gate_to_matrix(): "10", "negctrl @ ctrl @ negctrl @ z q[1], q[2], q[3], q[0];", ), + ( + Gate.GPhase(0.3), + QubitSet([]), + QubitSet([1]), + "1", + "ctrl @ gphase(0.3) q[1];", + ), ), ) def test_gate_control(gate, target, control, control_state, expected_ir):