Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Annotated operations #9846

Merged
merged 30 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
774005a
Defining and synthesizing AnnotatedOperation
alexanderivrii Mar 23, 2023
ecdd73c
reworking annotated operation tests
alexanderivrii Mar 23, 2023
24b3a9d
cleaning up annotated operation
alexanderivrii Mar 23, 2023
bc2fa67
running black
alexanderivrii Mar 23, 2023
b2d432a
lint
alexanderivrii Mar 23, 2023
f51a524
Merge branch 'main' into annotated-operations
alexanderivrii Mar 29, 2023
75d4dbe
adding recurse argument and fixing the way definition is handled
alexanderivrii Mar 29, 2023
874e863
treating the case when there is no definition
alexanderivrii Mar 29, 2023
ce6cd79
lint
alexanderivrii Mar 30, 2023
19e3c6c
Merge branch 'main' into annotated-operations
alexanderivrii Mar 30, 2023
a5aedd1
black
alexanderivrii Mar 30, 2023
5d25348
removing duplicated line resulting from merge
alexanderivrii Mar 31, 2023
f9152c8
improving comments
alexanderivrii Mar 31, 2023
c02449f
Merge branch 'main' into annotated-operations
alexanderivrii Jul 11, 2023
58cf9d3
fix to merge conflicts
alexanderivrii Jul 11, 2023
6fedcfa
Merge branch 'main' into annotated-operations
alexanderivrii Aug 13, 2023
623dfbc
black; additional fixes for prelim support of coupling map with annot…
alexanderivrii Aug 13, 2023
d9d59de
calling substitude_node with propagate_conditions=False
alexanderivrii Aug 13, 2023
e458584
black
alexanderivrii Aug 13, 2023
4e028d3
merge with main
alexanderivrii Sep 28, 2023
1c71712
removing unused imports
alexanderivrii Sep 28, 2023
77127e4
improving documentation of AnnotatedOperation
alexanderivrii Sep 28, 2023
ebfd0f2
minor
alexanderivrii Sep 29, 2023
f23d384
release notes
alexanderivrii Sep 29, 2023
4fcd575
release notes typos
alexanderivrii Sep 29, 2023
02d53ef
removing propagate_conditions
alexanderivrii Oct 4, 2023
dfc4da1
Merge branch 'main' into annotated-operations
alexanderivrii Oct 4, 2023
c77b810
applying suggestions from review
alexanderivrii Oct 5, 2023
fd0d7be
tests for full transpiler flow, with and without backend
alexanderivrii Oct 5, 2023
ba42605
not replace dag.op when op did not change
alexanderivrii Oct 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 188 additions & 0 deletions qiskit/circuit/annotated_operation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2023.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Annotated Operations."""

import dataclasses
from typing import Union, List

from qiskit.circuit.operation import Operation
from qiskit.circuit._utils import _compute_control_matrix, _ctrl_state_to_int
from qiskit.circuit.exceptions import CircuitError


class Modifier:
"""The base class that all modifiers of :class:`~.AnnotatedOperation` should
inherit from."""

pass


@dataclasses.dataclass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this have any meaning for this class as there are no attributes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The __repr()__ function is a bit different: for instance print(InverseModifier()) outputs <qiskit.circuit.annotated_operation.InverseModifier object at 0x00000233A87C0160> without the decorator, and InverseModifier() with the decorator. This is not a big deal, but it would probably make sense to keep this consistent across different modifiers.

class InverseModifier(Modifier):
"""Inverse modifier: specifies that the operation is inverted."""

pass


@dataclasses.dataclass
class ControlModifier(Modifier):
"""Control modifier: specifies that the operation is controlled by ``num_ctrl_qubits``
and has control state ``ctrl_state``."""

num_ctrl_qubits: int = 0
ctrl_state: Union[int, str, None] = None

def __init__(self, num_ctrl_qubits: int = 0, ctrl_state: Union[int, str, None] = None):
self.num_ctrl_qubits = num_ctrl_qubits
self.ctrl_state = _ctrl_state_to_int(ctrl_state, num_ctrl_qubits)


@dataclasses.dataclass
class PowerModifier(Modifier):
"""Power modifier: specifies that the operation is raised to the power ``power``."""

power: float


class AnnotatedOperation(Operation):
"""Annotated operation."""

def __init__(self, base_op: Operation, modifiers: Union[Modifier, List[Modifier]]):
"""
Create a new AnnotatedOperation.

An "annotated operation" allows to add a list of modifiers to the
"base" operation. For now, the only supported modifiers are of
types :class:`~.InverseModifier`, :class:`~.ControlModifier` and
:class:`~.PowerModifier`.

An annotated operation can be viewed as an extension of
:class:`~.ControlledGate` (which also allows adding control to the
base operation). However, an important difference is that the
circuit definition of an annotated operation is not constructed when
the operation is declared, and instead happens during transpilation,
specifically during the :class:`~.HighLevelSynthesis` transpiler pass.

An annotated operation can be also viewed as a "higher-level"
or "more abstract" object that can be added to a quantum circuit.
This enables writing transpiler optimization passes that make use of
this higher-level representation, for instance removing a gate
that is immediately followed by its inverse.

Args:
base_op: base operation being modified
modifiers: ordered list of modifiers. Supported modifiers include
``InverseModifier``, ``ControlModifier`` and ``PowerModifier``.

