diff --git a/qiskit/circuit/annotated_operation.py b/qiskit/circuit/annotated_operation.py index 6780cc2e330f..6006e68f58df 100644 --- a/qiskit/circuit/annotated_operation.py +++ b/qiskit/circuit/annotated_operation.py @@ -18,6 +18,7 @@ from typing import Union, List from qiskit.circuit.operation import Operation +from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit._utils import _compute_control_matrix, _ctrl_state_to_int from qiskit.circuit.exceptions import CircuitError @@ -219,6 +220,27 @@ def power(self, exponent: float, annotated: bool = False): extended_modifiers.append(PowerModifier(exponent)) return AnnotatedOperation(self.base_op, extended_modifiers) + @property + def params(self) -> list[ParameterValueType]: + """The params of the underlying base operation.""" + return getattr(self.base_op, "params", []) + + @params.setter + def params(self, value: list[ParameterValueType]): + if hasattr(self.base_op, "params"): + self.base_op.params = value + else: + raise AttributeError( + f"Cannot set attribute ``params`` on the base operation {self.base_op}." + ) + + def validate_parameter(self, parameter: ParameterValueType) -> ParameterValueType: + """Validate a parameter for the underlying base operation.""" + if hasattr(self.base_op, "validate_parameter"): + return self.base_op.validate_parameter(parameter) + + raise AttributeError(f"Cannot validate parameters on the base operation {self.base_op}.") + def _canonicalize_modifiers(modifiers): """ diff --git a/qiskit/circuit/gate.py b/qiskit/circuit/gate.py index d2c88f40bdb6..37fd19e2022a 100644 --- a/qiskit/circuit/gate.py +++ b/qiskit/circuit/gate.py @@ -104,10 +104,9 @@ def control( num_ctrl_qubits: int = 1, label: str | None = None, ctrl_state: int | str | None = None, - annotated: bool = False, + annotated: bool | None = None, ): - """ - Return the controlled version of itself. + """Return the controlled version of itself. Implemented either as a controlled gate (ref. :class:`.ControlledGate`) or as an annotated operation (ref. :class:`.AnnotatedOperation`). @@ -118,8 +117,12 @@ def control( operation. ctrl_state: the control state in decimal or as a bitstring (e.g. ``'111'``). If ``None``, use ``2**num_ctrl_qubits-1``. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate is implemented + as an annotated gate. If ``None``, this is set to ``False`` + if the controlled gate can directly be constructed, and otherwise + set to ``True``. This allows defering the construction process in case the + synthesis of the controlled gate requires more information (e.g. + values of unbound parameters). Returns: Controlled version of the given operation. @@ -127,7 +130,7 @@ def control( Raises: QiskitError: unrecognized mode or invalid ctrl_state """ - if not annotated: + if not annotated: # captures both None and False # pylint: disable=cyclic-import from .add_control import add_control diff --git a/qiskit/circuit/library/generalized_gates/unitary.py b/qiskit/circuit/library/generalized_gates/unitary.py index 6a6623ffce5d..9aa03be86318 100644 --- a/qiskit/circuit/library/generalized_gates/unitary.py +++ b/qiskit/circuit/library/generalized_gates/unitary.py @@ -165,7 +165,7 @@ def control( num_ctrl_qubits: int = 1, label: str | None = None, ctrl_state: int | str | None = None, - annotated: bool = False, + annotated: bool | None = None, ) -> ControlledGate | AnnotatedOperation: """Return controlled version of gate. @@ -174,8 +174,8 @@ def control( label: Optional gate label. ctrl_state: The control state in decimal or as a bit string (e.g. ``"1011"``). If ``None``, use ``2**num_ctrl_qubits - 1``. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. Returns: Controlled version of gate. diff --git a/qiskit/circuit/library/standard_gates/h.py b/qiskit/circuit/library/standard_gates/h.py index c07895ebbeaa..462ede2c93ae 100644 --- a/qiskit/circuit/library/standard_gates/h.py +++ b/qiskit/circuit/library/standard_gates/h.py @@ -11,6 +11,9 @@ # that they have been altered from the originals. """Hadamard gate.""" + +from __future__ import annotations + from math import sqrt, pi from typing import Optional, Union import numpy @@ -79,9 +82,9 @@ def _define(self): def control( self, num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[int, str]] = None, - annotated: bool = False, + label: str | None = None, + ctrl_state: int | str | None = None, + annotated: bool | None = None, ): """Return a (multi-)controlled-H gate. @@ -92,8 +95,8 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. Returns: ControlledGate: controlled version of this gate. diff --git a/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py b/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py index 98dedc05f5ef..6e31c99005b3 100644 --- a/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py +++ b/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py @@ -18,7 +18,7 @@ from typing import Optional, Union, Tuple, List import numpy as np -from qiskit.circuit import QuantumCircuit, QuantumRegister, Qubit +from qiskit.circuit import QuantumCircuit, QuantumRegister, Qubit, ParameterExpression from qiskit.circuit.library.standard_gates.x import MCXGate from qiskit.circuit.library.standard_gates.u3 import _generate_gray_code from qiskit.circuit.parameterexpression import ParameterValueType @@ -258,6 +258,9 @@ def mcrx( use_basis_gates=use_basis_gates, ) else: + if isinstance(theta, ParameterExpression): + raise QiskitError(f"Cannot synthesize MCRX with unbound parameter: {theta}.") + cgate = _mcsu2_real_diagonal( RXGate(theta).to_matrix(), num_controls=len(control_qubits), @@ -272,8 +275,8 @@ def mcry( q_controls: Union[QuantumRegister, List[Qubit]], q_target: Qubit, q_ancillae: Optional[Union[QuantumRegister, Tuple[QuantumRegister, int]]] = None, - mode: str = None, - use_basis_gates=False, + mode: Optional[str] = None, + use_basis_gates: bool = False, ): """ Apply Multiple-Controlled Y rotation gate @@ -333,6 +336,9 @@ def mcry( use_basis_gates=use_basis_gates, ) else: + if isinstance(theta, ParameterExpression): + raise QiskitError(f"Cannot synthesize MCRY with unbound parameter: {theta}.") + cgate = _mcsu2_real_diagonal( RYGate(theta).to_matrix(), num_controls=len(control_qubits), @@ -383,6 +389,9 @@ def mcrz( else: self.append(CRZGate(lam), control_qubits + [target_qubit]) else: + if isinstance(lam, ParameterExpression): + raise QiskitError(f"Cannot synthesize MCRZ with unbound parameter: {lam}.") + cgate = _mcsu2_real_diagonal( RZGate(lam).to_matrix(), num_controls=len(control_qubits), diff --git a/qiskit/circuit/library/standard_gates/p.py b/qiskit/circuit/library/standard_gates/p.py index 8c83aa464027..cb2c19bf51e9 100644 --- a/qiskit/circuit/library/standard_gates/p.py +++ b/qiskit/circuit/library/standard_gates/p.py @@ -99,7 +99,7 @@ def control( num_ctrl_qubits: int = 1, label: str | None = None, ctrl_state: str | int | None = None, - annotated: bool = False, + annotated: bool | None = None, ): """Return a (multi-)controlled-Phase gate. @@ -108,8 +108,8 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. Returns: ControlledGate: controlled version of this gate. @@ -255,7 +255,7 @@ def control( num_ctrl_qubits: int = 1, label: str | None = None, ctrl_state: str | int | None = None, - annotated: bool = False, + annotated: bool | None = None, ): """Controlled version of this gate. @@ -264,8 +264,8 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. Returns: ControlledGate: controlled version of this gate. @@ -396,7 +396,7 @@ def control( num_ctrl_qubits: int = 1, label: str | None = None, ctrl_state: str | int | None = None, - annotated: bool = False, + annotated: bool | None = None, ): """Controlled version of this gate. @@ -405,8 +405,8 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. Returns: ControlledGate: controlled version of this gate. diff --git a/qiskit/circuit/library/standard_gates/rx.py b/qiskit/circuit/library/standard_gates/rx.py index cb851a740d28..4b8c9e6b446a 100644 --- a/qiskit/circuit/library/standard_gates/rx.py +++ b/qiskit/circuit/library/standard_gates/rx.py @@ -12,6 +12,8 @@ """Rotation around the X axis.""" +from __future__ import annotations + import math from math import pi from typing import Optional, Union @@ -20,7 +22,7 @@ from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit._accelerate.circuit import StandardGate @@ -78,9 +80,9 @@ def _define(self): def control( self, num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[str, int]] = None, - annotated: bool = False, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, ): """Return a (multi-)controlled-RX gate. @@ -89,16 +91,24 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters and more than one control qubit, in which + case it cannot yet be synthesized. Otherwise it is set to ``False``. Returns: ControlledGate: controlled version of this gate. """ + # deliberately capture annotated in [None, False] here if not annotated and num_ctrl_qubits == 1: gate = CRXGate(self.params[0], label=label, ctrl_state=ctrl_state) gate.base_gate.label = self.label else: + # If the gate parameters contain free parameters, we cannot eagerly synthesize + # the controlled gate decomposition. In this case, we annotate the gate per default. + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + gate = super().control( num_ctrl_qubits=num_ctrl_qubits, label=label, diff --git a/qiskit/circuit/library/standard_gates/rxx.py b/qiskit/circuit/library/standard_gates/rxx.py index 1c06ae05a85b..3b069aa933bb 100644 --- a/qiskit/circuit/library/standard_gates/rxx.py +++ b/qiskit/circuit/library/standard_gates/rxx.py @@ -11,12 +11,15 @@ # that they have been altered from the originals. """Two-qubit XX-rotation gate.""" + +from __future__ import annotations + import math from typing import Optional import numpy from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit._accelerate.circuit import StandardGate @@ -111,6 +114,39 @@ def _define(self): self.definition = qc + def control( + self, + num_ctrl_qubits: int = 1, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, + ): + """Return a (multi-)controlled-RXX gate. + + Args: + num_ctrl_qubits: number of control qubits. + label: An optional label for the gate [Default: ``None``] + ctrl_state: control state expressed as integer, + string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters, in which case it cannot + yet be synthesized. + + Returns: + ControlledGate: controlled version of this gate. + """ + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + + gate = super().control( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + annotated=annotated, + ) + return gate + def inverse(self, annotated: bool = False): """Return inverse RXX gate (i.e. with the negative rotation angle). diff --git a/qiskit/circuit/library/standard_gates/ry.py b/qiskit/circuit/library/standard_gates/ry.py index b60b34ffde6f..614d4ef13a0f 100644 --- a/qiskit/circuit/library/standard_gates/ry.py +++ b/qiskit/circuit/library/standard_gates/ry.py @@ -12,6 +12,8 @@ """Rotation around the Y axis.""" +from __future__ import annotations + import math from math import pi from typing import Optional, Union @@ -19,7 +21,7 @@ from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit._accelerate.circuit import StandardGate @@ -77,9 +79,9 @@ def _define(self): def control( self, num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[str, int]] = None, - annotated: bool = False, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, ): """Return a (multi-)controlled-RY gate. @@ -88,16 +90,24 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters and more than one control qubit, in which + case it cannot yet be synthesized. Otherwise it is set to ``False``. Returns: ControlledGate: controlled version of this gate. """ + # deliberately capture annotated in [None, False] here if not annotated and num_ctrl_qubits == 1: gate = CRYGate(self.params[0], label=label, ctrl_state=ctrl_state) gate.base_gate.label = self.label else: + # If the gate parameters contain free parameters, we cannot eagerly synthesize + # the controlled gate decomposition. In this case, we annotate the gate per default. + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + gate = super().control( num_ctrl_qubits=num_ctrl_qubits, label=label, diff --git a/qiskit/circuit/library/standard_gates/ryy.py b/qiskit/circuit/library/standard_gates/ryy.py index 91d7d8096cf9..ad185e88d04b 100644 --- a/qiskit/circuit/library/standard_gates/ryy.py +++ b/qiskit/circuit/library/standard_gates/ryy.py @@ -11,12 +11,15 @@ # that they have been altered from the originals. """Two-qubit YY-rotation gate.""" + +from __future__ import annotations + import math from typing import Optional import numpy as np from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit._accelerate.circuit import StandardGate @@ -111,6 +114,39 @@ def _define(self): self.definition = qc + def control( + self, + num_ctrl_qubits: int = 1, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, + ): + """Return a (multi-)controlled-YY gate. + + Args: + num_ctrl_qubits: number of control qubits. + label: An optional label for the gate [Default: ``None``] + ctrl_state: control state expressed as integer, + string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters, in which case it cannot + yet be synthesized. + + Returns: + ControlledGate: controlled version of this gate. + """ + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + + gate = super().control( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + annotated=annotated, + ) + return gate + def inverse(self, annotated: bool = False): """Return inverse RYY gate (i.e. with the negative rotation angle). diff --git a/qiskit/circuit/library/standard_gates/rz.py b/qiskit/circuit/library/standard_gates/rz.py index 78cf20efa5c6..3abef37b7534 100644 --- a/qiskit/circuit/library/standard_gates/rz.py +++ b/qiskit/circuit/library/standard_gates/rz.py @@ -11,12 +11,15 @@ # that they have been altered from the originals. """Rotation around the Z axis.""" + +from __future__ import annotations + from cmath import exp from typing import Optional, Union from qiskit.circuit.gate import Gate from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit._accelerate.circuit import StandardGate @@ -88,9 +91,9 @@ def _define(self): def control( self, num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[str, int]] = None, - annotated: bool = False, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, ): """Return a (multi-)controlled-RZ gate. @@ -99,16 +102,24 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters and more than one control qubit, in which + case it cannot yet be synthesized. Otherwise it is set to ``False``. Returns: ControlledGate: controlled version of this gate. """ + # deliberately capture annotated in [None, False] here if not annotated and num_ctrl_qubits == 1: gate = CRZGate(self.params[0], label=label, ctrl_state=ctrl_state) gate.base_gate.label = self.label else: + # If the gate parameters contain free parameters, we cannot eagerly synthesize + # the controlled gate decomposition. In this case, we annotate the gate per default. + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + gate = super().control( num_ctrl_qubits=num_ctrl_qubits, label=label, diff --git a/qiskit/circuit/library/standard_gates/rzx.py b/qiskit/circuit/library/standard_gates/rzx.py index 90e7b71c0a33..003805cc6b55 100644 --- a/qiskit/circuit/library/standard_gates/rzx.py +++ b/qiskit/circuit/library/standard_gates/rzx.py @@ -11,11 +11,14 @@ # that they have been altered from the originals. """Two-qubit ZX-rotation gate.""" + +from __future__ import annotations + import math from typing import Optional from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit._accelerate.circuit import StandardGate @@ -155,6 +158,39 @@ def _define(self): self.definition = qc + def control( + self, + num_ctrl_qubits: int = 1, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, + ): + """Return a (multi-)controlled-RZX gate. + + Args: + num_ctrl_qubits: number of control qubits. + label: An optional label for the gate [Default: ``None``] + ctrl_state: control state expressed as integer, + string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters, in which case it cannot + yet be synthesized. + + Returns: + ControlledGate: controlled version of this gate. + """ + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + + gate = super().control( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + annotated=annotated, + ) + return gate + def inverse(self, annotated: bool = False): """Return inverse RZX gate (i.e. with the negative rotation angle). diff --git a/qiskit/circuit/library/standard_gates/rzz.py b/qiskit/circuit/library/standard_gates/rzz.py index 554ad4954a31..ca3e6d2db2da 100644 --- a/qiskit/circuit/library/standard_gates/rzz.py +++ b/qiskit/circuit/library/standard_gates/rzz.py @@ -11,11 +11,14 @@ # that they have been altered from the originals. """Two-qubit ZZ-rotation gate.""" + +from __future__ import annotations + from cmath import exp from typing import Optional from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit._accelerate.circuit import StandardGate @@ -119,6 +122,39 @@ def _define(self): self.definition = qc + def control( + self, + num_ctrl_qubits: int = 1, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, + ): + """Return a (multi-)controlled-RZZ gate. + + Args: + num_ctrl_qubits: number of control qubits. + label: An optional label for the gate [Default: ``None``] + ctrl_state: control state expressed as integer, + string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters, in which case it cannot + yet be synthesized. + + Returns: + ControlledGate: controlled version of this gate. + """ + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + + gate = super().control( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + annotated=annotated, + ) + return gate + def inverse(self, annotated: bool = False): """Return inverse RZZ gate (i.e. with the negative rotation angle). diff --git a/qiskit/circuit/library/standard_gates/s.py b/qiskit/circuit/library/standard_gates/s.py index 975d1cb3be8c..e859de4b5013 100644 --- a/qiskit/circuit/library/standard_gates/s.py +++ b/qiskit/circuit/library/standard_gates/s.py @@ -12,6 +12,8 @@ """The S, Sdg, CS and CSdg gates.""" +from __future__ import annotations + from math import pi from typing import Optional, Union @@ -83,6 +85,39 @@ def _define(self): self.definition = qc + def control( + self, + num_ctrl_qubits: int = 1, + label: str | None = None, + ctrl_state: int | str | None = None, + annotated: bool | None = None, + ): + """Return a (multi-)controlled-S gate. + + One control qubit returns a :class:`.CSGate`. + + Args: + num_ctrl_qubits: number of control qubits. + label: An optional label for the gate [Default: ``None``] + ctrl_state: control state expressed as integer, + string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. + + Returns: + ControlledGate: controlled version of this gate. + """ + if not annotated and num_ctrl_qubits == 1: + gate = CSGate(label=label, ctrl_state=ctrl_state, _base_label=self.label) + else: + gate = super().control( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + annotated=annotated, + ) + return gate + def inverse(self, annotated: bool = False): """Return inverse of S (SdgGate). @@ -162,6 +197,39 @@ def _define(self): self.definition = qc + def control( + self, + num_ctrl_qubits: int = 1, + label: str | None = None, + ctrl_state: int | str | None = None, + annotated: bool | None = None, + ): + """Return a (multi-)controlled-Sdg gate. + + One control qubit returns a :class:`.CSdgGate`. + + Args: + num_ctrl_qubits: number of control qubits. + label: An optional label for the gate [Default: ``None``] + ctrl_state: control state expressed as integer, + string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. + + Returns: + ControlledGate: controlled version of this gate. + """ + if not annotated and num_ctrl_qubits == 1: + gate = CSdgGate(label=label, ctrl_state=ctrl_state, _base_label=self.label) + else: + gate = super().control( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + annotated=annotated, + ) + return gate + def inverse(self, annotated: bool = False): """Return inverse of Sdg (SGate). diff --git a/qiskit/circuit/library/standard_gates/swap.py b/qiskit/circuit/library/standard_gates/swap.py index 5d33bc74b8d0..84ef3046746d 100644 --- a/qiskit/circuit/library/standard_gates/swap.py +++ b/qiskit/circuit/library/standard_gates/swap.py @@ -12,6 +12,8 @@ """Swap gate.""" +from __future__ import annotations + from typing import Optional, Union import numpy from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key @@ -90,9 +92,9 @@ def _define(self): def control( self, num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[str, int]] = None, - annotated: bool = False, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, ): """Return a (multi-)controlled-SWAP gate. @@ -103,8 +105,8 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. Returns: ControlledGate: controlled version of this gate. diff --git a/qiskit/circuit/library/standard_gates/sx.py b/qiskit/circuit/library/standard_gates/sx.py index ec3c87653148..ec1f57a83bd3 100644 --- a/qiskit/circuit/library/standard_gates/sx.py +++ b/qiskit/circuit/library/standard_gates/sx.py @@ -12,6 +12,8 @@ """Sqrt(X) and C-Sqrt(X) gates.""" +from __future__ import annotations + from math import pi from typing import Optional, Union from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key @@ -104,9 +106,9 @@ def inverse(self, annotated: bool = False): def control( self, num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[str, int]] = None, - annotated: bool = False, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, ): """Return a (multi-)controlled-SX gate. @@ -117,8 +119,8 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is handled as ``False``. Returns: SingletonControlledGate: controlled version of this gate. diff --git a/qiskit/circuit/library/standard_gates/u.py b/qiskit/circuit/library/standard_gates/u.py index 7f1d32eb914c..bed454897929 100644 --- a/qiskit/circuit/library/standard_gates/u.py +++ b/qiskit/circuit/library/standard_gates/u.py @@ -11,6 +11,9 @@ # that they have been altered from the originals. """Two-pulse single-qubit gate.""" + +from __future__ import annotations + import cmath import copy as _copy import math @@ -19,7 +22,7 @@ import numpy from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit.circuit.quantumregister import QuantumRegister from qiskit._accelerate.circuit import StandardGate @@ -103,9 +106,9 @@ def inverse(self, annotated: bool = False): def control( self, num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[str, int]] = None, - annotated: bool = False, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, ): """Return a (multi-)controlled-U gate. @@ -114,8 +117,10 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters and more than one control qubit, in which + case it cannot yet be synthesized. Otherwise it is set to ``False``. Returns: ControlledGate: controlled version of this gate. @@ -131,6 +136,11 @@ def control( ) gate.base_gate.label = self.label else: + # If the gate parameters contain free parameters, we cannot eagerly synthesize + # the controlled gate decomposition. In this case, we annotate the gate per default. + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + gate = super().control( num_ctrl_qubits=num_ctrl_qubits, label=label, diff --git a/qiskit/circuit/library/standard_gates/u1.py b/qiskit/circuit/library/standard_gates/u1.py index e62a132670ff..e9bbed871d1f 100644 --- a/qiskit/circuit/library/standard_gates/u1.py +++ b/qiskit/circuit/library/standard_gates/u1.py @@ -128,7 +128,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: @@ -278,7 +278,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: @@ -410,7 +410,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: diff --git a/qiskit/circuit/library/standard_gates/u3.py b/qiskit/circuit/library/standard_gates/u3.py index 80581bf55a5d..df229af7d819 100644 --- a/qiskit/circuit/library/standard_gates/u3.py +++ b/qiskit/circuit/library/standard_gates/u3.py @@ -11,13 +11,16 @@ # that they have been altered from the originals. """Two-pulse single-qubit gate.""" + +from __future__ import annotations + import math from cmath import exp from typing import Optional, Union import numpy from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit.circuit.quantumregister import QuantumRegister from qiskit._accelerate.circuit import StandardGate @@ -115,9 +118,9 @@ def inverse(self, annotated: bool = False): def control( self, num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[str, int]] = None, - annotated: bool = False, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, ): """Return a (multi-)controlled-U3 gate. @@ -126,8 +129,10 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented - as an annotated gate. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters and more than one control qubit, in which + case it cannot yet be synthesized. Otherwise it is set to ``False``. Returns: ControlledGate: controlled version of this gate. @@ -136,6 +141,11 @@ def control( gate = CU3Gate(*self.params, label=label, ctrl_state=ctrl_state) gate.base_gate.label = self.label else: + # If the gate parameters contain free parameters, we cannot eagerly synthesize + # the controlled gate decomposition. In this case, we annotate the gate per default. + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + gate = super().control( num_ctrl_qubits=num_ctrl_qubits, label=label, diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py index 3688d376538a..f3f7b5ebdb72 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -112,7 +112,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: @@ -257,7 +257,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: @@ -453,7 +453,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: @@ -801,7 +801,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: @@ -1047,7 +1047,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: @@ -1222,7 +1222,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: diff --git a/qiskit/circuit/library/standard_gates/xx_minus_yy.py b/qiskit/circuit/library/standard_gates/xx_minus_yy.py index db3c3dc89153..2fac02fd154d 100644 --- a/qiskit/circuit/library/standard_gates/xx_minus_yy.py +++ b/qiskit/circuit/library/standard_gates/xx_minus_yy.py @@ -11,6 +11,9 @@ # that they have been altered from the originals. """Two-qubit XX-YY gate.""" + +from __future__ import annotations + import math from cmath import exp from math import pi @@ -24,7 +27,7 @@ from qiskit.circuit.library.standard_gates.s import SdgGate, SGate from qiskit.circuit.library.standard_gates.sx import SXdgGate, SXGate from qiskit.circuit.library.standard_gates.x import CXGate -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister from qiskit._accelerate.circuit import StandardGate @@ -156,6 +159,39 @@ def _define(self): self.definition = circuit + def control( + self, + num_ctrl_qubits: int = 1, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, + ): + """Return a (multi-)controlled-(XX-YY) gate. + + Args: + num_ctrl_qubits: number of control qubits. + label: An optional label for the gate [Default: ``None``] + ctrl_state: control state expressed as integer, + string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters, in which case it cannot + yet be synthesized. + + Returns: + ControlledGate: controlled version of this gate. + """ + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + + gate = super().control( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + annotated=annotated, + ) + return gate + def inverse(self, annotated: bool = False): """Inverse gate. diff --git a/qiskit/circuit/library/standard_gates/xx_plus_yy.py b/qiskit/circuit/library/standard_gates/xx_plus_yy.py index 7920454d0b98..e0528a1f1792 100644 --- a/qiskit/circuit/library/standard_gates/xx_plus_yy.py +++ b/qiskit/circuit/library/standard_gates/xx_plus_yy.py @@ -11,6 +11,9 @@ # that they have been altered from the originals. """Two-qubit XX+YY gate.""" + +from __future__ import annotations + import math from cmath import exp from math import pi @@ -20,7 +23,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression from qiskit._accelerate.circuit import StandardGate @@ -160,6 +163,39 @@ def _define(self): self.definition = qc + def control( + self, + num_ctrl_qubits: int = 1, + label: str | None = None, + ctrl_state: str | int | None = None, + annotated: bool | None = None, + ): + """Return a (multi-)controlled-(XX+YY) gate. + + Args: + num_ctrl_qubits: number of control qubits. + label: An optional label for the gate [Default: ``None``] + ctrl_state: control state expressed as integer, + string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + annotated: indicates whether the controlled gate should be implemented + as an annotated gate. If ``None``, this is set to ``True`` if + the gate contains free parameters, in which case it cannot + yet be synthesized. + + Returns: + ControlledGate: controlled version of this gate. + """ + if annotated is None: + annotated = any(isinstance(p, ParameterExpression) for p in self.params) + + gate = super().control( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + annotated=annotated, + ) + return gate + def inverse(self, annotated: bool = False): """Return inverse XX+YY gate (i.e. with the negative rotation angle and same phase angle). diff --git a/qiskit/circuit/library/standard_gates/y.py b/qiskit/circuit/library/standard_gates/y.py index d62586aa2b9b..99d37ee08bd3 100644 --- a/qiskit/circuit/library/standard_gates/y.py +++ b/qiskit/circuit/library/standard_gates/y.py @@ -108,7 +108,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: diff --git a/qiskit/circuit/library/standard_gates/z.py b/qiskit/circuit/library/standard_gates/z.py index 4b2364178a94..dd83c3833d62 100644 --- a/qiskit/circuit/library/standard_gates/z.py +++ b/qiskit/circuit/library/standard_gates/z.py @@ -112,7 +112,7 @@ def control( label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index be8b66b38757..3b2da762c51f 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1717,7 +1717,7 @@ def control( label (str): An optional label to give the controlled operation for visualization. ctrl_state (str or int): The control state in decimal or as a bitstring (e.g. '111'). If None, use ``2**num_ctrl_qubits - 1``. - annotated: indicates whether the controlled gate can be implemented + annotated: indicates whether the controlled gate should be implemented as an annotated gate. Returns: diff --git a/releasenotes/notes/annotated-params-116288d5628f7ee8.yaml b/releasenotes/notes/annotated-params-116288d5628f7ee8.yaml new file mode 100644 index 000000000000..2f316a06ae8d --- /dev/null +++ b/releasenotes/notes/annotated-params-116288d5628f7ee8.yaml @@ -0,0 +1,30 @@ +--- +features_circuits: + - | + Added support for :meth:`.AnnotatedOperation.params` and + :meth:`.AnnotatedOperation.validate_parameter`, which enable + circuit-level parameter handling (such as binding parameters) for + annotated operations. +fixes: + - | + Fixed a series of issues when controlling parameterized standard gates. + The controlled version of some gates (e.g. :class:`.RXXGate` or + :class:`.RYGate` for more than 1 control) cannot be synthesized if + they contain unbound parameters. Previously, calling ``.control()`` but + now we create an :class:`.AnnotatedOperation` as placeholder. This + allows to insert the controlled gate into a circuit, bind the parameters + at a later stage, and then synthesize the operation. + Fixes `#10311 `_, + `#10697 `_, + and `#12135 `_. + - | + The :class:`.SGate` and :class:`.SdgGate` now correctly return a + :class:`.CSGate`, resp. :class:`.CSdgGate`, if they are controlled on + a single control qubit. +upgrade_circuits: + - | + The ``annotated`` argument of the :meth:`.Gate.control` method is now + ``None``, which allows Qiskit to choose whether to annotate a controlled operation. + If the concrete implementation (``annotated=False``) is available, it will be returned by + default. Otherwise, the annotated implementation will be returned (``annotated=True``). + This allows, for example, to defer the synthesis of controlled, parameterized gates. diff --git a/test/python/circuit/test_annotated_operation.py b/test/python/circuit/test_annotated_operation.py index f4228fc0485f..e2ca9f4af9a4 100644 --- a/test/python/circuit/test_annotated_operation.py +++ b/test/python/circuit/test_annotated_operation.py @@ -14,6 +14,7 @@ import unittest +from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit._utils import _compute_control_matrix from qiskit.circuit.annotated_operation import ( AnnotatedOperation, @@ -22,7 +23,7 @@ PowerModifier, _canonicalize_modifiers, ) -from qiskit.circuit.library import SGate, SdgGate +from qiskit.circuit.library import SGate, SdgGate, UGate, RXGate from qiskit.quantum_info import Operator from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -157,6 +158,52 @@ def test_canonicalize_inverse(self): expected_list = [] self.assertEqual(canonical_list, expected_list) + def test_params_access(self): + """Test access to the params field.""" + p, q = Parameter("p"), Parameter("q") + params = [0.2, -1, p] + gate = UGate(*params) + annotated = gate.control(10, annotated=True) + + with self.subTest(msg="reading params"): + self.assertListEqual(annotated.params, params) + + new_params = [q, 131, -1.2] + with self.subTest(msg="setting params"): + annotated.params = new_params + self.assertListEqual(annotated.params, new_params) + + def test_binding_annotated_gate(self): + """Test binding an annotated gate in a circuit.""" + p = Parameter("p") + annotated = RXGate(p).control(2, annotated=True) + circuit = QuantumCircuit(annotated.num_qubits) + circuit.h(circuit.qubits) + circuit.append(annotated, circuit.qubits) + + with self.subTest(msg="test parameter is reported"): + self.assertEqual(circuit.num_parameters, 1) + + with self.subTest(msg="test binding parameters worked"): + bound = circuit.assign_parameters([0.321]) + self.assertEqual(bound.num_parameters, 0) + + def test_invalid_params_access(self): + """Test params access to a operation not providing params.""" + op = Operator(SGate()) + annotated = AnnotatedOperation(op, InverseModifier()) + + with self.subTest(msg="accessing params returns an empty list"): + self.assertEqual(len(annotated.params), 0) + + with self.subTest(msg="setting params fails"): + with self.assertRaises(AttributeError): + annotated.params = [1.2] + + with self.subTest(msg="validating params fails"): + with self.assertRaises(AttributeError): + _ = annotated.validate_parameter(1.2) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index 6d7b237915fa..707f9d32cb94 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -19,7 +19,7 @@ from numpy import pi from ddt import ddt, data, unpack -from qiskit import QuantumRegister, QuantumCircuit, QiskitError +from qiskit import QuantumRegister, QuantumCircuit, QiskitError, transpile from qiskit.circuit import ControlledGate, Parameter, Gate from qiskit.circuit.annotated_operation import AnnotatedOperation from qiskit.circuit.singleton import SingletonControlledGate, _SingletonControlledGateOverrides @@ -46,9 +46,13 @@ CCXGate, HGate, RZGate, + RYGate, RXGate, + RZZGate, + RZXGate, + RYYGate, + RXXGate, CPhaseGate, - RYGate, CRYGate, CRXGate, CSwapGate, @@ -73,6 +77,8 @@ C3SXGate, C4XGate, MCPhaseGate, + XXMinusYYGate, + XXPlusYYGate, GlobalPhaseGate, UnitaryGate, ) @@ -761,7 +767,6 @@ def test_small_mcx_gates_yield_cx_count(self, num_ctrl_qubits): yields the expected number of cx gates.""" qc = QuantumCircuit(num_ctrl_qubits + 1) qc.append(MCXGate(num_ctrl_qubits), range(num_ctrl_qubits + 1)) - from qiskit import transpile cqc = transpile(qc, basis_gates=["u", "cx"]) cx_count = cqc.count_ops()["cx"] @@ -808,8 +813,6 @@ def test_mcx_gates(self, num_ctrl_qubits): def test_mcxvchain_dirty_ancilla_cx_count(self, num_ctrl_qubits): """Test if cx count of the v-chain mcx with dirty ancilla is less than upper bound.""" - from qiskit import transpile - mcx_vchain = MCXVChain(num_ctrl_qubits, dirty_ancillas=True) qc = QuantumCircuit(mcx_vchain.num_qubits) @@ -824,8 +827,6 @@ def test_mcxvchain_dirty_ancilla_cx_count(self, num_ctrl_qubits): def test_mcxrecursive_clean_ancilla_cx_count(self, num_ctrl_qubits): """Test if cx count of the mcx with one clean ancilla is less than upper bound.""" - from qiskit import transpile - mcx_recursive = MCXRecursive(num_ctrl_qubits) qc = QuantumCircuit(mcx_recursive.num_qubits) @@ -1491,6 +1492,45 @@ def test_control_zero_operand_gate(self, num_ctrl_qubits): target.flat[-1] = -1 self.assertEqual(Operator(controlled), Operator(target)) + @data( + RXGate, + RYGate, + RZGate, + RXXGate, + RYYGate, + RZXGate, + RZZGate, + UGate, + U3Gate, + XXMinusYYGate, + XXPlusYYGate, + ) + def test_mc_failure_without_annotation(self, gate_cls): + """Test error for gates that cannot be multi-controlled without annotation.""" + theta = Parameter("theta") + num_params = len(_get_free_params(gate_cls.__init__, ignore=["self"])) + params = [theta] + (num_params - 1) * [1.234] + + for annotated in [False, None]: + with self.subTest(annotated=annotated): + # if annotated is False, check that a sensible error is raised + if annotated is False: + with self.assertRaisesRegex(QiskitError, "unbound parameter"): + _ = gate_cls(*params).control(5, annotated=False) + + # else, check that the gate can be synthesized after all parameters + # have been bound + else: + mc_gate = gate_cls(*params).control(5) + + circuit = QuantumCircuit(mc_gate.num_qubits) + circuit.append(mc_gate, circuit.qubits) + + bound = circuit.assign_parameters([0.5123]) + unrolled = transpile(bound, basis_gates=["u", "cx"], optimization_level=0) + + self.assertEqual(unrolled.num_parameters, 0) + def assertEqualTranslated(self, circuit, unrolled_reference, basis): """Assert that the circuit is equal to the unrolled reference circuit.""" unroller = UnrollCustomDefinitions(std_eqlib, basis)