From 13fcd65503130c39a83a3b772cbb8a512e3ddd43 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 18 Feb 2025 14:24:08 +0000 Subject: [PATCH 1/3] Add representation of `box` This adds in the base `box` control-flow construction, with support for containing instructions and having a literal delay, like the `Delay` instruction. This supports basic output to OpenQASM 3, QPY and some rudimentary support in the text and mpl drawers. The transpiler largely handles things already, since control flow is handled generically in most places. Known issues: - QPY fails to round-trip the `unit` field from an instruction's duration. - We expect this to be able to accept stretches in its duration, just as `Delay` can, which will need a follow-up. - We expect `Box` to support "annotations" in a future release of Qiskit. - There is currently no way in OpenQASM 3 to represent a qubit that is idle during a `box` without inserting a magic instruction on it. - There's no support for import from OpenQASM 3 for `box` yet - that happens in different package (`qiskit-qasm3-import`) right now. - IBM backends don't claim support for `box` yet, so `transpile` against a backend will fail, though you can modify the `Target` to add the instruction manually. --- crates/circuit/src/dag_circuit.rs | 5 + crates/circuit/src/imports.rs | 2 + qiskit/circuit/__init__.py | 1 + qiskit/circuit/controlflow/__init__.py | 4 +- qiskit/circuit/controlflow/box.py | 163 +++++++++++++++++++++ qiskit/circuit/quantumcircuit.py | 118 ++++++++++++++- qiskit/dagcircuit/dagnode.py | 8 + qiskit/qasm3/ast.py | 16 ++ qiskit/qasm3/exporter.py | 40 +++-- qiskit/qasm3/printer.py | 11 ++ qiskit/qpy/binary_io/circuits.py | 10 +- qiskit/visualization/circuit/matplotlib.py | 9 +- qiskit/visualization/circuit/text.py | 10 +- 13 files changed, 372 insertions(+), 25 deletions(-) create mode 100644 qiskit/circuit/controlflow/box.py diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 0dbf6aff8979..d15efa935adc 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -2533,6 +2533,7 @@ def _format(operand): let condition_op_check = imports::CONDITION_OP_CHECK.get_bound(py); let switch_case_op_check = imports::SWITCH_CASE_OP_CHECK.get_bound(py); let for_loop_op_check = imports::FOR_LOOP_OP_CHECK.get_bound(py); + let box_op_check = imports::BOX_OP_CHECK.get_bound(py); let node_match = |n1: &NodeType, n2: &NodeType| -> PyResult { match [n1, n2] { [NodeType::Operation(inst1), NodeType::Operation(inst2)] => { @@ -2604,6 +2605,10 @@ def _format(operand): for_loop_op_check .call1((n1, n2, &self_bit_indices, &other_bit_indices))? .extract() + } else if name == "box" { + box_op_check + .call1((n1, n2, &self_bit_indices, &other_bit_indices))? + .extract() } else { Err(PyRuntimeError::new_err(format!( "unhandled control-flow operation: {}", diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index a591dfb1569e..3af6b3e07a46 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -110,6 +110,8 @@ pub static SWITCH_CASE_OP_CHECK: ImportOnceCell = ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_switch_case_eq"); pub static FOR_LOOP_OP_CHECK: ImportOnceCell = ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_for_loop_eq"); +pub static BOX_OP_CHECK: ImportOnceCell = + ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_box_eq"); pub static UUID: ImportOnceCell = ImportOnceCell::new("uuid", "UUID"); pub static BARRIER: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "Barrier"); pub static DELAY: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "Delay"); diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index 42cb8ddb83d1..8bb833b18464 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -1313,6 +1313,7 @@ def __array__(self, dtype=None, copy=None): from .controlflow import ( ControlFlowOp, + BoxOp, WhileLoopOp, ForLoopOp, IfElseOp, diff --git a/qiskit/circuit/controlflow/__init__.py b/qiskit/circuit/controlflow/__init__.py index 4f48e7229dab..aceebe32166e 100644 --- a/qiskit/circuit/controlflow/__init__.py +++ b/qiskit/circuit/controlflow/__init__.py @@ -18,13 +18,14 @@ from .continue_loop import ContinueLoopOp from .break_loop import BreakLoopOp +from .box import BoxOp from .if_else import IfElseOp from .while_loop import WhileLoopOp from .for_loop import ForLoopOp from .switch_case import SwitchCaseOp, CASE_DEFAULT -CONTROL_FLOW_OP_NAMES = frozenset(("for_loop", "while_loop", "if_else", "switch_case")) +CONTROL_FLOW_OP_NAMES = frozenset(("for_loop", "while_loop", "if_else", "switch_case", "box")) """Set of the instruction names of Qiskit's known control-flow operations.""" @@ -53,5 +54,6 @@ def get_control_flow_name_mapping(): "while_loop": WhileLoopOp, "for_loop": ForLoopOp, "switch_case": SwitchCaseOp, + "box": BoxOp, } return name_mapping diff --git a/qiskit/circuit/controlflow/box.py b/qiskit/circuit/controlflow/box.py new file mode 100644 index 000000000000..202c4f1f84cf --- /dev/null +++ b/qiskit/circuit/controlflow/box.py @@ -0,0 +1,163 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025. +# +# 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. + +"""Simple box basic block.""" + +from __future__ import annotations + +import typing + +from qiskit.circuit.exceptions import CircuitError +from .control_flow import ControlFlowOp + +if typing.TYPE_CHECKING: + from qiskit.circuit import QuantumCircuit + + +class BoxOp(ControlFlowOp): + """A scoped "box" of operations on a circuit that are treated atomically in the greater context. + + A "box" is a control-flow construct that is entered unconditionally. The contents of the box + behave somewhat as if the start and end of the box were barriers, except it is permissible to + commute operations "all the way" through the box. The box is also an explicit scope for the + purposes of variables, stretches and compiler passes. + + Typically you create this by using the builder-interface form of :meth:`.QuantumCircuit.box`. + """ + + def __init__( + self, + body: QuantumCircuit, + *, + duration: None = None, + unit: typing.Literal["dt", "s", "ms", "us", "ns", "ps"] = "dt", + label: str | None = None, + ): + """ + Default constructor of :class:`BoxOp`. + + Args: + body: the circuit to use as the body of the box. This should explicit close over any + :class:`.expr.Var` variables that must be incident from the outer circuit. The + expected number of qubit and clbits for the resulting instruction are inferred from + the number in the circuit, even if they are idle. + duration: an optional duration for the box as a whole. + unit: the unit of the ``duration``. + label: an optional string label for the instruction. + """ + super().__init__("box", body.num_qubits, body.num_clbits, [body], label=label) + self._duration = duration + self._unit = unit + + # The `duration` and `unit` properties are to override deprecation warnings in `Instruction`. + @property + def duration(self): + return self._duration + + @duration.setter + def duration(self, value): + self._duration = value + + @property + def unit(self): + return self._unit + + @unit.setter + def unit(self, value: typing.Literal["dt", "s", "ms", "us", "ns", "ps"]): + self._unit = value + + @property + def params(self): + return self._params + + @params.setter + def params(self, parameters): + # pylint: disable=cyclic-import + from qiskit.circuit import QuantumCircuit + + (body,) = parameters + + if not isinstance(body, QuantumCircuit): + raise CircuitError( + "BoxOp expects a body parameter of type " + f"QuantumCircuit, but received {type(body)}." + ) + + if body.num_qubits != self.num_qubits or body.num_clbits != self.num_clbits: + raise CircuitError( + "Attempted to assign a body parameter with a num_qubits or " + "num_clbits different than that of the BoxOp. " + f"BoxOp num_qubits/clbits: {self.num_qubits}/{self.num_clbits} " + f"Supplied body num_qubits/clbits: {body.num_qubits}/{body.num_clbits}." + ) + + self._params = [body] + + @property + def blocks(self): + return (self._params[0],) + + def replace_blocks(self, blocks): + (body,) = blocks + return BoxOp(body, duration=self.duration, label=self.label) + + +class BoxContext: + """Context-manager that powers :meth:`.QuantumCircuit.box`. + + This is not part of the public interface, and should not be instantiated by users. + """ + + __slots__ = ("_circuit", "_duration", "_unit", "_label") + + def __init__( + self, + circuit: QuantumCircuit, + *, + duration: None = None, + unit: typing.Literal["dt", "s", "ms", "us", "ns", "ps"] = "dt", + label: str | None = None, + ): + """ + Args: + circuit: the outermost scope of the circuit under construction. + duration: the final duration of the box. + unit: the unit of ``duration``. + label: an optional label for the box. + """ + self._circuit = circuit + self._duration = duration + self._unit = unit + self._label = label + + def __enter__(self): + # For a box to have the semantics of internal qubit alignment with a resolvable duration, we + # can't allow conditional jumps to exit it. Technically an unconditional `break` or + # `continue` could work, but we're not getting into that. + self._circuit._push_scope(allow_jumps=False) + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is not None: + # If we're leaving the context manager because an exception was raised, there's nothing + # to do except restore the circuit state. + self._circuit._pop_scope() + return False + scope = self._circuit._pop_scope() + # Boxes do not need to pass any further resources in, because there's no jumps out of a + # `box` permitted. + body = scope.build(scope.qubits(), scope.clbits()) + self._circuit.append( + BoxOp(body, duration=self._duration, unit=self._unit, label=self._label), + body.qubits, + body.clbits, + ) + return False diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index da0749226434..24fbaf7b3a0d 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -53,6 +53,7 @@ from .controlflow import ControlFlowOp, _builder_utils from .controlflow.builder import CircuitScopeInterface, ControlFlowBuilderBlock from .controlflow.break_loop import BreakLoopOp, BreakLoopPlaceholder +from .controlflow.box import BoxOp, BoxContext from .controlflow.continue_loop import ContinueLoopOp, ContinueLoopPlaceholder from .controlflow.for_loop import ForLoopOp, ForLoopContext from .controlflow.if_else import IfElseOp, IfContext @@ -744,13 +745,14 @@ class QuantumCircuit: ============================== ================================================================ :class:`QuantumCircuit` method Control-flow instruction ============================== ================================================================ - :meth:`if_test` :class:`.IfElseOp` with only a ``True`` body. - :meth:`if_else` :class:`.IfElseOp` with both ``True`` and ``False`` bodies. - :meth:`while_loop` :class:`.WhileLoopOp`. - :meth:`switch` :class:`.SwitchCaseOp`. - :meth:`for_loop` :class:`.ForLoopOp`. - :meth:`break_loop` :class:`.BreakLoopOp`. - :meth:`continue_loop` :class:`.ContinueLoopOp`. + :meth:`if_test` :class:`.IfElseOp` with only a ``True`` body + :meth:`if_else` :class:`.IfElseOp` with both ``True`` and ``False`` bodies + :meth:`while_loop` :class:`.WhileLoopOp` + :meth:`switch` :class:`.SwitchCaseOp` + :meth:`for_loop` :class:`.ForLoopOp` + :meth:`box` :class:`.BoxOp` + :meth:`break_loop` :class:`.BreakLoopOp` + :meth:`continue_loop` :class:`.ContinueLoopOp` ============================== ================================================================ :class:`QuantumCircuit` has corresponding methods for all of the control-flow operations that @@ -776,6 +778,7 @@ class QuantumCircuit: .. TODO: expand the examples of the builder interface. + .. automethod:: box .. automethod:: break_loop .. automethod:: continue_loop .. automethod:: for_loop @@ -6284,6 +6287,107 @@ def _pop_previous_instruction_in_scope(self) -> CircuitInstruction: instruction = self._data.pop() return instruction + def box( + self, + # Forbidding passing `body` by keyword is in anticipation of the constructor expanding to + # allow `annotations` to be passed as the positional argument in the context-manager form. + body: QuantumCircuit | None = None, + /, + qubits: Sequence[QubitSpecifier] | None = None, + clbits: Sequence[ClbitSpecifier] | None = None, + *, + label: str | None = None, + duration: None, + unit: Literal["dt", "s", "ms", "us", "ns", "ps"] = "dt", + ): + """Create a ``box`` of operations on this circuit that are treated atomically in the greater + context. + + A "box" is a control-flow construct that is entered unconditionally. The contents of the + box behave somewhat as if the start and end of the box were barriers (see :meth:`barrier`), + except it is permissible to commute operations "all the way" through the box. The box is + also an explicit scope for the purposes of variables, stretches and compiler passes. + + There are two forms for calling this function: + + * Pass a :class:`QuantumCircuit` positionally, and the ``qubits`` and ``clbits`` it acts + on. In this form, a :class:`.BoxOp` is immediately created and appended using the circuit + as the body. + + * Use in a ``with`` statement with no ``body``, ``qubits`` or ``clbits``. This is the + "builder-interface form", where you then use other :class:`QuantumCircuit` methods within + the Python ``with`` scope to add instructions to the ``box``. This is the preferred form, + and much less error prone. + + Examples: + + Using the builder interface to add two boxes in sequence. The two boxes in this circuit + can execute concurrently, and the second explicitly inserts a data-flow dependency on + qubit 8 for the duration of the box, even though the qubit is idle. + + .. code-block:: python + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(9) + with qc.box(): + qc.cz(0, 1) + qc.cz(2, 3) + with qc.box(): + qc.cz(4, 5) + qc.cz(6, 7) + qc.noop(8) + + Using the explicit construction of box. This creates the same circuit as above, and + should give an indication why the previous form is preferred for interactive use. + + .. code-block:: python + + from qiskit.circuit import QuantumCircuit, BoxOp + + body_0 = QuantumCircuit(4) + body_0.cz(0, 1) + body_0.cz(2, 3) + + # Note that the qubit indices inside a body related only to the body. The + # association with qubits in the containing circuit is made by the ``qubits`` + # argument to `QuantumCircuit.box`. + body_1 = QuantumCircuit(5) + body_1.cz(0, 1) + body_1.cz(2, 3) + + qc = QuantumCircuit(9) + qc.box(body_0, [0, 1, 2, 3], []) + qc.box(body_1, [4, 5, 6, 7, 8], []) + + Args: + body: if given, the :class:`QuantumCircuit` to use as the box's body in the explicit + construction. Not given in the context-manager form. + qubits: the qubits to apply the :class:`.BoxOp` to, in the explicit form. + clbits: the qubits to apply the :class:`.BoxOp` to, in the explicit form. + label: an optional string label for the instruction. + duration: an optional explicit duration for the :class:`.BoxOp`. Scheduling passes are + constrained to schedule the contained scope to match a given duration, including + delay insertion if required. + unit: the unit of the ``duration``. + """ + if isinstance(body, QuantumCircuit): + # Explicit-body form. + if qubits is None or clbits is None: + raise CircuitError("When using 'box' with a body, you must pass qubits and clbits.") + return self.append( + BoxOp(body, duration=duration, unit=unit, label=label), + qubits, + clbits, + copy=False, + ) + # Context-manager form. + if qubits is not None or clbits is not None: + raise CircuitError( + "When using 'box' as a context manager, you cannot pass qubits or clbits." + ) + return BoxContext(self, duration=duration, unit=unit, label=label) + @typing.overload def while_loop( self, diff --git a/qiskit/dagcircuit/dagnode.py b/qiskit/dagcircuit/dagnode.py index 151861d8028c..7780a3e7d90e 100644 --- a/qiskit/dagcircuit/dagnode.py +++ b/qiskit/dagcircuit/dagnode.py @@ -19,6 +19,7 @@ import qiskit._accelerate.circuit from qiskit.circuit import ( + BoxOp, Clbit, ClassicalRegister, IfElseOp, @@ -166,11 +167,18 @@ def _for_loop_eq(node1, node2, bit_indices1, bit_indices2): ) +def _box_eq(node1, node2, bit_indices1, bit_indices2): + return node1.op.duration == node2.op.duration and _circuit_to_dag( + node1.op.blocks[0], node1.qargs, node1.cargs, bit_indices1 + ) == _circuit_to_dag(node2.op.blocks[0], node2.qargs, node2.cargs, bit_indices2) + + _SEMANTIC_EQ_CONTROL_FLOW = { IfElseOp: _condition_op_eq, WhileLoopOp: _condition_op_eq, SwitchCaseOp: _switch_case_eq, ForLoopOp: _for_loop_eq, + BoxOp: _box_eq, } _SEMANTIC_EQ_SYMMETRIC = frozenset({"barrier", "swap", "break_loop", "continue_loop"}) diff --git a/qiskit/qasm3/ast.py b/qiskit/qasm3/ast.py index c723c443fef2..ec8ad9f3622d 100644 --- a/qiskit/qasm3/ast.py +++ b/qiskit/qasm3/ast.py @@ -14,6 +14,8 @@ """QASM3 AST Nodes""" +from __future__ import annotations + import enum from typing import Optional, List, Union, Iterable, Tuple, Sequence @@ -668,6 +670,20 @@ def __init__(self, condition: Expression, body: ProgramBlock): self.body = body +class BoxStatement(Statement): + """Like ``box[duration] { statements* }``.""" + + __slots__ = ("duration", "body") + + def __init__( + self, + body: ProgramBlock, + duration: Expression | None = None, + ): + self.body = body + self.duration = duration + + class BreakStatement(Statement): """AST node for ``break`` statements. Has no associated information.""" diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index b08b2a9719ef..f9d0757ec4b8 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -43,6 +43,7 @@ from qiskit.circuit.bit import Bit from qiskit.circuit.classical import expr, types from qiskit.circuit.controlflow import ( + BoxOp, IfElseOp, ForLoopOp, WhileLoopOp, @@ -968,6 +969,8 @@ def build_current_scope(self) -> List[ast.Statement]: statements.append(self.build_if_statement(instruction)) elif isinstance(instruction.operation, SwitchCaseOp): statements.extend(self.build_switch_statement(instruction)) + elif isinstance(instruction.operation, BoxOp): + statements.append(self.build_box(instruction)) else: raise RuntimeError(f"unhandled control-flow construct: {instruction.operation}") continue @@ -1085,6 +1088,15 @@ def case(values, case_block): ast.SwitchStatement(target, cases, default=default), ] + def build_box(self, instruction: CircuitInstruction) -> ast.BoxStatement: + """Build a :class:`.BoxOp` into a :class:`.ast.BoxStatement`.""" + duration = self.build_duration(instruction.operation.duration, instruction.operation.unit) + body_circuit = instruction.operation.blocks[0] + with self.new_scope(body_circuit, instruction.qubits, instruction.clbits): + # TODO: handle no-op qubits (see https://github.com/openqasm/openqasm/issues/584). + body = ast.ProgramBlock(self.build_current_scope()) + return ast.BoxStatement(body, duration) + def build_while_loop(self, instruction: CircuitInstruction) -> ast.WhileLoopStatement: """Build a :obj:`.WhileLoopOp` into a :obj:`.ast.WhileLoopStatement`.""" condition = self.build_expression(_lift_condition(instruction.operation.condition)) @@ -1134,20 +1146,24 @@ def build_delay(self, instruction: CircuitInstruction) -> ast.QuantumDelay: raise QASM3ExporterError( f"Found a delay instruction acting on classical bits: {instruction}" ) - duration_value, unit = instruction.operation.duration, instruction.operation.unit - if unit == "ps": - duration = ast.DurationLiteral(1000 * duration_value, ast.DurationUnit.NANOSECOND) - else: - unit_map = { - "ns": ast.DurationUnit.NANOSECOND, - "us": ast.DurationUnit.MICROSECOND, - "ms": ast.DurationUnit.MILLISECOND, - "s": ast.DurationUnit.SECOND, - "dt": ast.DurationUnit.SAMPLE, - } - duration = ast.DurationLiteral(duration_value, unit_map[unit]) + duration = self.build_duration(instruction.operation.duration, instruction.operation.unit) return ast.QuantumDelay(duration, [self._lookup_bit(qubit) for qubit in instruction.qubits]) + def build_duration(self, duration, unit) -> ast.Expression | None: + """Build the expression of a given duration (if not ``None``).""" + if duration is None: + return None + if unit == "ps": + return ast.DurationLiteral(1000 * duration, ast.DurationUnit.NANOSECOND) + unit_map = { + "ns": ast.DurationUnit.NANOSECOND, + "us": ast.DurationUnit.MICROSECOND, + "ms": ast.DurationUnit.MILLISECOND, + "s": ast.DurationUnit.SECOND, + "dt": ast.DurationUnit.SAMPLE, + } + return ast.DurationLiteral(duration, unit_map[unit]) + def build_integer(self, value) -> ast.IntegerLiteral: """Build an integer literal, raising a :obj:`.QASM3ExporterError` if the input is not actually an diff --git a/qiskit/qasm3/printer.py b/qiskit/qasm3/printer.py index 221c99bc4f90..44891b5ee274 100644 --- a/qiskit/qasm3/printer.py +++ b/qiskit/qasm3/printer.py @@ -575,3 +575,14 @@ def _visit_SwitchStatementPreview(self, node: ast.SwitchStatementPreview) -> Non def _visit_DefaultCase(self, _node: ast.DefaultCase) -> None: self.stream.write("default") + + def _visit_BoxStatement(self, node: ast.BoxStatement) -> None: + self._start_line() + self.stream.write("box") + if node.duration is not None: + self.stream.write("[") + self.visit(node.duration) + self.stream.write("]") + self.stream.write(" ") + self.visit(node.body) + self._end_line() diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 174acceb59e4..c84f1cc9c0e2 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -363,8 +363,11 @@ def _read_instruction( if instruction.label_size <= 0: label = None - if gate_name in {"IfElseOp", "WhileLoopOp"}: + if gate_name in ("IfElseOp", "WhileLoopOp"): gate = gate_class(condition, *params, label=label) + elif gate_name == "BoxOp": + *params, duration = params + gate = gate_class(*params, label=label, duration=duration) elif version >= 5 and issubclass(gate_class, ControlledGate): if gate_name in { "MCPhaseGate", @@ -796,6 +799,11 @@ def _write_instruction( instruction.operation.target, tuple(instruction.operation.cases_specifier()), ] + elif isinstance(instruction.operation, controlflow.BoxOp): + instruction_params = [ + instruction.operation.blocks[0], + instruction.operation.duration, + ] elif isinstance(instruction.operation, Clifford): instruction_params = [instruction.operation.tableau] elif isinstance(instruction.operation, AnnotatedOperation): diff --git a/qiskit/visualization/circuit/matplotlib.py b/qiskit/visualization/circuit/matplotlib.py index 6f7359021839..bc772c347438 100644 --- a/qiskit/visualization/circuit/matplotlib.py +++ b/qiskit/visualization/circuit/matplotlib.py @@ -29,6 +29,7 @@ ControlledGate, Measure, ControlFlowOp, + BoxOp, WhileLoopOp, IfElseOp, ForLoopOp, @@ -1584,8 +1585,10 @@ def _flow_op_gate(self, node, node_data, glob_data): flow_text = " For" elif isinstance(node.op, SwitchCaseOp): flow_text = "Switch" + elif isinstance(node.op, BoxOp): + flow_text = "" else: - flow_text = node.op.name + raise RuntimeError(f"unhandled control-flow op: {node.name}") # Some spacers. op_spacer moves 'Switch' back a bit for alignment, # expr_spacer moves the expr over to line up with 'Switch' and @@ -1595,6 +1598,10 @@ def _flow_op_gate(self, node, node_data, glob_data): op_spacer = 0.04 expr_spacer = 0.0 empty_default_spacer = 0.3 if len(node.op.blocks[-1]) == 0 else 0.0 + elif isinstance(node.op, BoxOp): + op_spacer = 0.0 + expr_spacer = 0.0 + empty_default_spacer = 0.0 else: op_spacer = 0.08 expr_spacer = 0.02 diff --git a/qiskit/visualization/circuit/text.py b/qiskit/visualization/circuit/text.py index d51a3efa6828..f019198246f7 100644 --- a/qiskit/visualization/circuit/text.py +++ b/qiskit/visualization/circuit/text.py @@ -22,7 +22,7 @@ from qiskit.circuit import Qubit, Clbit, ClassicalRegister, CircuitError from qiskit.circuit import ControlledGate, Reset, Measure -from qiskit.circuit import ControlFlowOp, WhileLoopOp, IfElseOp, ForLoopOp, SwitchCaseOp +from qiskit.circuit import ControlFlowOp, WhileLoopOp, IfElseOp, ForLoopOp, SwitchCaseOp, BoxOp from qiskit.circuit.classical import expr from qiskit.circuit.controlflow import node_resources from qiskit.circuit.library.standard_gates import IGate, RZZGate, SwapGate, SXGate, SXdgGate @@ -1335,7 +1335,7 @@ def lookup_var(var): if len(self._expr_text) > self.expr_len: self._expr_text = self._expr_text[: self.expr_len] + "..." else: - draw_conditional = not isinstance(node.op, ForLoopOp) + draw_conditional = isinstance(node.op, (IfElseOp, WhileLoopOp, SwitchCaseOp)) # # Draw a left box such as If, While, For, and Switch flow_layer = self.draw_flow_box(node, wire_map, CF_LEFT, conditional=draw_conditional) @@ -1421,8 +1421,12 @@ def draw_flow_box(self, node, flow_wire_map, section, circ_num=0, conditional=Fa else: index_str = str(indexset) label = "For-" + depth + " " + index_str - else: + elif isinstance(op, BoxOp): + label = "Box-" + depth + etext + elif isinstance(op, SwitchCaseOp): label = "Switch-" + depth + etext + else: + raise RuntimeError(f"unhandled control-flow operation: {node.name}") elif section == CF_MID: if isinstance(op, IfElseOp): label = "Else-" + depth From 34dad20467a4c6a81eef5024a1a1ce6d65ae6ffc Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 20 Feb 2025 18:23:58 +0000 Subject: [PATCH 2/3] Rollup-merge of kevin/stretchy-delay at 2ed26efaa7 --- crates/accelerate/src/unitary_synthesis.rs | 1018 ++++++++++------- crates/circuit/src/duration.rs | 36 + crates/circuit/src/lib.rs | 2 + crates/circuit/src/operations.rs | 5 +- pyproject.toml | 2 +- qiskit/circuit/__init__.py | 2 + qiskit/circuit/classical/expr/__init__.py | 8 + qiskit/circuit/classical/expr/constructors.py | 506 ++++++-- qiskit/circuit/classical/expr/expr.py | 13 +- qiskit/circuit/classical/expr/visitors.py | 6 +- qiskit/circuit/classical/types/__init__.py | 5 +- qiskit/circuit/classical/types/ordering.py | 63 +- qiskit/circuit/classical/types/types.py | 114 +- qiskit/circuit/delay.py | 36 +- qiskit/circuit/library/standard_gates/rz.py | 14 +- qiskit/circuit/quantumcircuit.py | 85 +- qiskit/compiler/transpiler.py | 26 +- qiskit/qasm3/ast.py | 24 + qiskit/qasm3/exporter.py | 24 + qiskit/qasm3/printer.py | 23 +- qiskit/qpy/__init__.py | 39 + qiskit/qpy/binary_io/circuits.py | 4 +- qiskit/qpy/binary_io/value.py | 197 +++- qiskit/qpy/common.py | 2 +- qiskit/qpy/formats.py | 50 +- qiskit/qpy/type_keys.py | 41 + .../transpiler/passes/layout/dense_layout.py | 29 +- qiskit/transpiler/passes/layout/vf2_layout.py | 11 +- .../passes/layout/vf2_post_layout.py | 140 +-- qiskit/transpiler/passes/layout/vf2_utils.py | 26 +- .../passes/scheduling/time_unit_conversion.py | 152 ++- .../synthesis/default_unitary_synth_plugin.py | 653 +++++++++++ .../synthesis/solovay_kitaev_synthesis.py | 18 +- .../passes/synthesis/unitary_synthesis.py | 649 +---------- qiskit/transpiler/passmanager_config.py | 14 - .../preset_passmanagers/builtin_plugins.py | 19 - .../transpiler/preset_passmanagers/common.py | 7 +- .../generate_preset_pass_manager.py | 53 +- .../notes/closes_12345-2356fd2d919e3f4a.yaml | 6 + .../notes/const-expr-397ff09042942b81.yaml | 21 + .../notes/float-expr-02b01d9ea89ad47a.yaml | 19 + ...end-props-transpiler-64aa771784084313.yaml | 25 + ...nore-unsupported-ops-8d7d5f6fca255ffb.yaml | 7 + .../classical/test_expr_constructors.py | 801 ++++++++++++- .../circuit/classical/test_expr_properties.py | 8 - .../circuit/classical/test_types_ordering.py | 403 +++++++ .../circuit/test_circuit_load_from_qpy.py | 31 + .../python/circuit/test_circuit_operations.py | 47 +- test/python/circuit/test_circuit_vars.py | 101 +- test/python/circuit/test_store.py | 17 + test/python/compiler/test_transpiler.py | 123 +- test/python/qasm3/test_export.py | 89 ++ .../transpiler/test_passmanager_config.py | 1 - test/python/transpiler/test_solovay_kitaev.py | 72 +- test/python/transpiler/test_target.py | 32 - .../transpiler/test_unitary_synthesis.py | 4 +- .../test_unitary_synthesis_plugin.py | 2 +- test/python/transpiler/test_vf2_layout.py | 60 +- .../python/transpiler/test_vf2_post_layout.py | 283 +---- test/qpy_compat/test_qpy.py | 56 + 60 files changed, 4294 insertions(+), 2030 deletions(-) create mode 100644 crates/circuit/src/duration.rs create mode 100644 qiskit/transpiler/passes/synthesis/default_unitary_synth_plugin.py create mode 100644 releasenotes/notes/closes_12345-2356fd2d919e3f4a.yaml create mode 100644 releasenotes/notes/const-expr-397ff09042942b81.yaml create mode 100644 releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml create mode 100644 releasenotes/notes/remove-backend-props-transpiler-64aa771784084313.yaml create mode 100644 releasenotes/notes/sk-ignore-unsupported-ops-8d7d5f6fca255ffb.yaml diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 9d167c5f083a..1d88f66e72de 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -62,53 +62,76 @@ enum DecomposerType { #[derive(Clone, Debug)] struct DecomposerElement { decomposer: DecomposerType, - gate: NormalOperation, + packed_op: PackedOperation, + params: SmallVec<[Param; 3]>, } #[derive(Clone, Debug)] struct TwoQubitUnitarySequence { gate_sequence: TwoQubitGateSequence, - decomp_gate: NormalOperation, + decomp_op: PackedOperation, + decomp_params: SmallVec<[Param; 3]>, } -// Used in get_2q_decomposers. If the found 2q basis is a subset of GOODBYE_SET, -// then we know TwoQubitBasisDecomposer is an ideal decomposition and there is -// no need to bother trying the XXDecomposer. +// These two variables are used to exit the decomposer search early in +// `get_2q_decomposers_from_target`. +// If the available 2q basis is a subset of GOODBYE_SET, TwoQubitBasisDecomposer provides +// an ideal decomposition and we can exit the decomposer search. Similarly, if it is a +// subset of PARAM_SET, TwoQubitControlledUDecomposer provides an ideal decompostion. static GOODBYE_SET: [&str; 3] = ["cx", "cz", "ecr"]; static PARAM_SET: [&str; 8] = ["rzz", "rxx", "ryy", "rzx", "crx", "cry", "crz", "cphase"]; +/// Given a list of basis gates, find a corresponding euler basis to use. +/// This will determine the available 1q synthesis basis for different decomposers. +fn get_euler_basis_set(basis_list: IndexSet<&str>) -> EulerBasisSet { + let mut euler_basis_set: EulerBasisSet = EulerBasisSet::new(); + EULER_BASES + .iter() + .enumerate() + .filter_map(|(idx, gates)| { + if !gates.iter().all(|gate| basis_list.contains(gate)) { + return None; + } + let basis = EULER_BASIS_NAMES[idx]; + Some(basis) + }) + .for_each(|basis| euler_basis_set.add_basis(basis)); + + if euler_basis_set.basis_supported(EulerBasis::U3) + && euler_basis_set.basis_supported(EulerBasis::U321) + { + euler_basis_set.remove(EulerBasis::U3); + } + if euler_basis_set.basis_supported(EulerBasis::ZSX) + && euler_basis_set.basis_supported(EulerBasis::ZSXX) + { + euler_basis_set.remove(EulerBasis::ZSX); + } + euler_basis_set +} + +/// Given a `Target`, find an euler basis that is supported for a specific `PhysicalQubit`. +/// This will determine the available 1q synthesis basis for different decomposers. fn get_target_basis_set(target: &Target, qubit: PhysicalQubit) -> EulerBasisSet { let mut target_basis_set: EulerBasisSet = EulerBasisSet::new(); let target_basis_list = target.operation_names_for_qargs(Some(&smallvec![qubit])); match target_basis_list { Ok(basis_list) => { - EULER_BASES - .iter() - .enumerate() - .filter_map(|(idx, gates)| { - if !gates.iter().all(|gate| basis_list.contains(gate)) { - return None; - } - let basis = EULER_BASIS_NAMES[idx]; - Some(basis) - }) - .for_each(|basis| target_basis_set.add_basis(basis)); + target_basis_set = get_euler_basis_set(basis_list.into_iter().collect()); + } + Err(_) => { + target_basis_set.support_all(); + target_basis_set.remove(EulerBasis::U3); + target_basis_set.remove(EulerBasis::ZSX); } - Err(_) => target_basis_set.support_all(), - } - if target_basis_set.basis_supported(EulerBasis::U3) - && target_basis_set.basis_supported(EulerBasis::U321) - { - target_basis_set.remove(EulerBasis::U3); - } - if target_basis_set.basis_supported(EulerBasis::ZSX) - && target_basis_set.basis_supported(EulerBasis::ZSXX) - { - target_basis_set.remove(EulerBasis::ZSX); } target_basis_set } +/// Apply synthesis output (`synth_dag`) to final `DAGCircuit` (`out_dag`). +/// `synth_dag` is a subgraph, and the `qubit_ids` are relative to the subgraph +/// size/orientation, so `out_qargs` is used to track the final qubit ids where +/// it should be applied. fn apply_synth_dag( py: Python<'_>, out_dag: &mut DAGCircuit, @@ -129,6 +152,10 @@ fn apply_synth_dag( Ok(()) } +/// Apply synthesis output (`sequence`) to final `DAGCircuit` (`out_dag`). +/// `sequence` contains a representation of gates to be applied to a subgraph, +/// and the `qubit_ids` are relative to the subgraph size/orientation, +/// so `out_qargs` is used to track the final qubit ids where they should be applied. fn apply_synth_sequence( py: Python<'_>, out_dag: &mut DAGCircuit, @@ -138,17 +165,17 @@ fn apply_synth_sequence( let mut instructions = Vec::with_capacity(sequence.gate_sequence.gates().len()); for (gate, params, qubit_ids) in sequence.gate_sequence.gates() { let packed_op = match gate { - None => &sequence.decomp_gate.operation, + None => &sequence.decomp_op, Some(gate) => &PackedOperation::from_standard_gate(*gate), }; let mapped_qargs: Vec = qubit_ids.iter().map(|id| out_qargs[*id as usize]).collect(); let new_params: Option>> = match gate { Some(_) => Some(Box::new(params.iter().map(|p| Param::Float(*p)).collect())), None => { - if !sequence.decomp_gate.params.is_empty() - && matches!(sequence.decomp_gate.params[0], Param::Float(_)) + if !sequence.decomp_params.is_empty() + && matches!(sequence.decomp_params[0], Param::Float(_)) { - Some(Box::new(sequence.decomp_gate.params.clone())) + Some(Box::new(sequence.decomp_params.clone())) } else { Some(Box::new(params.iter().map(|p| Param::Float(*p)).collect())) } @@ -201,76 +228,27 @@ fn apply_synth_sequence( Ok(()) } -fn synth_error( - py: Python<'_>, - synth_circuit: impl Iterator< - Item = ( - String, - Option>, - SmallVec<[PhysicalQubit; 2]>, - ), - >, - target: &Target, -) -> f64 { - let (lower_bound, upper_bound) = synth_circuit.size_hint(); - let mut gate_fidelities = match upper_bound { - Some(bound) => Vec::with_capacity(bound), - None => Vec::with_capacity(lower_bound), - }; - let mut score_instruction = - |inst_name: &str, - inst_params: &Option>, - inst_qubits: &SmallVec<[PhysicalQubit; 2]>| { - if let Ok(names) = target.operation_names_for_qargs(Some(inst_qubits)) { - for name in names { - if let Ok(target_op) = target.operation_from_name(name) { - let are_params_close = if let Some(params) = inst_params { - params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { - p1.is_close(py, p2, 1e-10) - .expect("Unexpected parameter expression error.") - }) - } else { - false - }; - let is_parametrized = target_op - .params - .iter() - .any(|param| matches!(param, Param::ParameterExpression(_))); - if target_op.operation.name() == inst_name - && (is_parametrized || are_params_close) - { - match target[name].get(Some(inst_qubits)) { - Some(Some(props)) => { - gate_fidelities.push(1.0 - props.error.unwrap_or(0.0)) - } - _ => gate_fidelities.push(1.0), - } - break; - } - } - } - } - }; - - for (inst_name, inst_params, inst_qubits) in synth_circuit { - score_instruction(&inst_name, &inst_params, &inst_qubits); - } - 1.0 - gate_fidelities.into_iter().product::() -} - -// This is the outer-most run function. It is meant to be called from Python -// in `UnitarySynthesis.run()`. +/// Iterate over `DAGCircuit` to perform unitary synthesis. +/// For each elegible gate: find decomposers, select the synthesis +/// method with the highest fidelity score and apply decompositions. The available methods are: +/// * 1q synthesis: OneQubitEulerDecomposer +/// * 2q synthesis: TwoQubitBasisDecomposer, TwoQubitControlledUDecomposer, XXDecomposer (Python, only if target is provided) +/// * 3q+ synthesis: QuantumShannonDecomposer (Python) +/// This function is currently used in the Python `UnitarySynthesis`` transpiler pass as a replacement for the `_run_main_loop` method. +/// It returns a new `DAGCircuit` with the different synthesized gates. #[pyfunction] -#[pyo3(name = "run_default_main_loop", signature=(dag, qubit_indices, min_qubits, target, coupling_edges, approximation_degree=None, natural_direction=None))] +#[pyo3(name = "run_main_loop", signature=(dag, qubit_indices, min_qubits, target, basis_gates, coupling_edges, approximation_degree=None, natural_direction=None, pulse_optimize=None))] fn py_run_main_loop( py: Python, dag: &mut DAGCircuit, qubit_indices: Vec, min_qubits: usize, - target: &Target, + target: Option<&Target>, + basis_gates: HashSet, coupling_edges: HashSet<[PhysicalQubit; 2]>, approximation_degree: Option, natural_direction: Option, + pulse_optimize: Option, ) -> PyResult { // We need to use the python converter because the currently available Rust conversion // is lossy. We need `QuantumCircuit` instances to be used in `replace_blocks`. @@ -311,9 +289,11 @@ fn py_run_main_loop( new_ids, min_qubits, target, + basis_gates.clone(), coupling_edges.clone(), approximation_degree, natural_direction, + pulse_optimize, )?; new_blocks.push(dag_to_circuit.call1((res,))?); } @@ -332,7 +312,7 @@ fn py_run_main_loop( py_op: new_node.unbind().into(), }; } - if !(matches!(packed_instr.op.view(), OperationRef::Unitary(_)) + if !(packed_instr.op.name() == "unitary" && packed_instr.op.num_qubits() >= min_qubits as u32) { out_dag.push_back(py, packed_instr)?; @@ -346,7 +326,14 @@ fn py_run_main_loop( // Run 1q synthesis [2, 2] => { let qubit = dag.get_qargs(packed_instr.qubits)[0]; - let target_basis_set = get_target_basis_set(target, PhysicalQubit::new(qubit.0)); + let target_basis_set = match target { + Some(target) => get_target_basis_set(target, PhysicalQubit::new(qubit.0)), + None => { + let basis_gates: IndexSet<&str> = + basis_gates.iter().map(String::as_str).collect(); + get_euler_basis_set(basis_gates) + } + }; let sequence = unitary_to_gate_sequence_inner( unitary.view(), &target_basis_set, @@ -397,8 +384,10 @@ fn py_run_main_loop( ref_qubits, &coupling_edges, target, + basis_gates.clone(), approximation_degree, natural_direction, + pulse_optimize, &mut out_dag, out_qargs, apply_original_op, @@ -406,223 +395,130 @@ fn py_run_main_loop( } // Run 3q+ synthesis _ => { - let qs_decomposition: &Bound<'_, PyAny> = imports::QS_DECOMPOSITION.get_bound(py); - let synth_circ = qs_decomposition.call1((unitary.into_pyarray(py),))?; - let synth_dag = circuit_to_dag( - py, - QuantumCircuitData::extract_bound(&synth_circ)?, - false, - None, - None, - )?; - let out_qargs = dag.get_qargs(packed_instr.qubits); - apply_synth_dag(py, &mut out_dag, out_qargs, &synth_dag)?; + if basis_gates.is_empty() && target.is_none() { + out_dag.push_back(py, packed_instr.clone())?; + } else { + let qs_decomposition: &Bound<'_, PyAny> = + imports::QS_DECOMPOSITION.get_bound(py); + let synth_circ = qs_decomposition.call1((unitary.into_pyarray(py),))?; + let synth_dag = circuit_to_dag( + py, + QuantumCircuitData::extract_bound(&synth_circ)?, + false, + None, + None, + )?; + let out_qargs = dag.get_qargs(packed_instr.qubits); + apply_synth_dag(py, &mut out_dag, out_qargs, &synth_dag)?; + } } } } Ok(out_dag) } -fn run_2q_unitary_synthesis( - py: Python, - unitary: Array2, - ref_qubits: &[PhysicalQubit; 2], - coupling_edges: &HashSet<[PhysicalQubit; 2]>, - target: &Target, +/// Return a single decomposer for the given `basis_gates`. If no decomposer is found, +/// return `None``. If a decomposer is found, the return type will be either +/// `DecomposerElement::TwoQubitBasis` or `DecomposerElement::TwoQubitControlledU`. +fn get_2q_decomposer_from_basis( + basis_gates: IndexSet<&str>, approximation_degree: Option, - natural_direction: Option, - out_dag: &mut DAGCircuit, - out_qargs: &[Qubit], - mut apply_original_op: impl FnMut(&mut DAGCircuit) -> PyResult<()>, -) -> PyResult<()> { - let decomposers = { - let decomposers_2q = - get_2q_decomposers_from_target(py, target, ref_qubits, approximation_degree)?; - decomposers_2q.unwrap_or_default() + pulse_optimize: Option, +) -> PyResult> { + // Non-parametrized 2q basis candidates (TwoQubitBasisDecomposer) + let basis_names: IndexMap<&str, StandardGate> = [ + ("cx", StandardGate::CXGate), + ("cz", StandardGate::CZGate), + ("iswap", StandardGate::ISwapGate), + ("ecr", StandardGate::ECRGate), + ] + .into_iter() + .collect(); + // Parametrized 2q basis candidates (TwoQubitControlledUDecomposer) + let param_basis_names: IndexMap<&str, StandardGate> = [ + ("rxx", StandardGate::RXXGate), + ("rzx", StandardGate::RZXGate), + ("rzz", StandardGate::RZZGate), + ("ryy", StandardGate::RYYGate), + ("cphase", StandardGate::CPhaseGate), + ("crx", StandardGate::CRXGate), + ("cry", StandardGate::CRYGate), + ("crz", StandardGate::CRZGate), + ] + .into_iter() + .collect(); + // 1q basis (both decomposers) + let euler_basis = match get_euler_basis_set(basis_gates.clone()) + .get_bases() + .map(|basis| basis.as_str()) + .next() + { + Some(basis) => basis, + None => return Ok(None), }; - // If there's a single decomposer, avoid computing synthesis score - if decomposers.len() == 1 { - let decomposer_item = decomposers.first().unwrap(); - let preferred_dir = preferred_direction( - decomposer_item, - ref_qubits, - natural_direction, - coupling_edges, - target, - )?; - match decomposer_item.decomposer { - DecomposerType::TwoQubitBasis(_) => { - let synth = synth_su4_sequence( - &unitary, - decomposer_item, - preferred_dir, - approximation_degree, - )?; - apply_synth_sequence(py, out_dag, out_qargs, &synth)?; - } - DecomposerType::TwoQubitControlledU(_) => { - let synth = synth_su4_sequence( - &unitary, - decomposer_item, - preferred_dir, - approximation_degree, - )?; - apply_synth_sequence(py, out_dag, out_qargs, &synth)?; - } - DecomposerType::XX(_) => { - let synth = synth_su4_dag( - py, - &unitary, - decomposer_item, - preferred_dir, - approximation_degree, - )?; - apply_synth_dag(py, out_dag, out_qargs, &synth)?; - } - } - return Ok(()); - } + // Try TwoQubitControlledUDecomposer first. + let kak_gates: Vec<&str> = param_basis_names + .keys() + .copied() + .collect::>() + .intersection(&basis_gates) + .copied() + .collect(); + if !kak_gates.is_empty() { + let std_gate = *param_basis_names.get(kak_gates[0]).unwrap(); + let rxx_equivalent_gate = RXXEquivalent::Standard(std_gate); + if let Ok(decomposer) = + TwoQubitControlledUDecomposer::new_inner(rxx_equivalent_gate, euler_basis) + { + return Ok(Some(DecomposerElement { + decomposer: DecomposerType::TwoQubitControlledU(Box::new(decomposer)), + packed_op: PackedOperation::from_standard_gate(std_gate), + params: SmallVec::new(), + })); + }; + }; - let mut synth_errors_sequence = Vec::new(); - let mut synth_errors_dag = Vec::new(); - for decomposer in &decomposers { - let preferred_dir = preferred_direction( - decomposer, - ref_qubits, - natural_direction, - coupling_edges, - target, + // If there is no suitable TwoQubitControlledUDecomposer, try TwoQubitBasisDecomposer. + let kak_gates: Vec<&str> = basis_names + .keys() + .copied() + .collect::>() + .intersection(&basis_gates) + .copied() + .collect(); + if !kak_gates.is_empty() { + let std_gate = *basis_names.get(kak_gates[0]).unwrap(); + let decomposer = TwoQubitBasisDecomposer::new_inner( + std_gate.name().to_string(), + std_gate.matrix(&[]).unwrap().view(), + approximation_degree.unwrap_or(1.0), + euler_basis, + pulse_optimize, )?; - match &decomposer.decomposer { - DecomposerType::TwoQubitBasis(_) => { - let sequence = - synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; - let scoring_info = - sequence - .gate_sequence - .gates() - .iter() - .map(|(gate, params, qubit_ids)| { - let inst_qubits = - qubit_ids.iter().map(|q| ref_qubits[*q as usize]).collect(); - match gate { - Some(gate) => ( - gate.name().to_string(), - Some(params.iter().map(|p| Param::Float(*p)).collect()), - inst_qubits, - ), - None => ( - sequence.decomp_gate.operation.name().to_string(), - Some(params.iter().map(|p| Param::Float(*p)).collect()), - inst_qubits, - ), - } - }); - let synth_error_from_target = synth_error(py, scoring_info, target); - synth_errors_sequence.push((sequence, synth_error_from_target)); - } - DecomposerType::TwoQubitControlledU(_) => { - let sequence = - synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; - let scoring_info = - sequence - .gate_sequence - .gates() - .iter() - .map(|(gate, params, qubit_ids)| { - let inst_qubits = - qubit_ids.iter().map(|q| ref_qubits[*q as usize]).collect(); - match gate { - Some(gate) => ( - gate.name().to_string(), - Some(params.iter().map(|p| Param::Float(*p)).collect()), - inst_qubits, - ), - None => ( - sequence.decomp_gate.operation.name().to_string(), - Some(params.iter().map(|p| Param::Float(*p)).collect()), - inst_qubits, - ), - } - }); - let synth_error_from_target = synth_error(py, scoring_info, target); - synth_errors_sequence.push((sequence, synth_error_from_target)); - } - DecomposerType::XX(_) => { - let synth_dag = synth_su4_dag( - py, - &unitary, - decomposer, - preferred_dir, - approximation_degree, - )?; - let scoring_info = synth_dag - .topological_op_nodes() - .expect("Unexpected error in dag.topological_op_nodes()") - .map(|node| { - let NodeType::Operation(inst) = &synth_dag[node] else { - unreachable!("DAG node must be an instruction") - }; - let inst_qubits = synth_dag - .get_qargs(inst.qubits) - .iter() - .map(|q| ref_qubits[q.0 as usize]) - .collect(); - ( - inst.op.name().to_string(), - inst.params.clone().map(|boxed| *boxed), - inst_qubits, - ) - }); - let synth_error_from_target = synth_error(py, scoring_info, target); - synth_errors_dag.push((synth_dag, synth_error_from_target)); - } - } + return Ok(Some(DecomposerElement { + decomposer: DecomposerType::TwoQubitBasis(Box::new(decomposer)), + packed_op: PackedOperation::from_standard_gate(std_gate), + params: SmallVec::new(), + })); } - - let synth_sequence = synth_errors_sequence - .iter() - .enumerate() - .min_by(|error1, error2| error1.1 .1.partial_cmp(&error2.1 .1).unwrap()) - .map(|(index, _)| &synth_errors_sequence[index]); - - let synth_dag = synth_errors_dag - .iter() - .enumerate() - .min_by(|error1, error2| error1.1 .1.partial_cmp(&error2.1 .1).unwrap()) - .map(|(index, _)| &synth_errors_dag[index]); - - match (synth_sequence, synth_dag) { - (None, None) => apply_original_op(out_dag)?, - (Some((sequence, _)), None) => apply_synth_sequence(py, out_dag, out_qargs, sequence)?, - (None, Some((dag, _))) => apply_synth_dag(py, out_dag, out_qargs, dag)?, - (Some((sequence, sequence_error)), Some((dag, dag_error))) => { - if sequence_error > dag_error { - apply_synth_dag(py, out_dag, out_qargs, dag)? - } else { - apply_synth_sequence(py, out_dag, out_qargs, sequence)? - } - } - }; - Ok(()) + Ok(None) } +/// Return a list of decomposers for the given `target`. If no decomposer is found, +/// return `None``. The list can contain any `DecomposerElement`. This function +/// will exit early if an ideal decomposition is found. fn get_2q_decomposers_from_target( py: Python, target: &Target, qubits: &[PhysicalQubit; 2], approximation_degree: Option, + pulse_optimize: Option, ) -> PyResult>> { + // Store elegible basis gates (1q and 2q) with corresponding qargs (PhysicalQubit) let qubits: SmallVec<[PhysicalQubit; 2]> = SmallVec::from_buf(*qubits); let reverse_qubits: SmallVec<[PhysicalQubit; 2]> = qubits.iter().rev().copied().collect(); - let mut available_2q_basis: IndexMap<&str, NormalOperation> = IndexMap::new(); - let mut available_2q_props: IndexMap<&str, (Option, Option)> = IndexMap::new(); - let mut available_2q_param_basis: IndexMap<&str, NormalOperation> = IndexMap::new(); - let mut available_2q_param_props: IndexMap<&str, (Option, Option)> = IndexMap::new(); - let mut qubit_gate_map = IndexMap::new(); - match target.operation_names_for_qargs(Some(&qubits)) { Ok(direct_keys) => { qubit_gate_map.insert(&qubits, direct_keys); @@ -641,13 +537,17 @@ fn get_2q_decomposers_from_target( } } - #[inline] - fn check_parametrized_gate(op: &NormalOperation) -> bool { - // The gate counts as parametrized if there is any - // non-float parameter - !op.params.iter().all(|p| matches!(p, Param::Float(_))) - } + // Define available 1q basis + let available_1q_basis: IndexSet<&str> = IndexSet::from_iter( + get_target_basis_set(target, qubits[0]) + .get_bases() + .map(|basis| basis.as_str()), + ); + // Define available 2q basis (setting apart parametrized 2q gates) + let mut available_2q_basis: IndexMap<&str, (NormalOperation, Option)> = IndexMap::new(); + let mut available_2q_param_basis: IndexMap<&str, (NormalOperation, Option)> = + IndexMap::new(); for (q_pair, gates) in qubit_gate_map { for key in gates { match target.operation_from_name(key) { @@ -661,49 +561,44 @@ fn get_2q_decomposers_from_target( if op.operation.num_qubits() != 2 { continue; } - if check_parametrized_gate(op) { - available_2q_param_basis.insert(key, op.clone()); - if target.contains_key(key) { - available_2q_param_props.insert( - key, + // Add to param_basis if the gate parameters aren't bound (not Float) + if !op.params.iter().all(|p| matches!(p, Param::Float(_))) { + available_2q_param_basis.insert( + key, + ( + op.clone(), match &target[key].get(Some(q_pair)) { - Some(Some(props)) => (props.duration, props.error), - _ => (None, None), + Some(Some(props)) => props.error, + _ => None, }, - ); - } else { - continue; - } + ), + ); } - available_2q_basis.insert(key, op.clone()); - if target.contains_key(key) { - available_2q_props.insert( - key, + available_2q_basis.insert( + key, + ( + op.clone(), match &target[key].get(Some(q_pair)) { - Some(Some(props)) => (props.duration, props.error), - _ => (None, None), + Some(Some(props)) => props.error, + _ => None, }, - ); - } else { - continue; - } + ), + ); } _ => continue, } } } - if available_2q_basis.is_empty() && available_2q_param_basis.is_empty() { return Err(QiskitError::new_err( "Target has no gates available on qubits to synthesize over.", )); } - let target_basis_set = get_target_basis_set(target, qubits[0]); - let available_1q_basis: IndexSet<&str> = - IndexSet::from_iter(target_basis_set.get_bases().map(|basis| basis.as_str())); + // If there are available 2q gates, start search for decomposers: let mut decomposers: Vec = Vec::new(); + // Step 1: Try TwoQubitBasisDecomposers #[inline] fn is_supercontrolled(op: &NormalOperation) -> bool { match op.operation.matrix(&op.params) { @@ -715,29 +610,15 @@ fn get_2q_decomposers_from_target( } } } - - #[inline] - fn is_controlled(op: &NormalOperation) -> bool { - match op.operation.matrix(&op.params) { - None => false, - Some(unitary_matrix) => { - let kak = TwoQubitWeylDecomposition::new_inner(unitary_matrix.view(), None, None) - .unwrap(); - relative_eq!(kak.b(), 0.0) && relative_eq!(kak.c(), 0.0) - } - } - } - - let supercontrolled_basis: IndexMap<&str, NormalOperation> = available_2q_basis + let supercontrolled_basis: IndexMap<&str, (NormalOperation, Option)> = available_2q_basis .iter() - .filter(|(_, v)| is_supercontrolled(v)) - .map(|(k, v)| (*k, v.clone())) + .filter(|(_, (gate, _))| is_supercontrolled(gate)) + .map(|(k, (gate, props))| (*k, (gate.clone(), *props))) .collect(); - for basis_1q in &available_1q_basis { - for (basis_2q, gate) in supercontrolled_basis.iter() { - let mut basis_2q_fidelity: f64 = match available_2q_props.get(basis_2q) { - Some(&(_, Some(e))) => 1.0 - e, + for (_, (gate, props)) in supercontrolled_basis.iter() { + let mut basis_2q_fidelity: f64 = match props { + Some(error) => 1.0 - error, _ => 1.0, }; if let Some(approx_degree) = approximation_degree { @@ -748,31 +629,28 @@ fn get_2q_decomposers_from_target( gate.operation.matrix(&gate.params).unwrap().view(), basis_2q_fidelity, basis_1q, - None, + pulse_optimize, )?; decomposers.push(DecomposerElement { decomposer: DecomposerType::TwoQubitBasis(Box::new(decomposer)), - gate: gate.clone(), + packed_op: gate.operation.clone(), + params: gate.params.clone(), }); } } - - // If our 2q basis gates are a subset of cx, ecr, or cz then we know TwoQubitBasisDecomposer - // is an ideal decomposition and there is no need to try other decomposers - let available_basis_set: IndexSet<&str> = available_2q_basis.keys().copied().collect(); - - #[inline] - fn check_goodbye(basis_set: &IndexSet<&str>) -> bool { - !basis_set.is_empty() && basis_set.iter().all(|gate| GOODBYE_SET.contains(gate)) - } - - if check_goodbye(&available_basis_set) { + // If the 2q basis gates are a subset of GOODBYE_SET, exit here. + if available_2q_basis + .keys() + .all(|gate| GOODBYE_SET.contains(gate)) + && !available_2q_basis.is_empty() + { return Ok(Some(decomposers)); } + // Step 2: Try TwoQubitControlledUDecomposers for basis_1q in &available_1q_basis { - for (_basis_2q, gate) in available_2q_param_basis.iter() { + for (_, (gate, _)) in available_2q_param_basis.iter() { let rxx_equivalent_gate = if let Some(std_gate) = gate.operation.try_standard_gate() { RXXEquivalent::Standard(std_gate) } else { @@ -790,42 +668,46 @@ fn get_2q_decomposers_from_target( Ok(decomposer) => { decomposers.push(DecomposerElement { decomposer: DecomposerType::TwoQubitControlledU(Box::new(decomposer)), - gate: gate.clone(), + packed_op: gate.operation.clone(), + params: gate.params.clone(), }); } Err(_) => continue, }; } } - - // If our 2q basis gates are a subset of PARAM_SET, then we will use the TwoQubitControlledUDecomposer - // and there is no need to try other decomposers - - let available_basis_param_set: IndexSet<&str> = - available_2q_param_basis.keys().copied().collect(); - - #[inline] - fn check_parametrized_goodbye(basis_set: &IndexSet<&str>) -> bool { - !basis_set.is_empty() && basis_set.iter().all(|gate| PARAM_SET.contains(gate)) - } - - if check_parametrized_goodbye(&available_basis_param_set) { + // If the 2q basis gates are a subset of PARAM_SET, exit here + if available_2q_param_basis + .keys() + .all(|gate| PARAM_SET.contains(gate)) + && !available_2q_param_basis.is_empty() + { return Ok(Some(decomposers)); } - // Let's now look for possible controlled decomposers (i.e. XXDecomposer) - let controlled_basis: IndexMap<&str, NormalOperation> = available_2q_basis + // Step 3: Try XXDecomposers (Python) + #[inline] + fn is_controlled(op: &NormalOperation) -> bool { + match op.operation.matrix(&op.params) { + None => false, + Some(unitary_matrix) => { + let kak = TwoQubitWeylDecomposition::new_inner(unitary_matrix.view(), None, None) + .unwrap(); + relative_eq!(kak.b(), 0.0) && relative_eq!(kak.c(), 0.0) + } + } + } + let controlled_basis: IndexMap<&str, (NormalOperation, Option)> = available_2q_basis .iter() - .filter(|(_, v)| is_controlled(v)) - .map(|(k, v)| (*k, v.clone())) + .filter(|(_, (gate, _))| is_controlled(gate)) + .map(|(k, (gate, props))| (*k, (gate.clone(), *props))) .collect(); let mut pi2_basis: Option<&str> = None; let xx_embodiments: &Bound<'_, PyAny> = imports::XX_EMBODIMENTS.get_bound(py); - - // The xx decomposer args are the interaction strength (f64), basis_2q_fidelity (f64), + // The Python XXDecomposer args are the interaction strength (f64), basis_2q_fidelity (f64), // and embodiments (Bound<'_, PyAny>). let xx_decomposer_args = controlled_basis.iter().map( - |(name, op)| -> PyResult<(f64, f64, pyo3::Bound<'_, pyo3::PyAny>)> { + |(name, (op, props))| -> PyResult<(f64, f64, pyo3::Bound<'_, pyo3::PyAny>)> { let strength = 2.0 * TwoQubitWeylDecomposition::new_inner( op.operation.matrix(&op.params).unwrap().view(), @@ -834,8 +716,8 @@ fn get_2q_decomposers_from_target( ) .unwrap() .a(); - let mut fidelity_value = match available_2q_props.get(name) { - Some(&(_, error)) => 1.0 - error.unwrap_or_default(), // default is 0.0 + let mut fidelity_value = match props { + Some(error) => 1.0 - error, None => 1.0, }; if let Some(approx_degree) = approximation_degree { @@ -854,15 +736,12 @@ fn get_2q_decomposers_from_target( Ok((strength, fidelity_value, embodiment)) }, ); - let basis_2q_fidelity_dict = PyDict::new(py); let embodiments_dict = PyDict::new(py); for (strength, fidelity, embodiment) in xx_decomposer_args.flatten() { basis_2q_fidelity_dict.set_item(strength, fidelity)?; embodiments_dict.set_item(strength, embodiment)?; } - - // Iterate over 2q fidelities and select decomposers if basis_2q_fidelity_dict.len() > 0 { let xx_decomposer: &Bound<'_, PyAny> = imports::XX_DECOMPOSER.get_bound(py); for basis_1q in available_1q_basis { @@ -901,49 +780,30 @@ fn get_2q_decomposers_from_target( decomposers.push(DecomposerElement { decomposer: DecomposerType::XX(decomposer.into()), - gate: decomposer_gate, + packed_op: decomposer_gate.operation, + params: decomposer_gate.params.clone(), }); } } Ok(Some(decomposers)) } +/// Function to evaluate hardware-native direction, this allows to correct +/// the synthesis output to match the target constraints. +/// Returns: +/// * `true` if gate qubits are in the hardware-native direction +/// * `false` if gate qubits must be flipped to match hardware-native direction fn preferred_direction( - decomposer: &DecomposerElement, ref_qubits: &[PhysicalQubit; 2], natural_direction: Option, coupling_edges: &HashSet<[PhysicalQubit; 2]>, - target: &Target, + target: Option<&Target>, + decomposer: &DecomposerElement, ) -> PyResult> { - // Returns: - // * true if gate qubits are in the hardware-native direction - // * false if gate qubits must be flipped to match hardware-native direction let qubits: [PhysicalQubit; 2] = *ref_qubits; let mut reverse_qubits: [PhysicalQubit; 2] = qubits; reverse_qubits.reverse(); - let compute_cost = - |lengths: bool, q_tuple: [PhysicalQubit; 2], in_cost: f64| -> PyResult { - let cost = match target.qargs_for_operation_name(decomposer.gate.operation.name()) { - Ok(_) => match target[decomposer.gate.operation.name()].get(Some( - &q_tuple - .into_iter() - .collect::>(), - )) { - Some(Some(_props)) => { - if lengths { - _props.duration.unwrap_or(in_cost) - } else { - _props.error.unwrap_or(in_cost) - } - } - _ => in_cost, - }, - Err(_) => in_cost, - }; - Ok(cost) - }; - let preferred_direction = match natural_direction { Some(false) => None, _ => { @@ -955,31 +815,60 @@ fn preferred_direction( (true, false) => Some(true), (false, true) => Some(false), _ => { - let mut cost_0_1: f64 = f64::INFINITY; - let mut cost_1_0: f64 = f64::INFINITY; - - // Try to find the cost in gate_lengths - cost_0_1 = compute_cost(true, qubits, cost_0_1)?; - cost_1_0 = compute_cost(true, reverse_qubits, cost_1_0)?; - - // If no valid cost was found in gate_lengths, check gate_errors - if !(cost_0_1 < f64::INFINITY || cost_1_0 < f64::INFINITY) { - cost_0_1 = compute_cost(false, qubits, cost_0_1)?; - cost_1_0 = compute_cost(false, reverse_qubits, cost_1_0)?; - } + match target { + Some(target) => { + let mut cost_0_1: f64 = f64::INFINITY; + let mut cost_1_0: f64 = f64::INFINITY; + + let compute_cost = |lengths: bool, + q_tuple: [PhysicalQubit; 2], + in_cost: f64| + -> PyResult { + let cost = match target + .qargs_for_operation_name(decomposer.packed_op.name()) + { + Ok(_) => match target[decomposer.packed_op.name()].get(Some( + &q_tuple + .into_iter() + .collect::>(), + )) { + Some(Some(_props)) => { + if lengths { + _props.duration.unwrap_or(in_cost) + } else { + _props.error.unwrap_or(in_cost) + } + } + _ => in_cost, + }, + Err(_) => in_cost, + }; + Ok(cost) + }; + // Try to find the cost in gate_lengths + cost_0_1 = compute_cost(true, qubits, cost_0_1)?; + cost_1_0 = compute_cost(true, reverse_qubits, cost_1_0)?; + + // If no valid cost was found in gate_lengths, check gate_errors + if !(cost_0_1 < f64::INFINITY || cost_1_0 < f64::INFINITY) { + cost_0_1 = compute_cost(false, qubits, cost_0_1)?; + cost_1_0 = compute_cost(false, reverse_qubits, cost_1_0)?; + } - if cost_0_1 < cost_1_0 { - Some(true) - } else if cost_1_0 < cost_0_1 { - Some(false) - } else { - None + if cost_0_1 < cost_1_0 { + Some(true) + } else if cost_1_0 < cost_0_1 { + Some(false) + } else { + None + } + } + None => None, } } } } }; - if natural_direction == Some(true) && preferred_direction.is_none() { return Err(QiskitError::new_err(format!( concat!( @@ -989,10 +878,10 @@ fn preferred_direction( qubits ))); } - Ok(preferred_direction) } +/// Apply synthesis for decomposers that return a SEQUENCE (TwoQubitBasis and TwoQubitControlledU). fn synth_su4_sequence( su4_mat: &Array2, decomposer_2q: &DecomposerElement, @@ -1009,9 +898,9 @@ fn synth_su4_sequence( }; let sequence = TwoQubitUnitarySequence { gate_sequence: synth, - decomp_gate: decomposer_2q.gate.clone(), + decomp_op: decomposer_2q.packed_op.clone(), + decomp_params: decomposer_2q.params.clone(), }; - match preferred_direction { None => Ok(sequence), Some(preferred_dir) => { @@ -1024,7 +913,6 @@ fn synth_su4_sequence( synth_direction = Some(qubits.clone()); } } - match synth_direction { None => Ok(sequence), Some(synth_direction) => { @@ -1048,6 +936,9 @@ fn synth_su4_sequence( } } +/// Apply reverse synthesis for decomposers that return a SEQUENCE (TwoQubitBasis and TwoQubitControlledU). +/// This function is called by `synth_su4_sequence`` if the "direct" synthesis +/// doesn't match the hardware restrictions. fn reversed_synth_su4_sequence( mut su4_mat: Array2, decomposer_2q: &DecomposerElement, @@ -1071,7 +962,6 @@ fn reversed_synth_su4_sequence( "reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer." ) }; - let flip_bits: [u8; 2] = [1, 0]; let mut reversed_gates = Vec::with_capacity(synth.gates().len()); for (gate, params, qubit_ids) in synth.gates() { @@ -1081,16 +971,17 @@ fn reversed_synth_su4_sequence( .collect::>(); reversed_gates.push((*gate, params.clone(), new_qubit_ids.clone())); } - let mut reversed_synth: TwoQubitGateSequence = TwoQubitGateSequence::new(); reversed_synth.set_state((reversed_gates, synth.global_phase())); let sequence = TwoQubitUnitarySequence { gate_sequence: reversed_synth, - decomp_gate: decomposer_2q.gate.clone(), + decomp_op: decomposer_2q.packed_op.clone(), + decomp_params: decomposer_2q.params.clone(), }; Ok(sequence) } +/// Apply synthesis for decomposers that return a DAG (XX). fn synth_su4_dag( py: Python, su4_mat: &Array2, @@ -1113,7 +1004,6 @@ fn synth_su4_dag( } else { unreachable!("synth_su4_dag should only be called for XXDecomposer.") }; - match preferred_direction { None => Ok(synth_dag), Some(preferred_dir) => { @@ -1149,6 +1039,9 @@ fn synth_su4_dag( } } +/// Apply reverse synthesis for decomposers that return a DAG (XX). +/// This function is called by `synth_su4_dag`` if the "direct" synthesis +/// doesn't match the hardware restrictions. fn reversed_synth_su4_dag( py: Python<'_>, mut su4_mat: Array2, @@ -1196,6 +1089,263 @@ fn reversed_synth_su4_dag( Ok(target_dag) } +/// Score the synthesis output (DAG or sequence) based on the expected gate fidelity/error score. +fn synth_error( + py: Python<'_>, + synth_circuit: impl Iterator< + Item = ( + String, + Option>, + SmallVec<[PhysicalQubit; 2]>, + ), + >, + target: &Target, +) -> f64 { + let (lower_bound, upper_bound) = synth_circuit.size_hint(); + let mut gate_fidelities = match upper_bound { + Some(bound) => Vec::with_capacity(bound), + None => Vec::with_capacity(lower_bound), + }; + let mut score_instruction = + |inst_name: &str, + inst_params: &Option>, + inst_qubits: &SmallVec<[PhysicalQubit; 2]>| { + if let Ok(names) = target.operation_names_for_qargs(Some(inst_qubits)) { + for name in names { + if let Ok(target_op) = target.operation_from_name(name) { + let are_params_close = if let Some(params) = inst_params { + params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { + p1.is_close(py, p2, 1e-10) + .expect("Unexpected parameter expression error.") + }) + } else { + false + }; + let is_parametrized = target_op + .params + .iter() + .any(|param| matches!(param, Param::ParameterExpression(_))); + if target_op.operation.name() == inst_name + && (is_parametrized || are_params_close) + { + match target[name].get(Some(inst_qubits)) { + Some(Some(props)) => { + gate_fidelities.push(1.0 - props.error.unwrap_or(0.0)) + } + _ => gate_fidelities.push(1.0), + } + break; + } + } + } + } + }; + + for (inst_name, inst_params, inst_qubits) in synth_circuit { + score_instruction(&inst_name, &inst_params, &inst_qubits); + } + 1.0 - gate_fidelities.into_iter().product::() +} + +/// Perform 2q unitary synthesis for a given `unitary`. If some `target` is provided, +/// the decomposition will be hardware-aware and take into the account the reported +/// gate errors to select the best method among the options. If `target` is `None``, +/// the decompostion will use the given `basis_gates` and the first valid decomposition +/// will be returned (no selection). +fn run_2q_unitary_synthesis( + py: Python, + unitary: Array2, + ref_qubits: &[PhysicalQubit; 2], + coupling_edges: &HashSet<[PhysicalQubit; 2]>, + target: Option<&Target>, + basis_gates: HashSet, + approximation_degree: Option, + natural_direction: Option, + pulse_optimize: Option, + out_dag: &mut DAGCircuit, + out_qargs: &[Qubit], + mut apply_original_op: impl FnMut(&mut DAGCircuit) -> PyResult<()>, +) -> PyResult<()> { + // Find decomposer candidates + let decomposers = match target { + Some(target) => { + let decomposers_2q = get_2q_decomposers_from_target( + py, + target, + ref_qubits, + approximation_degree, + pulse_optimize, + )?; + decomposers_2q.unwrap_or_default() + } + None => { + let basis_gates: IndexSet<&str> = basis_gates.iter().map(String::as_str).collect(); + let decomposer_item: Option = + get_2q_decomposer_from_basis(basis_gates, approximation_degree, pulse_optimize)?; + if decomposer_item.is_none() { + apply_original_op(out_dag)?; + return Ok(()); + }; + vec![decomposer_item.unwrap()] + } + }; + + // If there's a single decomposer candidate, avoid computing synthesis score. + // This will ALWAYS be the path if the `target` is `None` (`basis_gates` used). + if decomposers.len() == 1 { + let decomposer_item = decomposers.first().unwrap(); + let preferred_dir = preferred_direction( + ref_qubits, + natural_direction, + coupling_edges, + target, + decomposer_item, + )?; + + match decomposer_item.decomposer { + DecomposerType::TwoQubitBasis(_) => { + let synth = synth_su4_sequence( + &unitary, + decomposer_item, + preferred_dir, + approximation_degree, + )?; + apply_synth_sequence(py, out_dag, out_qargs, &synth)?; + } + DecomposerType::TwoQubitControlledU(_) => { + let synth = synth_su4_sequence( + &unitary, + decomposer_item, + preferred_dir, + approximation_degree, + )?; + apply_synth_sequence(py, out_dag, out_qargs, &synth)?; + } + DecomposerType::XX(_) => { + let synth = synth_su4_dag( + py, + &unitary, + decomposer_item, + preferred_dir, + approximation_degree, + )?; + apply_synth_dag(py, out_dag, out_qargs, &synth)?; + } + } + return Ok(()); + } + + // If there is more than one available decomposer, select the one with the best synthesis score. + // This will only happen if `target` is not `None`, so we can assume that there is some target from + // this point onwards. The scored SEQUENCEs and DAGs are stored in independent vectors to avoid defining + // yet another custom type. + let mut synth_errors_sequence = Vec::new(); + let mut synth_errors_dag = Vec::new(); + + // The sequence synthesis logic can be shared between TwoQubitBasis and TwoQubitControlledU, + // but the DAG logic needs to stay independent. + let synth_sequence = |decomposer, preferred_dir| -> PyResult<(TwoQubitUnitarySequence, f64)> { + let sequence = + synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; + let scoring_info = + sequence + .gate_sequence + .gates() + .iter() + .map(|(gate, params, qubit_ids)| { + let inst_qubits = qubit_ids.iter().map(|q| ref_qubits[*q as usize]).collect(); + match gate { + Some(gate) => ( + gate.name().to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + None => ( + sequence.decomp_op.name().to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + } + }); + let score = synth_error(py, scoring_info, target.unwrap()); + Ok((sequence, score)) + }; + + for decomposer in &decomposers { + let preferred_dir = preferred_direction( + ref_qubits, + natural_direction, + coupling_edges, + target, + decomposer, + )?; + match &decomposer.decomposer { + DecomposerType::TwoQubitBasis(_) => { + synth_errors_sequence.push(synth_sequence(decomposer, preferred_dir)?); + } + DecomposerType::TwoQubitControlledU(_) => { + synth_errors_sequence.push(synth_sequence(decomposer, preferred_dir)?); + } + DecomposerType::XX(_) => { + let synth_dag = synth_su4_dag( + py, + &unitary, + decomposer, + preferred_dir, + approximation_degree, + )?; + let scoring_info = synth_dag + .topological_op_nodes() + .expect("Unexpected error in dag.topological_op_nodes()") + .map(|node| { + let NodeType::Operation(inst) = &synth_dag[node] else { + unreachable!("DAG node must be an instruction") + }; + let inst_qubits = synth_dag + .get_qargs(inst.qubits) + .iter() + .map(|q| ref_qubits[q.0 as usize]) + .collect(); + ( + inst.op.name().to_string(), + inst.params.clone().map(|boxed| *boxed), + inst_qubits, + ) + }); + let score = synth_error(py, scoring_info, target.unwrap()); + synth_errors_dag.push((synth_dag, score)); + } + } + } + + // Resolve synthesis scores between sequence and DAG. + let synth_sequence = synth_errors_sequence + .iter() + .enumerate() + .min_by(|error1, error2| error1.1 .1.partial_cmp(&error2.1 .1).unwrap()) + .map(|(index, _)| &synth_errors_sequence[index]); + + let synth_dag = synth_errors_dag + .iter() + .enumerate() + .min_by(|error1, error2| error1.1 .1.partial_cmp(&error2.1 .1).unwrap()) + .map(|(index, _)| &synth_errors_dag[index]); + + match (synth_sequence, synth_dag) { + (None, None) => apply_original_op(out_dag)?, + (Some((sequence, _)), None) => apply_synth_sequence(py, out_dag, out_qargs, sequence)?, + (None, Some((dag, _))) => apply_synth_dag(py, out_dag, out_qargs, dag)?, + (Some((sequence, sequence_error)), Some((dag, dag_error))) => { + if sequence_error > dag_error { + apply_synth_dag(py, out_dag, out_qargs, dag)? + } else { + apply_synth_sequence(py, out_dag, out_qargs, sequence)? + } + } + }; + Ok(()) +} + #[pymodule] pub fn unitary_synthesis(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(py_run_main_loop))?; diff --git a/crates/circuit/src/duration.rs b/crates/circuit/src/duration.rs new file mode 100644 index 000000000000..554a8fe88bdc --- /dev/null +++ b/crates/circuit/src/duration.rs @@ -0,0 +1,36 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2025 +// +// 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. + +use pyo3::prelude::*; + +#[pyclass(eq, module = "qiskit._accelerate.circuit")] +#[derive(PartialEq, Clone, Copy, Debug)] +#[allow(non_camel_case_types)] +pub enum Duration { + dt(u64), + ns(f64), + us(f64), + ms(f64), + s(f64), +} + +impl Duration { + fn __repr__(&self) -> String { + match self { + Duration::ns(t) => format!("Duration.ns({})", t), + Duration::us(t) => format!("Duration.us({})", t), + Duration::ms(t) => format!("Duration.ms({})", t), + Duration::s(t) => format!("Duration.s({})", t), + Duration::dt(t) => format!("Duration.dt({})", t), + } + } +} diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 5138438dec33..84df43deda27 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -17,6 +17,7 @@ pub mod converters; pub mod dag_circuit; pub mod dag_node; mod dot_utils; +pub mod duration; pub mod error; pub mod gate_matrix; pub mod imports; @@ -157,6 +158,7 @@ macro_rules! impl_intopyobject_for_copy_pyclass { } pub fn circuit(m: &Bound) -> PyResult<()> { + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 0b6de35ae017..d174858b67b7 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -298,13 +298,14 @@ pub enum DelayUnit { MS, S, DT, + EXPR, } unsafe impl ::bytemuck::CheckedBitPattern for DelayUnit { type Bits = u8; fn is_valid_bit_pattern(bits: &Self::Bits) -> bool { - *bits < 6 + *bits < 7 } } unsafe impl ::bytemuck::NoUninit for DelayUnit {} @@ -321,6 +322,7 @@ impl fmt::Display for DelayUnit { DelayUnit::MS => "ms", DelayUnit::S => "s", DelayUnit::DT => "dt", + DelayUnit::EXPR => "expr", } ) } @@ -336,6 +338,7 @@ impl<'py> FromPyObject<'py> for DelayUnit { "ms" => DelayUnit::MS, "s" => DelayUnit::S, "dt" => DelayUnit::DT, + "expr" => DelayUnit::EXPR, unknown_unit => { return Err(PyValueError::new_err(format!( "Unit '{}' is invalid.", diff --git a/pyproject.toml b/pyproject.toml index 0ff81bf9add7..61d845484737 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ Issues = "https://github.com/Qiskit/qiskit/issues" Changelog = "https://docs.quantum.ibm.com/api/qiskit/release-notes" [project.entry-points."qiskit.unitary_synthesis"] -default = "qiskit.transpiler.passes.synthesis.unitary_synthesis:DefaultUnitarySynthesis" +default = "qiskit.transpiler.passes.synthesis.default_unitary_synth_plugin:DefaultUnitarySynthesis" aqc = "qiskit.transpiler.passes.synthesis.aqc_plugin:AQCSynthesisPlugin" sk = "qiskit.transpiler.passes.synthesis.solovay_kitaev_synthesis:SolovayKitaevSynthesis" diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index 8bb833b18464..aabb14768562 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -1282,6 +1282,8 @@ def __array__(self, dtype=None, copy=None): \end{pmatrix} """ +from qiskit._accelerate.circuit import Duration # pylint: disable=unused-import + from .exceptions import CircuitError from . import _utils from .quantumcircuit import QuantumCircuit diff --git a/qiskit/circuit/classical/expr/__init__.py b/qiskit/circuit/classical/expr/__init__.py index 00f1c2e06767..979a038d43cf 100644 --- a/qiskit/circuit/classical/expr/__init__.py +++ b/qiskit/circuit/classical/expr/__init__.py @@ -210,6 +210,10 @@ "greater", "greater_equal", "index", + "add", + "sub", + "mul", + "div", "lift_legacy_condition", ] @@ -234,5 +238,9 @@ shift_left, shift_right, index, + add, + sub, + mul, + div, lift_legacy_condition, ) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index de3875eef90c..e8fafbc68f12 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -19,6 +19,7 @@ __all__ = [ "lift", + "cast", "bit_not", "logic_not", "bit_and", @@ -32,6 +33,13 @@ "less_equal", "greater", "greater_equal", + "shift_left", + "shift_right", + "index", + "add", + "sub", + "mul", + "div", "lift_legacy_condition", ] @@ -91,10 +99,14 @@ def lift_legacy_condition( return Binary(Binary.Op.EQUAL, left, right, types.Bool()) -def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr: +def lift(value: typing.Any, /, type: types.Type | None = None, *, try_const: bool = False) -> Expr: """Lift the given Python ``value`` to a :class:`~.expr.Value` or :class:`~.expr.Var`. - If an explicit ``type`` is given, the typing in the output will reflect that. + By default, lifted scalars are not const. To lift supported scalars to const-typed + expressions, specify `try_const=True`. + + If an explicit ``type`` is given, the typing in the output will reflect that, + including its const-ness. The ``try_const`` parameter is ignored when this is specified. Examples: Lifting simple circuit objects to be :class:`~.expr.Var` instances:: @@ -102,9 +114,9 @@ def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr: >>> from qiskit.circuit import Clbit, ClassicalRegister >>> from qiskit.circuit.classical import expr >>> expr.lift(Clbit()) - Var(, Bool()) + Var(, Bool(const=False)) >>> expr.lift(ClassicalRegister(3, "c")) - Var(ClassicalRegister(3, "c"), Uint(3)) + Var(ClassicalRegister(3, "c"), Uint(3, const=False)) The type of the return value can be influenced, if the given value could be interpreted losslessly as the given type (use :func:`cast` to perform a full set of casting @@ -113,27 +125,51 @@ def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr: >>> from qiskit.circuit import ClassicalRegister >>> from qiskit.circuit.classical import expr, types >>> expr.lift(ClassicalRegister(3, "c"), types.Uint(5)) - Var(ClassicalRegister(3, "c"), Uint(5)) + Var(ClassicalRegister(3, "c"), Uint(5, const=False)) >>> expr.lift(5, types.Uint(4)) Value(5, Uint(4)) + + Lifting non-classical resource scalars to const values:: + + >>> from qiskit.circuit.classical import expr, types + >>> expr.lift(7) + Value(7, Uint(3, const=False)) + >>> expr.lift(7, try_const=True) + Value(7, Uint(3, const=True)) + >>> expr.lift(7, types.Uint(8, const=True)) + Value(7, Uint(8, const=True)) + """ if isinstance(value, Expr): if type is not None: raise ValueError("use 'cast' to cast existing expressions, not 'lift'") return value - from qiskit.circuit import Clbit, ClassicalRegister # pylint: disable=cyclic-import + from qiskit.circuit import Clbit, ClassicalRegister, Duration # pylint: disable=cyclic-import + if type is not None: + # If a type was specified, the inferred type must be the same + # const-ness. + try_const = type.const inferred: types.Type - if value is True or value is False or isinstance(value, Clbit): + if value is True or value is False: + inferred = types.Bool(const=try_const) + constructor = Value + elif isinstance(value, Clbit): inferred = types.Bool() - constructor = Value if value is True or value is False else Var + constructor = Var elif isinstance(value, ClassicalRegister): inferred = types.Uint(width=value.size) constructor = Var elif isinstance(value, int): if value < 0: raise ValueError("cannot represent a negative value") - inferred = types.Uint(width=value.bit_length() or 1) + inferred = types.Uint(width=value.bit_length() or 1, const=try_const) + constructor = Value + elif isinstance(value, float): + inferred = types.Float(const=try_const) + constructor = Value + elif isinstance(value, Duration): + inferred = types.Duration() constructor = Value else: raise TypeError(f"failed to infer a type for '{value}'") @@ -150,14 +186,23 @@ def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr: def cast(operand: typing.Any, type: types.Type, /) -> Expr: """Create an explicit cast from the given value to the given type. + This can also be used to cast-away const status. + Examples: Add an explicit cast node that explicitly casts a higher precision type to a lower precision one:: >>> from qiskit.circuit.classical import expr, types - >>> value = expr.value(5, types.Uint(32)) + >>> value = expr.Value(5, types.Uint(32)) >>> expr.cast(value, types.Uint(8)) - Cast(Value(5, types.Uint(32)), types.Uint(8), implicit=False) + Cast(Value(5, types.Uint(32, const=False)), types.Uint(8, const=False), implicit=False) + + Cast-away const status:: + + >>> from qiskit.circuit.classical import expr, types + >>> value = expr.Value(5, types.Uint(32, const=True)) + >>> expr.cast(value, types.Uint(32)) + Cast(Value(5, types.Uint(32, const=True)), types.Uint(32, const=False), implicit=False) """ operand = lift(operand) if cast_kind(operand.type, type) is CastKind.NONE: @@ -175,7 +220,8 @@ def bit_not(operand: typing.Any, /) -> Expr: >>> from qiskit.circuit import ClassicalRegister >>> from qiskit.circuit.classical import expr >>> expr.bit_not(ClassicalRegister(3, "c")) - Unary(Unary.Op.BIT_NOT, Var(ClassicalRegister(3, 'c'), Uint(3)), Uint(3)) + Unary(Unary.Op.BIT_NOT, \ +Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), Uint(3, const=False)) """ operand = lift(operand) if operand.type.kind not in (types.Bool, types.Uint): @@ -195,43 +241,75 @@ def logic_not(operand: typing.Any, /) -> Expr: >>> expr.logic_not(ClassicalRegister(3, "c")) Unary(\ Unary.Op.LOGIC_NOT, \ -Cast(Var(ClassicalRegister(3, 'c'), Uint(3)), Bool(), implicit=True), \ -Bool()) +Cast(Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), \ +Bool(const=False), implicit=True), \ +Bool(const=False)) """ - operand = _coerce_lossless(lift(operand), types.Bool()) - return Unary(Unary.Op.LOGIC_NOT, operand, operand.type) + operand = lift(operand) + try: + operand = _coerce_lossless(operand, types.Bool(const=operand.type.const)) + return Unary(Unary.Op.LOGIC_NOT, operand, operand.type) + except TypeError as ex: + raise TypeError(f"cannot apply '{Unary.Op.BIT_NOT}' to type '{operand.type}'") from ex def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Expr]: """Lift two binary operands simultaneously, inferring the widths of integer literals in either - position to match the other operand.""" + position to match the other operand. + + Const-ness is handled as follows: + * If neither operand is an expression, both are lifted as non-const. + * If only one operand is an expression, the other is lifted with the same const-ness, if possible. + Otherwise, the returned operands will have different const-ness, and thus may require a cast node + to be interoperable. + * If both operands are expressions, they are returned as-is, and may require a cast node. + """ + left_other_literal = isinstance(left, (bool, float)) left_int = isinstance(left, int) and not isinstance(left, bool) - right_int = isinstance(right, int) and not isinstance(right, bool) + right_other_literal = isinstance(right, (bool, float)) + right_int = isinstance(right, int) and not right_other_literal if not (left_int or right_int): - left = lift(left) - right = lift(right) + if left_other_literal == right_other_literal: + # They're either both literals or neither are, so we lift them + # independently. + left = lift(left) + right = lift(right) + elif not right_other_literal: + # Left is a literal, which should only be const if right is const. + right = lift(right) + left = lift(left, try_const=right.type.const) + elif not left_other_literal: + # Right is a literal, which should only be const if left is const. + left = lift(left) + right = lift(right, try_const=left.type.const) elif not right_int: + # Left is an int. right = lift(right) if right.type.kind is types.Uint: if left.bit_length() > right.type.width: raise TypeError( f"integer literal '{left}' is wider than the other operand '{right}'" ) + # Left will share const-ness of right. left = Value(left, right.type) else: - left = lift(left) + left = lift(left, try_const=right.type.const) elif not left_int: + # Right is an int. left = lift(left) if left.type.kind is types.Uint: if right.bit_length() > left.type.width: raise TypeError( f"integer literal '{right}' is wider than the other operand '{left}'" ) + # Right will share const-ness of left. right = Value(right, left.type) else: - right = lift(right) + right = lift(right, try_const=left.type.const) else: # Both are `int`, so we take our best case to make things work. + # If the caller needs a const type, they should lift one side to + # a const type explicitly before calling this function. uint = types.Uint(max(left.bit_length(), right.bit_length(), 1)) left = Value(left, uint) right = Value(right, uint) @@ -242,17 +320,17 @@ def _binary_bitwise(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: left, right = _lift_binary_operands(left, right) type: types.Type if left.type.kind is right.type.kind is types.Bool: - type = types.Bool() + type = types.Bool(const=(left.type.const and right.type.const)) elif left.type.kind is types.Uint and right.type.kind is types.Uint: - if left.type != right.type: + if left.type.width != right.type.width: raise TypeError( "binary bitwise operations are defined between unsigned integers of the same width," f" but got {left.type.width} and {right.type.width}." ) - type = left.type + type = types.Uint(width=left.type.width, const=(left.type.const and right.type.const)) else: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") - return Binary(op, left, right, type) + return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), type) def bit_and(left: typing.Any, right: typing.Any, /) -> Expr: @@ -267,9 +345,9 @@ def bit_and(left: typing.Any, right: typing.Any, /) -> Expr: >>> expr.bit_and(ClassicalRegister(3, "c"), 0b111) Binary(\ Binary.Op.BIT_AND, \ -Var(ClassicalRegister(3, 'c'), Uint(3)), \ -Value(7, Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), \ +Value(7, Uint(3, const=False)), \ +Uint(3, const=False)) """ return _binary_bitwise(Binary.Op.BIT_AND, left, right) @@ -286,9 +364,9 @@ def bit_or(left: typing.Any, right: typing.Any, /) -> Expr: >>> expr.bit_or(ClassicalRegister(3, "c"), 0b101) Binary(\ Binary.Op.BIT_OR, \ -Var(ClassicalRegister(3, 'c'), Uint(3)), \ -Value(5, Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), \ +Value(5, Uint(3, const=False)), \ +Uint(3, const=False)) """ return _binary_bitwise(Binary.Op.BIT_OR, left, right) @@ -305,18 +383,23 @@ def bit_xor(left: typing.Any, right: typing.Any, /) -> Expr: >>> expr.bit_xor(ClassicalRegister(3, "c"), 0b101) Binary(\ Binary.Op.BIT_XOR, \ -Var(ClassicalRegister(3, 'c'), Uint(3)), \ -Value(5, Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), \ +Value(5, Uint(3, const=False)), \ +Uint(3, const=False)) """ return _binary_bitwise(Binary.Op.BIT_XOR, left, right) def _binary_logical(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: - bool_ = types.Bool() - left = _coerce_lossless(lift(left), bool_) - right = _coerce_lossless(lift(right), bool_) - return Binary(op, left, right, bool_) + left = lift(left) + right = lift(right) + type = types.Bool(const=(left.type.const and right.type.const)) + try: + left = _coerce_lossless(left, type) + right = _coerce_lossless(right, type) + return Binary(op, left, right, type) + except TypeError as ex: + raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") from ex def logic_and(left: typing.Any, right: typing.Any, /) -> Expr: @@ -328,8 +411,11 @@ def logic_and(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit import Clbit >>> from qiskit.circuit.classical import expr - >>> expr.logical_and(Clbit(), Clbit()) - Binary(Binary.Op.LOGIC_AND, Var(, Bool()), Var(, Bool()), Bool()) + >>> expr.logic_and(Clbit(), Clbit()) + Binary(Binary.Op.LOGIC_AND, \ +Var(, Bool(const=False)), \ +Var(, Bool(const=False)), \ +Bool(const=False)) """ return _binary_logical(Binary.Op.LOGIC_AND, left, right) @@ -344,17 +430,29 @@ def logic_or(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit import Clbit >>> from qiskit.circuit.classical import expr >>> expr.logical_and(Clbit(), Clbit()) - Binary(Binary.Op.LOGIC_OR, Var(, Bool()), Var(, Bool()), Bool()) + Binary(Binary.Op.LOGIC_OR, \ +Var(, Bool(const=False)), \ +Var(, Bool(const=False)), \ +Bool(const=False)) """ return _binary_logical(Binary.Op.LOGIC_OR, left, right) def _equal_like(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: left, right = _lift_binary_operands(left, right) - if left.type.kind is not right.type.kind: + if ( + left.type.kind is not right.type.kind + or left.type.kind is types.Stretch + or types.order(left.type, right.type) is types.Ordering.NONE + ): raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") type = types.greater(left.type, right.type) - return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool()) + return Binary( + op, + _coerce_lossless(left, type), + _coerce_lossless(right, type), + types.Bool(const=type.const), + ) def equal(left: typing.Any, right: typing.Any, /) -> Expr: @@ -368,9 +466,9 @@ def equal(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.equal(ClassicalRegister(3, "c"), 7) Binary(Binary.Op.EQUAL, \ -Var(ClassicalRegister(3, "c"), Uint(3)), \ -Value(7, Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, "c"), Uint(3, const=False)), \ +Value(7, Uint(3, const=False)), \ +Uint(3, const=False)) """ return _equal_like(Binary.Op.EQUAL, left, right) @@ -386,19 +484,28 @@ def not_equal(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.not_equal(ClassicalRegister(3, "c"), 7) Binary(Binary.Op.NOT_EQUAL, \ -Var(ClassicalRegister(3, "c"), Uint(3)), \ -Value(7, Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, "c"), Uint(3, const=False)), \ +Value(7, Uint(3, const=False)), \ +Uint(3, const=False)) """ return _equal_like(Binary.Op.NOT_EQUAL, left, right) def _binary_relation(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: left, right = _lift_binary_operands(left, right) - if left.type.kind is not right.type.kind or left.type.kind is types.Bool: + if ( + left.type.kind is not right.type.kind + or left.type.kind in (types.Bool, types.Stretch) + or types.order(left.type, right.type) is types.Ordering.NONE + ): raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") type = types.greater(left.type, right.type) - return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool()) + return Binary( + op, + _coerce_lossless(left, type), + _coerce_lossless(right, type), + types.Bool(const=type.const), + ) def less(left: typing.Any, right: typing.Any, /) -> Expr: @@ -412,9 +519,9 @@ def less(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.less(ClassicalRegister(3, "c"), 5) Binary(Binary.Op.LESS, \ -Var(ClassicalRegister(3, "c"), Uint(3)), \ -Value(5, Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, "c"), Uint(3, const=False)), \ +Value(5, Uint(3, const=False)), \ +Uint(3, const=False)) """ return _binary_relation(Binary.Op.LESS, left, right) @@ -430,9 +537,9 @@ def less_equal(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.less(ClassicalRegister(3, "a"), ClassicalRegister(3, "b")) Binary(Binary.Op.LESS_EQUAL, \ -Var(ClassicalRegister(3, "a"), Uint(3)), \ -Var(ClassicalRegister(3, "b"), Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, "a"), Uint(3, const=False)), \ +Var(ClassicalRegister(3, "b"), Uint(3, const=False)), \ +Uint(3,const=False)) """ return _binary_relation(Binary.Op.LESS_EQUAL, left, right) @@ -448,9 +555,9 @@ def greater(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.less(ClassicalRegister(3, "c"), 5) Binary(Binary.Op.GREATER, \ -Var(ClassicalRegister(3, "c"), Uint(3)), \ -Value(5, Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, "c"), Uint(3, const=False)), \ +Value(5, Uint(3, const=False)), \ +Uint(3, const=False)) """ return _binary_relation(Binary.Op.GREATER, left, right) @@ -466,9 +573,9 @@ def greater_equal(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.less(ClassicalRegister(3, "a"), ClassicalRegister(3, "b")) Binary(Binary.Op.GREATER_EQUAL, \ -Var(ClassicalRegister(3, "a"), Uint(3)), \ -Var(ClassicalRegister(3, "b"), Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, "a"), Uint(3, const=False)), \ +Var(ClassicalRegister(3, "b"), Uint(3, const=False)), \ +Uint(3, const=False)) """ return _binary_relation(Binary.Op.GREATER_EQUAL, left, right) @@ -482,10 +589,15 @@ def _shift_like( left = _coerce_lossless(left, type) if type is not None else left else: left = lift(left, type) - right = lift(right) + right = lift(right, try_const=left.type.const) if left.type.kind != types.Uint or right.type.kind != types.Uint: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") - return Binary(op, left, right, left.type) + return Binary( + op, + left, + right, + types.Uint(width=left.type.width, const=(left.type.const and right.type.const)), + ) def shift_left(left: typing.Any, right: typing.Any, /, type: types.Type | None = None) -> Expr: @@ -501,17 +613,17 @@ def shift_left(left: typing.Any, right: typing.Any, /, type: types.Type | None = >>> a = expr.Var.new("a", types.Uint(8)) >>> expr.shift_left(a, 4) Binary(Binary.Op.SHIFT_LEFT, \ -Var(, Uint(8), name='a'), \ -Value(4, Uint(3)), \ -Uint(8)) +Var(, Uint(8, const=False), name='a'), \ +Value(4, Uint(3, const=False)), \ +Uint(8, const=False)) Shift an integer literal by a variable amount, coercing the type of the literal:: >>> expr.shift_left(3, a, types.Uint(16)) Binary(Binary.Op.SHIFT_LEFT, \ -Value(3, Uint(16)), \ -Var(, Uint(8), name='a'), \ -Uint(16)) +Value(3, Uint(16, const=False)), \ +Var(, Uint(8, const=False), name='a'), \ +Uint(16, const=False)) """ return _shift_like(Binary.Op.SHIFT_LEFT, left, right, type) @@ -529,9 +641,9 @@ def shift_right(left: typing.Any, right: typing.Any, /, type: types.Type | None >>> from qiskit.circuit.classical import expr >>> expr.shift_right(ClassicalRegister(8, "a"), 4) Binary(Binary.Op.SHIFT_RIGHT, \ -Var(ClassicalRegister(8, "a"), Uint(8)), \ -Value(4, Uint(3)), \ -Uint(8)) +Var(ClassicalRegister(8, "a"), Uint(8, const=False)), \ +Value(4, Uint(3, const=False)), \ +Uint(8, const=False)) """ return _shift_like(Binary.Op.SHIFT_RIGHT, left, right, type) @@ -548,9 +660,247 @@ def index(target: typing.Any, index: typing.Any, /) -> Expr: >>> from qiskit.circuit import ClassicalRegister >>> from qiskit.circuit.classical import expr >>> expr.index(ClassicalRegister(8, "a"), 3) - Index(Var(ClassicalRegister(8, "a"), Uint(8)), Value(3, Uint(2)), Bool()) + Index(\ +Var(ClassicalRegister(8, "a"), Uint(8, const=False)), \ +Value(3, Uint(2, const=False)), \ +Bool(const=False)) """ - target, index = lift(target), lift(index) + target = lift(target) + index = lift(index, try_const=target.type.const) if target.type.kind is not types.Uint or index.type.kind is not types.Uint: raise TypeError(f"invalid types for indexing: '{target.type}' and '{index.type}'") - return Index(target, index, types.Bool()) + return Index(target, index, types.Bool(const=target.type.const and index.type.const)) + + +def _binary_sum(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: + left, right = _lift_binary_operands(left, right) + if ( + left.type.kind is types.Bool + or right.type.kind is types.Bool + or left.type.kind is not right.type.kind + and types.order(left.type, right.type) is types.Ordering.NONE + ): + raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") + type = types.greater(left.type, right.type) + return Binary( + op, + _coerce_lossless(left, type), + _coerce_lossless(right, type), + type, + ) + + +def add(left: typing.Any, right: typing.Any, /) -> Expr: + """Create an addition expression node from the given values, resolving any implicit casts and + lifting the values into :class:`Value` nodes if required. + + Examples: + Addition of two const floating point numbers:: + + >>> from qiskit.circuit.classical import expr + >>> expr.add(expr.lift(5.0, try_const=True), 2.0) + Binary(\ +Binary.Op.ADD, \ +Value(5.0, Float(const=True)), \ +Value(2.0, Float(const=True)), \ +Float(const=True)) + + Addition of two durations:: + + >>> from qiskit.circuit import Duration + >>> from qiskit.circuit.classical import expr + >>> expr.add(Duration.dt(1000), Duration.dt(1000)) + Binary(\ +Binary.Op.ADD, \ +Value(Duration.dt(1000), Duration()), \ +Value(Duration.dt(1000), Duration()), \ +Duration()) + + Addition of stretch and duration:: + + >>> from qiskit.circuit import Duration + >>> from qiskit.circuit.classical import expr, types + >>> expr.add(expr.Var.new("a", types.Stretch()), Duration.dt(1000)) + Binary(\ +Binary.Op.ADD, \ +Var(, Stretch(), name='a'), \ +Cast(Value(Duration.dt(1000), Duration()), Stretch(), implicit=True), \ +Stretch()) + """ + return _binary_sum(Binary.Op.ADD, left, right) + + +def sub(left: typing.Any, right: typing.Any, /) -> Expr: + """Create a subtraction expression node from the given values, resolving any implicit casts and + lifting the values into :class:`Value` nodes if required. + + Examples: + Subtration of two const floating point numbers:: + + >>> from qiskit.circuit.classical import expr + >>> expr.sub(expr.lift(5.0, try_const=True), 2.0) + Binary(\ +Binary.Op.SUB, \ +Value(5.0, Float(const=True)), \ +Value(2.0, Float(const=True)), \ +Float(const=True)) + + Subtraction of two durations:: + + >>> from qiskit.circuit import Duration + >>> from qiskit.circuit.classical import expr + >>> expr.add(Duration.dt(1000), Duration.dt(1000)) + Binary(\ +Binary.Op.SUB, \ +Value(Duration.dt(1000), Duration()), \ +Value(Duration.dt(1000), Duration()), \ +Duration()) + + Subtraction of duration from stretch:: + + >>> from qiskit.circuit import Duration + >>> from qiskit.circuit.classical import expr, types + >>> expr.add(expr.Var.new("a", types.Stretch()), Duration.dt(1000)) + Binary(\ +Binary.Op.SUB, \ +Var(, Stretch(), name='a'), \ +Cast(Value(Duration.dt(1000), Duration()), Stretch(), implicit=True), \ +Stretch()) + """ + return _binary_sum(Binary.Op.SUB, left, right) + + +def mul(left: typing.Any, right: typing.Any) -> Expr: + """Create a multiplication expression node from the given values, resolving any implicit casts and + lifting the values into :class:`Value` nodes if required. + + This can be used to multiply numeric operands of the same type kind, or to multiply a timing + operand by a const :class:`~.types.Float`. + + Examples: + Multiplication of two const floating point numbers:: + + >>> from qiskit.circuit.classical import expr + >>> expr.mul(expr.lift(5.0, try_const=True), 2.0) + Binary(\ +Binary.Op.MUL, \ +Value(5.0, Float(const=True)), \ +Value(2.0, Float(const=True)), \ +Float(const=True)) + + Multiplication of a duration by a const float:: + + >>> from qiskit.circuit import Duration + >>> from qiskit.circuit.classical import expr + >>> expr.mul(Duration.dt(1000), 0.5) + Binary(\ +Binary.Op.MUL, \ +Value(Duration.dt(1000), Duration()), \ +Value(0.5, Float(const=True)), \ +Duration()) + + Multiplication of a stretch by a const float:: + + >>> from qiskit.circuit.classical import expr + >>> expr.mul(expr.Var.new("a", types.Stretch()), 0.5) + Binary(\ +Binary.Op.MUL, \ +Var(, Stretch(), name='a'), \ +Value(0.5, Float(const=True)), \ +Stretch()) + """ + left, right = _lift_binary_operands(left, right) + left_timing = left.type.kind in (types.Stretch, types.Duration) + right_timing = right.type.kind in (types.Stretch, types.Duration) + type: types.Type + if left_timing and right_timing: + raise TypeError(f"cannot multiply two timing operands: '{left.type}' and '{right.type}'") + if left_timing and right.type.kind is types.Float and right.type.const is True: + type = left.type + elif right_timing and left.type.kind is types.Float and left.type.const is True: + type = right.type + elif ( + left.type.kind is right.type.kind + and left.type.kind is not types.Bool + and types.order(left.type, right.type) is not types.Ordering.NONE + ): + type = types.greater(left.type, right.type) + left = _coerce_lossless(left, type) + right = _coerce_lossless(right, type) + else: + raise TypeError(f"invalid types for '{Binary.Op.MUL}': '{left.type}' and '{right.type}'") + return Binary( + Binary.Op.MUL, + left, + right, + type, + ) + + +def div(left: typing.Any, right: typing.Any) -> Expr: + """Create a division expression node from the given values, resolving any implicit casts and + lifting the values into :class:`Value` nodes if required. + + This can be used to divide numeric operands of the same type kind, to divide a timing + operand by a :class:`~.types.Float`, or to divide two :class`~.types.Duration` operands + which yields an expression of type const :class:`~.types.Float`. + + Examples: + Division of two const floating point numbers:: + + >>> from qiskit.circuit.classical import expr + >>> expr.div(expr.lift(5.0, try_const=True), 2.0) + Binary(\ +Binary.Op.DIV, \ +Value(5.0, Float(const=True)), \ +Value(2.0, Float(const=True)), \ +Float(const=True)) + + Division of two durations:: + + >>> from qiskit.circuit import Duration + >>> from qiskit.circuit.classical import expr + >>> expr.div(Duration.dt(10000), Duration.dt(1000)) + Binary(\ +Binary.Op.DIV, \ +Value(Duration.dt(10000), Duration()), \ +Value(Duration.dt(1000), Duration()), \ +Float(const=True)) + + + Division of a duration by a float:: + + >>> from qiskit.circuit import Duration + >>> from qiskit.circuit.classical import expr + >>> expr.div(Duration.dt(10000), 12.0) + Binary(\ +Binary.Op.DIV, \ +Value(Duration.dt(10000), Duration()), \ +Value(12.0, types.Float(const=True)), \ +Float(const=True)) + """ + left, right = _lift_binary_operands(left, right) + type: types.Type + if left.type.kind is right.type.kind is not types.Bool: + if left.type.kind is types.Stretch: + raise TypeError("cannot divide two stretch operands") + if left.type.kind is types.Duration: + type = types.Float(const=True) + elif types.order(left.type, right.type) is not types.Ordering.NONE: + type = types.greater(left.type, right.type) + left = _coerce_lossless(left, type) + right = _coerce_lossless(right, type) + elif ( + left.type.kind in (types.Stretch, types.Duration) + and right.type.kind is types.Float + and right.type.const is True + ): + type = left.type + else: + raise TypeError(f"invalid types for '{Binary.Op.DIV}': '{left.type}' and '{right.type}'") + return Binary( + Binary.Op.DIV, + left, + right, + type, + ) diff --git a/qiskit/circuit/classical/expr/expr.py b/qiskit/circuit/classical/expr/expr.py index 586b06ec9dbc..ca8d3038b6e3 100644 --- a/qiskit/circuit/classical/expr/expr.py +++ b/qiskit/circuit/classical/expr/expr.py @@ -151,8 +151,9 @@ def new(cls, name: str, type: types.Type) -> typing.Self: @property def standalone(self) -> bool: - """Whether this :class:`Var` is a standalone variable that owns its storage location. If - false, this is a wrapper :class:`Var` around a pre-existing circuit object.""" + """Whether this :class:`Var` is a standalone variable that owns its storage + location, if applicable. If false, this is a wrapper :class:`Var` around a + pre-existing circuit object.""" return isinstance(self.var, uuid.UUID) def accept(self, visitor, /): @@ -336,6 +337,14 @@ class Op(enum.Enum): """Zero-padding bitshift to the left. ``lhs << rhs``.""" SHIFT_RIGHT = 13 """Zero-padding bitshift to the right. ``lhs >> rhs``.""" + ADD = 14 + """Addition. ``lhs + rhs``.""" + SUB = 15 + """Subtraction. ``lhs - rhs``.""" + MUL = 16 + """Multiplication. ``lhs * rhs``.""" + DIV = 17 + """Division. ``lhs / rhs``.""" def __str__(self): return f"Binary.{super().__str__()}" diff --git a/qiskit/circuit/classical/expr/visitors.py b/qiskit/circuit/classical/expr/visitors.py index be7e9311c377..4d348fb848a9 100644 --- a/qiskit/circuit/classical/expr/visitors.py +++ b/qiskit/circuit/classical/expr/visitors.py @@ -276,13 +276,15 @@ def is_lvalue(node: expr.Expr, /) -> bool: >>> expr.is_lvalue(expr.lift(2)) False - :class:`~.expr.Var` nodes are always l-values, because they always have some associated - memory location:: + :class:`~.expr.Var` nodes are l-values (unless their resolution type is `const`!), because + they have some associated memory location:: >>> from qiskit.circuit.classical import types >>> from qiskit.circuit import Clbit >>> expr.is_lvalue(expr.Var.new("a", types.Bool())) True + >>> expr.is_lvalue(expr.Var.new("a", types.Bool(const=True))) + False >>> expr.is_lvalue(expr.lift(Clbit())) True diff --git a/qiskit/circuit/classical/types/__init__.py b/qiskit/circuit/classical/types/__init__.py index ae38a0d97fb5..29f6bf50d078 100644 --- a/qiskit/circuit/classical/types/__init__.py +++ b/qiskit/circuit/classical/types/__init__.py @@ -95,6 +95,9 @@ __all__ = [ "Type", "Bool", + "Duration", + "Float", + "Stretch", "Uint", "Ordering", "order", @@ -105,5 +108,5 @@ "cast_kind", ] -from .types import Type, Bool, Uint +from .types import Type, Bool, Duration, Float, Stretch, Uint from .ordering import Ordering, order, is_subtype, is_supertype, greater, CastKind, cast_kind diff --git a/qiskit/circuit/classical/types/ordering.py b/qiskit/circuit/classical/types/ordering.py index 5a4365b8e14a..f82311620923 100644 --- a/qiskit/circuit/classical/types/ordering.py +++ b/qiskit/circuit/classical/types/ordering.py @@ -26,7 +26,7 @@ import enum -from .types import Type, Bool, Uint +from .types import Type, Bool, Duration, Float, Stretch, Uint # While the type system is simple, it's overkill to represent the complete partial ordering graph of @@ -55,10 +55,6 @@ def __repr__(self): return str(self) -def _order_bool_bool(_a: Bool, _b: Bool, /) -> Ordering: - return Ordering.EQUAL - - def _order_uint_uint(left: Uint, right: Uint, /) -> Ordering: if left.width < right.width: return Ordering.LESS @@ -68,8 +64,13 @@ def _order_uint_uint(left: Uint, right: Uint, /) -> Ordering: _ORDERERS = { - (Bool, Bool): _order_bool_bool, + (Bool, Bool): lambda _a, _b, /: Ordering.EQUAL, (Uint, Uint): _order_uint_uint, + (Float, Float): lambda _a, _b, /: Ordering.EQUAL, + (Duration, Duration): lambda _a, _b, /: Ordering.EQUAL, + (Duration, Stretch): lambda _a, _b, /: Ordering.LESS, + (Stretch, Stretch): lambda _a, _b, /: Ordering.EQUAL, + (Stretch, Duration): lambda _a, _b, /: Ordering.GREATER, } @@ -83,14 +84,39 @@ def order(left: Type, right: Type, /) -> Ordering: >>> types.order(types.Uint(8), types.Uint(16)) Ordering.LESS + Compare two :class:`Bool` types of differing const-ness:: + + >>> from qiskit.circuit.classical import types + >>> types.order(types.Bool(), types.Bool(const=True)) + Ordering.GREATER + Compare two types that have no ordering between them:: >>> types.order(types.Uint(8), types.Bool()) Ordering.NONE + >>> types.order(types.Uint(8), types.Uint(16, const=True)) + Ordering.NONE """ if (orderer := _ORDERERS.get((left.kind, right.kind))) is None: return Ordering.NONE - return orderer(left, right) + order_ = orderer(left, right) + + # If the natural type ordering is equal (either one can represent both) + # but the types differ in const-ness, the non-const variant is greater. + # If one type is greater (and thus is the only type that can represent + # both) an ordering is only defined if that type is non-const or both + # types are const. + if left.const is True and right.const is False: + if order_ is Ordering.EQUAL: + return Ordering.LESS + if order_ is Ordering.GREATER: + return Ordering.NONE + if right.const is True and left.const is False: + if order_ is Ordering.EQUAL: + return Ordering.GREATER + if order_ is Ordering.LESS: + return Ordering.NONE + return order_ def is_subtype(left: Type, right: Type, /, strict: bool = False) -> bool: @@ -111,6 +137,8 @@ def is_subtype(left: Type, right: Type, /, strict: bool = False) -> bool: True >>> types.is_subtype(types.Bool(), types.Bool(), strict=True) False + >>> types.is_subtype(types.Bool(const=True), types.Bool(), strict=True) + True """ order_ = order(left, right) return order_ is Ordering.LESS or (not strict and order_ is Ordering.EQUAL) @@ -134,6 +162,8 @@ def is_supertype(left: Type, right: Type, /, strict: bool = False) -> bool: True >>> types.is_supertype(types.Bool(), types.Bool(), strict=True) False + >>> types.is_supertype(types.Bool(), types.Bool(const=True), strict=True) + True """ order_ = order(left, right) return order_ is Ordering.GREATER or (not strict and order_ is Ordering.EQUAL) @@ -195,8 +225,16 @@ def _uint_cast(from_: Uint, to_: Uint, /) -> CastKind: _ALLOWED_CASTS = { (Bool, Bool): lambda _a, _b, /: CastKind.EQUAL, (Bool, Uint): lambda _a, _b, /: CastKind.LOSSLESS, + (Bool, Float): lambda _a, _b, /: CastKind.LOSSLESS, (Uint, Bool): lambda _a, _b, /: CastKind.IMPLICIT, (Uint, Uint): _uint_cast, + (Uint, Float): lambda _a, _b, /: CastKind.DANGEROUS, + (Float, Float): lambda _a, _b, /: CastKind.EQUAL, + (Float, Uint): lambda _a, _b, /: CastKind.DANGEROUS, + (Float, Bool): lambda _a, _b, /: CastKind.DANGEROUS, + (Duration, Duration): lambda _a, _b, /: CastKind.EQUAL, + (Duration, Stretch): lambda _a, _b, /: CastKind.IMPLICIT, + (Stretch, Stretch): lambda _a, _b, /: CastKind.EQUAL, } @@ -215,11 +253,20 @@ def cast_kind(from_: Type, to_: Type, /) -> CastKind: >>> types.cast_kind(types.Uint(8), types.Bool()) + >>> types.cast_kind(types.Uint(8, const=True), types.Uint(8)) + >>> types.cast_kind(types.Bool(), types.Uint(8)) >>> types.cast_kind(types.Uint(16), types.Uint(8)) """ + if to_.const is True and from_.const is False: + # We can't cast to a const type. + return CastKind.NONE if (coercer := _ALLOWED_CASTS.get((from_.kind, to_.kind))) is None: return CastKind.NONE - return coercer(from_, to_) + cast_kind_ = coercer(from_, to_) + if cast_kind_ is CastKind.EQUAL and to_.const != from_.const: + # We need an implicit cast to drop const. + return CastKind.IMPLICIT + return cast_kind_ diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index d20e7b5fd746..f691e52d8e90 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -19,41 +19,24 @@ from __future__ import annotations -__all__ = [ - "Type", - "Bool", - "Uint", -] +__all__ = ["Type", "Bool", "Duration", "Float", "Stretch", "Uint"] import typing -class _Singleton(type): - """Metaclass to make the child, which should take zero initialization arguments, a singleton - object.""" - - def _get_singleton_instance(cls): - return cls._INSTANCE - - @classmethod - def __prepare__(mcs, name, bases): # pylint: disable=unused-argument - return {"__new__": mcs._get_singleton_instance} - - @staticmethod - def __new__(cls, name, bases, namespace): - out = super().__new__(cls, name, bases, namespace) - out._INSTANCE = object.__new__(out) # pylint: disable=invalid-name - return out - - class Type: """Root base class of all nodes in the type tree. The base case should never be instantiated directly. This must not be subclassed by users; subclasses form the internal data of the representation of - expressions, and it does not make sense to add more outside of Qiskit library code.""" + expressions, and it does not make sense to add more outside of Qiskit library code. - __slots__ = () + All subclasses are responsible for setting the ``const`` attribute in their ``__init__``. + """ + + __slots__ = ("const",) + + const: bool @property def kind(self): @@ -81,19 +64,22 @@ def __setstate__(self, state): @typing.final -class Bool(Type, metaclass=_Singleton): +class Bool(Type): """The Boolean type. This has exactly two values: ``True`` and ``False``.""" __slots__ = () + def __init__(self, *, const: bool = False): + super(Type, self).__setattr__("const", const) + def __repr__(self): - return "Bool()" + return f"Bool(const={self.const})" def __hash__(self): - return hash(self.__class__) + return hash((self.__class__, self.const)) def __eq__(self, other): - return isinstance(other, Bool) + return isinstance(other, Bool) and self.const == other.const @typing.final @@ -102,16 +88,78 @@ class Uint(Type): __slots__ = ("width",) - def __init__(self, width: int): + def __init__(self, width: int, *, const: bool = False): if isinstance(width, int) and width <= 0: raise ValueError("uint width must be greater than zero") + super(Type, self).__setattr__("const", const) super(Type, self).__setattr__("width", width) def __repr__(self): - return f"Uint({self.width})" + return f"Uint({self.width}, const={self.const})" + + def __hash__(self): + return hash((self.__class__, self.const, self.width)) + + def __eq__(self, other): + return isinstance(other, Uint) and self.const == other.const and self.width == other.width + + +@typing.final +class Float(Type): + """A floating point number of unspecified width. + In the future, this may also be used to represent a fixed-width float. + """ + + __slots__ = () + + def __init__(self, *, const: bool = False): + super(Type, self).__setattr__("const", const) + + def __repr__(self): + return f"Float(const={self.const})" + + def __hash__(self): + return hash((self.__class__, self.const)) + + def __eq__(self, other): + return isinstance(other, Float) and self.const == other.const + + +@typing.final +class Duration(Type): + """A length of time, possibly negative.""" + + __slots__ = () + + def __init__(self): + # A duration is always const. + super(Type, self).__setattr__("const", True) + + def __repr__(self): + return "Duration()" + + def __hash__(self): + return hash(self.__class__) + + def __eq__(self, other): + return isinstance(other, Duration) + + +@typing.final +class Stretch(Type): + """A special type that denotes some not-yet-known non-negative duration.""" + + __slots__ = () + + def __init__(self): + # A stretch is always const. + super(Type, self).__setattr__("const", True) + + def __repr__(self): + return "Stretch()" def __hash__(self): - return hash((self.__class__, self.width)) + return hash(self.__class__) def __eq__(self, other): - return isinstance(other, Uint) and self.width == other.width + return isinstance(other, Stretch) diff --git a/qiskit/circuit/delay.py b/qiskit/circuit/delay.py index 6376c8b1daaa..8f8ba97bfc4c 100644 --- a/qiskit/circuit/delay.py +++ b/qiskit/circuit/delay.py @@ -14,6 +14,8 @@ Delay instruction (for circuit module). """ import numpy as np + +from qiskit.circuit.classical import expr, types from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.instruction import Instruction from qiskit.circuit.gate import Gate @@ -29,13 +31,33 @@ class Delay(Instruction): _standard_instruction_type = StandardInstructionType.Delay - def __init__(self, duration, unit="dt"): + def __init__(self, duration, unit=None): """ Args: - duration: the length of time of the duration. Given in units of ``unit``. - unit: the unit of the duration. Must be ``"dt"`` or an SI-prefixed seconds unit. + duration: the length of time of the duration. If this is an + :class:`~.expr.Expr`, it must be of type :class:`~.types.Duration` + or :class:`~.types.Stretch` and the ``unit`` parameter must + not be specified. + unit: the unit of the duration, if ``duration`` is a numeric + value. Must be ``"dt"`` or an SI-prefixed seconds unit. + + Raises: + CircuitError: A ``duration`` expression was specified with a resolved + type that is not timing-based, or the ``unit`` was improperly specified. """ - if unit not in {"s", "ms", "us", "ns", "ps", "dt"}: + if isinstance(duration, expr.Expr): + if unit is not None and unit != "expr": + raise CircuitError( + "Argument 'unit' must not be specified for a duration expression." + ) + if duration.type.kind not in (types.Duration, types.Stretch): + raise CircuitError( + f"Expression of type '{duration.type}' is not valid for 'duration'." + ) + unit = "expr" + elif unit is None: + unit = "dt" + elif unit not in {"s", "ms", "us", "ns", "ps", "dt"}: raise CircuitError(f"Unknown unit {unit} is specified.") # Double underscore to differentiate from the private attribute in # `Instruction`. This can be changed to `_unit` in 2.0 after we @@ -94,7 +116,7 @@ def __repr__(self): return f"{self.__class__.__name__}(duration={self.params[0]}[unit={self.unit}])" def validate_parameter(self, parameter): - """Delay parameter (i.e. duration) must be int, float or ParameterExpression.""" + """Delay parameter (i.e. duration) must be Expr, int, float or ParameterExpression.""" if isinstance(parameter, int): if parameter < 0: raise CircuitError( @@ -112,6 +134,10 @@ def validate_parameter(self, parameter): raise CircuitError("Integer duration is expected for 'dt' unit.") return parameter_int return parameter + elif isinstance(parameter, expr.Expr): + if parameter.type.kind not in (types.Duration, types.Stretch): + raise CircuitError(f"Expression duration of type '{parameter.type}' is not valid.") + return parameter elif isinstance(parameter, ParameterExpression): if len(parameter.parameters) > 0: return parameter # expression has free parameters, we cannot validate it diff --git a/qiskit/circuit/library/standard_gates/rz.py b/qiskit/circuit/library/standard_gates/rz.py index ed0207658441..7177dc3ae895 100644 --- a/qiskit/circuit/library/standard_gates/rz.py +++ b/qiskit/circuit/library/standard_gates/rz.py @@ -37,17 +37,17 @@ class RZGate(Gate): .. code-block:: text ┌───────┐ - q_0: ┤ Rz(λ) ├ + q_0: ┤ Rz(φ) ├ └───────┘ **Matrix Representation:** .. math:: - RZ(\lambda) = \exp\left(-i\frac{\lambda}{2}Z\right) = + RZ(\phi) = \exp\left(-i\frac{\phi}{2}Z\right) = \begin{pmatrix} - e^{-i\frac{\lambda}{2}} & 0 \\ - 0 & e^{i\frac{\lambda}{2}} + e^{-i\frac{\phi}{2}} & 0 \\ + 0 & e^{i\frac{\phi}{2}} \end{pmatrix} .. seealso:: @@ -57,7 +57,7 @@ class RZGate(Gate): .. math:: - U1(\lambda) = e^{i{\lambda}/2}RZ(\lambda) + U1(\theta=\phi) = e^{i{\phi}/2}RZ(\phi) Reference for virtual Z gate implementation: `1612.00858 `_ @@ -181,10 +181,10 @@ class CRZGate(ControlledGate): .. math:: CRZ(\theta)\ q_0, q_1 = - I \otimes |0\rangle\langle 0| + RZ(\theta) \otimes |1\rangle\langle 1| = + I \otimes |0\rangle\langle 0| + RZ(\phi=\theta) \otimes |1\rangle\langle 1| = \begin{pmatrix} 1 & 0 & 0 & 0 \\ - 0 & e^{-i\frac{\lambda}{2}} & 0 & 0 \\ + 0 & e^{-i\frac{\theta}{2}} & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & e^{i\frac{\theta}{2}} \end{pmatrix} diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 24fbaf7b3a0d..bdca3af4a837 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1034,7 +1034,7 @@ def __init__( :meth:`QuantumCircuit.add_input`. The variables given in this argument will be passed directly to :meth:`add_input`. A circuit cannot have both ``inputs`` and ``captures``. - captures: any variables that that this circuit scope should capture from a containing + captures: any variables that this circuit scope should capture from a containing scope. The variables given here will be passed directly to :meth:`add_capture`. A circuit cannot have both ``inputs`` and ``captures``. declarations: any variables that this circuit should declare and initialize immediately. @@ -2837,6 +2837,31 @@ def _prepare_new_var( raise CircuitError(f"cannot add '{var}' as its name shadows the existing '{previous}'") return var + def add_stretch(self, name_or_var: str | expr.Var) -> expr.Var: + """Declares a new stretch variable scoped to this circuit. + + Args: + name_or_var: either a string of the stretch variable name, or an existing instance of + :class:`~.expr.Var` to re-use. Variables cannot shadow names that are already in + use within the circuit. The type of the variable must be + :class:`~.types.Stretch`. + Returns: + The created variable. If a :class:`~.expr.Var` instance was given, the exact same + object will be returned. + Raises: + CircuitError: if the stretch variable cannot be created due to shadowing an existing + variable, or the provided :class:`~.expr.Var` is not typed as a + :class:`~.types.Stretch`. + """ + if isinstance(name_or_var, str): + var = expr.Var.new(name_or_var, types.Stretch()) + elif name_or_var.type.kind is not types.Stretch: + raise CircuitError(f"cannot add stretch variable of type {name_or_var.type}") + else: + var = name_or_var + self._current_scope().add_uninitialized_var(var) + return var + def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.Var: """Add a classical variable with automatic storage and scope to this circuit. @@ -2846,13 +2871,14 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V Args: name_or_var: either a string of the variable name, or an existing instance of - :class:`~.expr.Var` to re-use. Variables cannot shadow names that are already in - use within the circuit. + a non-const-typed :class:`~.expr.Var` to re-use. Variables cannot shadow names + that are already in use within the circuit. initial: the value to initialize this variable with. If the first argument was given as a string name, the type of the resulting variable is inferred from the initial expression; to control this more manually, either use :meth:`.Var.new` to manually construct a new variable with the desired type, or use :func:`.expr.cast` to cast - the initializer to the desired type. + the initializer to the desired type. If a const-typed expression is provided, it + will be automatically cast to its non-const counterpart. This must be either a :class:`~.expr.Expr` node, or a value that can be lifted to one using :class:`.expr.lift`. @@ -2862,7 +2888,8 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V object will be returned. Raises: - CircuitError: if the variable cannot be created due to shadowing an existing variable. + CircuitError: if the variable cannot be created due to shadowing an existing variable + or a const variable was specified for ``name_or_var``. Examples: Define a new variable given just a name and an initializer expression:: @@ -2903,17 +2930,18 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V # Validate the initializer first to catch cases where the variable to be declared is being # used in the initializer. circuit_scope = self._current_scope() - # Convenience method to widen Python integer literals to the right width during the initial - # lift, if the type is already known via the variable. - if ( - isinstance(name_or_var, expr.Var) - and name_or_var.type.kind is types.Uint - and isinstance(initial, int) - and not isinstance(initial, bool) - ): - coerce_type = name_or_var.type - else: - coerce_type = None + coerce_type = None + if isinstance(name_or_var, expr.Var): + if name_or_var.type.const: + raise CircuitError("const variables are not supported.") + if ( + name_or_var.type.kind is types.Uint + and isinstance(initial, int) + and not isinstance(initial, bool) + ): + # Convenience method to widen Python integer literals to the right width during + # the initial lift, if the type is already known via the variable. + coerce_type = name_or_var.type initial = _validate_expr(circuit_scope, expr.lift(initial, coerce_type)) if isinstance(name_or_var, str): var = expr.Var.new(name_or_var, initial.type) @@ -2964,6 +2992,8 @@ def add_uninitialized_var(self, var: expr.Var, /): raise CircuitError("cannot add an uninitialized variable in a control-flow scope") if not var.standalone: raise CircuitError("cannot add a variable wrapping a bit or register to a circuit") + if var.type.const and var.type.kind is not types.Stretch: + raise CircuitError("const variables are not supported.") self._builder_api.add_uninitialized_var(var) def add_capture(self, var: expr.Var): @@ -3026,8 +3056,14 @@ def add_input( # pylint: disable=missing-raises-doc raise CircuitError("cannot add an input variable in a control-flow scope") if self._vars_capture: raise CircuitError("circuits to be enclosed with captures cannot have input variables") - if isinstance(name_or_var, expr.Var) and type_ is not None: - raise ValueError("cannot give an explicit type with an existing Var") + if isinstance(name_or_var, expr.Var): + if type_ is not None: + raise ValueError("cannot give an explicit type with an existing Var") + if name_or_var.type.const: + raise CircuitError("const variables are not supported") + elif type_ is not None and type_.const: + raise CircuitError("const variables are not supported") + var = self._prepare_new_var(name_or_var, type_) self._vars_input[var.name] = var return var @@ -3956,7 +3992,7 @@ def measure_active(self, inplace: bool = True) -> Optional["QuantumCircuit"]: circ = self.copy() dag = circuit_to_dag(circ) qubits_to_measure = [qubit for qubit in circ.qubits if qubit not in dag.idle_wires()] - new_creg = circ._create_creg(len(qubits_to_measure), "measure") + new_creg = circ._create_creg(len(qubits_to_measure), "meas") circ.add_register(new_creg) circ.barrier() circ.measure(qubits_to_measure, new_creg) @@ -4495,17 +4531,20 @@ def barrier(self, *qargs: QubitSpecifier, label=None) -> InstructionSet: def delay( self, - duration: ParameterValueType, + duration: ParameterValueType | expr.Expr, qarg: QubitSpecifier | None = None, - unit: str = "dt", + unit: str | None = None, ) -> InstructionSet: """Apply :class:`~.circuit.Delay`. If qarg is ``None``, applies to all qubits. When applying to multiple qubits, delays with the same duration will be created. Args: - duration (int or float or ParameterExpression): duration of the delay. + duration (int or float or ParameterExpression or :class:`~.expr.Expr`): + duration of the delay. If this is an :class:`~.expr.Expr`, it must be + of type :class:`~.types.Duration` or :class:`~.types.Stretch`. qarg (Object): qubit argument to apply this delay. - unit (str): unit of the duration. Supported units: ``'s'``, ``'ms'``, ``'us'``, + unit (str | None): unit of the duration, unless ``duration`` is an :class:`~.expr.Expr` + in which case it must not be specified. Supported units: ``'s'``, ``'ms'``, ``'us'``, ``'ns'``, ``'ps'``, and ``'dt'``. Default is ``'dt'``, i.e. integer time unit depending on the target backend. diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 4d705481ce71..69eb12f7c5af 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -23,7 +23,6 @@ from qiskit.dagcircuit import DAGCircuit from qiskit.providers.backend import Backend from qiskit.providers.backend_compat import BackendV2Converter -from qiskit.providers.models.backendproperties import BackendProperties from qiskit.pulse import Schedule, InstructionScheduleMap from qiskit.transpiler import Layout, CouplingMap, PropertySet from qiskit.transpiler.basepasses import BasePass @@ -58,14 +57,6 @@ "with defined timing constraints with " "`Target.from_configuration(..., timing_constraints=...)`", ) -@deprecate_arg( - name="backend_properties", - since="1.3", - package_name="Qiskit", - removal_timeline="in Qiskit 2.0", - additional_msg="The `target` parameter should be used instead. You can build a `Target` instance " - "with defined properties with Target.from_configuration(..., backend_properties=...)", -) @deprecate_pulse_arg("inst_map", predicate=lambda inst_map: inst_map is not None) def transpile( # pylint: disable=too-many-return-statements circuits: _CircuitT, @@ -73,7 +64,6 @@ def transpile( # pylint: disable=too-many-return-statements basis_gates: Optional[List[str]] = None, inst_map: Optional[List[InstructionScheduleMap]] = None, coupling_map: Optional[Union[CouplingMap, List[List[int]]]] = None, - backend_properties: Optional[BackendProperties] = None, initial_layout: Optional[Union[Layout, Dict, List]] = None, layout_method: Optional[str] = None, routing_method: Optional[str] = None, @@ -105,7 +95,7 @@ def transpile( # pylint: disable=too-many-return-statements The prioritization of transpilation target constraints works as follows: if a ``target`` input is provided, it will take priority over any ``backend`` input or loose constraints - (``basis_gates``, ``inst_map``, ``coupling_map``, ``backend_properties``, ``instruction_durations``, + (``basis_gates``, ``inst_map``, ``coupling_map``, ``instruction_durations``, ``dt`` or ``timing_constraints``). If a ``backend`` is provided together with any loose constraint from the list above, the loose constraint will take priority over the corresponding backend constraint. This behavior is independent of whether the ``backend`` instance is of type @@ -123,7 +113,6 @@ def transpile( # pylint: disable=too-many-return-statements **inst_map** target inst_map inst_map **dt** target dt dt **timing_constraints** target timing_constraints timing_constraints - **backend_properties** target backend_properties backend_properties ============================ ========= ======================== ======================= Args: @@ -148,10 +137,6 @@ def transpile( # pylint: disable=too-many-return-statements #. List, must be given as an adjacency matrix, where each entry specifies all directed two-qubit interactions supported by backend, e.g: ``[[0, 1], [0, 3], [1, 2], [1, 5], [2, 5], [4, 1], [5, 3]]`` - - backend_properties: properties returned by a backend, including information on gate - errors, readout errors, qubit coherence times, etc. Find a backend - that provides this information with: ``backend.properties()`` initial_layout: Initial position of virtual qubits on physical qubits. If this layout makes the circuit compatible with the coupling_map constraints, it will be used. The final layout is not guaranteed to be the same, @@ -394,7 +379,7 @@ def callback_func(**kwargs): # Edge cases require using the old model (loose constraints) instead of building a target, # but we don't populate the passmanager config with loose constraints unless it's one of # the known edge cases to control the execution path. - # Filter instruction_durations, timing_constraints, backend_properties and inst_map deprecation + # Filter instruction_durations, timing_constraints and inst_map deprecation with warnings.catch_warnings(): warnings.filterwarnings( "ignore", @@ -414,12 +399,6 @@ def callback_func(**kwargs): message=".*``instruction_durations`` is deprecated as of Qiskit 1.3.*", module="qiskit", ) - warnings.filterwarnings( - "ignore", - category=DeprecationWarning, - message=".*``backend_properties`` is deprecated as of Qiskit 1.3.*", - module="qiskit", - ) pm = generate_preset_pass_manager( optimization_level, target=target, @@ -427,7 +406,6 @@ def callback_func(**kwargs): basis_gates=basis_gates, coupling_map=coupling_map, instruction_durations=instruction_durations, - backend_properties=backend_properties, timing_constraints=timing_constraints, inst_map=inst_map, initial_layout=initial_layout, diff --git a/qiskit/qasm3/ast.py b/qiskit/qasm3/ast.py index ec8ad9f3622d..09f85a232d1a 100644 --- a/qiskit/qasm3/ast.py +++ b/qiskit/qasm3/ast.py @@ -138,6 +138,7 @@ class ClassicalType(ASTNode): class FloatType(ClassicalType, enum.Enum): """Allowed values for the width of floating-point types.""" + UNSPECIFIED = 0 HALF = 16 SINGLE = 32 DOUBLE = 64 @@ -175,6 +176,18 @@ class BitType(ClassicalType): __slots__ = () +class DurationType(ClassicalType): + """Type information for a duration.""" + + __slots__ = () + + +class StretchType(ClassicalType): + """Type information for a stretch.""" + + __slots__ = () + + class BitArrayType(ClassicalType): """Type information for a sized number of classical bits.""" @@ -244,6 +257,13 @@ def __init__(self, value): self.value = value +class FloatLiteral(Expression): + __slots__ = ("value",) + + def __init__(self, value): + self.value = value + + class BooleanLiteral(Expression): __slots__ = ("value",) @@ -306,6 +326,10 @@ class Op(enum.Enum): NOT_EQUAL = "!=" SHIFT_LEFT = "<<" SHIFT_RIGHT = ">>" + ADD = "+" + SUB = "-" + MUL = "*" + DIV = "/" def __init__(self, op: Op, left: Expression, right: Expression): self.op = op diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index f9d0757ec4b8..3eb0a5aa13c7 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -30,6 +30,7 @@ Barrier, CircuitInstruction, Clbit, + Duration, Gate, Measure, Parameter, @@ -1153,6 +1154,8 @@ def build_duration(self, duration, unit) -> ast.Expression | None: """Build the expression of a given duration (if not ``None``).""" if duration is None: return None + if unit == "expr": + return self.build_expression(duration) if unit == "ps": return ast.DurationLiteral(1000 * duration, ast.DurationUnit.NANOSECOND) unit_map = { @@ -1278,6 +1281,12 @@ def _build_ast_type(type_: types.Type) -> ast.ClassicalType: return ast.BoolType() if type_.kind is types.Uint: return ast.UintType(type_.width) + if type_.kind is types.Float: + return ast.FloatType.UNSPECIFIED + if type_.kind is types.Duration: + return ast.DurationType() + if type_.kind is types.Stretch: + return ast.StretchType() raise RuntimeError(f"unhandled expr type '{type_}'") @@ -1294,11 +1303,26 @@ def __init__(self, lookup): def visit_var(self, node, /): return self.lookup(node) if node.standalone else self.lookup(node.var) + # pylint: disable=R0911 def visit_value(self, node, /): if node.type.kind is types.Bool: return ast.BooleanLiteral(node.value) if node.type.kind is types.Uint: return ast.IntegerLiteral(node.value) + if node.type.kind is types.Float: + return ast.FloatLiteral(node.value) + if node.type.kind is types.Duration: + match node.value: + case Duration.dt(dt): + return ast.DurationLiteral(dt, ast.DurationUnit.SAMPLE) + case Duration.ns(ns): + return ast.DurationLiteral(ns, ast.DurationUnit.NANOSECOND) + case Duration.us(us): + return ast.DurationLiteral(us, ast.DurationUnit.MICROSECOND) + case Duration.ms(ms): + return ast.DurationLiteral(ms, ast.DurationUnit.MILLISECOND) + case Duration.s(sec): + return ast.DurationLiteral(sec, ast.DurationUnit.SECOND) raise RuntimeError(f"unhandled Value type '{node}'") def visit_cast(self, node, /): diff --git a/qiskit/qasm3/printer.py b/qiskit/qasm3/printer.py index 44891b5ee274..b7e691b46922 100644 --- a/qiskit/qasm3/printer.py +++ b/qiskit/qasm3/printer.py @@ -39,8 +39,12 @@ ast.Unary.Op.LOGIC_NOT: _BindingPower(right=22), ast.Unary.Op.BIT_NOT: _BindingPower(right=22), # - # Multiplication/division/modulo: (19, 20) - # Addition/subtraction: (17, 18) + # Modulo: (19, 20) + ast.Binary.Op.MUL: _BindingPower(19, 20), + ast.Binary.Op.DIV: _BindingPower(19, 20), + # + ast.Binary.Op.ADD: _BindingPower(17, 18), + ast.Binary.Op.SUB: _BindingPower(17, 18), # ast.Binary.Op.SHIFT_LEFT: _BindingPower(15, 16), ast.Binary.Op.SHIFT_RIGHT: _BindingPower(15, 16), @@ -78,7 +82,9 @@ class BasicPrinter: ast.QuantumGateModifierName.POW: "pow", } - _FLOAT_WIDTH_LOOKUP = {type: str(type.value) for type in ast.FloatType} + _FLOAT_TYPE_LOOKUP = {ast.FloatType.UNSPECIFIED: "float"} | { + type: f"float[{type.value}]" for type in ast.FloatType if type.value > 0 + } # The visitor names include the class names, so they mix snake_case with PascalCase. # pylint: disable=invalid-name @@ -205,11 +211,17 @@ def _visit_CalibrationGrammarDeclaration(self, node: ast.CalibrationGrammarDecla self._write_statement(f'defcalgrammar "{node.name}"') def _visit_FloatType(self, node: ast.FloatType) -> None: - self.stream.write(f"float[{self._FLOAT_WIDTH_LOOKUP[node]}]") + self.stream.write(self._FLOAT_TYPE_LOOKUP[node]) def _visit_BoolType(self, _node: ast.BoolType) -> None: self.stream.write("bool") + def _visit_DurationType(self, _node: ast.DurationType) -> None: + self.stream.write("duration") + + def _visit_StretchType(self, _node: ast.StretchType) -> None: + self.stream.write("stretch") + def _visit_IntType(self, node: ast.IntType) -> None: self.stream.write("int") if node.size is not None: @@ -282,6 +294,9 @@ def _visit_QuantumDelay(self, node: ast.QuantumDelay) -> None: def _visit_IntegerLiteral(self, node: ast.IntegerLiteral) -> None: self.stream.write(str(node.value)) + def _visit_FloatLiteral(self, node: ast.FloatLiteral) -> None: + self.stream.write(str(node.value)) + def _visit_BooleanLiteral(self, node: ast.BooleanLiteral): self.stream.write("true" if node.value else "false") diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 60922f3d3ec2..a909de8a6dce 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -370,6 +370,45 @@ def open(*args): by ``num_circuits`` in the file header). There is no padding between the circuits in the data. +.. _qpy_version_14: + +Version 14 +---------- + +Version 14 adds support for additional :class:`~.types.Type` classes, and adds support for +const-ness to existing types :class:`~.types.Bool` and :class:`~.types.Uint` in classical +expressions. + +Changes to EXPR_TYPE +~~~~~~~~~~~~~~~~~~~~ + +The ``EXPR_TYPE_BOOL`` and ``EXPR_TYPE_UNIT`` structs are now replaced by ``EXPR_TYPE_BOOL_V14`` +and ``EXPR_TYPE_UINT_V14``, respectively. + +The updated expression type encodings are shown below: + +====================== ========= ================================================================= +Qiskit class Type code Payload +====================== ========= ================================================================= +:class:`~.types.Bool` ``b`` One ``_Bool const``. + +:class:`~.types.Uint` ``u`` One ``uint32_t width``, followed by one ``_Bool const``. +:class:`~.types.Float` ``f`` One ``_Bool const``. +====================== ========= ================================================================= + +Changes to EXPR_VALUE +~~~~~~~~~~~~~~~~~~~~~ + +The classical expression's type system now supports new encoding types for value literals, in +addition to the existing encodings for int and bool. The new value type encodings are below: + +=========== ========= ============================================================================ +Python type Type code Payload +=========== ========= ============================================================================ +``float`` ``f`` One ``double value``. + +=========== ========= ============================================================================ + .. _qpy_version_13: Version 13 diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index c84f1cc9c0e2..edef78c80328 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -1265,7 +1265,7 @@ def write_circuit( file_obj.write(metadata_raw) # Write header payload file_obj.write(registers_raw) - standalone_var_indices = value.write_standalone_vars(file_obj, circuit) + standalone_var_indices = value.write_standalone_vars(file_obj, circuit, version) else: if circuit.num_vars: raise exceptions.UnsupportedFeatureForVersion( @@ -1433,7 +1433,7 @@ def read_circuit(file_obj, version, metadata_deserializer=None, use_symengine=Fa "q": [Qubit() for _ in out_bits["q"]], "c": [Clbit() for _ in out_bits["c"]], } - var_segments, standalone_var_indices = value.read_standalone_vars(file_obj, num_vars) + var_segments, standalone_var_indices = value.read_standalone_vars(file_obj, num_vars, version) circ = QuantumCircuit( out_bits["q"], out_bits["c"], diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 9799fdf3f459..bbef0621c609 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -23,7 +23,7 @@ import symengine -from qiskit.circuit import CASE_DEFAULT, Clbit, ClassicalRegister +from qiskit.circuit import CASE_DEFAULT, Clbit, ClassicalRegister, Duration from qiskit.circuit.classical import expr, types from qiskit.circuit.parameter import Parameter from qiskit.circuit.parameterexpression import ( @@ -261,12 +261,19 @@ def __init__(self, file_obj, clbit_indices, standalone_var_indices, version): self.standalone_var_indices = standalone_var_indices self.version = version + def write_expr_type(self, type_: types.Type): + """Write the expression's type using the appropriate QPY version.""" + if self.version < 14: + _write_expr_type(self.file_obj, type_, self.version) + else: + _write_expr_type_v14(self.file_obj, type_) + def visit_generic(self, node, /): raise exceptions.QpyError(f"unhandled Expr object '{node}'") def visit_var(self, node, /): self.file_obj.write(type_keys.Expression.VAR) - _write_expr_type(self.file_obj, node.type) + self.write_expr_type(node.type) if node.standalone: self.file_obj.write(type_keys.ExprVar.UUID) self.file_obj.write( @@ -296,7 +303,7 @@ def visit_var(self, node, /): def visit_value(self, node, /): self.file_obj.write(type_keys.Expression.VALUE) - _write_expr_type(self.file_obj, node.type) + self.write_expr_type(node.type) if node.value is True or node.value is False: self.file_obj.write(type_keys.ExprValue.BOOL) self.file_obj.write( @@ -317,12 +324,20 @@ def visit_value(self, node, /): struct.pack(formats.EXPR_VALUE_INT_PACK, *formats.EXPR_VALUE_INT(num_bytes)) ) self.file_obj.write(buffer) + elif isinstance(node.value, float): + self.file_obj.write(type_keys.ExprValue.FLOAT) + self.file_obj.write( + struct.pack(formats.EXPR_VALUE_FLOAT_PACK, *formats.EXPR_VALUE_FLOAT(node.value)) + ) + elif isinstance(node.value, Duration): + self.file_obj.write(type_keys.ExprValue.DURATION) + _write_duration(self.file_obj, node.value) else: raise exceptions.QpyError(f"unhandled Value object '{node.value}'") def visit_cast(self, node, /): self.file_obj.write(type_keys.Expression.CAST) - _write_expr_type(self.file_obj, node.type) + self.write_expr_type(node.type) self.file_obj.write( struct.pack(formats.EXPRESSION_CAST_PACK, *formats.EXPRESSION_CAST(node.implicit)) ) @@ -330,7 +345,7 @@ def visit_cast(self, node, /): def visit_unary(self, node, /): self.file_obj.write(type_keys.Expression.UNARY) - _write_expr_type(self.file_obj, node.type) + self.write_expr_type(node.type) self.file_obj.write( struct.pack(formats.EXPRESSION_UNARY_PACK, *formats.EXPRESSION_UNARY(node.op.value)) ) @@ -338,7 +353,7 @@ def visit_unary(self, node, /): def visit_binary(self, node, /): self.file_obj.write(type_keys.Expression.BINARY) - _write_expr_type(self.file_obj, node.type) + self.write_expr_type(node.type) self.file_obj.write( struct.pack(formats.EXPRESSION_BINARY_PACK, *formats.EXPRESSION_BINARY(node.op.value)) ) @@ -351,7 +366,7 @@ def visit_index(self, node, /): "the 'Index' expression", required=12, target=self.version ) self.file_obj.write(type_keys.Expression.INDEX) - _write_expr_type(self.file_obj, node.type) + self.write_expr_type(node.type) node.target.accept(self) node.index.accept(self) @@ -366,7 +381,11 @@ def _write_expr( node.accept(_ExprWriter(file_obj, clbit_indices, standalone_var_indices, version)) -def _write_expr_type(file_obj, type_: types.Type): +def _write_expr_type(file_obj, type_: types.Type, version): + if type_.const or type_.kind not in (types.Bool, types.Uint): + raise exceptions.UnsupportedFeatureForVersion( + "Qiskit 2.0 classical expressions", required=14, target=version + ) if type_.kind is types.Bool: file_obj.write(type_keys.ExprType.BOOL) elif type_.kind is types.Uint: @@ -378,6 +397,55 @@ def _write_expr_type(file_obj, type_: types.Type): raise exceptions.QpyError(f"unhandled Type object '{type_};") +def _write_expr_type_v14(file_obj, type_: types.Type): + if type_.kind is types.Bool: + file_obj.write(type_keys.ExprType.BOOL) + file_obj.write( + struct.pack(formats.EXPR_TYPE_BOOL_PACK_V14, *formats.EXPR_TYPE_BOOL_V14(type_.const)) + ) + elif type_.kind is types.Uint: + file_obj.write(type_keys.ExprType.UINT) + file_obj.write( + # TODO: make sure you're calling this correctly + struct.pack( + formats.EXPR_TYPE_UINT_PACK_V14, + *formats.EXPR_TYPE_UINT_V14(type_.width, type_.const), + ) + ) + elif type_.kind is types.Float: + file_obj.write(type_keys.ExprType.FLOAT) + file_obj.write( + struct.pack(formats.EXPR_TYPE_FLOAT_PACK, *formats.EXPR_TYPE_FLOAT(type_.const)) + ) + elif type_.kind is types.Duration: + file_obj.write(type_keys.ExprType.DURATION) + elif type_.kind is types.Stretch: + file_obj.write(type_keys.ExprType.STRETCH) + else: + raise exceptions.QpyError(f"unhandled Type object '{type_};") + + +def _write_duration(file_obj, duration: Duration): + match duration: + case Duration.dt(dt): + file_obj.write(type_keys.CircuitDuration.DT) + file_obj.write(struct.pack(formats.DURATION_DT_PACK, *formats.DURATION_DT(dt))) + case Duration.ns(ns): + file_obj.write(type_keys.CircuitDuration.NS) + file_obj.write(struct.pack(formats.DURATION_NS_PACK, *formats.DURATION_NS(ns))) + case Duration.us(us): + file_obj.write(type_keys.CircuitDuration.US) + file_obj.write(struct.pack(formats.DURATION_US_PACK, *formats.DURATION_US(us))) + case Duration.ms(ms): + file_obj.write(type_keys.CircuitDuration.MS) + file_obj.write(struct.pack(formats.DURATION_MS_PACK, *formats.DURATION_MS(ms))) + case Duration.s(sec): + file_obj.write(type_keys.CircuitDuration.S) + file_obj.write(struct.pack(formats.DURATION_S_PACK, *formats.DURATION_S(sec))) + case _: + raise exceptions.QpyError(f"unhandled Duration object '{duration};") + + def _read_parameter(file_obj): data = formats.PARAMETER( *struct.unpack(formats.PARAMETER_PACK, file_obj.read(formats.PARAMETER_SIZE)) @@ -635,10 +703,14 @@ def _read_expr( clbits: collections.abc.Sequence[Clbit], cregs: collections.abc.Mapping[str, ClassicalRegister], standalone_vars: collections.abc.Sequence[expr.Var], + version: int, ) -> expr.Expr: # pylint: disable=too-many-return-statements type_key = file_obj.read(formats.EXPRESSION_DISCRIMINATOR_SIZE) - type_ = _read_expr_type(file_obj) + if version < 14: + type_ = _read_expr_type(file_obj) + else: + type_ = _read_expr_type_v14(file_obj) if type_key == type_keys.Expression.VAR: var_type_key = file_obj.read(formats.EXPR_VAR_DISCRIMINATOR_SIZE) if var_type_key == type_keys.ExprVar.UUID: @@ -680,13 +752,25 @@ def _read_expr( return expr.Value( int.from_bytes(file_obj.read(payload.num_bytes), "big", signed=True), type_ ) + if value_type_key == type_keys.ExprValue.FLOAT: + payload = formats.EXPR_VALUE_FLOAT._make( + struct.unpack( + formats.EXPR_VALUE_FLOAT_PACK, file_obj.read(formats.EXPR_VALUE_FLOAT_SIZE) + ) + ) + return expr.Value(payload.value, type_) + if value_type_key == type_keys.ExprValue.DURATION: + value = _read_duration(file_obj) + return expr.Value(value, type_) raise exceptions.QpyError("Invalid classical-expression Value key '{value_type_key}'") if type_key == type_keys.Expression.CAST: payload = formats.EXPRESSION_CAST._make( struct.unpack(formats.EXPRESSION_CAST_PACK, file_obj.read(formats.EXPRESSION_CAST_SIZE)) ) return expr.Cast( - _read_expr(file_obj, clbits, cregs, standalone_vars), type_, implicit=payload.implicit + _read_expr(file_obj, clbits, cregs, standalone_vars, version), + type_, + implicit=payload.implicit, ) if type_key == type_keys.Expression.UNARY: payload = formats.EXPRESSION_UNARY._make( @@ -696,7 +780,7 @@ def _read_expr( ) return expr.Unary( expr.Unary.Op(payload.opcode), - _read_expr(file_obj, clbits, cregs, standalone_vars), + _read_expr(file_obj, clbits, cregs, standalone_vars, version), type_, ) if type_key == type_keys.Expression.BINARY: @@ -707,14 +791,14 @@ def _read_expr( ) return expr.Binary( expr.Binary.Op(payload.opcode), - _read_expr(file_obj, clbits, cregs, standalone_vars), - _read_expr(file_obj, clbits, cregs, standalone_vars), + _read_expr(file_obj, clbits, cregs, standalone_vars, version), + _read_expr(file_obj, clbits, cregs, standalone_vars, version), type_, ) if type_key == type_keys.Expression.INDEX: return expr.Index( - _read_expr(file_obj, clbits, cregs, standalone_vars), - _read_expr(file_obj, clbits, cregs, standalone_vars), + _read_expr(file_obj, clbits, cregs, standalone_vars, version), + _read_expr(file_obj, clbits, cregs, standalone_vars, version), type_, ) raise exceptions.QpyError(f"Invalid classical-expression Expr key '{type_key}'") @@ -732,12 +816,71 @@ def _read_expr_type(file_obj) -> types.Type: raise exceptions.QpyError(f"Invalid classical-expression Type key '{type_key}'") -def read_standalone_vars(file_obj, num_vars): +def _read_expr_type_v14(file_obj) -> types.Type: + type_key = file_obj.read(formats.EXPR_TYPE_DISCRIMINATOR_SIZE) + if type_key == type_keys.ExprType.BOOL: + elem = formats.EXPR_TYPE_BOOL_V14._make( + struct.unpack( + formats.EXPR_TYPE_BOOL_PACK_V14, file_obj.read(formats.EXPR_TYPE_BOOL_SIZE_V14) + ) + ) + return types.Bool(const=elem.const) + if type_key == type_keys.ExprType.UINT: + elem = formats.EXPR_TYPE_UINT_V14._make( + struct.unpack( + formats.EXPR_TYPE_UINT_PACK_V14, file_obj.read(formats.EXPR_TYPE_UINT_SIZE_V14) + ) + ) + return types.Uint(elem.width, const=elem.const) + if type_key == type_keys.ExprType.FLOAT: + elem = formats.EXPR_TYPE_FLOAT._make( + struct.unpack(formats.EXPR_TYPE_FLOAT_PACK, file_obj.read(formats.EXPR_TYPE_FLOAT_SIZE)) + ) + return types.Float(const=elem.const) + if type_key == type_keys.ExprType.DURATION: + return types.Duration() + if type_key == type_keys.ExprType.STRETCH: + return types.Stretch() + raise exceptions.QpyError(f"Invalid classical-expression Type key '{type_key}'") + + +def _read_duration(file_obj) -> Duration: + type_key = file_obj.read(formats.DURATION_DISCRIMINATOR_SIZE) + if type_key == type_keys.CircuitDuration.DT: + elem = formats.DURATION_DT._make( + struct.unpack(formats.DURATION_DT_PACK, file_obj.read(formats.DURATION_DT_SIZE)) + ) + return Duration.dt(elem.value) + if type_key == type_keys.CircuitDuration.NS: + elem = formats.DURATION_NS._make( + struct.unpack(formats.DURATION_NS_PACK, file_obj.read(formats.DURATION_NS_SIZE)) + ) + return Duration.ns(elem.value) + if type_key == type_keys.CircuitDuration.US: + elem = formats.DURATION_US._make( + struct.unpack(formats.DURATION_US_PACK, file_obj.read(formats.DURATION_US_SIZE)) + ) + return Duration.us(elem.value) + if type_key == type_keys.CircuitDuration.MS: + elem = formats.DURATION_MS._make( + struct.unpack(formats.DURATION_MS_PACK, file_obj.read(formats.DURATION_MS_SIZE)) + ) + return Duration.ms(elem.value) + if type_key == type_keys.CircuitDuration.S: + elem = formats.DURATION_S._make( + struct.unpack(formats.DURATION_S_PACK, file_obj.read(formats.DURATION_S_SIZE)) + ) + return Duration.s(elem.value) + raise exceptions.QpyError(f"Invalid duration Type key '{type_key}'") + + +def read_standalone_vars(file_obj, num_vars, version): """Read the ``num_vars`` standalone variable declarations from the file. Args: file_obj (File): a file-like object to read from. num_vars (int): the number of variables to read. + version (int): the target QPY version. Returns: tuple[dict, list]: the first item is a mapping of the ``ExprVarDeclaration`` type keys to @@ -757,7 +900,10 @@ def read_standalone_vars(file_obj, num_vars): file_obj.read(formats.EXPR_VAR_DECLARATION_SIZE), ) ) - type_ = _read_expr_type(file_obj) + if version < 14: + type_ = _read_expr_type(file_obj) + else: + type_ = _read_expr_type_v14(file_obj) name = file_obj.read(data.name_size).decode(common.ENCODE) var = expr.Var(uuid.UUID(bytes=data.uuid_bytes), type_, name=name) read_vars[data.usage].append(var) @@ -765,7 +911,7 @@ def read_standalone_vars(file_obj, num_vars): return read_vars, var_order -def _write_standalone_var(file_obj, var, type_key): +def _write_standalone_var(file_obj, var, type_key, version): name = var.name.encode(common.ENCODE) file_obj.write( struct.pack( @@ -773,16 +919,20 @@ def _write_standalone_var(file_obj, var, type_key): *formats.EXPR_VAR_DECLARATION(var.var.bytes, type_key, len(name)), ) ) - _write_expr_type(file_obj, var.type) + if version < 14: + _write_expr_type(file_obj, var.type, version) + else: + _write_expr_type_v14(file_obj, var.type) file_obj.write(name) -def write_standalone_vars(file_obj, circuit): +def write_standalone_vars(file_obj, circuit, version): """Write the standalone variables out from a circuit. Args: file_obj (File): the file-like object to write to. circuit (QuantumCircuit): the circuit to take the variables from. + version (int): the target QPY version. Returns: dict[expr.Var, int]: a mapping of the variables written to the index that they were written @@ -791,15 +941,15 @@ def write_standalone_vars(file_obj, circuit): index = 0 out = {} for var in circuit.iter_input_vars(): - _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.INPUT) + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.INPUT, version) out[var] = index index += 1 for var in circuit.iter_captured_vars(): - _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.CAPTURE) + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.CAPTURE, version) out[var] = index index += 1 for var in circuit.iter_declared_vars(): - _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.LOCAL) + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.LOCAL, version) out[var] = index index += 1 return out @@ -978,6 +1128,7 @@ def loads_value( clbits=clbits, cregs=cregs or {}, standalone_vars=standalone_vars, + version=version, ) raise exceptions.QpyError(f"Serialization for {type_key} is not implemented in value I/O.") diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index c2585d5be4d3..0f30e90cc342 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -25,7 +25,7 @@ from qiskit.qpy import formats, exceptions -QPY_VERSION = 13 +QPY_VERSION = 14 QPY_COMPATIBILITY_VERSION = 10 ENCODE = "utf8" diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index 7696cae94e2b..60b0c7aa700e 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -360,6 +360,26 @@ # EXPR_TYPE +EXPR_TYPE_STRETCH = namedtuple("EXPR_TYPE_STRETCH", []) +EXPR_TYPE_STRETCH_PACK = "!" +EXPR_TYPE_STRETCH_SIZE = struct.calcsize(EXPR_TYPE_STRETCH_PACK) + +EXPR_TYPE_DURATION = namedtuple("EXPR_TYPE_DURATION", []) +EXPR_TYPE_DURATION_PACK = "!" +EXPR_TYPE_DURATION_SIZE = struct.calcsize(EXPR_TYPE_DURATION_PACK) + +EXPR_TYPE_FLOAT = namedtuple("EXPR_TYPE_FLOAT", ["const"]) +EXPR_TYPE_FLOAT_PACK = "!?" +EXPR_TYPE_FLOAT_SIZE = struct.calcsize(EXPR_TYPE_FLOAT_PACK) + +EXPR_TYPE_BOOL_V14 = namedtuple("EXPR_TYPE_BOOL_V14", ["const"]) +EXPR_TYPE_BOOL_PACK_V14 = "!?" +EXPR_TYPE_BOOL_SIZE_V14 = struct.calcsize(EXPR_TYPE_BOOL_PACK_V14) + +EXPR_TYPE_UINT_V14 = namedtuple("EXPR_TYPE_UINT_V14", ["width", "const"]) +EXPR_TYPE_UINT_PACK_V14 = "!L?" +EXPR_TYPE_UINT_SIZE_V14 = struct.calcsize(EXPR_TYPE_UINT_PACK_V14) + EXPR_TYPE_DISCRIMINATOR_SIZE = 1 EXPR_TYPE_BOOL = namedtuple("EXPR_TYPE_BOOL", []) @@ -370,7 +390,6 @@ EXPR_TYPE_UINT_PACK = "!L" EXPR_TYPE_UINT_SIZE = struct.calcsize(EXPR_TYPE_UINT_PACK) - # EXPR_VAR EXPR_VAR_DISCRIMINATOR_SIZE = 1 @@ -387,11 +406,14 @@ EXPR_VAR_UUID_PACK = "!H" EXPR_VAR_UUID_SIZE = struct.calcsize(EXPR_VAR_UUID_PACK) - # EXPR_VALUE EXPR_VALUE_DISCRIMINATOR_SIZE = 1 +EXPR_VALUE_FLOAT = namedtuple("EXPR_VALUE_FLOAT", ["value"]) +EXPR_VALUE_FLOAT_PACK = "!d" +EXPR_VALUE_FLOAT_SIZE = struct.calcsize(EXPR_VALUE_FLOAT_PACK) + EXPR_VALUE_BOOL = namedtuple("EXPR_VALUE_BOOL", ["value"]) EXPR_VALUE_BOOL_PACK = "!?" EXPR_VALUE_BOOL_SIZE = struct.calcsize(EXPR_VALUE_BOOL_PACK) @@ -399,3 +421,27 @@ EXPR_VALUE_INT = namedtuple("EXPR_VALUE_INT", ["num_bytes"]) EXPR_VALUE_INT_PACK = "!B" EXPR_VALUE_INT_SIZE = struct.calcsize(EXPR_VALUE_INT_PACK) + +# DURATION + +DURATION_DISCRIMINATOR_SIZE = 1 + +DURATION_DT = namedtuple("DURATION_DT", ["value"]) +DURATION_DT_PACK = "!Q" +DURATION_DT_SIZE = struct.calcsize(DURATION_DT_PACK) + +DURATION_NS = namedtuple("DURATION_NS", ["value"]) +DURATION_NS_PACK = "!d" +DURATION_NS_SIZE = struct.calcsize(DURATION_NS_PACK) + +DURATION_US = namedtuple("DURATION_US", ["value"]) +DURATION_US_PACK = "!d" +DURATION_US_SIZE = struct.calcsize(DURATION_US_PACK) + +DURATION_MS = namedtuple("DURATION_MS", ["value"]) +DURATION_MS_PACK = "!d" +DURATION_MS_SIZE = struct.calcsize(DURATION_MS_PACK) + +DURATION_S = namedtuple("DURATION_S", ["value"]) +DURATION_S_PACK = "!d" +DURATION_S_SIZE = struct.calcsize(DURATION_S_PACK) diff --git a/qiskit/qpy/type_keys.py b/qiskit/qpy/type_keys.py index 60262440d033..016b4bc6f5cb 100644 --- a/qiskit/qpy/type_keys.py +++ b/qiskit/qpy/type_keys.py @@ -30,6 +30,7 @@ CASE_DEFAULT, Clbit, ClassicalRegister, + Duration, ) from qiskit.circuit.annotated_operation import AnnotatedOperation, Modifier from qiskit.circuit.classical import expr, types @@ -494,6 +495,9 @@ class ExprType(TypeKeyBase): BOOL = b"b" UINT = b"u" + FLOAT = b"f" + DURATION = b"d" + STRETCH = b"s" @classmethod def assign(cls, obj): @@ -538,6 +542,8 @@ class ExprValue(TypeKeyBase): BOOL = b"b" INT = b"i" + FLOAT = b"f" + DURATION = b"t" @classmethod def assign(cls, obj): @@ -545,6 +551,41 @@ def assign(cls, obj): return cls.BOOL if isinstance(obj, int): return cls.INT + if isinstance(obj, float): + return cls.FLOAT + if isinstance(obj, Duration): + return cls.DURATION + raise exceptions.QpyError( + f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." + ) + + @classmethod + def retrieve(cls, type_key): + raise NotImplementedError + + +class CircuitDuration(TypeKeyBase): + """Type keys for the ``DURATION`` QPY item.""" + + DT = b"t" + NS = b"n" + US = b"u" + MS = b"m" + S = b"s" + + @classmethod + def assign(cls, obj): + match obj: + case Duration.dt(_): + return cls.DT + case Duration.ns(_): + return cls.NS + case Duration.us(_): + return cls.US + case Duration.ms(_): + return cls.MS + case Duration.s(_): + return cls.S raise exceptions.QpyError( f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." ) diff --git a/qiskit/transpiler/passes/layout/dense_layout.py b/qiskit/transpiler/passes/layout/dense_layout.py index 71c69739990d..cbd9b548dcc8 100644 --- a/qiskit/transpiler/passes/layout/dense_layout.py +++ b/qiskit/transpiler/passes/layout/dense_layout.py @@ -36,17 +36,15 @@ class DenseLayout(AnalysisPass): by being set in ``property_set``. """ - def __init__(self, coupling_map=None, backend_prop=None, target=None): + def __init__(self, coupling_map=None, target=None): """DenseLayout initializer. Args: coupling_map (Coupling): directed graph representing a coupling map. - backend_prop (BackendProperties): backend properties object target (Target): A target representing the target backend. """ super().__init__() self.coupling_map = coupling_map - self.backend_prop = backend_prop self.target = target self.adjacency_matrix = None if target is not None: @@ -127,8 +125,6 @@ def _best_subset(self, num_qubits, num_meas, num_cx, coupling_map): error_mat, use_error = _build_error_matrix( coupling_map.size(), reverse_index_map, - backend_prop=self.backend_prop, - coupling_map=self.coupling_map, target=self.target, ) @@ -148,7 +144,7 @@ def _best_subset(self, num_qubits, num_meas, num_cx, coupling_map): return best_map -def _build_error_matrix(num_qubits, qubit_map, target=None, coupling_map=None, backend_prop=None): +def _build_error_matrix(num_qubits, qubit_map, target=None): error_mat = np.zeros((num_qubits, num_qubits)) use_error = False if target is not None and target.qargs is not None: @@ -178,25 +174,4 @@ def _build_error_matrix(num_qubits, qubit_map, target=None, coupling_map=None, b elif len(qargs) == 2: error_mat[qubit_map[qargs[0]]][qubit_map[qargs[1]]] = max_error use_error = True - elif backend_prop and coupling_map: - error_dict = { - tuple(gate.qubits): gate.parameters[0].value - for gate in backend_prop.gates - if len(gate.qubits) == 2 - } - for edge in coupling_map.get_edges(): - gate_error = error_dict.get(edge) - if gate_error is not None: - if edge[0] not in qubit_map or edge[1] not in qubit_map: - continue - error_mat[qubit_map[edge[0]]][qubit_map[edge[1]]] = gate_error - use_error = True - for index, qubit_data in enumerate(backend_prop.qubits): - if index not in qubit_map: - continue - for item in qubit_data: - if item.name == "readout_error": - mapped_index = qubit_map[index] - error_mat[mapped_index][mapped_index] = item.value - use_error = True return error_mat, use_error diff --git a/qiskit/transpiler/passes/layout/vf2_layout.py b/qiskit/transpiler/passes/layout/vf2_layout.py index 2e799ffa4d95..fc73cfd8c1ef 100644 --- a/qiskit/transpiler/passes/layout/vf2_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_layout.py @@ -80,7 +80,6 @@ def __init__( seed=None, call_limit=None, time_limit=None, - properties=None, max_trials=None, target=None, ): @@ -94,16 +93,13 @@ def __init__( call_limit (int): The number of state visits to attempt in each execution of VF2. time_limit (float): The total time limit in seconds to run ``VF2Layout`` - properties (BackendProperties): The backend properties for the backend. If - :meth:`~qiskit.providers.models.BackendProperties.readout_error` is available - it is used to score the layout. max_trials (int): The maximum number of trials to run VF2 to find a layout. If this is not specified the number of trials will be limited based on the number of edges in the interaction graph or the coupling graph (whichever is larger) if no other limits are set. If set to a value <= 0 no limit on the number of trials will be set. target (Target): A target representing the backend device to run ``VF2Layout`` on. - If specified it will supersede a set value for ``properties`` and + If specified it will supersede a set value for ``coupling_map`` if the :class:`.Target` contains connectivity constraints. If the value of ``target`` models an ideal backend without any constraints then the value of ``coupling_map`` @@ -121,7 +117,6 @@ def __init__( self.coupling_map = target_coupling_map else: self.coupling_map = coupling_map - self.properties = properties self.strict_direction = strict_direction self.seed = seed self.call_limit = call_limit @@ -135,9 +130,7 @@ def run(self, dag): raise TranspilerError("coupling_map or target must be specified.") self.avg_error_map = self.property_set["vf2_avg_error_map"] if self.avg_error_map is None: - self.avg_error_map = vf2_utils.build_average_error_map( - self.target, self.properties, self.coupling_map - ) + self.avg_error_map = vf2_utils.build_average_error_map(self.target, self.coupling_map) result = vf2_utils.build_interaction_graph(dag, self.strict_direction) if result is None: diff --git a/qiskit/transpiler/passes/layout/vf2_post_layout.py b/qiskit/transpiler/passes/layout/vf2_post_layout.py index 1a29b0cb4506..f6d5f538e139 100644 --- a/qiskit/transpiler/passes/layout/vf2_post_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_post_layout.py @@ -78,8 +78,7 @@ class VF2PostLayout(AnalysisPass): * ``">2q gates in basis"``: If VF2PostLayout can't work with the basis of the circuit. By default, this pass will construct a heuristic scoring map based on - the error rates in the provided ``target`` (or ``properties`` if ``target`` - is not provided). However, analysis passes can be run prior to this pass + the error rates in the provided ``target``. However, analysis passes can be run prior to this pass and set ``vf2_avg_error_map`` in the property set with a :class:`~.ErrorMap` instance. If a value is ``NaN`` that is treated as an ideal edge For example if an error map is created as:: @@ -102,8 +101,6 @@ class VF2PostLayout(AnalysisPass): def __init__( self, target=None, - coupling_map=None, - properties=None, seed=None, call_limit=None, time_limit=None, @@ -114,12 +111,6 @@ def __init__( Args: target (Target): A target representing the backend device to run ``VF2PostLayout`` on. - If specified it will supersede a set value for ``properties`` and - ``coupling_map``. - coupling_map (CouplingMap): Directed graph representing a coupling map. - properties (BackendProperties): The backend properties for the backend. If - :meth:`~qiskit.providers.models.BackendProperties.readout_error` is available - it is used to score the layout. seed (int): Sets the seed of the PRNG. -1 Means no node shuffling. call_limit (int): The number of state visits to attempt in each execution of VF2. @@ -138,12 +129,10 @@ def __init__( a layout. A value of ``0`` (the default) means 'unlimited'. Raises: - TypeError: At runtime, if neither ``coupling_map`` or ``target`` are provided. + TypeError: At runtime, if ``target`` isn't provided. """ super().__init__() self.target = target - self.coupling_map = coupling_map - self.properties = properties self.call_limit = call_limit self.time_limit = time_limit self.max_trials = max_trials @@ -153,16 +142,12 @@ def __init__( def run(self, dag): """run the layout method""" - if self.target is None and (self.coupling_map is None or self.properties is None): - raise TranspilerError( - "A target must be specified or a coupling map and properties must be provided" - ) + if self.target is None: + raise TranspilerError("A target must be specified or a coupling map must be provided") if not self.strict_direction: self.avg_error_map = self.property_set["vf2_avg_error_map"] if self.avg_error_map is None: - self.avg_error_map = vf2_utils.build_average_error_map( - self.target, self.properties, self.coupling_map - ) + self.avg_error_map = vf2_utils.build_average_error_map(self.target, None) result = vf2_utils.build_interaction_graph(dag, self.strict_direction) if result is None: @@ -172,67 +157,62 @@ def run(self, dag): scoring_bit_list = vf2_utils.build_bit_list(im_graph, im_graph_node_map) scoring_edge_list = vf2_utils.build_edge_list(im_graph) - if self.target is not None: - # If qargs is None then target is global and ideal so no - # scoring is needed - if self.target.qargs is None: - return - if self.strict_direction: - cm_graph = PyDiGraph(multigraph=False) - else: - cm_graph = PyGraph(multigraph=False) - # If None is present in qargs there are globally defined ideal operations - # we should add these to all entries based on the number of qubits, so we - # treat that as a valid operation even if there is no scoring for the - # strict direction case - global_ops = None - if None in self.target.qargs: - global_ops = {1: [], 2: []} - for op in self.target.operation_names_for_qargs(None): - operation = self.target.operation_for_name(op) - # If operation is a class this is a variable width ideal instruction - # so we treat it as available on both 1 and 2 qubits - if inspect.isclass(operation): - global_ops[1].append(op) - global_ops[2].append(op) - else: - num_qubits = operation.num_qubits - if num_qubits in global_ops: - global_ops[num_qubits].append(op) - op_names = [] - for i in range(self.target.num_qubits): - try: - entry = set(self.target.operation_names_for_qargs((i,))) - except KeyError: - entry = set() - if global_ops is not None: - entry.update(global_ops[1]) - op_names.append(entry) - cm_graph.add_nodes_from(op_names) - for qargs in self.target.qargs: - len_args = len(qargs) - # If qargs == 1 we already populated it and if qargs > 2 there are no instructions - # using those in the circuit because we'd have already returned by this point - if len_args == 2: - ops = set(self.target.operation_names_for_qargs(qargs)) - if global_ops is not None: - ops.update(global_ops[2]) - cm_graph.add_edge(qargs[0], qargs[1], ops) - cm_nodes = list(cm_graph.node_indexes()) - # Filter qubits without any supported operations. If they - # don't support any operations, they're not valid for layout selection. - # This is only needed in the undirected case because in strict direction - # mode the node matcher will not match since none of the circuit ops - # will match the cmap ops. - if not self.strict_direction: - has_operations = set(itertools.chain.from_iterable(self.target.qargs)) - to_remove = set(cm_graph.node_indices()).difference(has_operations) - if to_remove: - cm_graph.remove_nodes_from(list(to_remove)) + # If qargs is None then target is global and ideal so no + # scoring is needed + if self.target.qargs is None: + return + if self.strict_direction: + cm_graph = PyDiGraph(multigraph=False) else: - cm_graph, cm_nodes = vf2_utils.shuffle_coupling_graph( - self.coupling_map, self.seed, self.strict_direction - ) + cm_graph = PyGraph(multigraph=False) + # If None is present in qargs there are globally defined ideal operations + # we should add these to all entries based on the number of qubits, so we + # treat that as a valid operation even if there is no scoring for the + # strict direction case + global_ops = None + if None in self.target.qargs: + global_ops = {1: [], 2: []} + for op in self.target.operation_names_for_qargs(None): + operation = self.target.operation_for_name(op) + # If operation is a class this is a variable width ideal instruction + # so we treat it as available on both 1 and 2 qubits + if inspect.isclass(operation): + global_ops[1].append(op) + global_ops[2].append(op) + else: + num_qubits = operation.num_qubits + if num_qubits in global_ops: + global_ops[num_qubits].append(op) + op_names = [] + for i in range(self.target.num_qubits): + try: + entry = set(self.target.operation_names_for_qargs((i,))) + except KeyError: + entry = set() + if global_ops is not None: + entry.update(global_ops[1]) + op_names.append(entry) + cm_graph.add_nodes_from(op_names) + for qargs in self.target.qargs: + len_args = len(qargs) + # If qargs == 1 we already populated it and if qargs > 2 there are no instructions + # using those in the circuit because we'd have already returned by this point + if len_args == 2: + ops = set(self.target.operation_names_for_qargs(qargs)) + if global_ops is not None: + ops.update(global_ops[2]) + cm_graph.add_edge(qargs[0], qargs[1], ops) + cm_nodes = list(cm_graph.node_indexes()) + # Filter qubits without any supported operations. If they + # don't support any operations, they're not valid for layout selection. + # This is only needed in the undirected case because in strict direction + # mode the node matcher will not match since none of the circuit ops + # will match the cmap ops. + if not self.strict_direction: + has_operations = set(itertools.chain.from_iterable(self.target.qargs)) + to_remove = set(cm_graph.node_indices()).difference(has_operations) + if to_remove: + cm_graph.remove_nodes_from(list(to_remove)) logger.debug("Running VF2 to find post transpile mappings") if self.target and self.strict_direction: diff --git a/qiskit/transpiler/passes/layout/vf2_utils.py b/qiskit/transpiler/passes/layout/vf2_utils.py index 037ccc37155d..c2958d12610a 100644 --- a/qiskit/transpiler/passes/layout/vf2_utils.py +++ b/qiskit/transpiler/passes/layout/vf2_utils.py @@ -13,7 +13,6 @@ """This module contains common utils for vf2 layout passes.""" from collections import defaultdict -import statistics import random import numpy as np @@ -142,7 +141,7 @@ def score_layout( ) -def build_average_error_map(target, properties, coupling_map): +def build_average_error_map(target, coupling_map): """Build an average error map used for scoring layouts pre-basis translation.""" num_qubits = 0 if target is not None and target.qargs is not None: @@ -173,29 +172,6 @@ def build_average_error_map(target, properties, coupling_map): qargs = (qargs[0], qargs[0]) avg_map.add_error(qargs, qarg_error / count) built = True - elif properties is not None: - errors = defaultdict(list) - for qubit in range(len(properties.qubits)): - errors[(qubit,)].append(properties.readout_error(qubit)) - for gate in properties.gates: - qubits = tuple(gate.qubits) - for param in gate.parameters: - if param.name == "gate_error": - errors[qubits].append(param.value) - for k, v in errors.items(): - if len(k) == 1: - qargs = (k[0], k[0]) - else: - qargs = k - # If the properties payload contains an index outside the number of qubits - # the properties are invalid for the given input. This normally happens either - # with a malconstructed properties payload or if the faulty qubits feature of - # BackendV1/BackendPropeties is being used. In such cases we map noise characteristics - # so we should just treat the mapping as an ideal case. - if qargs[0] >= num_qubits or qargs[1] >= num_qubits: - continue - avg_map.add_error(qargs, statistics.mean(v)) - built = True # if there are no error rates in the target we should fallback to using the degree heuristic # used for a coupling map. To do this we can build the coupling map from the target before # running the fallback heuristic diff --git a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py index 8bb743ce6b3e..1b4d1d91bce1 100644 --- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py +++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py @@ -14,12 +14,14 @@ from typing import Set import warnings -from qiskit.circuit import Delay +from qiskit.circuit import Delay, Duration +from qiskit.circuit.classical import expr, types from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.instruction_durations import InstructionDurations from qiskit.transpiler.target import Target +from qiskit.utils import apply_prefix class TimeUnitConversion(TransformationPass): @@ -69,43 +71,73 @@ def run(self, dag: DAGCircuit): inst_durations = self._update_inst_durations(dag) + # The float-value converted units for delay expressions, either all in 'dt' + # or all in seconds. + expression_durations = {} + # Choose unit - if inst_durations.dt is not None: - time_unit = "dt" - else: - # Check what units are used in delays and other instructions: dt or SI or mixed - units_delay = self._units_used_in_delays(dag) - if self._unified(units_delay) == "mixed": + has_dt = False + has_si = False + + # We _always_ need to traverse duration expressions to convert them to + # a float. But we also use the opportunity to note if they intermix cycles + # and wall-time, in case we don't have a `dt` to use to unify all instruction + # durations. + for node in dag.op_nodes(op=Delay): + if isinstance(node.op.duration, expr.Expr): + if node.op.duration.type.kind is types.Stretch: + # If any of the delays use a stretch expression, we can't run scheduling + # passes anyway, so we bail out. In theory, we _could_ still traverse + # through the stretch expression and replace any Duration value nodes it may + # contain with ones of the same units, but it'd be complex and probably unuseful. + self.property_set["time_unit"] = "stretch" + return dag + + visitor = _EvalDurationImpl(inst_durations.dt) + duration = node.op.duration.accept(visitor) + expression_durations[node._node_id] = duration + if visitor.in_cycles(): + has_dt = True + else: + has_si = True + else: + if node.op.unit == "dt": + has_dt = True + else: + has_si = True + if inst_durations.dt is None and has_dt and has_si: raise TranspilerError( "Fail to unify time units in delays. SI units " "and dt unit must not be mixed when dt is not supplied." ) - units_other = inst_durations.units_used() - if self._unified(units_other) == "mixed": - raise TranspilerError( - "Fail to unify time units in instruction_durations. SI units " - "and dt unit must not be mixed when dt is not supplied." - ) - unified_unit = self._unified(units_delay | units_other) - if unified_unit == "SI": + if inst_durations.dt is None: + # Check what units are used in other instructions: dt or SI or mixed + units_other = inst_durations.units_used() + unified_unit = self._unified(units_other) + if unified_unit == "SI" and not has_dt: time_unit = "s" - elif unified_unit == "dt": + elif unified_unit == "dt" and not has_si: time_unit = "dt" else: raise TranspilerError( "Fail to unify time units. SI units " "and dt unit must not be mixed when dt is not supplied." ) + else: + time_unit = "dt" # Make units consistent for node in dag.op_nodes(): - try: - duration = inst_durations.get( - node.op, [dag.find_bit(qarg).index for qarg in node.qargs], unit=time_unit - ) - except TranspilerError: - continue + if node._node_id in expression_durations: + duration = expression_durations[node._node_id] + else: + try: + duration = inst_durations.get( + node.op, [dag.find_bit(qarg).index for qarg in node.qargs], unit=time_unit + ) + except TranspilerError: + continue op = node.op.to_mutable() op.duration = duration op.unit = time_unit @@ -142,6 +174,12 @@ def _update_inst_durations(self, dag): def _units_used_in_delays(dag: DAGCircuit) -> Set[str]: units_used = set() for node in dag.op_nodes(op=Delay): + if ( + isinstance(node.op.duration, expr.Expr) + and node.op.duration.type.kind is types.Stretch + ): + units_used.add("stretch") + continue units_used.add(node.op.unit) return units_used @@ -163,3 +201,73 @@ def _unified(unit_set: Set[str]) -> str: return "SI" return "mixed" + + +_DURATION_KIND_NAME = { + Duration.dt: "dt", + Duration.ns: "ns", + Duration.us: "us", + Duration.ms: "ms", + Duration.s: "s", +} + + +class _EvalDurationImpl(expr.ExprVisitor[float]): + """Evaluates the expression to a single float result. + + If `dt` is provided or all durations are already in `dt`, the result is in `dt`. + Otherwise, the result will be in seconds, and all durations MUST be in wall-time (SI). + """ + + __slots__ = ("dt", "has_dt", "has_si") + + def __init__(self, dt: float | None = None): + self.dt = dt if dt is not None else 1 + self.has_dt = False + self.has_si = False + + def in_cycles(self): + return self.has_dt or self.dt != 1 + + def visit_value(self, node, /) -> float: + if isinstance(node.value, float): + return node.value + if isinstance(node.value, Duration.dt): + if self.has_si and self.dt == 1: + raise TranspilerError( + "Fail to unify time units in delays. SI units " + "and dt unit must not be mixed when dt is not supplied." + ) + self.has_dt = True + return node.value[0] + if isinstance(node.value, Duration): + if self.has_dt and self.dt == 1: + raise TranspilerError( + "Fail to unify time units in delays. SI units " + "and dt unit must not be mixed when dt is not supplied." + ) + self.has_si = True + if isinstance(node.value, Duration.s): + return node.value[0] / self.dt + from_unit = _DURATION_KIND_NAME.get(type(node.value)) + return apply_prefix(node.value[0], from_unit) / self.dt + raise TranspilerError(f"invalid duration expression: {node}") + + def visit_binary(self, node, /) -> float: + left = node.left.accept(self) + right = node.right.accept(self) + if node.op == expr.Binary.Op.ADD: + return left + right + if node.op == expr.Binary.Op.SUB: + return left - right + if node.op == expr.Binary.Op.MUL: + return left * right + if node.op == expr.Binary.Op.DIV: + return left / right + raise TranspilerError(f"invalid duration expression: {node}") + + def visit_cast(self, node, /) -> float: + return node.operand.accept(self) + + def visit_index(self, node, /) -> float: + return node.target.accept(self) diff --git a/qiskit/transpiler/passes/synthesis/default_unitary_synth_plugin.py b/qiskit/transpiler/passes/synthesis/default_unitary_synth_plugin.py new file mode 100644 index 000000000000..4cf675cb15b8 --- /dev/null +++ b/qiskit/transpiler/passes/synthesis/default_unitary_synth_plugin.py @@ -0,0 +1,653 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2020. +# +# 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. + +""" +========================================================================================= +Unitary Synthesis Plugin (in :mod:`qiskit.transpiler.passes.synthesis.unitary_synthesis`) +========================================================================================= + +.. autosummary:: + :toctree: ../stubs/ + + DefaultUnitarySynthesis +""" + +from __future__ import annotations +from math import pi, inf, isclose +from itertools import product +from functools import partial +import numpy as np + +from qiskit.circuit import Gate, Parameter +from qiskit.circuit.library.standard_gates import ( + iSwapGate, + CXGate, + CZGate, + RXXGate, + RZXGate, + RZZGate, + RYYGate, + ECRGate, + RXGate, + SXGate, + XGate, + RZGate, + UGate, + PhaseGate, + U1Gate, + U2Gate, + U3Gate, + RYGate, + RGate, + CRXGate, + CRYGate, + CRZGate, + CPhaseGate, +) +from qiskit.converters import circuit_to_dag +from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit.dagcircuit.dagnode import DAGOpNode +from qiskit.exceptions import QiskitError +from qiskit.quantum_info import Operator +from qiskit.synthesis.two_qubit.xx_decompose import XXDecomposer, XXEmbodiments +from qiskit.synthesis.two_qubit.two_qubit_decompose import ( + TwoQubitBasisDecomposer, + TwoQubitWeylDecomposition, + TwoQubitControlledUDecomposer, +) +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.passes.optimization.optimize_1q_decomposition import ( + Optimize1qGatesDecomposition, + _possible_decomposers, +) +from qiskit.transpiler.passes.synthesis import plugin + + +GATE_NAME_MAP = { + "cx": CXGate._standard_gate, + "rx": RXGate._standard_gate, + "sx": SXGate._standard_gate, + "x": XGate._standard_gate, + "rz": RZGate._standard_gate, + "u": UGate._standard_gate, + "p": PhaseGate._standard_gate, + "u1": U1Gate._standard_gate, + "u2": U2Gate._standard_gate, + "u3": U3Gate._standard_gate, + "ry": RYGate._standard_gate, + "r": RGate._standard_gate, + "rzz": RZZGate._standard_gate, + "ryy": RYYGate._standard_gate, + "rxx": RXXGate._standard_gate, + "rzx": RXXGate._standard_gate, + "cp": CPhaseGate._standard_gate, + "crx": RXXGate._standard_gate, + "cry": RXXGate._standard_gate, + "crz": RXXGate._standard_gate, +} + +KAK_GATE_PARAM_NAMES = { + "rxx": RXXGate, + "rzz": RZZGate, + "ryy": RYYGate, + "rzx": RZXGate, + "cphase": CPhaseGate, + "crx": CRXGate, + "cry": CRYGate, + "crz": CRZGate, +} + +KAK_GATE_NAMES = { + "cx": CXGate(), + "cz": CZGate(), + "iswap": iSwapGate(), + "ecr": ECRGate(), +} + + +def _choose_kak_gate(basis_gates): + """Choose the first available 2q gate to use in the KAK decomposition.""" + kak_gate = None + kak_gates = sorted(set(basis_gates or []).intersection(KAK_GATE_NAMES.keys())) + kak_gates_params = sorted(set(basis_gates or []).intersection(KAK_GATE_PARAM_NAMES.keys())) + + if kak_gates_params: + kak_gate = KAK_GATE_PARAM_NAMES[kak_gates_params[0]] + + elif kak_gates: + kak_gate = KAK_GATE_NAMES[kak_gates[0]] + + return kak_gate + + +def _choose_euler_basis(basis_gates): + """Choose the first available 1q basis to use in the Euler decomposition.""" + basis_set = set(basis_gates or []) + decomposers = _possible_decomposers(basis_set) + if decomposers: + return decomposers[0] + return "U" + + +def _find_matching_euler_bases(target, qubit): + """Find matching available 1q basis to use in the Euler decomposition.""" + basis_set = target.operation_names_for_qargs((qubit,)) + return _possible_decomposers(basis_set) + + +def _decomposer_2q_from_basis_gates(basis_gates, pulse_optimize=None, approximation_degree=None): + decomposer2q = None + kak_gate = _choose_kak_gate(basis_gates) + euler_basis = _choose_euler_basis(basis_gates) + basis_fidelity = approximation_degree or 1.0 + + if kak_gate in KAK_GATE_PARAM_NAMES.values(): + decomposer2q = TwoQubitControlledUDecomposer(kak_gate, euler_basis) + elif kak_gate is not None: + decomposer2q = TwoQubitBasisDecomposer( + kak_gate, + basis_fidelity=basis_fidelity, + euler_basis=euler_basis, + pulse_optimize=pulse_optimize, + ) + return decomposer2q + + +def _error(circuit, target=None, qubits=None): + """ + Calculate a rough error for a `circuit` that runs on specific + `qubits` of `target`. + + Use basis errors from target if available, otherwise use length + of circuit as a weak proxy for error. + """ + if target is None: + if isinstance(circuit, DAGCircuit): + return len(circuit.op_nodes()) + else: + return len(circuit) + gate_fidelities = [] + gate_durations = [] + + def score_instruction(inst, inst_qubits): + try: + keys = target.operation_names_for_qargs(inst_qubits) + for key in keys: + target_op = target.operation_from_name(key) + if isinstance(circuit, DAGCircuit): + op = inst.op + else: + op = inst.operation + if isinstance(target_op, op.base_class) and ( + target_op.is_parameterized() + or all( + isclose(float(p1), float(p2)) for p1, p2 in zip(target_op.params, op.params) + ) + ): + inst_props = target[key].get(inst_qubits, None) + if inst_props is not None: + error = getattr(inst_props, "error", 0.0) or 0.0 + duration = getattr(inst_props, "duration", 0.0) or 0.0 + gate_fidelities.append(1 - error) + gate_durations.append(duration) + else: + gate_fidelities.append(1.0) + gate_durations.append(0.0) + + break + else: + raise KeyError + except KeyError as error: + if isinstance(circuit, DAGCircuit): + op = inst.op + else: + op = inst.operation + raise TranspilerError( + f"Encountered a bad synthesis. " f"Target has no {op} on qubits {qubits}." + ) from error + + if isinstance(circuit, DAGCircuit): + for inst in circuit.topological_op_nodes(): + inst_qubits = tuple(qubits[circuit.find_bit(q).index] for q in inst.qargs) + score_instruction(inst, inst_qubits) + else: + for inst in circuit: + inst_qubits = tuple(qubits[circuit.find_bit(q).index] for q in inst.qubits) + score_instruction(inst, inst_qubits) + # TODO:return np.sum(gate_durations) + return 1 - np.prod(gate_fidelities) + + +def _preferred_direction( + decomposer2q, qubits, natural_direction, coupling_map=None, gate_lengths=None, gate_errors=None +): + """ + `decomposer2q` decomposes an SU(4) over `qubits`. A user sets `natural_direction` + to indicate whether they prefer synthesis in a hardware-native direction. + If yes, we return the `preferred_direction` here. If no hardware direction is + preferred, we raise an error (unless natural_direction is None). + We infer this from `coupling_map`, `gate_lengths`, `gate_errors`. + + Returns [0, 1] if qubits are correct in the hardware-native direction. + Returns [1, 0] if qubits must be flipped to match hardware-native direction. + """ + qubits_tuple = tuple(qubits) + reverse_tuple = qubits_tuple[::-1] + + preferred_direction = None + if natural_direction in {None, True}: + # find native gate directions from a (non-bidirectional) coupling map + if coupling_map is not None: + neighbors0 = coupling_map.neighbors(qubits[0]) + zero_one = qubits[1] in neighbors0 + neighbors1 = coupling_map.neighbors(qubits[1]) + one_zero = qubits[0] in neighbors1 + if zero_one and not one_zero: + preferred_direction = [0, 1] + if one_zero and not zero_one: + preferred_direction = [1, 0] + # otherwise infer natural directions from gate durations or gate errors + if preferred_direction is None and (gate_lengths or gate_errors): + cost_0_1 = inf + cost_1_0 = inf + try: + cost_0_1 = next( + duration + for gate, duration in gate_lengths.get(qubits_tuple, []) + if gate == decomposer2q.gate + ) + except StopIteration: + pass + try: + cost_1_0 = next( + duration + for gate, duration in gate_lengths.get(reverse_tuple, []) + if gate == decomposer2q.gate + ) + except StopIteration: + pass + if not (cost_0_1 < inf or cost_1_0 < inf): + try: + cost_0_1 = next( + error + for gate, error in gate_errors.get(qubits_tuple, []) + if gate == decomposer2q.gate + ) + except StopIteration: + pass + try: + cost_1_0 = next( + error + for gate, error in gate_errors.get(reverse_tuple, []) + if gate == decomposer2q.gate + ) + except StopIteration: + pass + if cost_0_1 < cost_1_0: + preferred_direction = [0, 1] + elif cost_1_0 < cost_0_1: + preferred_direction = [1, 0] + if natural_direction is True and preferred_direction is None: + raise TranspilerError( + f"No preferred direction of gate on qubits {qubits} " + "could be determined from coupling map or " + "gate lengths / gate errors." + ) + return preferred_direction + + +class DefaultUnitarySynthesis(plugin.UnitarySynthesisPlugin): + """The default unitary synthesis plugin.""" + + @property + def supports_basis_gates(self): + return True + + @property + def supports_coupling_map(self): + return True + + @property + def supports_natural_direction(self): + return True + + @property + def supports_pulse_optimize(self): + return True + + @property + def supports_gate_lengths(self): + return False + + @property + def supports_gate_errors(self): + return False + + @property + def supports_gate_lengths_by_qubit(self): + return True + + @property + def supports_gate_errors_by_qubit(self): + return True + + @property + def max_qubits(self): + return None + + @property + def min_qubits(self): + return None + + @property + def supported_bases(self): + return None + + @property + def supports_target(self): + return True + + def __init__(self): + super().__init__() + self._decomposer_cache = {} + + def _decomposer_2q_from_target(self, target, qubits, approximation_degree): + # we just need 2-qubit decomposers, in any direction. + # we'll fix the synthesis direction later. + qubits_tuple = tuple(sorted(qubits)) + reverse_tuple = qubits_tuple[::-1] + if qubits_tuple in self._decomposer_cache: + return self._decomposer_cache[qubits_tuple] + + # available instructions on this qubit pair, and their associated property. + available_2q_basis = {} + available_2q_props = {} + + # 2q gates sent to 2q decomposers must not have any symbolic parameters. The + # gates must be convertable to a numeric matrix. If a basis gate supports an arbitrary + # angle, we have to choose one angle (or more.) + def _replace_parameterized_gate(op): + if isinstance(op, RXXGate) and isinstance(op.params[0], Parameter): + op = RXXGate(pi / 2) + elif isinstance(op, RZXGate) and isinstance(op.params[0], Parameter): + op = RZXGate(pi / 4) + elif isinstance(op, RZZGate) and isinstance(op.params[0], Parameter): + op = RZZGate(pi / 2) + return op + + try: + keys = target.operation_names_for_qargs(qubits_tuple) + for key in keys: + op = target.operation_from_name(key) + if not isinstance(op, Gate): + continue + available_2q_basis[key] = _replace_parameterized_gate(op) + available_2q_props[key] = target[key][qubits_tuple] + except KeyError: + pass + try: + keys = target.operation_names_for_qargs(reverse_tuple) + for key in keys: + if key not in available_2q_basis: + op = target.operation_from_name(key) + if not isinstance(op, Gate): + continue + available_2q_basis[key] = _replace_parameterized_gate(op) + available_2q_props[key] = target[key][reverse_tuple] + except KeyError: + pass + if not available_2q_basis: + raise TranspilerError( + f"Target has no gates available on qubits {qubits} to synthesize over." + ) + # available decomposition basis on each of the qubits of the pair + # NOTE: assumes both qubits have the same single-qubit gates + available_1q_basis = _find_matching_euler_bases(target, qubits_tuple[0]) + + # find all decomposers + # TODO: reduce number of decomposers here somehow + decomposers = [] + + def is_supercontrolled(gate): + try: + operator = Operator(gate) + except QiskitError: + return False + kak = TwoQubitWeylDecomposition(operator.data) + return isclose(kak.a, pi / 4) and isclose(kak.c, 0.0) + + def is_controlled(gate): + try: + operator = Operator(gate) + except QiskitError: + return False + kak = TwoQubitWeylDecomposition(operator.data) + return isclose(kak.b, 0.0) and isclose(kak.c, 0.0) + + # possible supercontrolled decomposers (i.e. TwoQubitBasisDecomposer) + supercontrolled_basis = { + k: v for k, v in available_2q_basis.items() if is_supercontrolled(v) + } + for basis_1q, basis_2q in product(available_1q_basis, supercontrolled_basis.keys()): + props = available_2q_props.get(basis_2q) + if props is None: + basis_2q_fidelity = 1.0 + else: + error = getattr(props, "error", 0.0) + if error is None: + error = 0.0 + basis_2q_fidelity = 1 - error + if approximation_degree is not None: + basis_2q_fidelity *= approximation_degree + decomposer = TwoQubitBasisDecomposer( + supercontrolled_basis[basis_2q], + euler_basis=basis_1q, + basis_fidelity=basis_2q_fidelity, + ) + decomposers.append(decomposer) + + # If our 2q basis gates are a subset of cx, ecr, or cz then we know TwoQubitBasisDecomposer + # is an ideal decomposition and there is no need to bother calculating the XX embodiments + # or try the XX decomposer + if {"cx", "cz", "ecr"}.issuperset(available_2q_basis): + self._decomposer_cache[qubits_tuple] = decomposers + return decomposers + + # possible controlled decomposers (i.e. XXDecomposer) + controlled_basis = {k: v for k, v in available_2q_basis.items() if is_controlled(v)} + basis_2q_fidelity = {} + embodiments = {} + pi2_basis = None + for k, v in controlled_basis.items(): + strength = 2 * TwoQubitWeylDecomposition(Operator(v).data).a # pi/2: fully entangling + # each strength has its own fidelity + props = available_2q_props.get(k) + if props is None: + basis_2q_fidelity[strength] = 1.0 + else: + error = getattr(props, "error", 0.0) + if error is None: + error = 0.0 + basis_2q_fidelity[strength] = 1 - error + # rewrite XX of the same strength in terms of it + embodiment = XXEmbodiments[v.base_class] + if len(embodiment.parameters) == 1: + embodiments[strength] = embodiment.assign_parameters([strength]) + else: + embodiments[strength] = embodiment + # basis equivalent to CX are well optimized so use for the pi/2 angle if available + if isclose(strength, pi / 2) and k in supercontrolled_basis: + pi2_basis = v + # if we are using the approximation_degree knob, use it to scale already-given fidelities + if approximation_degree is not None: + basis_2q_fidelity = {k: v * approximation_degree for k, v in basis_2q_fidelity.items()} + if basis_2q_fidelity: + for basis_1q in available_1q_basis: + if isinstance(pi2_basis, CXGate) and basis_1q == "ZSX": + # If we're going to use the pulse optimal decomposition + # in TwoQubitBasisDecomposer we need to compute the basis + # fidelity to use for the decomposition. Either use the + # cx error rate if approximation degree is None, or + # the approximation degree value if it's a float + if approximation_degree is None: + props = target["cx"].get(qubits_tuple) + if props is not None: + fidelity = 1.0 - getattr(props, "error", 0.0) + else: + fidelity = 1.0 + else: + fidelity = approximation_degree + pi2_decomposer = TwoQubitBasisDecomposer( + pi2_basis, + euler_basis=basis_1q, + basis_fidelity=fidelity, + pulse_optimize=True, + ) + embodiments.update({pi / 2: XXEmbodiments[pi2_decomposer.gate.base_class]}) + else: + pi2_decomposer = None + decomposer = XXDecomposer( + basis_fidelity=basis_2q_fidelity, + euler_basis=basis_1q, + embodiments=embodiments, + backup_optimizer=pi2_decomposer, + ) + decomposers.append(decomposer) + + self._decomposer_cache[qubits_tuple] = decomposers + return decomposers + + def run(self, unitary, **options): + # Approximation degree is set directly as an attribute on the + # instance by the UnitarySynthesis pass here as it's not part of + # plugin interface. However if for some reason it's not set assume + # it's 1. + approximation_degree = getattr(self, "_approximation_degree", 1.0) + basis_gates = options["basis_gates"] + coupling_map = options["coupling_map"][0] + natural_direction = options["natural_direction"] + pulse_optimize = options["pulse_optimize"] + gate_lengths = options["gate_lengths_by_qubit"] + gate_errors = options["gate_errors_by_qubit"] + qubits = options["coupling_map"][1] + target = options["target"] + + if unitary.shape == (2, 2): + _decomposer1q = Optimize1qGatesDecomposition(basis_gates, target) + sequence = _decomposer1q._resynthesize_run(unitary, qubits[0]) + if sequence is None: + return None + return _decomposer1q._gate_sequence_to_dag(sequence) + elif unitary.shape == (4, 4): + # select synthesizers that can lower to the target + if target is not None: + decomposers2q = self._decomposer_2q_from_target( + target, qubits, approximation_degree + ) + else: + decomposer2q = _decomposer_2q_from_basis_gates( + basis_gates, pulse_optimize, approximation_degree + ) + decomposers2q = [decomposer2q] if decomposer2q is not None else [] + # choose the cheapest output among synthesized circuits + synth_circuits = [] + # If we have a single TwoQubitBasisDecomposer skip dag creation as we don't need to + # store and can instead manually create the synthesized gates directly in the output dag + if len(decomposers2q) == 1 and isinstance(decomposers2q[0], TwoQubitBasisDecomposer): + preferred_direction = _preferred_direction( + decomposers2q[0], + qubits, + natural_direction, + coupling_map, + gate_lengths, + gate_errors, + ) + return self._synth_su4_no_dag( + unitary, decomposers2q[0], preferred_direction, approximation_degree + ) + for decomposer2q in decomposers2q: + preferred_direction = _preferred_direction( + decomposer2q, qubits, natural_direction, coupling_map, gate_lengths, gate_errors + ) + synth_circuit = self._synth_su4( + unitary, decomposer2q, preferred_direction, approximation_degree + ) + synth_circuits.append(synth_circuit) + synth_circuit = min( + synth_circuits, + key=partial(_error, target=target, qubits=tuple(qubits)), + default=None, + ) + else: + from qiskit.synthesis.unitary.qsd import ( # pylint: disable=cyclic-import + qs_decomposition, + ) + + # only decompose if needed. TODO: handle basis better + synth_circuit = qs_decomposition(unitary) if (basis_gates or target) else None + if synth_circuit is None: + return None + if isinstance(synth_circuit, DAGCircuit): + return synth_circuit + return circuit_to_dag(synth_circuit) + + def _synth_su4_no_dag(self, unitary, decomposer2q, preferred_direction, approximation_degree): + approximate = not approximation_degree == 1.0 + synth_circ = decomposer2q._inner_decomposer(unitary, approximate=approximate) + if not preferred_direction: + return (synth_circ, synth_circ.global_phase, decomposer2q.gate) + + synth_direction = None + # if the gates in synthesis are in the opposite direction of the preferred direction + # resynthesize a new operator which is the original conjugated by swaps. + # this new operator is doubly mirrored from the original and is locally equivalent. + for gate, _params, qubits in synth_circ: + if gate is None or gate == CXGate._standard_gate: + synth_direction = qubits + if synth_direction is not None and synth_direction != preferred_direction: + # TODO: Avoid using a dag to correct the synthesis direction + return self._reversed_synth_su4(unitary, decomposer2q, approximation_degree) + return (synth_circ, synth_circ.global_phase, decomposer2q.gate) + + def _synth_su4(self, su4_mat, decomposer2q, preferred_direction, approximation_degree): + approximate = not approximation_degree == 1.0 + synth_circ = decomposer2q(su4_mat, approximate=approximate, use_dag=True) + if not preferred_direction: + return synth_circ + synth_direction = None + # if the gates in synthesis are in the opposite direction of the preferred direction + # resynthesize a new operator which is the original conjugated by swaps. + # this new operator is doubly mirrored from the original and is locally equivalent. + for inst in synth_circ.topological_op_nodes(): + if inst.op.num_qubits == 2: + synth_direction = [synth_circ.find_bit(q).index for q in inst.qargs] + if synth_direction is not None and synth_direction != preferred_direction: + return self._reversed_synth_su4(su4_mat, decomposer2q, approximation_degree) + return synth_circ + + def _reversed_synth_su4(self, su4_mat, decomposer2q, approximation_degree): + approximate = not approximation_degree == 1.0 + su4_mat_mm = su4_mat.copy() + su4_mat_mm[[1, 2]] = su4_mat_mm[[2, 1]] + su4_mat_mm[:, [1, 2]] = su4_mat_mm[:, [2, 1]] + synth_circ = decomposer2q(su4_mat_mm, approximate=approximate, use_dag=True) + out_dag = DAGCircuit() + out_dag.global_phase = synth_circ.global_phase + out_dag.add_qubits(list(reversed(synth_circ.qubits))) + flip_bits = out_dag.qubits[::-1] + for node in synth_circ.topological_op_nodes(): + qubits = tuple(flip_bits[synth_circ.find_bit(x).index] for x in node.qargs) + node = DAGOpNode.from_instruction( + node._to_circuit_instruction().replace(qubits=qubits, params=node.params) + ) + out_dag._apply_op_node_back(node) + return out_dag diff --git a/qiskit/transpiler/passes/synthesis/solovay_kitaev_synthesis.py b/qiskit/transpiler/passes/synthesis/solovay_kitaev_synthesis.py index bb31e5cc002c..5410d1302577 100644 --- a/qiskit/transpiler/passes/synthesis/solovay_kitaev_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/solovay_kitaev_synthesis.py @@ -33,7 +33,7 @@ generate_basic_approximations, ) from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.passes.utils.control_flow import trivial_recurse from .plugin import UnitarySynthesisPlugin @@ -155,6 +155,7 @@ def __init__( self.recursion_degree = recursion_degree self._sk = SolovayKitaevDecomposition(basic_approximations) + @trivial_recurse def run(self, dag: DAGCircuit) -> DAGCircuit: """Run the ``SolovayKitaev`` pass on `dag`. @@ -168,18 +169,19 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: TranspilerError: if a gates does not have to_matrix """ for node in dag.op_nodes(): - if not node.op.num_qubits == 1: - continue # ignore all non-single qubit gates + + # ignore operations on which the algorithm cannot run + if ( + (node.op.num_qubits != 1) + or node.is_parameterized() + or (not hasattr(node.op, "to_matrix")) + ): + continue # we do not check the input matrix as we know it comes from a Qiskit gate, as this # we know it will generate a valid SU(2) matrix check_input = not isinstance(node.op, Gate) - if not hasattr(node.op, "to_matrix"): - raise TranspilerError( - f"SolovayKitaev does not support gate without to_matrix method: {node.op.name}" - ) - matrix = node.op.to_matrix() # call solovay kitaev diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 3dfd6caff65c..d0c98e956927 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -11,147 +11,26 @@ # that they have been altered from the originals. """ -========================================================================================= -Unitary Synthesis Plugin (in :mod:`qiskit.transpiler.passes.synthesis.unitary_synthesis`) -========================================================================================= - -.. autosummary:: - :toctree: ../stubs/ - - DefaultUnitarySynthesis +Unitary Synthesis Transpiler Pass """ from __future__ import annotations -from math import pi, inf, isclose from typing import Any -from itertools import product -from functools import partial -import numpy as np from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES -from qiskit.circuit import Gate, Parameter, CircuitInstruction -from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping -from qiskit.circuit.library.standard_gates import ( - iSwapGate, - CXGate, - CZGate, - RXXGate, - RZXGate, - RZZGate, - RYYGate, - ECRGate, - RXGate, - SXGate, - XGate, - RZGate, - UGate, - PhaseGate, - U1Gate, - U2Gate, - U3Gate, - RYGate, - RGate, - CRXGate, - CRYGate, - CRZGate, - CPhaseGate, -) +from qiskit.circuit import CircuitInstruction from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.dagcircuit.dagnode import DAGOpNode -from qiskit.exceptions import QiskitError -from qiskit.quantum_info import Operator from qiskit.synthesis.one_qubit import one_qubit_decompose -from qiskit.synthesis.two_qubit.xx_decompose import XXDecomposer, XXEmbodiments -from qiskit.synthesis.two_qubit.two_qubit_decompose import ( - TwoQubitBasisDecomposer, - TwoQubitWeylDecomposition, - TwoQubitControlledUDecomposer, -) + from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.coupling import CouplingMap from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.passes.optimization.optimize_1q_decomposition import ( - Optimize1qGatesDecomposition, - _possible_decomposers, -) from qiskit.transpiler.passes.synthesis import plugin from qiskit.transpiler.target import Target -from qiskit._accelerate.unitary_synthesis import run_default_main_loop - -GATE_NAME_MAP = { - "cx": CXGate._standard_gate, - "rx": RXGate._standard_gate, - "sx": SXGate._standard_gate, - "x": XGate._standard_gate, - "rz": RZGate._standard_gate, - "u": UGate._standard_gate, - "p": PhaseGate._standard_gate, - "u1": U1Gate._standard_gate, - "u2": U2Gate._standard_gate, - "u3": U3Gate._standard_gate, - "ry": RYGate._standard_gate, - "r": RGate._standard_gate, - "rzz": RZZGate._standard_gate, - "ryy": RYYGate._standard_gate, - "rxx": RXXGate._standard_gate, - "rzx": RXXGate._standard_gate, - "cp": CPhaseGate._standard_gate, - "crx": RXXGate._standard_gate, - "cry": RXXGate._standard_gate, - "crz": RXXGate._standard_gate, -} - -KAK_GATE_PARAM_NAMES = { - "rxx": RXXGate, - "rzz": RZZGate, - "ryy": RYYGate, - "rzx": RZXGate, - "cphase": CPhaseGate, - "crx": CRXGate, - "cry": CRYGate, - "crz": CRZGate, -} - -KAK_GATE_NAMES = { - "cx": CXGate(), - "cz": CZGate(), - "iswap": iSwapGate(), - "ecr": ECRGate(), -} - -GateNameToGate = get_standard_gate_name_mapping() - - -def _choose_kak_gate(basis_gates): - """Choose the first available 2q gate to use in the KAK decomposition.""" - kak_gate = None - kak_gates = sorted(set(basis_gates or []).intersection(KAK_GATE_NAMES.keys())) - kak_gates_params = sorted(set(basis_gates or []).intersection(KAK_GATE_PARAM_NAMES.keys())) - - if kak_gates_params: - kak_gate = KAK_GATE_PARAM_NAMES[kak_gates_params[0]] - - elif kak_gates: - kak_gate = KAK_GATE_NAMES[kak_gates[0]] - - return kak_gate - - -def _choose_euler_basis(basis_gates): - """Choose the first available 1q basis to use in the Euler decomposition.""" - basis_set = set(basis_gates or []) - decomposers = _possible_decomposers(basis_set) - if decomposers: - return decomposers[0] - return "U" - - -def _find_matching_euler_bases(target, qubit): - """Find matching available 1q basis to use in the Euler decomposition.""" - basis_set = target.operation_names_for_qargs((qubit,)) - return _possible_decomposers(basis_set) +from qiskit._accelerate.unitary_synthesis import run_main_loop def _choose_bases(basis_gates, basis_dict=None): @@ -172,167 +51,6 @@ def _choose_bases(basis_gates, basis_dict=None): return out_basis -def _decomposer_2q_from_basis_gates(basis_gates, pulse_optimize=None, approximation_degree=None): - decomposer2q = None - kak_gate = _choose_kak_gate(basis_gates) - euler_basis = _choose_euler_basis(basis_gates) - basis_fidelity = approximation_degree or 1.0 - - if kak_gate in KAK_GATE_PARAM_NAMES.values(): - decomposer2q = TwoQubitControlledUDecomposer(kak_gate, euler_basis) - elif kak_gate is not None: - decomposer2q = TwoQubitBasisDecomposer( - kak_gate, - basis_fidelity=basis_fidelity, - euler_basis=euler_basis, - pulse_optimize=pulse_optimize, - ) - return decomposer2q - - -def _error(circuit, target=None, qubits=None): - """ - Calculate a rough error for a `circuit` that runs on specific - `qubits` of `target`. - - Use basis errors from target if available, otherwise use length - of circuit as a weak proxy for error. - """ - if target is None: - if isinstance(circuit, DAGCircuit): - return len(circuit.op_nodes()) - else: - return len(circuit) - gate_fidelities = [] - gate_durations = [] - - def score_instruction(inst, inst_qubits): - try: - keys = target.operation_names_for_qargs(inst_qubits) - for key in keys: - target_op = target.operation_from_name(key) - if isinstance(circuit, DAGCircuit): - op = inst.op - else: - op = inst.operation - if isinstance(target_op, op.base_class) and ( - target_op.is_parameterized() - or all( - isclose(float(p1), float(p2)) for p1, p2 in zip(target_op.params, op.params) - ) - ): - inst_props = target[key].get(inst_qubits, None) - if inst_props is not None: - error = getattr(inst_props, "error", 0.0) or 0.0 - duration = getattr(inst_props, "duration", 0.0) or 0.0 - gate_fidelities.append(1 - error) - gate_durations.append(duration) - else: - gate_fidelities.append(1.0) - gate_durations.append(0.0) - - break - else: - raise KeyError - except KeyError as error: - if isinstance(circuit, DAGCircuit): - op = inst.op - else: - op = inst.operation - raise TranspilerError( - f"Encountered a bad synthesis. " f"Target has no {op} on qubits {qubits}." - ) from error - - if isinstance(circuit, DAGCircuit): - for inst in circuit.topological_op_nodes(): - inst_qubits = tuple(qubits[circuit.find_bit(q).index] for q in inst.qargs) - score_instruction(inst, inst_qubits) - else: - for inst in circuit: - inst_qubits = tuple(qubits[circuit.find_bit(q).index] for q in inst.qubits) - score_instruction(inst, inst_qubits) - # TODO:return np.sum(gate_durations) - return 1 - np.prod(gate_fidelities) - - -def _preferred_direction( - decomposer2q, qubits, natural_direction, coupling_map=None, gate_lengths=None, gate_errors=None -): - """ - `decomposer2q` decomposes an SU(4) over `qubits`. A user sets `natural_direction` - to indicate whether they prefer synthesis in a hardware-native direction. - If yes, we return the `preferred_direction` here. If no hardware direction is - preferred, we raise an error (unless natural_direction is None). - We infer this from `coupling_map`, `gate_lengths`, `gate_errors`. - - Returns [0, 1] if qubits are correct in the hardware-native direction. - Returns [1, 0] if qubits must be flipped to match hardware-native direction. - """ - qubits_tuple = tuple(qubits) - reverse_tuple = qubits_tuple[::-1] - - preferred_direction = None - if natural_direction in {None, True}: - # find native gate directions from a (non-bidirectional) coupling map - if coupling_map is not None: - neighbors0 = coupling_map.neighbors(qubits[0]) - zero_one = qubits[1] in neighbors0 - neighbors1 = coupling_map.neighbors(qubits[1]) - one_zero = qubits[0] in neighbors1 - if zero_one and not one_zero: - preferred_direction = [0, 1] - if one_zero and not zero_one: - preferred_direction = [1, 0] - # otherwise infer natural directions from gate durations or gate errors - if preferred_direction is None and (gate_lengths or gate_errors): - cost_0_1 = inf - cost_1_0 = inf - try: - cost_0_1 = next( - duration - for gate, duration in gate_lengths.get(qubits_tuple, []) - if gate == decomposer2q.gate - ) - except StopIteration: - pass - try: - cost_1_0 = next( - duration - for gate, duration in gate_lengths.get(reverse_tuple, []) - if gate == decomposer2q.gate - ) - except StopIteration: - pass - if not (cost_0_1 < inf or cost_1_0 < inf): - try: - cost_0_1 = next( - error - for gate, error in gate_errors.get(qubits_tuple, []) - if gate == decomposer2q.gate - ) - except StopIteration: - pass - try: - cost_1_0 = next( - error - for gate, error in gate_errors.get(reverse_tuple, []) - if gate == decomposer2q.gate - ) - except StopIteration: - pass - if cost_0_1 < cost_1_0: - preferred_direction = [0, 1] - elif cost_1_0 < cost_0_1: - preferred_direction = [1, 0] - if natural_direction is True and preferred_direction is None: - raise TranspilerError( - f"No preferred direction of gate on qubits {qubits} " - "could be determined from coupling map or " - "gate lengths / gate errors." - ) - return preferred_direction - - class UnitarySynthesis(TransformationPass): """Synthesize gates according to their basis gates.""" @@ -454,6 +172,10 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if self.plugins: plugin_method = self.plugins.ext_plugins[self.method].obj else: + from qiskit.transpiler.passes.synthesis.default_unitary_synth_plugin import ( + DefaultUnitarySynthesis, + ) + plugin_method = DefaultUnitarySynthesis() plugin_kwargs: dict[str, Any] = {"config": self._plugin_config} _gate_lengths = _gate_errors = None @@ -489,19 +211,20 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: else {} ) - if self.method == "default" and self._target is not None: + if self.method == "default": _coupling_edges = ( set(self._coupling_map.get_edges()) if self._coupling_map is not None else set() ) - - out = run_default_main_loop( + out = run_main_loop( dag, list(qubit_indices.values()), self._min_qubits, self._target, + self._basis_gates, _coupling_edges, self._approximation_degree, self._natural_direction, + self._pulse_optimize, ) return out else: @@ -697,351 +420,3 @@ def _build_gate_errors_by_qubit(target=None): if operation_and_errors: gate_errors[qubits] = operation_and_errors return gate_errors - - -class DefaultUnitarySynthesis(plugin.UnitarySynthesisPlugin): - """The default unitary synthesis plugin.""" - - @property - def supports_basis_gates(self): - return True - - @property - def supports_coupling_map(self): - return True - - @property - def supports_natural_direction(self): - return True - - @property - def supports_pulse_optimize(self): - return True - - @property - def supports_gate_lengths(self): - return False - - @property - def supports_gate_errors(self): - return False - - @property - def supports_gate_lengths_by_qubit(self): - return True - - @property - def supports_gate_errors_by_qubit(self): - return True - - @property - def max_qubits(self): - return None - - @property - def min_qubits(self): - return None - - @property - def supported_bases(self): - return None - - @property - def supports_target(self): - return True - - def __init__(self): - super().__init__() - self._decomposer_cache = {} - - def _decomposer_2q_from_target(self, target, qubits, approximation_degree): - # we just need 2-qubit decomposers, in any direction. - # we'll fix the synthesis direction later. - qubits_tuple = tuple(sorted(qubits)) - reverse_tuple = qubits_tuple[::-1] - if qubits_tuple in self._decomposer_cache: - return self._decomposer_cache[qubits_tuple] - - # available instructions on this qubit pair, and their associated property. - available_2q_basis = {} - available_2q_props = {} - - # 2q gates sent to 2q decomposers must not have any symbolic parameters. The - # gates must be convertable to a numeric matrix. If a basis gate supports an arbitrary - # angle, we have to choose one angle (or more.) - def _replace_parameterized_gate(op): - if isinstance(op, RXXGate) and isinstance(op.params[0], Parameter): - op = RXXGate(pi / 2) - elif isinstance(op, RZXGate) and isinstance(op.params[0], Parameter): - op = RZXGate(pi / 4) - elif isinstance(op, RZZGate) and isinstance(op.params[0], Parameter): - op = RZZGate(pi / 2) - return op - - try: - keys = target.operation_names_for_qargs(qubits_tuple) - for key in keys: - op = target.operation_from_name(key) - if not isinstance(op, Gate): - continue - available_2q_basis[key] = _replace_parameterized_gate(op) - available_2q_props[key] = target[key][qubits_tuple] - except KeyError: - pass - try: - keys = target.operation_names_for_qargs(reverse_tuple) - for key in keys: - if key not in available_2q_basis: - op = target.operation_from_name(key) - if not isinstance(op, Gate): - continue - available_2q_basis[key] = _replace_parameterized_gate(op) - available_2q_props[key] = target[key][reverse_tuple] - except KeyError: - pass - if not available_2q_basis: - raise TranspilerError( - f"Target has no gates available on qubits {qubits} to synthesize over." - ) - # available decomposition basis on each of the qubits of the pair - # NOTE: assumes both qubits have the same single-qubit gates - available_1q_basis = _find_matching_euler_bases(target, qubits_tuple[0]) - - # find all decomposers - # TODO: reduce number of decomposers here somehow - decomposers = [] - - def is_supercontrolled(gate): - try: - operator = Operator(gate) - except QiskitError: - return False - kak = TwoQubitWeylDecomposition(operator.data) - return isclose(kak.a, pi / 4) and isclose(kak.c, 0.0) - - def is_controlled(gate): - try: - operator = Operator(gate) - except QiskitError: - return False - kak = TwoQubitWeylDecomposition(operator.data) - return isclose(kak.b, 0.0) and isclose(kak.c, 0.0) - - # possible supercontrolled decomposers (i.e. TwoQubitBasisDecomposer) - supercontrolled_basis = { - k: v for k, v in available_2q_basis.items() if is_supercontrolled(v) - } - for basis_1q, basis_2q in product(available_1q_basis, supercontrolled_basis.keys()): - props = available_2q_props.get(basis_2q) - if props is None: - basis_2q_fidelity = 1.0 - else: - error = getattr(props, "error", 0.0) - if error is None: - error = 0.0 - basis_2q_fidelity = 1 - error - if approximation_degree is not None: - basis_2q_fidelity *= approximation_degree - decomposer = TwoQubitBasisDecomposer( - supercontrolled_basis[basis_2q], - euler_basis=basis_1q, - basis_fidelity=basis_2q_fidelity, - ) - decomposers.append(decomposer) - - # If our 2q basis gates are a subset of cx, ecr, or cz then we know TwoQubitBasisDecomposer - # is an ideal decomposition and there is no need to bother calculating the XX embodiments - # or try the XX decomposer - if {"cx", "cz", "ecr"}.issuperset(available_2q_basis): - self._decomposer_cache[qubits_tuple] = decomposers - return decomposers - - # possible controlled decomposers (i.e. XXDecomposer) - controlled_basis = {k: v for k, v in available_2q_basis.items() if is_controlled(v)} - basis_2q_fidelity = {} - embodiments = {} - pi2_basis = None - for k, v in controlled_basis.items(): - strength = 2 * TwoQubitWeylDecomposition(Operator(v).data).a # pi/2: fully entangling - # each strength has its own fidelity - props = available_2q_props.get(k) - if props is None: - basis_2q_fidelity[strength] = 1.0 - else: - error = getattr(props, "error", 0.0) - if error is None: - error = 0.0 - basis_2q_fidelity[strength] = 1 - error - # rewrite XX of the same strength in terms of it - embodiment = XXEmbodiments[v.base_class] - if len(embodiment.parameters) == 1: - embodiments[strength] = embodiment.assign_parameters([strength]) - else: - embodiments[strength] = embodiment - # basis equivalent to CX are well optimized so use for the pi/2 angle if available - if isclose(strength, pi / 2) and k in supercontrolled_basis: - pi2_basis = v - # if we are using the approximation_degree knob, use it to scale already-given fidelities - if approximation_degree is not None: - basis_2q_fidelity = {k: v * approximation_degree for k, v in basis_2q_fidelity.items()} - if basis_2q_fidelity: - for basis_1q in available_1q_basis: - if isinstance(pi2_basis, CXGate) and basis_1q == "ZSX": - # If we're going to use the pulse optimal decomposition - # in TwoQubitBasisDecomposer we need to compute the basis - # fidelity to use for the decomposition. Either use the - # cx error rate if approximation degree is None, or - # the approximation degree value if it's a float - if approximation_degree is None: - props = target["cx"].get(qubits_tuple) - if props is not None: - fidelity = 1.0 - getattr(props, "error", 0.0) - else: - fidelity = 1.0 - else: - fidelity = approximation_degree - pi2_decomposer = TwoQubitBasisDecomposer( - pi2_basis, - euler_basis=basis_1q, - basis_fidelity=fidelity, - pulse_optimize=True, - ) - embodiments.update({pi / 2: XXEmbodiments[pi2_decomposer.gate.base_class]}) - else: - pi2_decomposer = None - decomposer = XXDecomposer( - basis_fidelity=basis_2q_fidelity, - euler_basis=basis_1q, - embodiments=embodiments, - backup_optimizer=pi2_decomposer, - ) - decomposers.append(decomposer) - - self._decomposer_cache[qubits_tuple] = decomposers - return decomposers - - def run(self, unitary, **options): - # Approximation degree is set directly as an attribute on the - # instance by the UnitarySynthesis pass here as it's not part of - # plugin interface. However if for some reason it's not set assume - # it's 1. - approximation_degree = getattr(self, "_approximation_degree", 1.0) - basis_gates = options["basis_gates"] - coupling_map = options["coupling_map"][0] - natural_direction = options["natural_direction"] - pulse_optimize = options["pulse_optimize"] - gate_lengths = options["gate_lengths_by_qubit"] - gate_errors = options["gate_errors_by_qubit"] - qubits = options["coupling_map"][1] - target = options["target"] - - if unitary.shape == (2, 2): - _decomposer1q = Optimize1qGatesDecomposition(basis_gates, target) - sequence = _decomposer1q._resynthesize_run(unitary, qubits[0]) - if sequence is None: - return None - return _decomposer1q._gate_sequence_to_dag(sequence) - elif unitary.shape == (4, 4): - # select synthesizers that can lower to the target - if target is not None: - decomposers2q = self._decomposer_2q_from_target( - target, qubits, approximation_degree - ) - else: - decomposer2q = _decomposer_2q_from_basis_gates( - basis_gates, pulse_optimize, approximation_degree - ) - decomposers2q = [decomposer2q] if decomposer2q is not None else [] - # choose the cheapest output among synthesized circuits - synth_circuits = [] - # If we have a single TwoQubitBasisDecomposer skip dag creation as we don't need to - # store and can instead manually create the synthesized gates directly in the output dag - if len(decomposers2q) == 1 and isinstance(decomposers2q[0], TwoQubitBasisDecomposer): - preferred_direction = _preferred_direction( - decomposers2q[0], - qubits, - natural_direction, - coupling_map, - gate_lengths, - gate_errors, - ) - return self._synth_su4_no_dag( - unitary, decomposers2q[0], preferred_direction, approximation_degree - ) - for decomposer2q in decomposers2q: - preferred_direction = _preferred_direction( - decomposer2q, qubits, natural_direction, coupling_map, gate_lengths, gate_errors - ) - synth_circuit = self._synth_su4( - unitary, decomposer2q, preferred_direction, approximation_degree - ) - synth_circuits.append(synth_circuit) - synth_circuit = min( - synth_circuits, - key=partial(_error, target=target, qubits=tuple(qubits)), - default=None, - ) - else: - from qiskit.synthesis.unitary.qsd import ( # pylint: disable=cyclic-import - qs_decomposition, - ) - - # only decompose if needed. TODO: handle basis better - synth_circuit = qs_decomposition(unitary) if (basis_gates or target) else None - if synth_circuit is None: - return None - if isinstance(synth_circuit, DAGCircuit): - return synth_circuit - return circuit_to_dag(synth_circuit) - - def _synth_su4_no_dag(self, unitary, decomposer2q, preferred_direction, approximation_degree): - approximate = not approximation_degree == 1.0 - synth_circ = decomposer2q._inner_decomposer(unitary, approximate=approximate) - if not preferred_direction: - return (synth_circ, synth_circ.global_phase, decomposer2q.gate) - - synth_direction = None - # if the gates in synthesis are in the opposite direction of the preferred direction - # resynthesize a new operator which is the original conjugated by swaps. - # this new operator is doubly mirrored from the original and is locally equivalent. - for gate, _params, qubits in synth_circ: - if gate is None or gate == CXGate._standard_gate: - synth_direction = qubits - if synth_direction is not None and synth_direction != preferred_direction: - # TODO: Avoid using a dag to correct the synthesis direction - return self._reversed_synth_su4(unitary, decomposer2q, approximation_degree) - return (synth_circ, synth_circ.global_phase, decomposer2q.gate) - - def _synth_su4(self, su4_mat, decomposer2q, preferred_direction, approximation_degree): - approximate = not approximation_degree == 1.0 - synth_circ = decomposer2q(su4_mat, approximate=approximate, use_dag=True) - if not preferred_direction: - return synth_circ - synth_direction = None - # if the gates in synthesis are in the opposite direction of the preferred direction - # resynthesize a new operator which is the original conjugated by swaps. - # this new operator is doubly mirrored from the original and is locally equivalent. - for inst in synth_circ.topological_op_nodes(): - if inst.op.num_qubits == 2: - synth_direction = [synth_circ.find_bit(q).index for q in inst.qargs] - if synth_direction is not None and synth_direction != preferred_direction: - return self._reversed_synth_su4(su4_mat, decomposer2q, approximation_degree) - return synth_circ - - def _reversed_synth_su4(self, su4_mat, decomposer2q, approximation_degree): - approximate = not approximation_degree == 1.0 - su4_mat_mm = su4_mat.copy() - su4_mat_mm[[1, 2]] = su4_mat_mm[[2, 1]] - su4_mat_mm[:, [1, 2]] = su4_mat_mm[:, [2, 1]] - synth_circ = decomposer2q(su4_mat_mm, approximate=approximate, use_dag=True) - out_dag = DAGCircuit() - out_dag.global_phase = synth_circ.global_phase - out_dag.add_qubits(list(reversed(synth_circ.qubits))) - flip_bits = out_dag.qubits[::-1] - for node in synth_circ.topological_op_nodes(): - qubits = tuple(flip_bits[synth_circ.find_bit(x).index] for x in node.qargs) - node = DAGOpNode.from_instruction( - node._to_circuit_instruction().replace(qubits=qubits, params=node.params) - ) - out_dag._apply_op_node_back(node) - return out_dag diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index 52f3d65449e5..1d473014b981 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -12,7 +12,6 @@ """Pass Manager Configuration class.""" -import pprint import warnings from qiskit.transpiler.coupling import CouplingMap @@ -35,7 +34,6 @@ def __init__( translation_method=None, scheduling_method=None, instruction_durations=None, - backend_properties=None, approximation_degree=None, seed_transpiler=None, timing_constraints=None, @@ -69,9 +67,6 @@ def __init__( be a plugin name if an external scheduling stage plugin is being used. instruction_durations (InstructionDurations): Dictionary of duration (in dt) for each instruction. - backend_properties (BackendProperties): Properties returned by a - backend, including information on gate errors, readout errors, - qubit coherence times, etc. approximation_degree (float): heuristic dial used for circuit approximation (1.0=no approximation, 0.0=maximal approximation) seed_transpiler (int): Sets random seed for the stochastic parts of @@ -102,7 +97,6 @@ def __init__( self.optimization_method = optimization_method self.scheduling_method = scheduling_method self.instruction_durations = instruction_durations - self.backend_properties = backend_properties self.approximation_degree = approximation_degree self.seed_transpiler = seed_transpiler self.timing_constraints = timing_constraints @@ -175,8 +169,6 @@ def from_backend(cls, backend, _skip_target=False, **pass_manager_options): res.instruction_durations = InstructionDurations.from_backend(backend) else: res.instruction_durations = backend.instruction_durations - if res.backend_properties is None and backend_version < 2: - res.backend_properties = backend.properties() if res.target is None and not _skip_target: if backend_version >= 2: res.target = backend.target @@ -189,11 +181,6 @@ def from_backend(cls, backend, _skip_target=False, **pass_manager_options): def __str__(self): newline = "\n" newline_tab = "\n\t" - if self.backend_properties is not None: - backend_props = pprint.pformat(self.backend_properties.to_dict()) - backend_props = backend_props.replace(newline, newline_tab) - else: - backend_props = str(None) return ( "Pass Manager Config:\n" f"\tinitial_layout: {self.initial_layout}\n" @@ -205,7 +192,6 @@ def __str__(self): f"\ttranslation_method: {self.translation_method}\n" f"\tscheduling_method: {self.scheduling_method}\n" f"\tinstruction_durations: {str(self.instruction_durations).replace(newline, newline_tab)}\n" - f"\tbackend_properties: {backend_props}\n" f"\tapproximation_degree: {self.approximation_degree}\n" f"\tseed_transpiler: {self.seed_transpiler}\n" f"\ttiming_constraints: {self.timing_constraints}\n" diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index a120a14c51a4..c2cd08eba90e 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -256,7 +256,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana """Build routing stage PassManager.""" target = pass_manager_config.target coupling_map = pass_manager_config.coupling_map - backend_properties = pass_manager_config.backend_properties if target is None: routing_pass = BasicSwap(coupling_map) else: @@ -282,7 +281,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, check_trivial=True, use_barrier_before_measurement=True, @@ -294,7 +292,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, use_barrier_before_measurement=True, ) @@ -305,7 +302,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, use_barrier_before_measurement=True, ) @@ -323,7 +319,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map_routing = target if coupling_map_routing is None: coupling_map_routing = coupling_map - backend_properties = pass_manager_config.backend_properties vf2_call_limit, vf2_max_trials = common.get_vf2_limits( optimization_level, pass_manager_config.layout_method, @@ -349,7 +344,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, check_trivial=True, use_barrier_before_measurement=True, @@ -361,7 +355,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, use_barrier_before_measurement=True, ) @@ -378,7 +371,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map_routing = target if coupling_map_routing is None: coupling_map_routing = coupling_map - backend_properties = pass_manager_config.backend_properties vf2_call_limit, vf2_max_trials = common.get_vf2_limits( optimization_level, pass_manager_config.layout_method, @@ -401,7 +393,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, check_trivial=True, use_barrier_before_measurement=True, @@ -414,7 +405,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, use_barrier_before_measurement=True, ) @@ -426,7 +416,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, use_barrier_before_measurement=True, ) @@ -444,7 +433,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map_routing = target if coupling_map_routing is None: coupling_map_routing = coupling_map - backend_properties = pass_manager_config.backend_properties vf2_call_limit, vf2_max_trials = common.get_vf2_limits( optimization_level, pass_manager_config.layout_method, @@ -479,7 +467,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, check_trivial=True, use_barrier_before_measurement=True, @@ -499,7 +486,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, use_barrier_before_measurement=True, ) @@ -517,7 +503,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, - backend_properties=backend_properties, seed_transpiler=-1, use_barrier_before_measurement=True, ) @@ -806,7 +791,6 @@ def _swap_mapped(property_set): coupling_map=pass_manager_config.coupling_map, seed=-1, call_limit=int(5e4), # Set call limit to ~100ms with rustworkx 0.10.2 - properties=pass_manager_config.backend_properties, target=pass_manager_config.target, max_trials=2500, # Limits layout scoring to < 600ms on ~400 qubit devices ) @@ -839,7 +823,6 @@ def _swap_mapped(property_set): coupling_map=pass_manager_config.coupling_map, seed=-1, call_limit=int(5e6), # Set call limit to ~10s with rustworkx 0.10.2 - properties=pass_manager_config.backend_properties, target=pass_manager_config.target, max_trials=2500, # Limits layout scoring to < 600ms on ~400 qubit devices ) @@ -874,7 +857,6 @@ def _swap_mapped(property_set): coupling_map=pass_manager_config.coupling_map, seed=-1, call_limit=int(3e7), # Set call limit to ~60s with rustworkx 0.10.2 - properties=pass_manager_config.backend_properties, target=pass_manager_config.target, max_trials=250000, # Limits layout scoring to < 60s on ~400 qubit devices ) @@ -955,7 +937,6 @@ def _choose_layout_condition(property_set): ConditionalController( DenseLayout( coupling_map=pass_manager_config.coupling_map, - backend_prop=pass_manager_config.backend_properties, target=pass_manager_config.target, ), condition=_choose_layout_condition, diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 0f0a6b7ea0a7..2c60a657e422 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -280,7 +280,6 @@ def generate_routing_passmanager( target, coupling_map=None, vf2_call_limit=None, - backend_properties=None, seed_transpiler=-1, check_trivial=False, use_barrier_before_measurement=True, @@ -297,8 +296,6 @@ def generate_routing_passmanager( vf2_call_limit (int): The internal call limit for the vf2 post layout pass. If this is ``None`` or ``0`` the vf2 post layout will not be run. - backend_properties (BackendProperties): Properties of a backend to - synthesize for (e.g. gate fidelities). seed_transpiler (int): Sets random seed for the stochastic parts of the transpiler. This is currently only used for :class:`.VF2PostLayout` and the default value of ``-1`` is strongly recommended (which is no randomization). @@ -354,13 +351,11 @@ def _swap_condition(property_set): routing.append(ConditionalController(routing_pass, condition=_swap_condition)) is_vf2_fully_bounded = vf2_call_limit and vf2_max_trials - if (target is not None or backend_properties is not None) and is_vf2_fully_bounded: + if target is not None and is_vf2_fully_bounded: routing.append( ConditionalController( VF2PostLayout( target, - coupling_map, - backend_properties, seed=seed_transpiler, call_limit=vf2_call_limit, max_trials=vf2_max_trials, diff --git a/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py b/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py index 62f991990d4e..0deb1879a07e 100644 --- a/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py +++ b/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py @@ -27,7 +27,7 @@ from qiskit.transpiler.instruction_durations import InstructionDurations from qiskit.transpiler.layout import Layout from qiskit.transpiler.passmanager_config import PassManagerConfig -from qiskit.transpiler.target import Target, target_to_backend_properties +from qiskit.transpiler.target import Target from qiskit.transpiler.timing_constraints import TimingConstraints from qiskit.utils import deprecate_arg from qiskit.utils.deprecate_pulse import deprecate_pulse_arg @@ -56,14 +56,6 @@ "with defined timing constraints with " "`Target.from_configuration(..., timing_constraints=...)`", ) -@deprecate_arg( - name="backend_properties", - since="1.3", - package_name="Qiskit", - removal_timeline="in Qiskit 2.0", - additional_msg="The `target` parameter should be used instead. You can build a `Target` instance " - "with defined properties with Target.from_configuration(..., backend_properties=...)", -) @deprecate_pulse_arg("inst_map", predicate=lambda inst_map: inst_map is not None) def generate_preset_pass_manager( optimization_level=2, @@ -73,7 +65,6 @@ def generate_preset_pass_manager( inst_map=None, coupling_map=None, instruction_durations=None, - backend_properties=None, timing_constraints=None, initial_layout=None, layout_method=None, @@ -102,7 +93,7 @@ def generate_preset_pass_manager( The target constraints for the pass manager construction can be specified through a :class:`.Target` instance, a :class:`.BackendV1` or :class:`.BackendV2` instance, or via loose constraints - (``basis_gates``, ``inst_map``, ``coupling_map``, ``backend_properties``, ``instruction_durations``, + (``basis_gates``, ``inst_map``, ``coupling_map``, ``instruction_durations``, ``dt`` or ``timing_constraints``). The order of priorities for target constraints works as follows: if a ``target`` input is provided, it will take priority over any ``backend`` input or loose constraints. @@ -123,7 +114,6 @@ def generate_preset_pass_manager( **inst_map** target inst_map inst_map **dt** target dt dt **timing_constraints** target timing_constraints timing_constraints - **backend_properties** target backend_properties backend_properties ============================ ========= ======================== ======================= Args: @@ -140,15 +130,14 @@ def generate_preset_pass_manager( backend (Backend): An optional backend object which can be used as the source of the default values for the ``basis_gates``, ``inst_map``, - ``coupling_map``, ``backend_properties``, ``instruction_durations``, + ``coupling_map``, ``instruction_durations``, ``timing_constraints``, and ``target``. If any of those other arguments are specified in addition to ``backend`` they will take precedence over the value contained in the backend. target (Target): The :class:`~.Target` representing a backend compilation target. The following attributes will be inferred from this argument if they are not set: ``coupling_map``, ``basis_gates``, - ``instruction_durations``, ``inst_map``, ``timing_constraints`` - and ``backend_properties``. + ``instruction_durations``, ``inst_map`` and ``timing_constraints``. basis_gates (list): List of basis gate names to unroll to (e.g: ``['u1', 'u2', 'u3', 'cx']``). inst_map (InstructionScheduleMap): DEPRECATED. Mapping object that maps gates to schedules. @@ -230,9 +219,6 @@ def generate_preset_pass_manager( for the ``scheduling`` stage of the output :class:`~.StagedPassManager`. You can see a list of installed plugins by using :func:`~.list_stage_plugins` with ``"scheduling"`` for the ``stage_name`` argument. - backend_properties (BackendProperties): Properties returned by a - backend, including information on gate errors, readout errors, - qubit coherence times, etc. approximation_degree (float): Heuristic dial used for circuit approximation (1.0=no approximation, 0.0=maximal approximation). seed_transpiler (int): Sets random seed for the stochastic parts of @@ -305,7 +291,6 @@ def generate_preset_pass_manager( and coupling_map is None and dt is None and instruction_durations is None - and backend_properties is None and timing_constraints is None ) # If it's an edge case => do not build target @@ -336,7 +321,6 @@ def generate_preset_pass_manager( elif not _skip_target: # Only parse backend properties when the target isn't skipped to # preserve the former behavior of transpile. - backend_properties = _parse_backend_properties(backend_properties, backend) with warnings.catch_warnings(): # TODO: inst_map will be removed in 2.0 warnings.filterwarnings( @@ -353,7 +337,6 @@ def generate_preset_pass_manager( # If the instruction map has custom gates, do not give as config, the information # will be added to the target with update_from_instruction_schedule_map inst_map=inst_map if inst_map and not inst_map.has_custom_gate() else None, - backend_properties=backend_properties, instruction_durations=instruction_durations, concurrent_measurements=( backend.target.concurrent_measurements if backend is not None else None @@ -380,18 +363,6 @@ def generate_preset_pass_manager( inst_map = target._get_instruction_schedule_map() if timing_constraints is None: timing_constraints = target.timing_constraints() - if backend_properties is None: - with warnings.catch_warnings(): - # TODO this approach (target-to-properties) is going to be removed soon (1.3) in favor - # of backend-to-target approach - # https://github.com/Qiskit/qiskit/pull/12850 - warnings.filterwarnings( - "ignore", - category=DeprecationWarning, - message=r".+qiskit\.transpiler\.target\.target_to_backend_properties.+", - module="qiskit", - ) - backend_properties = target_to_backend_properties(target) # Parse non-target dependent pm options initial_layout = _parse_initial_layout(initial_layout) @@ -404,7 +375,6 @@ def generate_preset_pass_manager( "inst_map": inst_map, "coupling_map": coupling_map, "instruction_durations": instruction_durations, - "backend_properties": backend_properties, "timing_constraints": timing_constraints, "layout_method": layout_method, "routing_method": routing_method, @@ -520,21 +490,6 @@ def _parse_inst_map(inst_map, backend): return inst_map -def _parse_backend_properties(backend_properties, backend): - # try getting backend_props from user, else backend - if backend_properties is None and backend is not None: - with warnings.catch_warnings(): - # filter target_to_backend_properties warning - warnings.filterwarnings( - "ignore", - category=DeprecationWarning, - message=".*``qiskit.transpiler.target.target_to_backend_properties\\(\\)``.*", - module="qiskit", - ) - backend_properties = target_to_backend_properties(backend.target) - return backend_properties - - def _parse_dt(dt, backend): # try getting dt from user, else backend if dt is None and backend is not None: diff --git a/releasenotes/notes/closes_12345-2356fd2d919e3f4a.yaml b/releasenotes/notes/closes_12345-2356fd2d919e3f4a.yaml new file mode 100644 index 000000000000..ded47fb846ec --- /dev/null +++ b/releasenotes/notes/closes_12345-2356fd2d919e3f4a.yaml @@ -0,0 +1,6 @@ +--- +upgrade_circuits: + - | + The method :meth:`.QuantumCircuit.measure_active` has changed the name of the classical register it creates, + as the previous name conflicted with an OpenQASM reserved word. Instead of ``measure``, it is now called ``meas``, + aligning with the register name used by :meth:`~.QuantumCircuit.measure_all`. diff --git a/releasenotes/notes/const-expr-397ff09042942b81.yaml b/releasenotes/notes/const-expr-397ff09042942b81.yaml new file mode 100644 index 000000000000..ebbccd32d066 --- /dev/null +++ b/releasenotes/notes/const-expr-397ff09042942b81.yaml @@ -0,0 +1,21 @@ +--- +features_circuits: + - | + The classical realtime-expressions module :mod:`qiskit.circuit.classical` can now represent + constant types and operations on them. All :class:`~.types.Type` classes now have a bool + :attr:`~.types.Type.const` property which is used to mark const-ness and enforce const + invariants across the types system. + + To create a const value expression use :func:`~.expr.lift`, setting ``try_const=True``:: + + from qiskit.circuit.classical import expr + + expr.lift(5, try_const=True) + # >>> Value(5, Uint(3, const=True)) + + The result type of an operation applied to const types is also const:: + + from qiskit.circuit.classical import expr + + expr.bit_and(expr.lift(5, try_const=True), expr.lift(6, try_const=True)).type.const + # >>> True diff --git a/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml b/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml new file mode 100644 index 000000000000..23c1117e07f3 --- /dev/null +++ b/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml @@ -0,0 +1,19 @@ +--- +features_circuits: + - | + The classical realtime-expressions module :mod:`qiskit.circuit.classical` can now represent + floating point values of unspecified width, using the new type :class:`~.types.Float`. + + The :func:`~.expr.lift` function can be used to create a value expression from a Python + float:: + + from qiskit.circuit.classical import expr + + expr.lift(5.0) + # >>> Value(5.0, Float(const=False)) + expr.lift(5.0, try_const=True) + # >>> Value(5.0, Float(const=True)) + + This type is intended primarily for use in timing-related (duration and stretch) + expressions. It is not compatible with bitwise or logical operations, though it + can be used (dangerously) with these if first explicitly cast to something else. diff --git a/releasenotes/notes/remove-backend-props-transpiler-64aa771784084313.yaml b/releasenotes/notes/remove-backend-props-transpiler-64aa771784084313.yaml new file mode 100644 index 000000000000..c1206d0a1e80 --- /dev/null +++ b/releasenotes/notes/remove-backend-props-transpiler-64aa771784084313.yaml @@ -0,0 +1,25 @@ +--- +upgrade_transpiler: + - | + The following deprecated uses of the ``BackendProperties`` object in the transpilation + pipeline have been removed in Qiskit 2.0: + + * ``backend_properties`` input argument in :func:`.transpile` + * ``backend_properties`` input argument in :class:`.PassManagerConfig` + * ``backend_properties`` input argument in :func:`.generate_preset_pass_manager` + * ``backend_properties`` input argument in :func:`.generate_routing_passmanager` + * ``backend_properties`` input argument in :func:`.generate_translation_passmanager` + * ``backend_properties`` input argument :meth:`.Target.from_configuration` + + The following passes have also been updated to only accept a ``target`` instead of: + + * ``backend_prop`` input argument in :class:`.DenseLayout` + * ``properties`` input argument in :class:`.VF2Layout` + * ``properties`` and ``coupling_map`` input arguments in :class:`.VF2PostLayout` + * ``backend_props`` input argument in :class:`.UnitarySynthesis` + + The ``BackendProperties`` class has been deprecated since Qiskit 1.2, as it was part + of the BackendV1 workflow. Specific instruction properties such as gate errors or + durations can be added to a :class:`.Target` upon construction through the + :meth:`.Target.add_instruction` method, and communicated to the relevant transpiler + passes through the `target` input argument. \ No newline at end of file diff --git a/releasenotes/notes/sk-ignore-unsupported-ops-8d7d5f6fca255ffb.yaml b/releasenotes/notes/sk-ignore-unsupported-ops-8d7d5f6fca255ffb.yaml new file mode 100644 index 000000000000..28e0d85a3d77 --- /dev/null +++ b/releasenotes/notes/sk-ignore-unsupported-ops-8d7d5f6fca255ffb.yaml @@ -0,0 +1,7 @@ +--- +upgrade_transpiler: + - | + The :class:`.SolovayKitaev` transpiler pass no longer raises an exception on circuits + that contain single-qubit operations without a ``to_matrix`` method (such as measures, + barriers, control-flow operations) or parameterized single-qubit operations, + but will leave them unchanged. diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index 6500e5593ac2..7eed3c8cb751 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -14,7 +14,7 @@ import ddt -from qiskit.circuit import Clbit, ClassicalRegister, Instruction +from qiskit.circuit import Clbit, ClassicalRegister, Duration, Instruction from qiskit.circuit.classical import expr, types from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -82,17 +82,40 @@ def test_value_lifts_qiskit_scalars(self): clbit = Clbit() self.assertEqual(expr.lift(clbit), expr.Var(clbit, types.Bool())) + duration = Duration.dt(1000) + self.assertEqual(expr.lift(duration), expr.Value(duration, types.Duration())) + self.assertEqual( + expr.lift(duration, try_const=True), expr.Value(duration, types.Duration()) + ) + self.assertEqual( + expr.lift(duration, types.Stretch()), expr.Value(duration, types.Stretch()) + ) + def test_value_lifts_python_builtins(self): self.assertEqual(expr.lift(True), expr.Value(True, types.Bool())) + self.assertEqual(expr.lift(True, try_const=True), expr.Value(True, types.Bool(const=True))) self.assertEqual(expr.lift(False), expr.Value(False, types.Bool())) + self.assertEqual( + expr.lift(False, try_const=True), expr.Value(False, types.Bool(const=True)) + ) self.assertEqual(expr.lift(7), expr.Value(7, types.Uint(3))) + self.assertEqual(expr.lift(7, try_const=True), expr.Value(7, types.Uint(3, const=True))) + self.assertEqual(expr.lift(7.0), expr.Value(7.0, types.Float())) + self.assertEqual(expr.lift(7.0, try_const=True), expr.Value(7.0, types.Float(const=True))) def test_value_ensures_nonzero_width(self): self.assertEqual(expr.lift(0), expr.Value(0, types.Uint(1))) + self.assertEqual(expr.lift(0, try_const=True), expr.Value(0, types.Uint(1, const=True))) def test_value_type_representation(self): self.assertEqual(expr.lift(5), expr.Value(5, types.Uint((5).bit_length()))) + self.assertEqual( + expr.lift(5, try_const=True), expr.Value(5, types.Uint((5).bit_length(), const=True)) + ) self.assertEqual(expr.lift(5, types.Uint(8)), expr.Value(5, types.Uint(8))) + self.assertEqual( + expr.lift(5, types.Uint(8, const=True)), expr.Value(5, types.Uint(8, const=True)) + ) cr = ClassicalRegister(3, "c") self.assertEqual(expr.lift(cr, types.Uint(8)), expr.Var(cr, types.Uint(8))) @@ -100,6 +123,8 @@ def test_value_type_representation(self): def test_value_does_not_allow_downcast(self): with self.assertRaisesRegex(TypeError, "the explicit type .* is not suitable"): expr.lift(0xFF, types.Uint(2)) + with self.assertRaisesRegex(TypeError, "the explicit type .* is not suitable"): + expr.lift(1.1, types.Uint(2)) def test_value_rejects_bad_values(self): with self.assertRaisesRegex(TypeError, "failed to infer a type"): @@ -115,6 +140,12 @@ def test_cast_adds_explicit_nodes(self): expr.cast(base, types.Uint(8)), expr.Cast(base, types.Uint(8), implicit=False) ) + def test_cast_adds_node_when_shedding_const(self): + base = expr.Value(5, types.Uint(8, const=True)) + self.assertEqual( + expr.cast(base, types.Uint(8)), expr.Cast(base, types.Uint(8), implicit=False) + ) + def test_cast_allows_lossy_downcasting(self): """An explicit 'cast' call should allow lossy casts to be performed.""" base = expr.Value(5, types.Uint(16)) @@ -124,6 +155,9 @@ def test_cast_allows_lossy_downcasting(self): self.assertEqual( expr.cast(base, types.Bool()), expr.Cast(base, types.Bool(), implicit=False) ) + self.assertEqual( + expr.cast(base, types.Float()), expr.Cast(base, types.Float(), implicit=False) + ) @ddt.data( (expr.bit_not, ClassicalRegister(3)), @@ -148,6 +182,23 @@ def test_bit_not_explicit(self): expr.bit_not(clbit), expr.Unary(expr.Unary.Op.BIT_NOT, expr.Var(clbit, types.Bool()), types.Bool()), ) + self.assertEqual( + expr.bit_not(expr.Value(3, types.Uint(2, const=True))), + expr.Unary( + expr.Unary.Op.BIT_NOT, + expr.Value(3, types.Uint(2, const=True)), + types.Uint(2, const=True), + ), + ) + + @ddt.data(expr.bit_not) + def test_urnary_bitwise_forbidden(self, function): + with self.assertRaisesRegex(TypeError, "cannot apply"): + function(7.0) + with self.assertRaisesRegex(TypeError, "cannot apply"): + function(Duration.dt(1000)) + with self.assertRaisesRegex(TypeError, "cannot apply"): + function(expr.Var.new("a", types.Stretch())) def test_logic_not_explicit(self): cr = ClassicalRegister(3) @@ -164,6 +215,25 @@ def test_logic_not_explicit(self): expr.logic_not(clbit), expr.Unary(expr.Unary.Op.LOGIC_NOT, expr.Var(clbit, types.Bool()), types.Bool()), ) + self.assertEqual( + expr.logic_not(expr.Value(3, types.Uint(2, const=True))), + expr.Unary( + expr.Unary.Op.LOGIC_NOT, + expr.Cast( + expr.Value(3, types.Uint(2, const=True)), types.Bool(const=True), implicit=True + ), + types.Bool(const=True), + ), + ) + + @ddt.data(expr.logic_not) + def test_urnary_logical_forbidden(self, function): + with self.assertRaisesRegex(TypeError, "cannot apply"): + function(7.0) + with self.assertRaisesRegex(TypeError, "cannot apply"): + function(Duration.dt(1000)) + with self.assertRaisesRegex(TypeError, "cannot apply"): + function(expr.Var.new("a", types.Stretch())) @ddt.data( (expr.bit_and, ClassicalRegister(3), ClassicalRegister(3)), @@ -177,6 +247,29 @@ def test_logic_not_explicit(self): (expr.less_equal, ClassicalRegister(3), 5), (expr.greater, 4, ClassicalRegister(3)), (expr.greater_equal, ClassicalRegister(3), 5), + (expr.add, ClassicalRegister(3), 6), + (expr.sub, ClassicalRegister(3), 5), + (expr.mul, 4, ClassicalRegister(3)), + (expr.div, ClassicalRegister(3), 5), + (expr.equal, 8.0, 255.0), + (expr.not_equal, 8.0, 255.0), + (expr.less, 3.0, 6.0), + (expr.less_equal, 3.0, 5.0), + (expr.greater, 4.0, 3.0), + (expr.greater_equal, 3.0, 5.0), + (expr.add, 3.0, 6.0), + (expr.sub, 3.0, 5.0), + (expr.mul, 4.0, 3.0), + (expr.div, 3.0, 5.0), + (expr.equal, Duration.dt(1000), Duration.dt(1000)), + (expr.not_equal, Duration.dt(1000), Duration.dt(1000)), + (expr.less, Duration.dt(1000), Duration.dt(1000)), + (expr.less_equal, Duration.dt(1000), Duration.dt(1000)), + (expr.greater, Duration.dt(1000), Duration.dt(1000)), + (expr.greater_equal, Duration.dt(1000), Duration.dt(1000)), + (expr.add, Duration.dt(1000), Duration.dt(1000)), + (expr.sub, Duration.dt(1000), Duration.dt(1000)), + (expr.div, Duration.dt(1000), Duration.dt(1000)), ) @ddt.unpack def test_binary_functions_lift_scalars(self, function, left, right): @@ -184,6 +277,51 @@ def test_binary_functions_lift_scalars(self, function, left, right): self.assertEqual(function(left, right), function(left, expr.lift(right))) self.assertEqual(function(left, right), function(expr.lift(left), expr.lift(right))) + @ddt.data( + (expr.bit_and, 6, 7), + (expr.bit_or, 5, 6), + (expr.bit_xor, 255, 254), + (expr.equal, 254, 255), + (expr.not_equal, 255, 255), + (expr.less, 5, 4), + (expr.less_equal, 3, 3), + (expr.greater, 254, 255), + (expr.greater_equal, 4, 5), + (expr.add, 5, 4), + (expr.sub, 3, 3), + (expr.mul, 254, 255), + (expr.div, 4, 5), + (expr.equal, 254.0, 255.0), + (expr.not_equal, 255.0, 255.0), + (expr.less, 5.0, 4.0), + (expr.less_equal, 3.0, 3.0), + (expr.greater, 254.0, 255.0), + (expr.greater_equal, 4.0, 5.0), + (expr.add, 5.0, 4.0), + (expr.sub, 3.0, 3.0), + (expr.mul, 254.0, 255.0), + (expr.div, 4.0, 5.0), + (expr.mul, 254.0, Duration.dt(1000)), + (expr.mul, Duration.dt(1000), 5.0), + (expr.div, Duration.dt(1000), 5.0), + (expr.mul, 254.0, expr.Var.new("a", types.Stretch())), + (expr.mul, expr.Var.new("a", types.Stretch()), 5.0), + (expr.div, expr.Var.new("a", types.Stretch()), 5.0), + ) + @ddt.unpack + def test_binary_functions_lift_scalars_const(self, function, left, right): + """If one operand is an expr with a const type, the other scalar should be lifted as const. + Note that logical operators (e.g. logic_and, logic_or) are excluded since these lift operands + independently.""" + self.assertEqual( + function(expr.lift(left, try_const=True), right), + function(expr.lift(left, try_const=True), expr.lift(right, try_const=True)), + ) + self.assertEqual( + function(left, expr.lift(right, try_const=True)), + function(expr.lift(left, try_const=True), expr.lift(right, try_const=True)), + ) + @ddt.data( (expr.bit_and, expr.Binary.Op.BIT_AND), (expr.bit_or, expr.Binary.Op.BIT_OR), @@ -204,7 +342,15 @@ def test_binary_bitwise_explicit(self, function, opcode): opcode, expr.Value(255, types.Uint(8)), expr.Var(cr, types.Uint(8)), types.Uint(8) ), ) - + self.assertEqual( + function(expr.lift(255, try_const=True), cr), + expr.Binary( + opcode, + expr.Cast(expr.Value(255, types.Uint(8, const=True)), types.Uint(8), implicit=True), + expr.Var(cr, types.Uint(8)), + types.Uint(8), + ), + ) clbit = Clbit() self.assertEqual( function(True, clbit), @@ -224,6 +370,24 @@ def test_binary_bitwise_explicit(self, function, opcode): types.Bool(), ), ) + self.assertEqual( + function(255, 255), + expr.Binary( + opcode, + expr.Value(255, types.Uint(8)), + expr.Value(255, types.Uint(8)), + types.Uint(8), + ), + ) + self.assertEqual( + function(expr.lift(255, try_const=True), 255), + expr.Binary( + opcode, + expr.Value(255, types.Uint(8, const=True)), + expr.Value(255, types.Uint(8, const=True)), + types.Uint(8, const=True), + ), + ) @ddt.data( (expr.bit_and, expr.Binary.Op.BIT_AND), @@ -271,6 +435,20 @@ def test_binary_bitwise_uint_inference(self, function, opcode): def test_binary_bitwise_forbidden(self, function): with self.assertRaisesRegex(TypeError, "invalid types"): function(ClassicalRegister(3, "c"), Clbit()) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3.0, 3.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3, 3.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3.0, 3) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Duration.dt(1000), Duration.dt(1000)) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(expr.Var.new("a", types.Stretch()), Duration.dt(1000)) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Duration.dt(1000), expr.Var.new("a", types.Stretch())) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(expr.Var.new("a", types.Stretch()), expr.Var.new("b", types.Stretch())) # Unlike most other functions, the bitwise functions should error if the two bit-like types # aren't of the same width, except for the special inference for integer literals. with self.assertRaisesRegex(TypeError, "binary bitwise operations .* same width"): @@ -305,6 +483,16 @@ def test_binary_logical_explicit(self, function, opcode): ), ) + self.assertEqual( + function(cr, expr.lift(3, try_const=True)), + expr.Binary( + opcode, + expr.Cast(expr.Var(cr, types.Uint(cr.size)), types.Bool(), implicit=True), + expr.Cast(expr.Value(3, types.Uint(2, const=True)), types.Bool(), implicit=True), + types.Bool(), + ), + ) + self.assertEqual( function(False, clbit), expr.Binary( @@ -315,6 +503,34 @@ def test_binary_logical_explicit(self, function, opcode): ), ) + # Logical operations lift their operands independently. + self.assertEqual( + function(expr.lift(False, try_const=True), 1), + expr.Binary( + opcode, + expr.Cast(expr.Value(False, types.Bool(const=True)), types.Bool(), implicit=True), + expr.Cast(expr.Value(1, types.Uint(1)), types.Bool(), implicit=True), + types.Bool(), + ), + ) + + @ddt.data(expr.logic_and, expr.logic_or) + def test_binary_logic_forbidden(self, function): + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3.0, 3.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3, 3.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3.0, 3) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Duration.dt(1000), Duration.dt(1000)) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(expr.Var.new("a", types.Stretch()), Duration.dt(1000)) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Duration.dt(1000), expr.Var.new("a", types.Stretch())) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(expr.Var.new("a", types.Stretch()), expr.Var.new("b", types.Stretch())) + @ddt.data( (expr.equal, expr.Binary.Op.EQUAL), (expr.not_equal, expr.Binary.Op.NOT_EQUAL), @@ -341,6 +557,17 @@ def test_binary_equal_explicit(self, function, opcode): ), ) + self.assertEqual( + function(expr.lift(7, try_const=True), cr), + expr.Binary( + opcode, + # Explicit cast required to get from Uint(3) to Uint(8) + expr.Cast(expr.Value(7, types.Uint(3, const=True)), types.Uint(8), implicit=False), + expr.Var(cr, types.Uint(8)), + types.Bool(), + ), + ) + self.assertEqual( function(clbit, True), expr.Binary( @@ -351,6 +578,36 @@ def test_binary_equal_explicit(self, function, opcode): ), ) + self.assertEqual( + function(expr.lift(False, try_const=True), True), + expr.Binary( + opcode, + expr.Value(False, types.Bool(const=True)), + expr.Value(True, types.Bool(const=True)), + types.Bool(const=True), + ), + ) + + self.assertEqual( + function(expr.lift(7.0, try_const=True), 7.0), + expr.Binary( + opcode, + expr.Value(7.0, types.Float(const=True)), + expr.Value(7.0, types.Float(const=True)), + types.Bool(const=True), + ), + ) + + self.assertEqual( + function(expr.lift(Duration.ms(1000)), Duration.s(1)), + expr.Binary( + opcode, + expr.Value(Duration.ms(1000), types.Duration()), + expr.Value(Duration.s(1), types.Duration()), + types.Bool(const=True), + ), + ) + @ddt.data(expr.equal, expr.not_equal) def test_binary_equal_forbidden(self, function): with self.assertRaisesRegex(TypeError, "invalid types"): @@ -359,6 +616,21 @@ def test_binary_equal_forbidden(self, function): function(ClassicalRegister(3, "c"), False) with self.assertRaisesRegex(TypeError, "invalid types"): function(5, True) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(True, 5.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(5, 5.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(ClassicalRegister(3, "c"), 5.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + # No order between a smaller non-const int and larger const. + function(expr.lift(0xFF, types.Uint(8)), expr.lift(0xFFFF, types.Uint(16, const=True))) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(expr.Var.new("a", types.Stretch()), Duration.dt(1000)) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Duration.dt(1000), expr.Var.new("a", types.Stretch())) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(expr.Var.new("a", types.Stretch()), expr.Var.new("b", types.Stretch())) @ddt.data( (expr.less, expr.Binary.Op.LESS), @@ -387,6 +659,54 @@ def test_binary_relation_explicit(self, function, opcode): ), ) + self.assertEqual( + function(expr.lift(12, try_const=True), cr), + expr.Binary( + opcode, + # Explicit cast required to get from Uint(4) to Uint(8) + expr.Cast(expr.Value(12, types.Uint(4, const=True)), types.Uint(8), implicit=False), + expr.Var(cr, types.Uint(8)), + types.Bool(), + ), + ) + + self.assertEqual( + function(expr.lift(12, types.Uint(8, const=True)), expr.lift(12, try_const=True)), + expr.Binary( + opcode, + expr.Value(12, types.Uint(8, const=True)), + expr.Cast( + expr.Value(12, types.Uint(4, const=True)), + types.Uint(8, const=True), + implicit=False, + ), + types.Bool(const=True), + ), + ) + + self.assertEqual( + function(expr.lift(12.0, types.Float(const=True)), expr.lift(12.0, try_const=True)), + expr.Binary( + opcode, + expr.Value(12.0, types.Float(const=True)), + expr.Value(12.0, types.Float(const=True)), + types.Bool(const=True), + ), + ) + + self.assertEqual( + function( + expr.lift(Duration.ms(1000), types.Duration()), + expr.lift(Duration.s(1), try_const=True), + ), + expr.Binary( + opcode, + expr.Value(Duration.ms(1000), types.Duration()), + expr.Value(Duration.s(1), types.Duration()), + types.Bool(const=True), + ), + ) + @ddt.data(expr.less, expr.less_equal, expr.greater, expr.greater_equal) def test_binary_relation_forbidden(self, function): with self.assertRaisesRegex(TypeError, "invalid types"): @@ -395,6 +715,21 @@ def test_binary_relation_forbidden(self, function): function(ClassicalRegister(3, "c"), False) with self.assertRaisesRegex(TypeError, "invalid types"): function(Clbit(), Clbit()) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(True, 5.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(5, 5.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(ClassicalRegister(3, "c"), 5.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + # No order between a smaller non-const int and larger const. + function(expr.lift(0xFF, types.Uint(8)), expr.lift(0xFFFF, types.Uint(16, const=True))) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(expr.Var.new("a", types.Stretch()), Duration.dt(1000)) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Duration.dt(1000), expr.Var.new("a", types.Stretch())) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(expr.Var.new("a", types.Stretch()), expr.Var.new("b", types.Stretch())) def test_index_explicit(self): cr = ClassicalRegister(4, "c") @@ -408,12 +743,44 @@ def test_index_explicit(self): expr.index(a, cr), expr.Index(a, expr.Var(cr, types.Uint(4)), types.Bool()), ) + # The index arg gets lifted to match the const-ness of the target. + self.assertEqual( + expr.index(expr.lift(0xFF, try_const=True), 2), + expr.Index( + expr.Value(0xFF, types.Uint(8, const=True)), + expr.Value(2, types.Uint(2, const=True)), + types.Bool(const=True), + ), + ) + # ...but not the other way around. + self.assertEqual( + expr.index(expr.lift(0xFF), expr.lift(2, try_const=True)), + expr.Index( + expr.Value(0xFF, types.Uint(8)), + expr.Value(2, types.Uint(2, const=True)), + types.Bool(), + ), + ) def test_index_forbidden(self): with self.assertRaisesRegex(TypeError, "invalid types"): expr.index(Clbit(), 3) with self.assertRaisesRegex(TypeError, "invalid types"): expr.index(ClassicalRegister(3, "a"), False) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.index(ClassicalRegister(3, "a"), 1.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.index(0xFFFF, 1.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.index(Duration.dt(1000), 1) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.index(Duration.dt(1000), Duration.dt(1000)) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.index(expr.Var.new("a", types.Stretch()), Duration.dt(1000)) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.index(Duration.dt(1000), expr.Var.new("a", types.Stretch())) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.index(expr.Var.new("a", types.Stretch()), expr.Var.new("b", types.Stretch())) @ddt.data( (expr.shift_left, expr.Binary.Op.SHIFT_LEFT), @@ -430,6 +797,15 @@ def test_shift_explicit(self, function, opcode): opcode, expr.Var(cr, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8) ), ) + self.assertEqual( + function(cr, expr.lift(5, try_const=True)), + expr.Binary( + opcode, + expr.Var(cr, types.Uint(8)), + expr.Value(5, types.Uint(3, const=True)), + types.Uint(8), + ), + ) self.assertEqual( function(a, cr), expr.Binary(opcode, a, expr.Var(cr, types.Uint(8)), types.Uint(4)), @@ -440,6 +816,38 @@ def test_shift_explicit(self, function, opcode): opcode, expr.Value(3, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8) ), ) + self.assertEqual( + function(3, 5, types.Uint(8, const=True)), + expr.Binary( + opcode, + expr.Value(3, types.Uint(8, const=True)), + expr.Value(5, types.Uint(3, const=True)), + types.Uint(8, const=True), + ), + ) + self.assertEqual( + function(expr.lift(3, try_const=True), 5, types.Uint(8, const=True)), + expr.Binary( + opcode, + expr.Cast( + expr.Value(3, types.Uint(2, const=True)), + types.Uint(8, const=True), + implicit=False, + ), + expr.Value(5, types.Uint(3, const=True)), + types.Uint(8, const=True), + ), + ) + self.assertEqual( + function(expr.lift(3, try_const=True), 5, types.Uint(8)), + expr.Binary( + opcode, + expr.Cast(expr.Value(3, types.Uint(2, const=True)), types.Uint(8), implicit=False), + # Lifts as non-const because target type types.Uint(8) is non-const. + expr.Value(5, types.Uint(3)), + types.Uint(8), + ), + ) @ddt.data(expr.shift_left, expr.shift_right) def test_shift_forbidden(self, function): @@ -449,3 +857,392 @@ def test_shift_forbidden(self, function): function(ClassicalRegister(3, "c"), False) with self.assertRaisesRegex(TypeError, "invalid types"): function(Clbit(), Clbit()) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(0xFFFF, 2.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(255.0, 1) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Duration.dt(1000), 1) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Duration.dt(1000), Duration.dt(1000)) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(expr.Var.new("a", types.Stretch()), Duration.dt(1000)) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Duration.dt(1000), expr.Var.new("a", types.Stretch())) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(expr.Var.new("a", types.Stretch()), expr.Var.new("b", types.Stretch())) + + @ddt.data( + (expr.add, expr.Binary.Op.ADD), + (expr.sub, expr.Binary.Op.SUB), + ) + @ddt.unpack + def test_binary_sum_explicit(self, function, opcode): + cr = ClassicalRegister(8, "c") + a = expr.Var.new("a", types.Stretch()) + b = expr.Var.new("b", types.Stretch()) + + self.assertEqual( + function(cr, 200), + expr.Binary( + opcode, expr.Var(cr, types.Uint(8)), expr.Value(200, types.Uint(8)), types.Uint(8) + ), + ) + + self.assertEqual( + function(12, cr), + expr.Binary( + opcode, + expr.Value(12, types.Uint(8)), + expr.Var(cr, types.Uint(8)), + types.Uint(8), + ), + ) + + self.assertEqual( + function(expr.lift(12, try_const=True), cr), + expr.Binary( + opcode, + # Explicit cast required to get from Uint(4) to Uint(8) + expr.Cast(expr.Value(12, types.Uint(4, const=True)), types.Uint(8), implicit=False), + expr.Var(cr, types.Uint(8)), + types.Uint(8), + ), + ) + + self.assertEqual( + function(expr.lift(12, types.Uint(8, const=True)), expr.lift(12, try_const=True)), + expr.Binary( + opcode, + expr.Value(12, types.Uint(8, const=True)), + expr.Cast( + expr.Value(12, types.Uint(4, const=True)), + types.Uint(8, const=True), + implicit=False, + ), + types.Uint(8, const=True), + ), + ) + + self.assertEqual( + function(expr.lift(12.0, types.Float(const=True)), expr.lift(12.0, try_const=True)), + expr.Binary( + opcode, + expr.Value(12.0, types.Float(const=True)), + expr.Value(12.0, types.Float(const=True)), + types.Float(const=True), + ), + ) + + self.assertEqual( + function( + expr.lift(Duration.ms(1000), types.Duration()), + expr.lift(Duration.s(1), try_const=True), + ), + expr.Binary( + opcode, + expr.Value(Duration.ms(1000), types.Duration()), + expr.Value(Duration.s(1), types.Duration()), + types.Duration(), + ), + ) + + self.assertEqual( + function(a, Duration.s(1)), + expr.Binary( + opcode, + a, + expr.Cast( + expr.Value(Duration.s(1), types.Duration()), types.Stretch(), implicit=True + ), + types.Stretch(), + ), + ) + + self.assertEqual( + function(Duration.s(1), a), + expr.Binary( + opcode, + expr.Cast( + expr.Value(Duration.s(1), types.Duration()), types.Stretch(), implicit=True + ), + a, + types.Stretch(), + ), + ) + + self.assertEqual( + function(a, b), + expr.Binary(opcode, a, b, types.Stretch()), + ) + + @ddt.data(expr.add, expr.sub) + def test_binary_sum_forbidden(self, function): + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Clbit(), ClassicalRegister(3, "c")) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(ClassicalRegister(3, "c"), False) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Clbit(), Clbit()) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(0xFFFF, 2.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(255.0, 1) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Duration.dt(1000), 1) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Duration.dt(1000), 1.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Duration.dt(1000), expr.lift(1.0)) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(expr.Var.new("a", types.Stretch()), 1) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(expr.Var.new("a", types.Stretch()), 1.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(expr.Var.new("a", types.Stretch()), expr.lift(1.0)) + + def test_mul_explicit(self): + cr = ClassicalRegister(8, "c") + a = expr.Var.new("a", types.Stretch()) + + self.assertEqual( + expr.mul(cr, 200), + expr.Binary( + expr.Binary.Op.MUL, + expr.Var(cr, types.Uint(8)), + expr.Value(200, types.Uint(8)), + types.Uint(8), + ), + ) + + self.assertEqual( + expr.mul(12, cr), + expr.Binary( + expr.Binary.Op.MUL, + expr.Value(12, types.Uint(8)), + expr.Var(cr, types.Uint(8)), + types.Uint(8), + ), + ) + + self.assertEqual( + expr.mul(expr.lift(12, try_const=True), cr), + expr.Binary( + expr.Binary.Op.MUL, + # Explicit cast required to get from Uint(4) to Uint(8) + expr.Cast(expr.Value(12, types.Uint(4, const=True)), types.Uint(8), implicit=False), + expr.Var(cr, types.Uint(8)), + types.Uint(8), + ), + ) + + self.assertEqual( + expr.mul(expr.lift(12, types.Uint(8, const=True)), expr.lift(12, try_const=True)), + expr.Binary( + expr.Binary.Op.MUL, + expr.Value(12, types.Uint(8, const=True)), + expr.Cast( + expr.Value(12, types.Uint(4, const=True)), + types.Uint(8, const=True), + implicit=False, + ), + types.Uint(8, const=True), + ), + ) + + self.assertEqual( + expr.mul(expr.lift(12.0, types.Float(const=True)), expr.lift(12.0, try_const=True)), + expr.Binary( + expr.Binary.Op.MUL, + expr.Value(12.0, types.Float(const=True)), + expr.Value(12.0, types.Float(const=True)), + types.Float(const=True), + ), + ) + + self.assertEqual( + expr.mul(Duration.ms(1000), 2.0), + expr.Binary( + expr.Binary.Op.MUL, + expr.Value(Duration.ms(1000), types.Duration()), + expr.Value(2.0, types.Float(const=True)), + types.Duration(), + ), + ) + + self.assertEqual( + expr.mul(2.0, Duration.ms(1000)), + expr.Binary( + expr.Binary.Op.MUL, + expr.Value(2.0, types.Float(const=True)), + expr.Value(Duration.ms(1000), types.Duration()), + types.Duration(), + ), + ) + + self.assertEqual( + expr.mul(a, 12.0), + expr.Binary( + expr.Binary.Op.MUL, + a, + expr.Value(12.0, types.Float(const=True)), + types.Stretch(), + ), + ) + + self.assertEqual( + expr.mul(12.0, a), + expr.Binary( + expr.Binary.Op.MUL, + expr.Value(12.0, types.Float(const=True)), + a, + types.Stretch(), + ), + ) + + def test_mul_forbidden(self): + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.mul(Clbit(), ClassicalRegister(3, "c")) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.mul(ClassicalRegister(3, "c"), False) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.mul(Clbit(), Clbit()) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.mul(0xFFFF, 2.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.mul(255.0, 1) + with self.assertRaisesRegex(TypeError, "cannot multiply two timing operands"): + expr.mul(Duration.dt(1000), Duration.dt(1000)) + with self.assertRaisesRegex(TypeError, "cannot multiply two timing operands"): + expr.mul(Duration.dt(1000), expr.Var.new("a", types.Stretch())) + with self.assertRaisesRegex(TypeError, "cannot multiply two timing operands"): + expr.mul(expr.Var.new("a", types.Stretch()), Duration.dt(1000)) + with self.assertRaisesRegex(TypeError, "cannot multiply two timing operands"): + expr.mul(expr.Var.new("a", types.Stretch()), expr.Var.new("b", types.Stretch())) + + # Multiply timing expressions by non-const floats: + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.mul(Duration.dt(1000), expr.lift(1.0, try_const=False)) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.mul(expr.lift(1.0, try_const=False), Duration.dt(1000)) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.mul(expr.Var.new("a", types.Stretch()), expr.lift(1.0, try_const=False)) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.mul(expr.lift(1.0, try_const=False), expr.Var.new("a", types.Stretch())) + + def test_div_explicit(self): + cr = ClassicalRegister(8, "c") + a = expr.Var.new("a", types.Stretch()) + + self.assertEqual( + expr.div(cr, 200), + expr.Binary( + expr.Binary.Op.DIV, + expr.Var(cr, types.Uint(8)), + expr.Value(200, types.Uint(8)), + types.Uint(8), + ), + ) + + self.assertEqual( + expr.div(12, cr), + expr.Binary( + expr.Binary.Op.DIV, + expr.Value(12, types.Uint(8)), + expr.Var(cr, types.Uint(8)), + types.Uint(8), + ), + ) + + self.assertEqual( + expr.div(expr.lift(12, try_const=True), cr), + expr.Binary( + expr.Binary.Op.DIV, + # Explicit cast required to get from Uint(4) to Uint(8) + expr.Cast(expr.Value(12, types.Uint(4, const=True)), types.Uint(8), implicit=False), + expr.Var(cr, types.Uint(8)), + types.Uint(8), + ), + ) + + self.assertEqual( + expr.div(expr.lift(12, types.Uint(8, const=True)), expr.lift(12, try_const=True)), + expr.Binary( + expr.Binary.Op.DIV, + expr.Value(12, types.Uint(8, const=True)), + expr.Cast( + expr.Value(12, types.Uint(4, const=True)), + types.Uint(8, const=True), + implicit=False, + ), + types.Uint(8, const=True), + ), + ) + + self.assertEqual( + expr.div(expr.lift(12.0, types.Float(const=True)), expr.lift(12.0, try_const=True)), + expr.Binary( + expr.Binary.Op.DIV, + expr.Value(12.0, types.Float(const=True)), + expr.Value(12.0, types.Float(const=True)), + types.Float(const=True), + ), + ) + + self.assertEqual( + expr.div(Duration.ms(1000), 2.0), + expr.Binary( + expr.Binary.Op.DIV, + expr.Value(Duration.ms(1000), types.Duration()), + expr.Value(2.0, types.Float(const=True)), + types.Duration(), + ), + ) + + self.assertEqual( + expr.div(a, 12.0), + expr.Binary( + expr.Binary.Op.DIV, + a, + expr.Value(12.0, types.Float(const=True)), + types.Stretch(), + ), + ) + + self.assertEqual( + expr.div(Duration.ms(1000), Duration.ms(1000)), + expr.Binary( + expr.Binary.Op.DIV, + expr.Value(Duration.ms(1000), types.Duration()), + expr.Value(Duration.ms(1000), types.Duration()), + types.Float(const=True), + ), + ) + + def test_div_forbidden(self): + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.div(Clbit(), ClassicalRegister(3, "c")) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.div(ClassicalRegister(3, "c"), False) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.div(Clbit(), Clbit()) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.div(0xFFFF, 2.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.div(255.0, 1) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.div(255.0, Duration.dt(1000)) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.div(255.0, expr.Var.new("a", types.Stretch())) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.div(Duration.dt(1000), expr.Var.new("a", types.Stretch())) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.div(expr.Var.new("a", types.Stretch()), Duration.dt(1000)) + with self.assertRaisesRegex(TypeError, "cannot divide two stretch operands"): + expr.div(expr.Var.new("a", types.Stretch()), expr.Var.new("b", types.Stretch())) + + # Divide timing expressions by non-const floats: + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.div(Duration.dt(1000), expr.lift(1.0, try_const=False)) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.div(expr.Var.new("a", types.Stretch()), expr.lift(1.0, try_const=False)) diff --git a/test/python/circuit/classical/test_expr_properties.py b/test/python/circuit/classical/test_expr_properties.py index 625db22cc12d..60a4b1f9080e 100644 --- a/test/python/circuit/classical/test_expr_properties.py +++ b/test/python/circuit/classical/test_expr_properties.py @@ -25,14 +25,6 @@ @ddt.ddt class TestExprProperties(QiskitTestCase): - def test_bool_type_is_singleton(self): - """The `Bool` type is meant (and used) as a Python singleton object for efficiency. It must - always be referentially equal to all other references to it.""" - self.assertIs(types.Bool(), types.Bool()) - self.assertIs(types.Bool(), copy.copy(types.Bool())) - self.assertIs(types.Bool(), copy.deepcopy(types.Bool())) - self.assertIs(types.Bool(), pickle.loads(pickle.dumps(types.Bool()))) - @ddt.data(types.Bool(), types.Uint(8)) def test_types_can_be_cloned(self, obj): """Test that various ways of cloning a `Type` object are valid and produce equal output.""" diff --git a/test/python/circuit/classical/test_types_ordering.py b/test/python/circuit/classical/test_types_ordering.py index 16c3791f70fa..addd475987fe 100644 --- a/test/python/circuit/classical/test_types_ordering.py +++ b/test/python/circuit/classical/test_types_ordering.py @@ -19,52 +19,455 @@ class TestTypesOrdering(QiskitTestCase): def test_order(self): self.assertIs(types.order(types.Uint(8), types.Uint(16)), types.Ordering.LESS) + self.assertIs(types.order(types.Uint(8, const=True), types.Uint(16)), types.Ordering.LESS) + self.assertIs(types.order(types.Uint(8), types.Uint(16, const=True)), types.Ordering.NONE) + self.assertIs( + types.order(types.Uint(8, const=True), types.Uint(16, const=True)), types.Ordering.LESS + ) + self.assertIs(types.order(types.Uint(16), types.Uint(8)), types.Ordering.GREATER) + self.assertIs( + types.order(types.Uint(16), types.Uint(8, const=True)), types.Ordering.GREATER + ) + self.assertIs(types.order(types.Uint(16, const=True), types.Uint(8)), types.Ordering.NONE) + self.assertIs( + types.order(types.Uint(16, const=True), types.Uint(8, const=True)), + types.Ordering.GREATER, + ) + self.assertIs(types.order(types.Uint(8), types.Uint(8)), types.Ordering.EQUAL) + self.assertIs(types.order(types.Uint(8, const=True), types.Uint(8)), types.Ordering.LESS) + self.assertIs(types.order(types.Uint(8), types.Uint(8, const=True)), types.Ordering.GREATER) + self.assertIs( + types.order(types.Uint(8, const=True), types.Uint(8, const=True)), types.Ordering.EQUAL + ) self.assertIs(types.order(types.Bool(), types.Bool()), types.Ordering.EQUAL) + self.assertIs(types.order(types.Bool(const=True), types.Bool()), types.Ordering.LESS) + self.assertIs(types.order(types.Bool(), types.Bool(const=True)), types.Ordering.GREATER) + self.assertIs( + types.order(types.Bool(const=True), types.Bool(const=True)), types.Ordering.EQUAL + ) + + self.assertIs(types.order(types.Float(), types.Float()), types.Ordering.EQUAL) + self.assertIs(types.order(types.Float(const=True), types.Float()), types.Ordering.LESS) + self.assertIs(types.order(types.Float(), types.Float(const=True)), types.Ordering.GREATER) + self.assertIs( + types.order(types.Float(const=True), types.Float(const=True)), types.Ordering.EQUAL + ) + + self.assertIs(types.order(types.Duration(), types.Duration()), types.Ordering.EQUAL) + self.assertIs(types.order(types.Duration(), types.Stretch()), types.Ordering.LESS) + self.assertIs(types.order(types.Stretch(), types.Duration()), types.Ordering.GREATER) + self.assertIs(types.order(types.Stretch(), types.Stretch()), types.Ordering.EQUAL) self.assertIs(types.order(types.Bool(), types.Uint(8)), types.Ordering.NONE) + self.assertIs(types.order(types.Bool(), types.Float()), types.Ordering.NONE) + self.assertIs(types.order(types.Bool(), types.Duration()), types.Ordering.NONE) + self.assertIs(types.order(types.Bool(), types.Stretch()), types.Ordering.NONE) self.assertIs(types.order(types.Uint(8), types.Bool()), types.Ordering.NONE) + self.assertIs(types.order(types.Uint(8), types.Float()), types.Ordering.NONE) + self.assertIs(types.order(types.Uint(8), types.Duration()), types.Ordering.NONE) + self.assertIs(types.order(types.Uint(8), types.Stretch()), types.Ordering.NONE) + self.assertIs(types.order(types.Float(), types.Uint(8)), types.Ordering.NONE) + self.assertIs(types.order(types.Float(), types.Bool()), types.Ordering.NONE) + self.assertIs(types.order(types.Float(), types.Duration()), types.Ordering.NONE) + self.assertIs(types.order(types.Float(), types.Stretch()), types.Ordering.NONE) + self.assertIs(types.order(types.Duration(), types.Bool()), types.Ordering.NONE) + self.assertIs(types.order(types.Duration(), types.Uint(8)), types.Ordering.NONE) + self.assertIs(types.order(types.Duration(), types.Float()), types.Ordering.NONE) + self.assertIs(types.order(types.Stretch(), types.Bool()), types.Ordering.NONE) + self.assertIs(types.order(types.Stretch(), types.Uint(8)), types.Ordering.NONE) + self.assertIs(types.order(types.Stretch(), types.Float()), types.Ordering.NONE) def test_is_subtype(self): self.assertTrue(types.is_subtype(types.Uint(8), types.Uint(16))) + self.assertTrue(types.is_subtype(types.Uint(8, const=True), types.Uint(16))) self.assertFalse(types.is_subtype(types.Uint(16), types.Uint(8))) + self.assertFalse(types.is_subtype(types.Uint(16, const=True), types.Uint(8))) + self.assertFalse(types.is_subtype(types.Uint(16), types.Uint(8, const=True))) self.assertTrue(types.is_subtype(types.Uint(8), types.Uint(8))) self.assertFalse(types.is_subtype(types.Uint(8), types.Uint(8), strict=True)) + self.assertTrue(types.is_subtype(types.Uint(8, const=True), types.Uint(8), strict=True)) self.assertTrue(types.is_subtype(types.Bool(), types.Bool())) self.assertFalse(types.is_subtype(types.Bool(), types.Bool(), strict=True)) + self.assertTrue(types.is_subtype(types.Bool(const=True), types.Bool(), strict=True)) + + self.assertTrue(types.is_subtype(types.Float(), types.Float())) + self.assertFalse(types.is_subtype(types.Float(), types.Float(), strict=True)) + self.assertTrue(types.is_subtype(types.Float(const=True), types.Float(), strict=True)) + + self.assertTrue(types.is_subtype(types.Duration(), types.Duration())) + self.assertFalse(types.is_subtype(types.Duration(), types.Duration(), strict=True)) + self.assertTrue(types.is_subtype(types.Duration(), types.Stretch())) + self.assertTrue(types.is_subtype(types.Duration(), types.Stretch(), strict=True)) + + self.assertTrue(types.is_subtype(types.Stretch(), types.Stretch())) + self.assertFalse(types.is_subtype(types.Stretch(), types.Stretch(), strict=True)) self.assertFalse(types.is_subtype(types.Bool(), types.Uint(8))) + self.assertFalse(types.is_subtype(types.Bool(), types.Float())) + self.assertFalse(types.is_subtype(types.Bool(), types.Duration())) + self.assertFalse(types.is_subtype(types.Bool(), types.Stretch())) self.assertFalse(types.is_subtype(types.Uint(8), types.Bool())) + self.assertFalse(types.is_subtype(types.Uint(8), types.Float())) + self.assertFalse(types.is_subtype(types.Uint(8), types.Duration())) + self.assertFalse(types.is_subtype(types.Uint(8), types.Stretch())) + self.assertFalse(types.is_subtype(types.Float(), types.Uint(8))) + self.assertFalse(types.is_subtype(types.Float(), types.Bool())) + self.assertFalse(types.is_subtype(types.Float(), types.Duration())) + self.assertFalse(types.is_subtype(types.Float(), types.Stretch())) + self.assertFalse(types.is_subtype(types.Stretch(), types.Bool())) + self.assertFalse(types.is_subtype(types.Stretch(), types.Uint(8))) + self.assertFalse(types.is_subtype(types.Stretch(), types.Float())) + self.assertFalse(types.is_subtype(types.Stretch(), types.Duration())) + self.assertFalse(types.is_subtype(types.Duration(), types.Bool())) + self.assertFalse(types.is_subtype(types.Duration(), types.Uint(8))) + self.assertFalse(types.is_subtype(types.Duration(), types.Float())) def test_is_supertype(self): self.assertFalse(types.is_supertype(types.Uint(8), types.Uint(16))) + self.assertFalse(types.is_supertype(types.Uint(8, const=True), types.Uint(16))) self.assertTrue(types.is_supertype(types.Uint(16), types.Uint(8))) + self.assertTrue(types.is_supertype(types.Uint(16), types.Uint(8, const=True))) + self.assertTrue(types.is_supertype(types.Uint(16, const=True), types.Uint(8, const=True))) + self.assertFalse(types.is_supertype(types.Uint(16, const=True), types.Uint(8))) self.assertTrue(types.is_supertype(types.Uint(8), types.Uint(8))) self.assertFalse(types.is_supertype(types.Uint(8), types.Uint(8), strict=True)) + self.assertTrue(types.is_supertype(types.Uint(8), types.Uint(8, const=True), strict=True)) self.assertTrue(types.is_supertype(types.Bool(), types.Bool())) self.assertFalse(types.is_supertype(types.Bool(), types.Bool(), strict=True)) + self.assertTrue(types.is_supertype(types.Bool(), types.Bool(const=True), strict=True)) + + self.assertTrue(types.is_supertype(types.Float(), types.Float())) + self.assertFalse(types.is_supertype(types.Float(), types.Float(), strict=True)) + self.assertTrue(types.is_supertype(types.Float(), types.Float(const=True), strict=True)) + + self.assertTrue(types.is_supertype(types.Duration(), types.Duration())) + self.assertFalse(types.is_supertype(types.Duration(), types.Duration(), strict=True)) + + self.assertTrue(types.is_supertype(types.Stretch(), types.Stretch())) + self.assertFalse(types.is_supertype(types.Stretch(), types.Stretch(), strict=True)) + self.assertTrue(types.is_supertype(types.Stretch(), types.Duration())) + self.assertTrue(types.is_supertype(types.Stretch(), types.Duration(), strict=True)) self.assertFalse(types.is_supertype(types.Bool(), types.Uint(8))) + self.assertFalse(types.is_supertype(types.Bool(), types.Float())) + self.assertFalse(types.is_supertype(types.Bool(), types.Duration())) + self.assertFalse(types.is_supertype(types.Bool(), types.Stretch())) self.assertFalse(types.is_supertype(types.Uint(8), types.Bool())) + self.assertFalse(types.is_supertype(types.Uint(8), types.Float())) + self.assertFalse(types.is_supertype(types.Uint(8), types.Duration())) + self.assertFalse(types.is_supertype(types.Uint(8), types.Stretch())) + self.assertFalse(types.is_supertype(types.Float(), types.Uint(8))) + self.assertFalse(types.is_supertype(types.Float(), types.Bool())) + self.assertFalse(types.is_supertype(types.Float(), types.Duration())) + self.assertFalse(types.is_supertype(types.Float(), types.Stretch())) + self.assertFalse(types.is_supertype(types.Stretch(), types.Bool())) + self.assertFalse(types.is_supertype(types.Stretch(), types.Uint(8))) + self.assertFalse(types.is_supertype(types.Stretch(), types.Float())) + self.assertFalse(types.is_supertype(types.Duration(), types.Bool())) + self.assertFalse(types.is_supertype(types.Duration(), types.Uint(8))) + self.assertFalse(types.is_supertype(types.Duration(), types.Float())) + self.assertFalse(types.is_supertype(types.Duration(), types.Stretch())) def test_greater(self): self.assertEqual(types.greater(types.Uint(16), types.Uint(8)), types.Uint(16)) + self.assertEqual(types.greater(types.Uint(16), types.Uint(8, const=True)), types.Uint(16)) self.assertEqual(types.greater(types.Uint(8), types.Uint(16)), types.Uint(16)) + self.assertEqual(types.greater(types.Uint(8, const=True), types.Uint(16)), types.Uint(16)) self.assertEqual(types.greater(types.Uint(8), types.Uint(8)), types.Uint(8)) + self.assertEqual(types.greater(types.Uint(8), types.Uint(8, const=True)), types.Uint(8)) + self.assertEqual(types.greater(types.Uint(8, const=True), types.Uint(8)), types.Uint(8)) + self.assertEqual( + types.greater(types.Uint(8, const=True), types.Uint(8, const=True)), + types.Uint(8, const=True), + ) self.assertEqual(types.greater(types.Bool(), types.Bool()), types.Bool()) + self.assertEqual(types.greater(types.Bool(const=True), types.Bool()), types.Bool()) + self.assertEqual(types.greater(types.Bool(), types.Bool(const=True)), types.Bool()) + self.assertEqual( + types.greater(types.Bool(const=True), types.Bool(const=True)), types.Bool(const=True) + ) + self.assertEqual(types.greater(types.Float(), types.Float()), types.Float()) + self.assertEqual(types.greater(types.Float(const=True), types.Float()), types.Float()) + self.assertEqual(types.greater(types.Float(), types.Float(const=True)), types.Float()) + self.assertEqual( + types.greater(types.Float(const=True), types.Float(const=True)), types.Float(const=True) + ) + self.assertEqual(types.greater(types.Duration(), types.Duration()), types.Duration()) + self.assertEqual(types.greater(types.Stretch(), types.Duration()), types.Stretch()) + self.assertEqual(types.greater(types.Duration(), types.Stretch()), types.Stretch()) with self.assertRaisesRegex(TypeError, "no ordering"): types.greater(types.Bool(), types.Uint(8)) + with self.assertRaisesRegex(TypeError, "no ordering"): + types.greater(types.Bool(), types.Float()) + with self.assertRaisesRegex(TypeError, "no ordering"): + types.greater(types.Bool(), types.Duration()) + with self.assertRaisesRegex(TypeError, "no ordering"): + types.greater(types.Bool(), types.Stretch()) + with self.assertRaisesRegex(TypeError, "no ordering"): + types.greater(types.Uint(8), types.Stretch()) + with self.assertRaisesRegex(TypeError, "no ordering"): + types.greater(types.Uint(8), types.Duration()) + with self.assertRaisesRegex(TypeError, "no ordering"): + types.greater(types.Uint(16, const=True), types.Uint(8)) + with self.assertRaisesRegex(TypeError, "no ordering"): + types.greater(types.Uint(8), types.Uint(16, const=True)) + with self.assertRaisesRegex(TypeError, "no ordering"): + types.greater(types.Float(), types.Duration()) + with self.assertRaisesRegex(TypeError, "no ordering"): + types.greater(types.Float(), types.Stretch()) class TestTypesCastKind(QiskitTestCase): def test_basic_examples(self): """This is used extensively throughout the expression construction functions, but since it is public API, it should have some direct unit tests as well.""" + # Bool -> Bool self.assertIs(types.cast_kind(types.Bool(), types.Bool()), types.CastKind.EQUAL) + self.assertIs( + types.cast_kind(types.Bool(const=True), types.Bool(const=True)), types.CastKind.EQUAL + ) + self.assertIs( + types.cast_kind(types.Bool(const=True), types.Bool()), types.CastKind.IMPLICIT + ) + self.assertIs(types.cast_kind(types.Bool(), types.Bool(const=True)), types.CastKind.NONE) + + # Float -> Float + self.assertIs(types.cast_kind(types.Float(), types.Float()), types.CastKind.EQUAL) + self.assertIs( + types.cast_kind(types.Float(const=True), types.Float(const=True)), types.CastKind.EQUAL + ) + self.assertIs( + types.cast_kind(types.Float(const=True), types.Float()), types.CastKind.IMPLICIT + ) + self.assertIs(types.cast_kind(types.Float(), types.Float(const=True)), types.CastKind.NONE) + + # Uint -> Bool self.assertIs(types.cast_kind(types.Uint(8), types.Bool()), types.CastKind.IMPLICIT) + self.assertIs( + types.cast_kind(types.Uint(8, const=True), types.Bool(const=True)), + types.CastKind.IMPLICIT, + ) + self.assertIs( + types.cast_kind(types.Uint(8, const=True), types.Bool()), types.CastKind.IMPLICIT + ) + self.assertIs(types.cast_kind(types.Uint(8), types.Bool(const=True)), types.CastKind.NONE) + + # Float -> Bool + self.assertIs(types.cast_kind(types.Float(), types.Bool()), types.CastKind.DANGEROUS) + self.assertIs( + types.cast_kind(types.Float(const=True), types.Bool(const=True)), + types.CastKind.DANGEROUS, + ) + self.assertIs( + types.cast_kind(types.Float(const=True), types.Bool()), types.CastKind.DANGEROUS + ) + self.assertIs(types.cast_kind(types.Float(), types.Bool(const=True)), types.CastKind.NONE) + + # Bool -> Uint self.assertIs(types.cast_kind(types.Bool(), types.Uint(8)), types.CastKind.LOSSLESS) + self.assertIs( + types.cast_kind(types.Bool(const=True), types.Uint(8, const=True)), + types.CastKind.LOSSLESS, + ) + self.assertIs( + types.cast_kind(types.Bool(const=True), types.Uint(8)), types.CastKind.LOSSLESS + ) + self.assertIs(types.cast_kind(types.Bool(), types.Uint(8, const=True)), types.CastKind.NONE) + + # Uint(16) -> Uint(8) self.assertIs(types.cast_kind(types.Uint(16), types.Uint(8)), types.CastKind.DANGEROUS) + self.assertIs( + types.cast_kind(types.Uint(16, const=True), types.Uint(8, const=True)), + types.CastKind.DANGEROUS, + ) + self.assertIs( + types.cast_kind(types.Uint(16, const=True), types.Uint(8)), types.CastKind.DANGEROUS + ) + self.assertIs( + types.cast_kind(types.Uint(16), types.Uint(8, const=True)), types.CastKind.NONE + ) + + # Uint widening + self.assertIs(types.cast_kind(types.Uint(8), types.Uint(16)), types.CastKind.LOSSLESS) + self.assertIs( + types.cast_kind(types.Uint(8, const=True), types.Uint(16, const=True)), + types.CastKind.LOSSLESS, + ) + self.assertIs( + types.cast_kind(types.Uint(8, const=True), types.Uint(16)), + types.CastKind.LOSSLESS, + ) + self.assertIs( + types.cast_kind(types.Uint(8), types.Uint(16, const=True)), types.CastKind.NONE + ) + + # Uint -> Float + self.assertIs(types.cast_kind(types.Uint(16), types.Float()), types.CastKind.DANGEROUS) + self.assertIs( + types.cast_kind(types.Uint(16, const=True), types.Float(const=True)), + types.CastKind.DANGEROUS, + ) + self.assertIs( + types.cast_kind(types.Uint(16, const=True), types.Float()), types.CastKind.DANGEROUS + ) + self.assertIs(types.cast_kind(types.Uint(16), types.Float(const=True)), types.CastKind.NONE) + + # Float -> Uint(8) + self.assertIs(types.cast_kind(types.Float(), types.Uint(8)), types.CastKind.DANGEROUS) + self.assertIs( + types.cast_kind(types.Float(const=True), types.Uint(8, const=True)), + types.CastKind.DANGEROUS, + ) + self.assertIs( + types.cast_kind(types.Float(const=True), types.Uint(8)), + types.CastKind.DANGEROUS, + ) + self.assertIs( + types.cast_kind(types.Float(), types.Uint(8, const=True)), + types.CastKind.NONE, + ) + + # Float -> Uint(16) + self.assertIs(types.cast_kind(types.Float(), types.Uint(16)), types.CastKind.DANGEROUS) + self.assertIs( + types.cast_kind(types.Float(const=True), types.Uint(16, const=True)), + types.CastKind.DANGEROUS, + ) + self.assertIs( + types.cast_kind(types.Float(const=True), types.Uint(16)), + types.CastKind.DANGEROUS, + ) + self.assertIs( + types.cast_kind(types.Float(), types.Uint(16, const=True)), + types.CastKind.NONE, + ) + + # Bool -> Float + self.assertIs(types.cast_kind(types.Bool(), types.Float()), types.CastKind.LOSSLESS) + self.assertIs( + types.cast_kind(types.Bool(const=True), types.Float(const=True)), + types.CastKind.LOSSLESS, + ) + self.assertIs( + types.cast_kind(types.Bool(const=True), types.Float()), types.CastKind.LOSSLESS + ) + self.assertIs(types.cast_kind(types.Bool(), types.Float(const=True)), types.CastKind.NONE) + + # Uint -> Uint with const qualifiers + self.assertIs( + types.cast_kind(types.Uint(16), types.Uint(16, const=True)), types.CastKind.NONE + ) + self.assertIs( + types.cast_kind(types.Uint(16, const=True), types.Uint(16)), types.CastKind.IMPLICIT + ) + self.assertIs( + types.cast_kind(types.Uint(8), types.Uint(8, const=True)), types.CastKind.NONE + ) + self.assertIs( + types.cast_kind(types.Uint(8, const=True), types.Uint(8)), types.CastKind.IMPLICIT + ) + + # Bool -> Uint with const qualifiers + self.assertIs(types.cast_kind(types.Bool(), types.Uint(8, const=True)), types.CastKind.NONE) + self.assertIs( + types.cast_kind(types.Bool(const=True), types.Uint(8)), types.CastKind.LOSSLESS + ) + self.assertIs( + types.cast_kind(types.Bool(const=True), types.Uint(8, const=True)), + types.CastKind.LOSSLESS, + ) + + # Bool -> Float with const qualifiers + self.assertIs(types.cast_kind(types.Bool(), types.Float(const=True)), types.CastKind.NONE) + self.assertIs( + types.cast_kind(types.Bool(const=True), types.Float()), types.CastKind.LOSSLESS + ) + self.assertIs( + types.cast_kind(types.Bool(const=True), types.Float(const=True)), + types.CastKind.LOSSLESS, + ) + + # Duration -> Duration + self.assertIs(types.cast_kind(types.Duration(), types.Duration()), types.CastKind.EQUAL) + + # Duration -> Stretch (allowed, implicit) + self.assertIs(types.cast_kind(types.Duration(), types.Stretch()), types.CastKind.IMPLICIT) + + # Stretch -> Stretch + self.assertIs(types.cast_kind(types.Stretch(), types.Stretch()), types.CastKind.EQUAL) + + # Stretch -> Duration (not allowed) + self.assertIs(types.cast_kind(types.Stretch(), types.Duration()), types.CastKind.NONE) + + # Duration -> Other types (not allowed, including const variants) + self.assertIs(types.cast_kind(types.Duration(), types.Bool()), types.CastKind.NONE) + self.assertIs( + types.cast_kind(types.Duration(), types.Bool(const=True)), types.CastKind.NONE + ) + self.assertIs(types.cast_kind(types.Duration(), types.Uint(8)), types.CastKind.NONE) + self.assertIs( + types.cast_kind(types.Duration(), types.Uint(8, const=True)), types.CastKind.NONE + ) + self.assertIs(types.cast_kind(types.Duration(), types.Uint(16)), types.CastKind.NONE) + self.assertIs( + types.cast_kind(types.Duration(), types.Uint(16, const=True)), types.CastKind.NONE + ) + self.assertIs(types.cast_kind(types.Duration(), types.Float()), types.CastKind.NONE) + self.assertIs( + types.cast_kind(types.Duration(), types.Float(const=True)), types.CastKind.NONE + ) + + # Stretch -> Other types (not allowed, including const variants) + self.assertIs(types.cast_kind(types.Stretch(), types.Bool()), types.CastKind.NONE) + self.assertIs(types.cast_kind(types.Stretch(), types.Bool(const=True)), types.CastKind.NONE) + self.assertIs(types.cast_kind(types.Stretch(), types.Uint(8)), types.CastKind.NONE) + self.assertIs( + types.cast_kind(types.Stretch(), types.Uint(8, const=True)), types.CastKind.NONE + ) + self.assertIs(types.cast_kind(types.Stretch(), types.Uint(16)), types.CastKind.NONE) + self.assertIs( + types.cast_kind(types.Stretch(), types.Uint(16, const=True)), types.CastKind.NONE + ) + self.assertIs(types.cast_kind(types.Stretch(), types.Float()), types.CastKind.NONE) + self.assertIs( + types.cast_kind(types.Stretch(), types.Float(const=True)), types.CastKind.NONE + ) + + # Other types -> Duration (not allowed, including const variants) + self.assertIs(types.cast_kind(types.Bool(), types.Duration()), types.CastKind.NONE) + self.assertIs( + types.cast_kind(types.Bool(const=True), types.Duration()), types.CastKind.NONE + ) + self.assertIs(types.cast_kind(types.Uint(8), types.Duration()), types.CastKind.NONE) + self.assertIs( + types.cast_kind(types.Uint(8, const=True), types.Duration()), types.CastKind.NONE + ) + self.assertIs(types.cast_kind(types.Uint(16), types.Duration()), types.CastKind.NONE) + self.assertIs( + types.cast_kind(types.Uint(16, const=True), types.Duration()), types.CastKind.NONE + ) + self.assertIs(types.cast_kind(types.Float(), types.Duration()), types.CastKind.NONE) + self.assertIs( + types.cast_kind(types.Float(const=True), types.Duration()), types.CastKind.NONE + ) + + # Other types -> Stretch (not allowed, including const variants) + self.assertIs(types.cast_kind(types.Bool(), types.Stretch()), types.CastKind.NONE) + self.assertIs(types.cast_kind(types.Bool(const=True), types.Stretch()), types.CastKind.NONE) + self.assertIs(types.cast_kind(types.Uint(8), types.Stretch()), types.CastKind.NONE) + self.assertIs( + types.cast_kind(types.Uint(8, const=True), types.Stretch()), types.CastKind.NONE + ) + self.assertIs(types.cast_kind(types.Uint(16), types.Stretch()), types.CastKind.NONE) + self.assertIs( + types.cast_kind(types.Uint(16, const=True), types.Stretch()), types.CastKind.NONE + ) + self.assertIs(types.cast_kind(types.Float(), types.Stretch()), types.CastKind.NONE) + self.assertIs( + types.cast_kind(types.Float(const=True), types.Stretch()), types.CastKind.NONE + ) diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 962dd22ac79b..6ef1e1f25d7b 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -1985,6 +1985,37 @@ def test_pre_v12_rejects_index(self, version): ): dump(qc, fptr, version=version) + @ddt.idata(range(QPY_COMPATIBILITY_VERSION, 14)) + def test_pre_v14_rejects_qiskit_2_0_expr(self, version): + """Test that dumping to older QPY versions rejects const-typed expressions.""" + qc = QuantumCircuit() + with qc.if_test( + expr.not_equal( + expr.equal(expr.lift(1, types.Uint(1, const=True)), 1), + expr.lift(False, types.Bool(const=True)), + ) + ): + pass + + with ( + io.BytesIO() as fptr, + self.assertRaisesRegex( + UnsupportedFeatureForVersion, "version 14 is required.*Qiskit 2" + ), + ): + dump(qc, fptr, version=version) + + qc = QuantumCircuit() + with qc.if_test(expr.less(1.0, 2.0)): + pass + with ( + io.BytesIO() as fptr, + self.assertRaisesRegex( + UnsupportedFeatureForVersion, "version 14 is required.*Qiskit 2" + ), + ): + dump(qc, fptr, version=version) + class TestSymengineLoadFromQPY(QiskitTestCase): """Test use of symengine in qpy set of methods.""" diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index bd35c8d920a5..5a31a841da2b 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -417,17 +417,21 @@ def test_copy_empty_like_circuit(self): copied = qc.copy_empty_like("copy") self.assertEqual(copied.name, "copy") + # pylint: disable=invalid-name def test_copy_variables(self): """Test that a full copy of circuits including variables copies them across.""" a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Uint(8)) c = expr.Var.new("c", types.Bool()) d = expr.Var.new("d", types.Uint(8)) + e = expr.Var.new("e", types.Stretch()) + f = expr.Var.new("f", types.Stretch()) qc = QuantumCircuit(inputs=[a], declarations=[(c, expr.lift(False))]) + qc.add_stretch(e) copied = qc.copy() self.assertEqual({a}, set(copied.iter_input_vars())) - self.assertEqual({c}, set(copied.iter_declared_vars())) + self.assertEqual({c, e}, set(copied.iter_declared_vars())) self.assertEqual( [instruction.operation for instruction in qc], [instruction.operation for instruction in copied.data], @@ -437,9 +441,9 @@ def test_copy_variables(self): copied.add_input(b) copied.add_var(d, 0xFF) self.assertEqual({a, b}, set(copied.iter_input_vars())) - self.assertEqual({c, d}, set(copied.iter_declared_vars())) + self.assertEqual({c, e, d}, set(copied.iter_declared_vars())) self.assertEqual({a}, set(qc.iter_input_vars())) - self.assertEqual({c}, set(qc.iter_declared_vars())) + self.assertEqual({c, e}, set(qc.iter_declared_vars())) qc = QuantumCircuit(captures=[b], declarations=[(a, expr.lift(False)), (c, a)]) copied = qc.copy() @@ -452,9 +456,13 @@ def test_copy_variables(self): # Check that the original circuit is not mutated. copied.add_capture(d) + copied.add_stretch(f) self.assertEqual({b, d}, set(copied.iter_captured_vars())) + self.assertEqual({a, c, f}, set(copied.iter_declared_vars())) self.assertEqual({b}, set(qc.iter_captured_vars())) + self.assertEqual({a, c}, set(qc.iter_declared_vars())) + # pylint: disable=invalid-name def test_copy_empty_variables(self): """Test that an empty copy of circuits including variables copies them across, but does not initialise them.""" @@ -472,8 +480,9 @@ def test_copy_empty_variables(self): # Check that the original circuit is not mutated. copied.add_input(b) copied.add_var(d, 0xFF) + e = copied.add_stretch("e") self.assertEqual({a, b}, set(copied.iter_input_vars())) - self.assertEqual({c, d}, set(copied.iter_declared_vars())) + self.assertEqual({c, d, e}, set(copied.iter_declared_vars())) self.assertEqual({a}, set(qc.iter_input_vars())) self.assertEqual({c}, set(qc.iter_declared_vars())) @@ -485,9 +494,11 @@ def test_copy_empty_variables(self): # Check that the original circuit is not mutated. copied.add_capture(d) - self.assertEqual({b, d}, set(copied.iter_captured_vars())) + copied.add_capture(e) + self.assertEqual({b, d, e}, set(copied.iter_captured_vars())) self.assertEqual({b}, set(qc.iter_captured_vars())) + # pylint: disable=invalid-name def test_copy_empty_variables_alike(self): """Test that an empty copy of circuits including variables copies them across, but does not initialise them. This is the same as the default, just spelled explicitly.""" @@ -495,6 +506,7 @@ def test_copy_empty_variables_alike(self): b = expr.Var.new("b", types.Uint(8)) c = expr.Var.new("c", types.Bool()) d = expr.Var.new("d", types.Uint(8)) + e = expr.Var.new("e", types.Stretch()) qc = QuantumCircuit(inputs=[a], declarations=[(c, expr.lift(False))]) copied = qc.copy_empty_like(vars_mode="alike") @@ -505,8 +517,9 @@ def test_copy_empty_variables_alike(self): # Check that the original circuit is not mutated. copied.add_input(b) copied.add_var(d, 0xFF) + copied.add_stretch(e) self.assertEqual({a, b}, set(copied.iter_input_vars())) - self.assertEqual({c, d}, set(copied.iter_declared_vars())) + self.assertEqual({c, d, e}, set(copied.iter_declared_vars())) self.assertEqual({a}, set(qc.iter_input_vars())) self.assertEqual({c}, set(qc.iter_declared_vars())) @@ -518,9 +531,11 @@ def test_copy_empty_variables_alike(self): # Check that the original circuit is not mutated. copied.add_capture(d) - self.assertEqual({b, d}, set(copied.iter_captured_vars())) + copied.add_capture(e) + self.assertEqual({b, d, e}, set(copied.iter_captured_vars())) self.assertEqual({b}, set(qc.iter_captured_vars())) + # pylint: disable=invalid-name def test_copy_empty_variables_to_captures(self): """``vars_mode="captures"`` should convert all variables to captures.""" a = expr.Var.new("a", types.Bool()) @@ -529,15 +544,16 @@ def test_copy_empty_variables_to_captures(self): d = expr.Var.new("d", types.Uint(8)) qc = QuantumCircuit(inputs=[a, b], declarations=[(c, expr.lift(False))]) + e = qc.add_stretch("e") copied = qc.copy_empty_like(vars_mode="captures") - self.assertEqual({a, b, c}, set(copied.iter_captured_vars())) - self.assertEqual({a, b, c}, set(copied.iter_vars())) + self.assertEqual({a, b, c, e}, set(copied.iter_captured_vars())) + self.assertEqual({a, b, c, e}, set(copied.iter_vars())) self.assertEqual([], list(copied.data)) - qc = QuantumCircuit(captures=[c, d]) + qc = QuantumCircuit(captures=[c, d, e]) copied = qc.copy_empty_like(vars_mode="captures") - self.assertEqual({c, d}, set(copied.iter_captured_vars())) - self.assertEqual({c, d}, set(copied.iter_vars())) + self.assertEqual({c, d, e}, set(copied.iter_captured_vars())) + self.assertEqual({c, d, e}, set(copied.iter_vars())) self.assertEqual([], list(copied.data)) def test_copy_empty_variables_drop(self): @@ -545,8 +561,9 @@ def test_copy_empty_variables_drop(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Uint(8)) c = expr.Var.new("c", types.Bool()) + d = expr.Var.new("s", types.Stretch()) - qc = QuantumCircuit(inputs=[a, b], declarations=[(c, expr.lift(False))]) + qc = QuantumCircuit(captures=[a, b, d], declarations=[(c, expr.lift(False))]) copied = qc.copy_empty_like(vars_mode="drop") self.assertEqual(set(), set(copied.iter_vars())) self.assertEqual([], list(copied.data)) @@ -636,7 +653,7 @@ def test_measure_active(self): the amount of non-idle qubits to store the measured values. """ qr = QuantumRegister(4) - cr = ClassicalRegister(2, "measure") + cr = ClassicalRegister(2, "meas") circuit = QuantumCircuit(qr) circuit.h(qr[0]) @@ -658,7 +675,7 @@ def test_measure_active_copy(self): the amount of non-idle qubits to store the measured values. """ qr = QuantumRegister(4) - cr = ClassicalRegister(2, "measure") + cr = ClassicalRegister(2, "meas") circuit = QuantumCircuit(qr) circuit.h(qr[0]) diff --git a/test/python/circuit/test_circuit_vars.py b/test/python/circuit/test_circuit_vars.py index f6916dcb72db..7e3dd14904c5 100644 --- a/test/python/circuit/test_circuit_vars.py +++ b/test/python/circuit/test_circuit_vars.py @@ -23,7 +23,10 @@ class TestCircuitVars(QiskitTestCase): tested in the suites of the specific methods.""" def test_initialise_inputs(self): - vars_ = [expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Uint(16))] + vars_ = [ + expr.Var.new("a", types.Bool()), + expr.Var.new("b", types.Uint(16)), + ] qc = QuantumCircuit(inputs=vars_) self.assertEqual(set(vars_), set(qc.iter_vars())) self.assertEqual(qc.num_vars, len(vars_)) @@ -32,7 +35,11 @@ def test_initialise_inputs(self): self.assertEqual(qc.num_declared_vars, 0) def test_initialise_captures(self): - vars_ = [expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Uint(16))] + vars_ = [ + expr.Var.new("a", types.Bool()), + expr.Var.new("b", types.Uint(16)), + expr.Var.new("c", types.Stretch()), + ] qc = QuantumCircuit(captures=vars_) self.assertEqual(set(vars_), set(qc.iter_vars())) self.assertEqual(qc.num_vars, len(vars_)) @@ -56,7 +63,10 @@ def test_initialise_declarations_iterable(self): (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) for instruction in qc.data ] - self.assertEqual(operations, [("store", lvalue, rvalue) for lvalue, rvalue in vars_]) + self.assertEqual( + operations, + [("store", lvalue, rvalue) for lvalue, rvalue in vars_], + ) def test_initialise_declarations_mapping(self): # Dictionary iteration order is guaranteed to be insertion order. @@ -92,6 +102,12 @@ def test_initialise_declarations_dependencies(self): ] self.assertEqual(operations, [("store", lvalue, rvalue) for lvalue, rvalue in vars_]) + def test_initialise_declarations_rejects_const_vars(self): + a = expr.Var.new("a", types.Uint(16, const=True)) + a_init = expr.lift(12, try_const=True) + with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): + QuantumCircuit(declarations=[(a, a_init)]) + def test_initialise_inputs_declarations(self): a = expr.Var.new("a", types.Uint(16)) b = expr.Var.new("b", types.Uint(16)) @@ -111,6 +127,11 @@ def test_initialise_inputs_declarations(self): ] self.assertEqual(operations, [("store", b, b_init)]) + def test_initialise_inputs_declarations_rejects_const_vars(self): + a = expr.Var.new("a", types.Uint(16, const=True)) + with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): + QuantumCircuit(inputs=[a]) + def test_initialise_captures_declarations(self): a = expr.Var.new("a", types.Uint(16)) b = expr.Var.new("b", types.Uint(16)) @@ -137,6 +158,12 @@ def test_add_uninitialized_var(self): self.assertEqual({a}, set(qc.iter_vars())) self.assertEqual([], list(qc.data)) + def test_add_uninitialized_var_rejects_const_lvalue(self): + a = expr.Var.new("a", types.Bool(const=True)) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): + qc.add_uninitialized_var(a) + def test_add_var_returns_good_var(self): qc = QuantumCircuit() a = qc.add_var("a", expr.lift(True)) @@ -147,6 +174,12 @@ def test_add_var_returns_good_var(self): self.assertEqual(b.name, "b") self.assertEqual(b.type, types.Uint(8)) + def test_add_stretch_returns_good_var(self): + qc = QuantumCircuit() + a = qc.add_stretch("a") + self.assertEqual(a.name, "a") + self.assertEqual(a.type, types.Stretch()) + def test_add_var_returns_input(self): """Test that the `Var` returned by `add_var` is the same as the input if `Var`.""" a = expr.Var.new("a", types.Bool()) @@ -154,6 +187,40 @@ def test_add_var_returns_input(self): a_other = qc.add_var(a, expr.lift(True)) self.assertIs(a, a_other) + def test_add_stretch_returns_input(self): + a = expr.Var.new("a", types.Stretch()) + qc = QuantumCircuit() + a_other = qc.add_stretch(a) + self.assertIs(a, a_other) + + def test_add_var_rejects_const_lvalue(self): + a = expr.Var.new("a", types.Bool(const=True)) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): + qc.add_var(a, True) + + def test_add_var_implicitly_casts_const_rvalue(self): + a = expr.Var.new("a", types.Bool()) + qc = QuantumCircuit() + qc.add_var(a, expr.lift(True, try_const=True)) + self.assertEqual(qc.num_vars, 1) + operations = [ + (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) + for instruction in qc.data + ] + self.assertEqual( + operations, + [ + ( + "store", + a, + expr.Cast( + expr.Value(True, types.Bool(const=True)), types.Bool(), implicit=True + ), + ) + ], + ) + def test_add_input_returns_good_var(self): qc = QuantumCircuit() a = qc.add_input("a", types.Bool()) @@ -171,6 +238,14 @@ def test_add_input_returns_input(self): a_other = qc.add_input(a) self.assertIs(a, a_other) + def test_add_input_rejects_const_var(self): + a = expr.Var.new("a", types.Bool(const=True)) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): + qc.add_input(a) + with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): + qc.add_input("a", types.Bool(const=True)) + def test_cannot_have_both_inputs_and_captures(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Bool()) @@ -214,11 +289,13 @@ def test_initialise_inputs_equal_to_add_input(self): def test_initialise_captures_equal_to_add_capture(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Uint(16)) + c = expr.Var.new("c", types.Stretch()) - qc_init = QuantumCircuit(captures=[a, b]) + qc_init = QuantumCircuit(captures=[a, b, c]) qc_manual = QuantumCircuit() qc_manual.add_capture(a) qc_manual.add_capture(b) + qc_manual.add_capture(c) self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) def test_initialise_declarations_equal_to_add_var(self): @@ -379,7 +456,13 @@ def test_get_var_success(self): self.assertIs(qc.get_var("a"), a) self.assertIs(qc.get_var("b"), b) - qc = QuantumCircuit(declarations={a: expr.lift(True), b: expr.Value(0xFF, types.Uint(8))}) + qc = QuantumCircuit( + inputs=[], + declarations={ + a: expr.lift(True), + b: expr.Value(0xFF, types.Uint(8)), + }, + ) self.assertIs(qc.get_var("a"), a) self.assertIs(qc.get_var("b"), b) @@ -399,9 +482,11 @@ def test_get_var_default(self): missing = "default" a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Stretch()) qc.add_input(a) - self.assertIs(qc.get_var("b", missing), missing) - self.assertIs(qc.get_var("b", a), a) + self.assertIs(qc.get_var("c", missing), missing) + self.assertIs(qc.get_var("c", a), a) + self.assertIs(qc.get_var("c", b), b) def test_has_var(self): a = expr.Var.new("a", types.Bool()) @@ -416,3 +501,5 @@ def test_has_var(self): # When giving an `Var`, the match must be exact, not just the name. self.assertFalse(QuantumCircuit(inputs=[a]).has_var(expr.Var.new("a", types.Uint(8)))) self.assertFalse(QuantumCircuit(inputs=[a]).has_var(expr.Var.new("a", types.Bool()))) + self.assertFalse(QuantumCircuit(inputs=[a]).has_var(expr.Var.new("a", types.Float()))) + self.assertFalse(QuantumCircuit(inputs=[a]).has_var(expr.Var.new("a", types.Stretch()))) diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py index ecb98681bbd2..5f4d7de6702f 100644 --- a/test/python/circuit/test_store.py +++ b/test/python/circuit/test_store.py @@ -45,6 +45,14 @@ def test_implicit_cast(self): self.assertEqual(constructed.lvalue, lvalue) self.assertEqual(constructed.rvalue, expr.Cast(rvalue, types.Bool(), implicit=True)) + def test_implicit_const_cast(self): + lvalue = expr.Var.new("a", types.Bool()) + rvalue = expr.Value("b", types.Bool(const=True)) + constructed = Store(lvalue, rvalue) + self.assertIsInstance(constructed, Store) + self.assertEqual(constructed.lvalue, lvalue) + self.assertEqual(constructed.rvalue, expr.Cast(rvalue, types.Bool(), implicit=True)) + def test_rejects_non_lvalue(self): not_an_lvalue = expr.logic_and( expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Bool()) @@ -170,6 +178,15 @@ def test_lifts_integer_literals_to_full_width(self): qc.store(a, 255) self.assertEqual(qc.data[-1].operation, Store(a, expr.Value(255, a.type))) + def test_implicitly_casts_const_scalars(self): + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit(inputs=[a]) + qc.store(a, expr.lift(1, types.Uint(8, const=True))) + self.assertEqual( + qc.data[-1].operation, + Store(a, expr.Cast(expr.Value(1, types.Uint(8, const=True)), a.type, implicit=True)), + ) + def test_does_not_widen_bool_literal(self): # `bool` is a subclass of `int` in Python (except some arithmetic operations have different # semantics...). It's not in Qiskit's value type system, though. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index a0ea0dc118da..ec053af6cc40 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -41,6 +41,7 @@ Qubit, SwitchCaseOp, WhileLoopOp, + Duration, ) from qiskit.circuit.classical import expr, types from qiskit.circuit.annotated_operation import ( @@ -91,7 +92,6 @@ InstructionProperties, Target, InstructionDurations, - target_to_backend_properties, ) from test import QiskitTestCase, combine, slow_test # pylint: disable=wrong-import-order @@ -1498,6 +1498,21 @@ def test_delay_converts_to_dt(self): out = transpile(qc, dt=1e-9, seed_transpiler=42) self.assertEqual(out.data[0].operation.unit, "dt") + def test_delay_converts_to_dt_expr(self): + """Test that a delay instruction with a duration expression of type Duration + is converted to units of dt given a backend.""" + qc = QuantumCircuit(2) + qc.delay(expr.lift(Duration.us(1000)), [0]) + + backend = GenericBackendV2(num_qubits=4) + backend.target.dt = 0.5e-6 + out = transpile([qc, qc], backend, seed_transpiler=42) + self.assertEqual(out[0].data[0].operation.unit, "dt") + self.assertEqual(out[1].data[0].operation.unit, "dt") + + out = transpile(qc, dt=1e-9, seed_transpiler=42) + self.assertEqual(out.data[0].operation.unit, "dt") + def test_scheduling_backend_v2(self): """Test that scheduling method works with Backendv2.""" qc = QuantumCircuit(2) @@ -1606,78 +1621,6 @@ def test_scheduling_dt_constraints(self): scheduled = transpile(qc, backend=backend_v2, scheduling_method="asap", dt=original_dt / 2) self.assertEqual(scheduled.duration, original_duration * 2) - def test_backend_props_constraints(self): - """Test that loose transpile constraints work with both BackendV1 and BackendV2.""" - - with self.assertWarns(DeprecationWarning): - backend_v1 = Fake20QV1() - backend_v2 = BackendV2Converter(backend_v1) - qr1 = QuantumRegister(3, "qr1") - qr2 = QuantumRegister(2, "qr2") - qc = QuantumCircuit(qr1, qr2) - qc.cx(qr1[0], qr1[1]) - qc.cx(qr1[1], qr1[2]) - qc.cx(qr1[2], qr2[0]) - qc.cx(qr2[0], qr2[1]) - - # generate a fake backend with same number of qubits - # but different backend properties - fake_backend = GenericBackendV2(num_qubits=20, seed=42) - with self.assertWarns(DeprecationWarning): - custom_backend_properties = target_to_backend_properties(fake_backend.target) - - # expected layout for custom_backend_properties - # (different from expected layout for Fake20QV1) - vf2_layout = { - 18: Qubit(QuantumRegister(3, "qr1"), 1), - 13: Qubit(QuantumRegister(3, "qr1"), 2), - 19: Qubit(QuantumRegister(3, "qr1"), 0), - 14: Qubit(QuantumRegister(2, "qr2"), 0), - 9: Qubit(QuantumRegister(2, "qr2"), 1), - 0: Qubit(QuantumRegister(15, "ancilla"), 0), - 1: Qubit(QuantumRegister(15, "ancilla"), 1), - 2: Qubit(QuantumRegister(15, "ancilla"), 2), - 3: Qubit(QuantumRegister(15, "ancilla"), 3), - 4: Qubit(QuantumRegister(15, "ancilla"), 4), - 5: Qubit(QuantumRegister(15, "ancilla"), 5), - 6: Qubit(QuantumRegister(15, "ancilla"), 6), - 7: Qubit(QuantumRegister(15, "ancilla"), 7), - 8: Qubit(QuantumRegister(15, "ancilla"), 8), - 10: Qubit(QuantumRegister(15, "ancilla"), 9), - 11: Qubit(QuantumRegister(15, "ancilla"), 10), - 12: Qubit(QuantumRegister(15, "ancilla"), 11), - 15: Qubit(QuantumRegister(15, "ancilla"), 12), - 16: Qubit(QuantumRegister(15, "ancilla"), 13), - 17: Qubit(QuantumRegister(15, "ancilla"), 14), - } - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will stop supporting inputs of type `BackendV1` ", - ): - result = transpile( - qc, - backend=backend_v1, - backend_properties=custom_backend_properties, - optimization_level=2, - seed_transpiler=42, - ) - - self.assertEqual(result._layout.initial_layout._p2v, vf2_layout) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `target` parameter should be used instead", - ): - result = transpile( - qc, - backend=backend_v2, - backend_properties=custom_backend_properties, - optimization_level=2, - seed_transpiler=42, - ) - - self.assertEqual(result._layout.initial_layout._p2v, vf2_layout) - @data(1, 2, 3) def test_no_infinite_loop(self, optimization_level): """Verify circuit cost always descends and optimization does not flip flop indefinitely.""" @@ -2240,13 +2183,43 @@ def _control_flow_expr_circuit(self): base.append(CustomCX(), [2, 4]) base.ry(a, 4) base.measure(4, 2) - with base.switch(expr.bit_and(base.cregs[0], 2)) as case_: + with base.switch(expr.bit_and(base.cregs[0], expr.lift(2, try_const=True))) as case_: with case_(0, 1): base.cz(3, 5) with case_(case_.DEFAULT): base.cz(1, 4) base.append(CustomCX(), [2, 4]) base.append(CustomCX(), [3, 4]) + with base.if_test(expr.less(1.0, 2.0)): + base.cx(0, 1) + with base.if_test( + expr.logic_and( + expr.logic_and( + expr.equal(Duration.dt(1), Duration.ns(2)), + expr.equal(Duration.us(3), Duration.ms(4)), + ), + expr.equal(Duration.s(5), Duration.dt(6)), + ) + ): + base.cx(0, 1) + with base.if_test( + expr.logic_and( + expr.logic_and( + expr.equal(expr.mul(Duration.dt(1), 2.0), expr.div(Duration.ns(2), 2.0)), + expr.equal( + expr.add(Duration.us(3), Duration.us(4)), + expr.sub(Duration.ms(5), Duration.ms(6)), + ), + ), + expr.logic_and( + expr.equal(expr.mul(expr.lift(1.0, try_const=True), 2.0), expr.div(4.0, 2.0)), + expr.equal( + expr.add(3.0, 4.0), expr.sub(10.5, expr.lift(4.3, types.Float(const=True))) + ), + ), + ) + ): + base.cx(0, 1) return base def _standalone_var_circuit(self): @@ -2256,6 +2229,7 @@ def _standalone_var_circuit(self): qc = QuantumCircuit(5, 5, inputs=[a]) qc.add_var(b, 12) + qc.add_stretch("d") qc.h(0) qc.cx(0, 1) qc.measure([0, 1], [0, 1]) @@ -2642,7 +2616,6 @@ def test_custom_multiple_circuits(self): initial_layout=None, basis_gates=["u1", "u2", "u3", "cx"], coupling_map=CouplingMap([[0, 1]]), - backend_properties=None, seed_transpiler=1, ) passmanager = level_0_pass_manager(pm_conf) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 1b9c24ca5410..ddf5707247b3 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -872,15 +872,19 @@ def test_delay_statement(self): """Test that delay operations get output into valid OpenQASM 3.""" qreg = QuantumRegister(2, "qr") qc = QuantumCircuit(qreg) + s = qc.add_stretch("s") qc.delay(100, qreg[0], unit="ms") qc.delay(2, qreg[1], unit="ps") # "ps" is not a valid unit in OQ3, so we need to convert. + qc.delay(expr.div(s, 2.0), qreg[1]) expected_qasm = "\n".join( [ "OPENQASM 3.0;", "qubit[2] qr;", + "stretch s;", "delay[100ms] qr[0];", "delay[2000ns] qr[1];", + "delay[s / 2.0] qr[1];", "", ] ) @@ -1711,6 +1715,10 @@ def test_expr_associativity_left(self): ) qc.if_test(expr.logic_and(expr.logic_and(cr1[0], cr1[1]), cr1[2]), body.copy(), [], []) qc.if_test(expr.logic_or(expr.logic_or(cr1[0], cr1[1]), cr1[2]), body.copy(), [], []) + qc.if_test(expr.equal(expr.add(expr.add(cr1, cr2), cr3), 7), body.copy(), [], []) + qc.if_test(expr.equal(expr.sub(expr.sub(cr1, cr2), cr3), 7), body.copy(), [], []) + qc.if_test(expr.equal(expr.mul(expr.mul(cr1, cr2), cr3), 7), body.copy(), [], []) + qc.if_test(expr.equal(expr.div(expr.div(cr1, cr2), cr3), 7), body.copy(), [], []) # Note that bitwise operations except shift have lower priority than `==` so there's extra # parentheses. All these operators are left-associative in OQ3. @@ -1736,6 +1744,14 @@ def test_expr_associativity_left(self): } if (cr1[0] || cr1[1] || cr1[2]) { } +if (cr1 + cr2 + cr3 == 7) { +} +if (cr1 - cr2 - cr3 == 7) { +} +if (cr1 * cr2 * cr3 == 7) { +} +if (cr1 / cr2 / cr3 == 7) { +} """ self.assertEqual(dumps(qc), expected) @@ -1762,6 +1778,10 @@ def test_expr_associativity_right(self): ) qc.if_test(expr.logic_and(cr1[0], expr.logic_and(cr1[1], cr1[2])), body.copy(), [], []) qc.if_test(expr.logic_or(cr1[0], expr.logic_or(cr1[1], cr1[2])), body.copy(), [], []) + qc.if_test(expr.equal(expr.add(cr1, expr.add(cr2, cr3)), 7), body.copy(), [], []) + qc.if_test(expr.equal(expr.sub(cr1, expr.sub(cr2, cr3)), 7), body.copy(), [], []) + qc.if_test(expr.equal(expr.mul(cr1, expr.mul(cr2, cr3)), 7), body.copy(), [], []) + qc.if_test(expr.equal(expr.div(cr1, expr.div(cr2, cr3)), 7), body.copy(), [], []) # Note that bitwise operations have lower priority than `==` so there's extra parentheses. # All these operators are left-associative in OQ3, so we need parentheses for them to be @@ -1789,6 +1809,14 @@ def test_expr_associativity_right(self): } if (cr1[0] || (cr1[1] || cr1[2])) { } +if (cr1 + (cr2 + cr3) == 7) { +} +if (cr1 - (cr2 - cr3) == 7) { +} +if (cr1 * (cr2 * cr3) == 7) { +} +if (cr1 / (cr2 / cr3) == 7) { +} """ self.assertEqual(dumps(qc), expected) @@ -1864,11 +1892,17 @@ def test_expr_precedence(self): ), ) + arithmetic = expr.equal( + expr.add(expr.mul(cr, expr.sub(cr, cr)), expr.div(expr.add(cr, cr), cr)), + expr.sub(expr.div(expr.mul(cr, cr), expr.add(cr, cr)), expr.mul(cr, expr.add(cr, cr))), + ) + qc = QuantumCircuit(cr) qc.if_test(inside_out, body.copy(), [], []) qc.if_test(outside_in, body.copy(), [], []) qc.if_test(logics, body.copy(), [], []) qc.if_test(bitshifts, body.copy(), [], []) + qc.if_test(arithmetic, body.copy(), [], []) expected = """\ OPENQASM 3.0; @@ -1884,6 +1918,8 @@ def test_expr_precedence(self): } if (((cr ^ cr) & cr) << (cr | cr) == (cr >> 3 ^ cr << 4 | cr << 1)) { } +if (cr * (cr - cr) + (cr + cr) / cr == cr * cr / (cr + cr) - cr * (cr + cr)) { +} """ self.assertEqual(dumps(qc), expected) @@ -1908,6 +1944,57 @@ def test_no_unnecessary_cast(self): """ self.assertEqual(dumps(qc), expected) + def test_const_expr(self): + """Test that const-typed expressions are implicitly converted without a cast.""" + qubit = Qubit() + creg = ClassicalRegister(2, "c") + circuit = QuantumCircuit([qubit], creg) + + body = QuantumCircuit([qubit], creg) + body.x(0) + body.y(0) + + circuit.if_test(expr.lift(True, types.Bool(const=True)), body, [0], body.clbits) + circuit.if_test( + expr.equal(creg, expr.lift(1, types.Uint(2, const=True))), body, [0], body.clbits + ) + circuit.if_test( + expr.equal(expr.lift(1, types.Uint(2)), expr.lift(2, types.Uint(2, const=True))), + body, + [0], + body.clbits, + ) + circuit.if_test( + expr.less(expr.lift(1.0, types.Float(const=True)), expr.lift(2.0, types.Float())), + body, + [0], + body.clbits, + ) + test = dumps(circuit) + expected = """\ +OPENQASM 3.0; +include "stdgates.inc"; +bit[2] c; +qubit _qubit0; +if (true) { + x _qubit0; + y _qubit0; +} +if (c == 1) { + x _qubit0; + y _qubit0; +} +if (1 == 2) { + x _qubit0; + y _qubit0; +} +if (1.0 < 2.0) { + x _qubit0; + y _qubit0; +} +""" + self.assertEqual(test, expected) + def test_var_use(self): """Test that input and declared vars work in simple local scopes and can be set.""" qc = QuantumCircuit() @@ -1918,6 +2005,7 @@ def test_var_use(self): qc.add_var("c", expr.bit_not(b)) # All inputs should come first, regardless of declaration order. qc.add_input("d", types.Bool()) + qc.add_stretch("f") expected = """\ OPENQASM 3.0; @@ -1926,6 +2014,7 @@ def test_var_use(self): input uint[8] b; input bool d; uint[8] c; +stretch f; a = !a; b = b & 8; c = ~b; diff --git a/test/python/transpiler/test_passmanager_config.py b/test/python/transpiler/test_passmanager_config.py index fccd68f0a261..aa93ab2af4fe 100644 --- a/test/python/transpiler/test_passmanager_config.py +++ b/test/python/transpiler/test_passmanager_config.py @@ -135,7 +135,6 @@ def test_str(self): \ttranslation_method: None \tscheduling_method: None \tinstruction_durations:\u0020 -\tbackend_properties: None \tapproximation_degree: None \tseed_transpiler: None \ttiming_constraints: None diff --git a/test/python/transpiler/test_solovay_kitaev.py b/test/python/transpiler/test_solovay_kitaev.py index 62b811c8e3bd..471c4791c114 100644 --- a/test/python/transpiler/test_solovay_kitaev.py +++ b/test/python/transpiler/test_solovay_kitaev.py @@ -22,8 +22,10 @@ from ddt import ddt, data from qiskit import transpile -from qiskit.circuit import QuantumCircuit +from qiskit.circuit import QuantumCircuit, Parameter +from qiskit.circuit.classicalregister import ClassicalRegister from qiskit.circuit.library import TGate, TdgGate, HGate, SGate, SdgGate, IGate, QFT +from qiskit.circuit.quantumregister import QuantumRegister from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.quantum_info import Operator from qiskit.synthesis.discrete_basis.generate_basis_approximations import ( @@ -32,7 +34,6 @@ from qiskit.synthesis.discrete_basis.commutator_decompose import commutator_decompose from qiskit.synthesis.discrete_basis.gate_sequence import GateSequence from qiskit.transpiler import PassManager -from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes import UnitarySynthesis, Collect1qRuns, ConsolidateBlocks from qiskit.transpiler.passes.synthesis import SolovayKitaev, SolovayKitaevSynthesis from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -152,23 +153,6 @@ def test_exact_decomposition_acts_trivially(self): decomposed_circuit = dag_to_circuit(decomposed_dag) self.assertEqual(circuit, decomposed_circuit) - def test_fails_with_no_to_matrix(self): - """Test failer if gate does not have to_matrix.""" - circuit = QuantumCircuit(1) - circuit.initialize("0") - - synth = SolovayKitaev(3, self.basic_approx) - - dag = circuit_to_dag(circuit) - - with self.assertRaises(TranspilerError) as cm: - _ = synth.run(dag) - - self.assertEqual( - "SolovayKitaev does not support gate without to_matrix method: initialize", - cm.exception.message, - ) - def test_str_basis_gates(self): """Test specifying the basis gates by string works.""" circuit = QuantumCircuit(1) @@ -261,6 +245,56 @@ def test_load_from_file(self): self.assertEqual(discretized, reference) + def test_measure(self): + """Test the Solovay-Kitaev transpiler pass on circuits with measure operators.""" + qc = QuantumCircuit(1, 1) + qc.x(0) + qc.measure(0, 0) + transpiled = SolovayKitaev()(qc) + self.assertEqual(set(transpiled.count_ops()), {"h", "t", "measure"}) + + def test_barrier(self): + """Test the Solovay-Kitaev transpiler pass on circuits with barriers.""" + qc = QuantumCircuit(1) + qc.x(0) + qc.barrier(0) + transpiled = SolovayKitaev()(qc) + self.assertEqual(set(transpiled.count_ops()), {"h", "t", "barrier"}) + + def test_parameterized_gates(self): + """Test the Solovay-Kitaev transpiler pass on circuits with parameterized gates.""" + qc = QuantumCircuit(1) + qc.x(0) + qc.rz(Parameter("t"), 0) + transpiled = SolovayKitaev()(qc) + self.assertEqual(set(transpiled.count_ops()), {"h", "t", "rz"}) + + def test_control_flow_if(self): + """Test the Solovay-Kitaev transpiler pass on circuits with control flow ops""" + qr = QuantumRegister(1) + cr = ClassicalRegister(1) + qc = QuantumCircuit(qr, cr) + + with qc.if_test((cr[0], 0)) as else_: + qc.y(0) + with else_: + qc.z(0) + transpiled = SolovayKitaev()(qc) + + # check that we still have an if-else block and all the operations within + # have been recursively synthesized + self.assertEqual(transpiled[0].name, "if_else") + for block in transpiled[0].operation.blocks: + self.assertLessEqual(set(block.count_ops()), {"h", "t", "tdg"}) + + def test_no_to_matrix(self): + """Test the Solovay-Kitaev transpiler pass ignores gates without to_matrix.""" + qc = QuantumCircuit(1) + qc.initialize("0") + + transpiled = SolovayKitaev()(qc) + self.assertEqual(set(transpiled.count_ops()), {"initialize"}) + @ddt class TestGateSequence(QiskitTestCase): diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index 694ef38dc4fa..6092fcad664b 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -1723,41 +1723,10 @@ def test_basis_gates_coupling_map(self): self.assertEqual({(0,), (1,), (2,)}, target["u"].keys()) self.assertEqual({(0, 1), (1, 2), (2, 0)}, target["cx"].keys()) - def test_properties(self): - with self.assertWarns(DeprecationWarning): - fake_backend = Fake5QV1() - config = fake_backend.configuration() - properties = fake_backend.properties() - target = Target.from_configuration( - basis_gates=config.basis_gates, - num_qubits=config.num_qubits, - coupling_map=CouplingMap(config.coupling_map), - backend_properties=properties, - ) - self.assertEqual(0, target["rz"][(0,)].error) - self.assertEqual(0, target["rz"][(0,)].duration) - - def test_properties_with_durations(self): - with self.assertWarns(DeprecationWarning): - fake_backend = Fake5QV1() - config = fake_backend.configuration() - properties = fake_backend.properties() - durations = InstructionDurations([("rz", 0, 0.5)], dt=1.0) - target = Target.from_configuration( - basis_gates=config.basis_gates, - num_qubits=config.num_qubits, - coupling_map=CouplingMap(config.coupling_map), - backend_properties=properties, - instruction_durations=durations, - dt=config.dt, - ) - self.assertEqual(0.5, target["rz"][(0,)].duration) - def test_inst_map(self): with self.assertWarns(DeprecationWarning): fake_backend = Fake7QPulseV1() config = fake_backend.configuration() - properties = fake_backend.properties() defaults = fake_backend.defaults() constraints = TimingConstraints(**config.timing_constraints) with self.assertWarns(DeprecationWarning): @@ -1765,7 +1734,6 @@ def test_inst_map(self): basis_gates=config.basis_gates, num_qubits=config.num_qubits, coupling_map=CouplingMap(config.coupling_map), - backend_properties=properties, dt=config.dt, inst_map=defaults.instruction_schedule_map, timing_constraints=constraints, diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index d93d6800985c..311816ab447c 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -32,7 +32,6 @@ from qiskit.quantum_info.operators import Operator from qiskit.quantum_info.random import random_unitary from qiskit.transpiler import PassManager, CouplingMap, Target, InstructionProperties -from qiskit.transpiler.exceptions import TranspilerError from qiskit.exceptions import QiskitError from qiskit.transpiler.passes import ( Collect2qBlocks, @@ -126,7 +125,6 @@ def test_empty_basis_gates(self): qc.unitary(op_1q.data, [0]) qc.unitary(op_2q.data, [0, 1]) qc.unitary(op_3q.data, [0, 1, 2]) - out = UnitarySynthesis(basis_gates=None, min_qubits=2)(qc) self.assertEqual(out.count_ops(), {"unitary": 3}) @@ -250,7 +248,7 @@ def test_two_qubit_natural_direction_true_gate_length_raises(self): natural_direction=True, ) pm = PassManager([triv_layout_pass, unisynth_pass]) - with self.assertRaises(TranspilerError): + with self.assertRaises(QiskitError): pm.run(qc) def test_two_qubit_pulse_optimal_none_optimal(self): diff --git a/test/python/transpiler/test_unitary_synthesis_plugin.py b/test/python/transpiler/test_unitary_synthesis_plugin.py index f6790e8ed14d..f4012bcff044 100644 --- a/test/python/transpiler/test_unitary_synthesis_plugin.py +++ b/test/python/transpiler/test_unitary_synthesis_plugin.py @@ -30,7 +30,7 @@ UnitarySynthesisPluginManager, unitary_synthesis_plugin_names, ) -from qiskit.transpiler.passes.synthesis.unitary_synthesis import DefaultUnitarySynthesis +from qiskit.transpiler.passes.synthesis.default_unitary_synth_plugin import DefaultUnitarySynthesis from test import QiskitTestCase # pylint: disable=wrong-import-order diff --git a/test/python/transpiler/test_vf2_layout.py b/test/python/transpiler/test_vf2_layout.py index 9fbc9ac45480..c03bb0498618 100644 --- a/test/python/transpiler/test_vf2_layout.py +++ b/test/python/transpiler/test_vf2_layout.py @@ -27,7 +27,7 @@ from qiskit.transpiler.passes.layout.vf2_layout import VF2Layout, VF2LayoutStopReason from qiskit._accelerate.error_map import ErrorMap from qiskit.converters import circuit_to_dag -from qiskit.providers.fake_provider import Fake5QV1, Fake127QPulseV1, GenericBackendV2 +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.circuit import Measure from qiskit.circuit.library import GraphStateGate, CXGate, XGate, HGate from qiskit.transpiler import PassManager, AnalysisPass @@ -35,7 +35,7 @@ from qiskit.transpiler.preset_passmanagers.common import generate_embed_passmanager from test import QiskitTestCase # pylint: disable=wrong-import-order -from ..legacy_cmaps import TENERIFE_CMAP, RUESCHLIKON_CMAP, MANHATTAN_CMAP +from ..legacy_cmaps import TENERIFE_CMAP, RUESCHLIKON_CMAP, MANHATTAN_CMAP, YORKTOWN_CMAP class LayoutTestCase(QiskitTestCase): @@ -631,31 +631,28 @@ def test_no_properties(self): def test_with_properties(self): """Test it finds the least noise perfect layout with no properties.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() qr = QuantumRegister(2) qc = QuantumCircuit(qr) qc.x(qr) qc.measure_all() - cmap = CouplingMap(backend.configuration().coupling_map) - properties = backend.properties() - vf2_pass = VF2Layout(cmap, properties=properties) + cmap = CouplingMap(YORKTOWN_CMAP) + backend = GenericBackendV2(num_qubits=5, coupling_map=cmap, seed=15) + vf2_pass = VF2Layout(target=backend.target) property_set = {} vf2_pass(qc, property_set) self.assertEqual(set(property_set["layout"].get_physical_bits()), {1, 3}) def test_max_trials_exceeded(self): """Test it exits when max_trials is reached.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() + qr = QuantumRegister(2) qc = QuantumCircuit(qr) qc.x(qr) qc.cx(0, 1) qc.measure_all() - cmap = CouplingMap(backend.configuration().coupling_map) - properties = backend.properties() - vf2_pass = VF2Layout(cmap, properties=properties, seed=-1, max_trials=1) + cmap = CouplingMap(YORKTOWN_CMAP) + backend = GenericBackendV2(num_qubits=5, coupling_map=cmap, seed=1) + vf2_pass = VF2Layout(target=backend.target, seed=-1, max_trials=1) property_set = {} with self.assertLogs("qiskit.transpiler.passes.layout.vf2_layout", level="DEBUG") as cm: vf2_pass(qc, property_set) @@ -667,16 +664,14 @@ def test_max_trials_exceeded(self): def test_time_limit_exceeded(self): """Test the pass stops after time_limit is reached.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() qr = QuantumRegister(2) qc = QuantumCircuit(qr) qc.x(qr) qc.cx(0, 1) qc.measure_all() - cmap = CouplingMap(backend.configuration().coupling_map) - properties = backend.properties() - vf2_pass = VF2Layout(cmap, properties=properties, seed=-1, time_limit=0.0) + cmap = CouplingMap(YORKTOWN_CMAP) + backend = GenericBackendV2(num_qubits=5, coupling_map=cmap, seed=1) + vf2_pass = VF2Layout(target=backend.target, seed=-1, time_limit=0.0) property_set = {} with self.assertLogs("qiskit.transpiler.passes.layout.vf2_layout", level="DEBUG") as cm: vf2_pass(qc, property_set) @@ -690,27 +685,6 @@ def test_time_limit_exceeded(self): self.assertEqual(set(property_set["layout"].get_physical_bits()), {2, 0}) - def test_reasonable_limits_for_simple_layouts_v1(self): - """Test that the default trials is set to a reasonable number. - REMOVE ONCE Fake127QPulseV1 IS GONE""" - with self.assertWarns(DeprecationWarning): - backend = Fake127QPulseV1() - qc = QuantumCircuit(5) - qc.cx(2, 3) - qc.cx(0, 1) - cmap = CouplingMap(backend.configuration().coupling_map) - properties = backend.properties() - # Run without any limits set - vf2_pass = VF2Layout(cmap, properties=properties, seed=42) - property_set = {} - with self.assertLogs("qiskit.transpiler.passes.layout.vf2_layout", level="DEBUG") as cm: - vf2_pass(qc, property_set) - self.assertIn( - "DEBUG:qiskit.transpiler.passes.layout.vf2_layout:Trial 299 is >= configured max trials 299", - cm.output, - ) - self.assertEqual(set(property_set["layout"].get_physical_bits()), {57, 58, 61, 62, 0}) - def test_reasonable_limits_for_simple_layouts(self): """Test that the default trials is set to a reasonable number.""" backend = GenericBackendV2(27, seed=42) @@ -731,16 +705,14 @@ def test_reasonable_limits_for_simple_layouts(self): def test_no_limits_with_negative(self): """Test that we're not enforcing a trial limit if set to negative.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() qc = QuantumCircuit(3) qc.h(0) - cmap = CouplingMap(backend.configuration().coupling_map) - properties = backend.properties() + cmap = CouplingMap(YORKTOWN_CMAP) + backend = GenericBackendV2(num_qubits=5, coupling_map=cmap, seed=4) + # Run without any limits set vf2_pass = VF2Layout( - cmap, - properties=properties, + target=backend.target, seed=42, max_trials=0, ) diff --git a/test/python/transpiler/test_vf2_post_layout.py b/test/python/transpiler/test_vf2_post_layout.py index 9aaab695197a..8b2548e38b6f 100644 --- a/test/python/transpiler/test_vf2_post_layout.py +++ b/test/python/transpiler/test_vf2_post_layout.py @@ -17,10 +17,10 @@ from qiskit import QuantumRegister, QuantumCircuit from qiskit.circuit import ControlFlowOp from qiskit.circuit.library import CXGate, XGate -from qiskit.transpiler import CouplingMap, Layout, TranspilerError +from qiskit.transpiler import Layout, TranspilerError from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayout, VF2PostLayoutStopReason from qiskit.converters import circuit_to_dag -from qiskit.providers.fake_provider import Fake5QV1, GenericBackendV2 +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.circuit import Qubit from qiskit.compiler.transpiler import transpile from qiskit.transpiler.target import Target, InstructionProperties @@ -34,36 +34,9 @@ class TestVF2PostLayout(QiskitTestCase): seed = 42 - def assertLayout(self, dag, coupling_map, property_set): - """Checks if the circuit in dag was a perfect layout in property_set for the given - coupling_map""" - self.assertEqual( - property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.SOLUTION_FOUND - ) - - layout = property_set["post_layout"] - edges = coupling_map.graph.edge_list() - - def run(dag, wire_map): - for gate in dag.two_qubit_ops(): - with self.assertWarns(DeprecationWarning): - if dag.has_calibration_for(gate) or isinstance(gate.op, ControlFlowOp): - continue - physical_q0 = wire_map[gate.qargs[0]] - physical_q1 = wire_map[gate.qargs[1]] - self.assertTrue((physical_q0, physical_q1) in edges) - for node in dag.op_nodes(ControlFlowOp): - for block in node.op.blocks: - inner_wire_map = { - inner: wire_map[outer] for outer, inner in zip(node.qargs, block.qubits) - } - run(circuit_to_dag(block), inner_wire_map) - - run(dag, {bit: layout[bit] for bit in dag.qubits if bit in layout}) - def assertLayoutV2(self, dag, target, property_set): """Checks if the circuit in dag was a perfect layout in property_set for the given - coupling_map""" + target""" self.assertEqual( property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.SOLUTION_FOUND ) @@ -95,27 +68,6 @@ def test_no_constraints(self): with self.assertRaises(TranspilerError): empty_pass.run(circuit_to_dag(qc)) - def test_no_backend_properties(self): - """Test we raise at runtime if no properties are provided with a coupling graph.""" - qc = QuantumCircuit(2) - empty_pass = VF2PostLayout(coupling_map=CouplingMap([(0, 1), (1, 2)])) - with self.assertRaises(TranspilerError): - empty_pass.run(circuit_to_dag(qc)) - - def test_empty_circuit(self): - """Test no solution found for empty circuit""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - qc = QuantumCircuit(2, 2) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props) - vf2_pass.run(circuit_to_dag(qc)) - self.assertEqual( - vf2_pass.property_set["VF2PostLayout_stop_reason"], - VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND, - ) - def test_empty_circuit_v2(self): """Test no solution found for empty circuit with v2 backend""" qc = QuantumCircuit(2, 2) @@ -129,35 +81,6 @@ def test_empty_circuit_v2(self): VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND, ) - def test_skip_3q_circuit(self): - """Test that the pass is a no-op on circuits with >2q gates.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - qc = QuantumCircuit(3) - qc.ccx(0, 1, 2) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props) - vf2_pass.run(circuit_to_dag(qc)) - self.assertEqual( - vf2_pass.property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.MORE_THAN_2Q - ) - - def test_skip_3q_circuit_control_flow(self): - """Test that the pass is a no-op on circuits with >2q gates.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - qc = QuantumCircuit(3) - with qc.for_loop((1,)): - qc.ccx(0, 1, 2) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props) - vf2_pass.run(circuit_to_dag(qc)) - self.assertEqual( - vf2_pass.property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.MORE_THAN_2Q - ) - def test_skip_3q_circuit_v2(self): """Test that the pass is a no-op on circuits with >2q gates with a target.""" qc = QuantumCircuit(3) @@ -185,58 +108,6 @@ def test_skip_3q_circuit_control_flow_v2(self): vf2_pass.property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.MORE_THAN_2Q ) - def test_2q_circuit_5q_backend(self): - """A simple example, without considering the direction - 0 - 1 - qr1 - qr0 - """ - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - - qr = QuantumRegister(2, "qr") - circuit = QuantumCircuit(qr) - circuit.cx(qr[1], qr[0]) # qr1 -> qr0 - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - tqc = transpile(circuit, backend, layout_method="dense") - initial_layout = tqc._layout - dag = circuit_to_dag(tqc) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - pass_ = VF2PostLayout(coupling_map=cmap, properties=props, seed=self.seed) - pass_.run(dag) - self.assertLayout(dag, cmap, pass_.property_set) - self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) - - def test_2q_circuit_5q_backend_controlflow(self): - """A simple example, without considering the direction - 0 - 1 - qr1 - qr0 - """ - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - - circuit = QuantumCircuit(2, 1) - with circuit.for_loop((1,)): - circuit.cx(1, 0) # qr1 -> qr0 - with circuit.if_test((circuit.clbits[0], True)) as else_: - pass - with else_: - with circuit.while_loop((circuit.clbits[0], True)): - circuit.cx(1, 0) # qr1 -> qr0 - initial_layout = Layout(dict(enumerate(circuit.qubits))) - circuit._layout = initial_layout - dag = circuit_to_dag(circuit) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - pass_ = VF2PostLayout(coupling_map=cmap, properties=props, seed=self.seed) - pass_.run(dag) - self.assertLayout(dag, cmap, pass_.property_set) - self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) - def test_2q_circuit_5q_backend_max_trials(self): """A simple example, without considering the direction 0 - 1 @@ -256,7 +127,6 @@ def test_2q_circuit_5q_backend_max_trials(self): tqc = transpile(circuit, backend, layout_method="dense") initial_layout = tqc._layout dag = circuit_to_dag(tqc) - cmap = CouplingMap(backend.coupling_map) pass_ = VF2PostLayout(target=backend.target, seed=self.seed, max_trials=max_trials) with self.assertLogs( "qiskit.transpiler.passes.layout.vf2_post_layout", level="DEBUG" @@ -270,46 +140,7 @@ def test_2q_circuit_5q_backend_max_trials(self): self.assertEqual( pass_.property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.SOLUTION_FOUND ) - self.assertLayout(dag, cmap, pass_.property_set) - self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) - - def test_2q_circuit_5q_backend_max_trials_v1(self): - """A simple example, without considering the direction - 0 - 1 - qr1 - qr0 - """ - max_trials = 11 - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - - qr = QuantumRegister(2, "qr") - circuit = QuantumCircuit(qr) - circuit.cx(qr[1], qr[0]) # qr1 -> qr0 - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - tqc = transpile(circuit, backend, layout_method="dense") - initial_layout = tqc._layout - dag = circuit_to_dag(tqc) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - pass_ = VF2PostLayout( - coupling_map=cmap, properties=props, seed=self.seed, max_trials=max_trials - ) - - with self.assertLogs( - "qiskit.transpiler.passes.layout.vf2_post_layout", level="DEBUG" - ) as cm: - pass_.run(dag) - self.assertIn( - f"DEBUG:qiskit.transpiler.passes.layout.vf2_post_layout:Trial {max_trials} " - f"is >= configured max trials {max_trials}", - cm.output, - ) - - self.assertLayout(dag, cmap, pass_.property_set) + self.assertLayoutV2(dag, backend.target, pass_.property_set) self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) def test_best_mapping_ghz_state_full_device_multiple_qregs(self): @@ -492,7 +323,7 @@ def test_last_qubits_best(self): ) dag = circuit_to_dag(circuit) vf2_pass.run(dag) - self.assertLayout(dag, target_last_qubits_best.build_coupling_map(), vf2_pass.property_set) + self.assertLayoutV2(dag, target_last_qubits_best, vf2_pass.property_set) class TestVF2PostLayoutScoring(QiskitTestCase): @@ -587,30 +418,6 @@ def test_no_constraints(self): with self.assertRaises(TranspilerError): empty_pass.run(circuit_to_dag(qc)) - def test_no_backend_properties(self): - """Test we raise at runtime if no properties are provided with a coupling graph.""" - qc = QuantumCircuit(2) - empty_pass = VF2PostLayout( - coupling_map=CouplingMap([(0, 1), (1, 2)]), strict_direction=False - ) - with self.assertRaises(TranspilerError): - empty_pass.run(circuit_to_dag(qc)) - - def test_empty_circuit(self): - """Test no solution found for empty circuit""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - - qc = QuantumCircuit(2, 2) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props, strict_direction=False) - vf2_pass.run(circuit_to_dag(qc)) - self.assertEqual( - vf2_pass.property_set["VF2PostLayout_stop_reason"], - VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND, - ) - def test_empty_circuit_v2(self): """Test no solution found for empty circuit with v2 backend""" qc = QuantumCircuit(2, 2) @@ -627,22 +434,6 @@ def test_empty_circuit_v2(self): VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND, ) - def test_skip_3q_circuit(self): - """Test that the pass is a no-op on circuits with >2q gates.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - - qc = QuantumCircuit(3) - qc.ccx(0, 1, 2) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props, strict_direction=False) - vf2_pass.run(circuit_to_dag(qc)) - self.assertEqual( - vf2_pass.property_set["VF2PostLayout_stop_reason"], - VF2PostLayoutStopReason.MORE_THAN_2Q, - ) - def test_skip_3q_circuit_v2(self): """Test that the pass is a no-op on circuits with >2q gates with a target.""" qc = QuantumCircuit(3) @@ -680,40 +471,9 @@ def test_best_mapping_ghz_state_full_device_multiple_qregs(self): tqc = transpile(qc, seed_transpiler=self.seed, layout_method="trivial") initial_layout = tqc._layout dag = circuit_to_dag(tqc) - cmap = CouplingMap(backend.coupling_map) pass_ = VF2PostLayout(target=backend.target, seed=self.seed, strict_direction=False) pass_.run(dag) - self.assertLayout(dag, cmap, pass_.property_set) - self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) - - def test_best_mapping_ghz_state_full_device_multiple_qregs_v1(self): - """Test best mappings with multiple registers""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - qr_a = QuantumRegister(2) - qr_b = QuantumRegister(3) - qc = QuantumCircuit(qr_a, qr_b) - qc.h(qr_a[0]) - qc.cx(qr_a[0], qr_a[1]) - qc.cx(qr_a[0], qr_b[0]) - qc.cx(qr_a[0], qr_b[1]) - qc.cx(qr_a[0], qr_b[2]) - qc.measure_all() - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - tqc = transpile(qc, backend, seed_transpiler=self.seed, layout_method="trivial") - initial_layout = tqc._layout - dag = circuit_to_dag(tqc) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - pass_ = VF2PostLayout( - coupling_map=cmap, properties=props, seed=self.seed, strict_direction=False - ) - pass_.run(dag) - self.assertLayout(dag, cmap, pass_.property_set) + self.assertLayoutV2(dag, backend.target, pass_.property_set) self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) def test_2q_circuit_5q_backend(self): @@ -733,38 +493,9 @@ def test_2q_circuit_5q_backend(self): tqc = transpile(circuit, backend, layout_method="dense") initial_layout = tqc._layout dag = circuit_to_dag(tqc) - cmap = CouplingMap(backend.coupling_map) pass_ = VF2PostLayout(target=backend.target, seed=self.seed, strict_direction=False) pass_.run(dag) - self.assertLayout(dag, cmap, pass_.property_set) - self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) - - def test_2q_circuit_5q_backend_v1(self): - """A simple example, without considering the direction - 0 - 1 - qr1 - qr0 - """ - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - - qr = QuantumRegister(2, "qr") - circuit = QuantumCircuit(qr) - circuit.cx(qr[1], qr[0]) # qr1 -> qr0 - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - tqc = transpile(circuit, backend, layout_method="dense") - initial_layout = tqc._layout - dag = circuit_to_dag(tqc) - cmap = CouplingMap(backend.configuration().coupling_map) - props = backend.properties() - pass_ = VF2PostLayout( - coupling_map=cmap, properties=props, seed=self.seed, strict_direction=False - ) - pass_.run(dag) - self.assertLayout(dag, cmap, pass_.property_set) + self.assertLayoutV2(dag, backend.target, pass_.property_set) self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) def test_best_mapping_ghz_state_full_device_multiple_qregs_v2(self): diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index cc70cccf7d4d..551e2d14fd1f 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -820,6 +820,60 @@ def generate_v12_expr(): return [index, shift] +def generate_v14_expr(): + """Circuits that contain expressions new in QPY v14, including constant types, + duration types, and floats.""" + from qiskit.circuit.classical import expr, types + from qiskit.circuit import Duration + + const_expr = QuantumCircuit(name="const_expr") + with const_expr.if_test( + expr.not_equal( + expr.equal(expr.lift(1, types.Uint(1, const=True)), 1), + expr.lift(False, types.Bool(const=True)), + ) + ): + pass + + float_expr = QuantumCircuit(name="float_expr") + with float_expr.if_test(expr.less(1.0, 2.0)): + pass + + duration_expr = QuantumCircuit(name="duration_expr") + with duration_expr.if_test( + expr.logic_and( + expr.logic_and( + expr.equal(Duration.dt(1), Duration.ns(2)), + expr.equal(Duration.us(3), Duration.ms(4)), + ), + expr.equal(Duration.s(5), Duration.dt(6)), + ) + ): + pass + + math_expr = QuantumCircuit(name="math_expr") + with math_expr.if_test( + expr.logic_and( + expr.logic_and( + expr.equal(expr.mul(Duration.dt(1), 2.0), expr.div(Duration.ns(2), 2.0)), + expr.equal( + expr.add(Duration.us(3), Duration.us(4)), + expr.sub(Duration.ms(5), Duration.ms(6)), + ), + ), + expr.logic_and( + expr.equal(expr.mul(expr.lift(1.0, try_const=True), 2.0), expr.div(4.0, 2.0)), + expr.equal( + expr.add(3.0, 4.0), expr.sub(10.5, expr.lift(4.3, types.Float(const=True))) + ), + ), + ) + ): + pass + + return [const_expr, float_expr, duration_expr, math_expr] + + def generate_circuits(version_parts): """Generate reference circuits.""" output_circuits = { @@ -871,6 +925,8 @@ def generate_circuits(version_parts): if version_parts >= (1, 1, 0): output_circuits["standalone_vars.qpy"] = generate_standalone_var() output_circuits["v12_expr.qpy"] = generate_v12_expr() + if version_parts >= (2, 0, 0): + output_circuits["v14_expr.qpy"] = generate_v14_expr() return output_circuits From 634e54ac0f470217f86241e610c1779ded11940a Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 20 Feb 2025 18:30:33 +0000 Subject: [PATCH 3/3] Support stretchy delays in `box` This is currently built on top of a roll-up merge of Kevin's `stretchy-delay` branch, and just unifies the handling between `Delay` and `Box` so that they both support the `Expr` stuff. There's no need for Rust-space handling for `box`, because the entirety of the control-flow system isn't handled in Python-space yet. This might need further modification based on Kevin's work on transpiler support. This commit will be rebased and amended along with the rest of the `stretch` patch series. --- qiskit/circuit/controlflow/box.py | 6 ++++-- qiskit/circuit/delay.py | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/qiskit/circuit/controlflow/box.py b/qiskit/circuit/controlflow/box.py index 202c4f1f84cf..47b49101febf 100644 --- a/qiskit/circuit/controlflow/box.py +++ b/qiskit/circuit/controlflow/box.py @@ -16,6 +16,7 @@ import typing +from qiskit.circuit.delay import Delay from qiskit.circuit.exceptions import CircuitError from .control_flow import ControlFlowOp @@ -39,7 +40,7 @@ def __init__( body: QuantumCircuit, *, duration: None = None, - unit: typing.Literal["dt", "s", "ms", "us", "ns", "ps"] = "dt", + unit: typing.Literal["dt", "s", "ms", "us", "ns", "ps", "expr"] | None = None, label: str | None = None, ): """ @@ -55,6 +56,7 @@ def __init__( label: an optional string label for the instruction. """ super().__init__("box", body.num_qubits, body.num_clbits, [body], label=label) + self._duration, self._unit = Delay._validate_arguments(duration, unit) self._duration = duration self._unit = unit @@ -72,7 +74,7 @@ def unit(self): return self._unit @unit.setter - def unit(self, value: typing.Literal["dt", "s", "ms", "us", "ns", "ps"]): + def unit(self, value: typing.Literal["dt", "s", "ms", "us", "ns", "ps", "expr"]): self._unit = value @property diff --git a/qiskit/circuit/delay.py b/qiskit/circuit/delay.py index 8f8ba97bfc4c..d64e4d99d11f 100644 --- a/qiskit/circuit/delay.py +++ b/qiskit/circuit/delay.py @@ -45,6 +45,17 @@ def __init__(self, duration, unit=None): CircuitError: A ``duration`` expression was specified with a resolved type that is not timing-based, or the ``unit`` was improperly specified. """ + # Double underscore to differentiate from the private attribute in + # `Instruction`. This can be changed to `_unit` in 2.0 after we + # remove `unit` and `duration` from the standard instruction model + # as it only will exist in `Delay` after that point. + duration, self.__unit = self._validate_arguments(duration, unit) + super().__init__("delay", 1, 0, params=[duration]) + + @staticmethod + def _validate_arguments(duration, unit): + # This method is a centralization of the unit-handling logic, so used elsewhere in Qiskit + # (e.g. in `BoxOp`). if isinstance(duration, expr.Expr): if unit is not None and unit != "expr": raise CircuitError( @@ -59,12 +70,7 @@ def __init__(self, duration, unit=None): unit = "dt" elif unit not in {"s", "ms", "us", "ns", "ps", "dt"}: raise CircuitError(f"Unknown unit {unit} is specified.") - # Double underscore to differentiate from the private attribute in - # `Instruction`. This can be changed to `_unit` in 2.0 after we - # remove `unit` and `duration` from the standard instruction model - # as it only will exist in `Delay` after that point. - self.__unit = unit - super().__init__("delay", 1, 0, params=[duration]) + return duration, unit broadcast_arguments = Gate.broadcast_arguments