Examples::

op1 = AnnotatedOperation(SGate(), [InverseModifier(), ControlModifier(2)])

op2_inner = AnnotatedGate(SGate(), InverseModifier())
op2 = AnnotatedGate(op2_inner, ControlModifier(2))

Both op1 and op2 are semantically equivalent to an ``SGate()`` which is first
inverted and then controlled by 2 qubits.
"""
self.base_op = base_op
self.modifiers = modifiers if isinstance(modifiers, List) else [modifiers]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize you could use the type classes from typing in isinstance checks like this. Looking at the cPython code it just wraps the underlying list type in an alias class and the __subclasscheck__ just compares against the inner list type if there given class isn't also an alias.


@property
def name(self):
"""Unique string identifier for operation type."""
return "annotated"

@property
def num_qubits(self):
"""Number of qubits."""
num_ctrl_qubits = 0
for modifier in self.modifiers:
if isinstance(modifier, ControlModifier):
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
num_ctrl_qubits += modifier.num_ctrl_qubits

return num_ctrl_qubits + self.base_op.num_qubits

@property
def num_clbits(self):
"""Number of classical bits."""
return self.base_op.num_clbits

def __eq__(self, other) -> bool:
"""Checks if two AnnotatedOperations are equal."""
return (
isinstance(other, AnnotatedOperation)
and self.modifiers == other.modifiers
and self.base_op == other.base_op
)

def copy(self) -> "AnnotatedOperation":
"""Return a copy of the :class:`~.AnnotatedOperation`."""
return AnnotatedOperation(base_op=self.base_op, modifiers=self.modifiers.copy())

def to_matrix(self):
"""Return a matrix representation (allowing to construct Operator)."""
from qiskit.quantum_info.operators import Operator # pylint: disable=cyclic-import

operator = Operator(self.base_op)

for modifier in self.modifiers:
if isinstance(modifier, InverseModifier):
operator = operator.power(-1)
elif isinstance(modifier, ControlModifier):
operator = Operator(
_compute_control_matrix(
operator.data, modifier.num_ctrl_qubits, modifier.ctrl_state
)
)
elif isinstance(modifier, PowerModifier):
operator = operator.power(modifier.power)
else:
raise CircuitError(f"Unknown modifier {modifier}.")
return operator


def _canonicalize_modifiers(modifiers):
"""
Returns the canonical representative of the modifier list. This is possible
since all the modifiers commute; also note that InverseModifier is a special
case of PowerModifier. The current solution is to compute the total number
of control qubits / control state and the total power. The InverseModifier
will be present if total power is negative, whereas the power modifier will
be present only with positive powers different from 1.
"""
power = 1
num_ctrl_qubits = 0
ctrl_state = 0

for modifier in modifiers:
if isinstance(modifier, InverseModifier):
power *= -1
elif isinstance(modifier, ControlModifier):
num_ctrl_qubits += modifier.num_ctrl_qubits
ctrl_state = (ctrl_state << modifier.num_ctrl_qubits) | modifier.ctrl_state
elif isinstance(modifier, PowerModifier):
power *= modifier.power
else:
raise CircuitError(f"Unknown modifier {modifier}.")

canonical_modifiers = []
if power < 0:
canonical_modifiers.append(InverseModifier())
power *= -1

if power != 1:
canonical_modifiers.append(PowerModifier(power))
if num_ctrl_qubits > 0:
canonical_modifiers.append(ControlModifier(num_ctrl_qubits, ctrl_state))

return canonical_modifiers
12 changes: 11 additions & 1 deletion qiskit/converters/circuit_to_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,21 @@


"""Helper function for converting a circuit to a gate"""
from qiskit.circuit.annotated_operation import AnnotatedOperation
from qiskit.circuit.gate import Gate
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit.exceptions import QiskitError


def _check_is_gate(op):
"""Checks whether op can be converted to Gate."""
if isinstance(op, Gate):
return True
elif isinstance(op, AnnotatedOperation):
return _check_is_gate(op.base_op)
return False


def circuit_to_gate(circuit, parameter_map=None, equivalence_library=None, label=None):
"""Build a :class:`.Gate` object from a :class:`.QuantumCircuit`.

Expand Down Expand Up @@ -50,7 +60,7 @@ def circuit_to_gate(circuit, parameter_map=None, equivalence_library=None, label
raise QiskitError("Circuit with classical bits cannot be converted to gate.")

for instruction in circuit.data:
if not isinstance(instruction.operation, Gate):
if not _check_is_gate(instruction.operation):
raise QiskitError(
(
"One or more instructions cannot be converted to"
Expand Down
5 changes: 3 additions & 2 deletions qiskit/quantum_info/operators/operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,9 +706,10 @@ def _instruction_to_matrix(cls, obj):

# pylint: disable=cyclic-import
from qiskit.quantum_info import Clifford
from qiskit.circuit.annotated_operation import AnnotatedOperation

if not isinstance(obj, (Instruction, Clifford)):
raise QiskitError("Input is neither an Instruction nor Clifford.")
if not isinstance(obj, (Instruction, Clifford, AnnotatedOperation)):
raise QiskitError("Input is neither Instruction, Clifford or AnnotatedOperation.")
mat = None
if hasattr(obj, "to_matrix"):
# If instruction is a gate first we see if it has a
Expand Down
Loading