From 741295107ad86466b315de1f31c586153233d4b4 Mon Sep 17 00:00:00 2001 From: Santhosh G S Date: Tue, 14 Nov 2023 16:03:22 +0530 Subject: [PATCH 01/15] Singletonized `Measure` and `Reset` instructions (#11170) * Singletonized 'Measure' and 'Reset' instructions and drafted a release note * Added singleton key functionality and test cases for Measure() and Reset() singleton instructions * Implemented corrections from review on test_singleton module --- qiskit/circuit/measure.py | 6 ++- qiskit/circuit/quantumcircuit.py | 6 ++- qiskit/circuit/reset.py | 6 ++- qiskit/qasm2/parse.py | 6 +-- ...etonize-instructions-78723f68cd0ac03f.yaml | 32 +++++++++++++ test/python/circuit/test_singleton.py | 47 +++++++++++++------ 6 files changed, 79 insertions(+), 24 deletions(-) create mode 100644 releasenotes/notes/singletonize-instructions-78723f68cd0ac03f.yaml diff --git a/qiskit/circuit/measure.py b/qiskit/circuit/measure.py index 1da5923e953f..548aaecd81a5 100644 --- a/qiskit/circuit/measure.py +++ b/qiskit/circuit/measure.py @@ -14,17 +14,19 @@ Quantum measurement in the computational basis. """ -from qiskit.circuit.instruction import Instruction +from qiskit.circuit.singleton import SingletonInstruction, stdlib_singleton_key from qiskit.circuit.exceptions import CircuitError -class Measure(Instruction): +class Measure(SingletonInstruction): """Quantum measurement in the computational basis.""" def __init__(self, label=None, *, duration=None, unit="dt"): """Create new measurement instruction.""" super().__init__("measure", 1, 1, [], label=label, duration=duration, unit=unit) + _singleton_lookup_key = stdlib_singleton_key() + def broadcast_arguments(self, qargs, cargs): qarg = qargs[0] carg = cargs[0] diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 76dc11ea55b6..bc89e9740e2f 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -59,8 +59,6 @@ from .bit import Bit from .quantumcircuitdata import QuantumCircuitData, CircuitInstruction from .delay import Delay -from .measure import Measure -from .reset import Reset if typing.TYPE_CHECKING: import qiskit # pylint: disable=cyclic-import @@ -2180,6 +2178,8 @@ def reset(self, qubit: QubitSpecifier) -> InstructionSet: Returns: qiskit.circuit.InstructionSet: handle to the added instruction. """ + from .reset import Reset + return self.append(Reset(), [qubit], []) def measure(self, qubit: QubitSpecifier, cbit: ClbitSpecifier) -> InstructionSet: @@ -2255,6 +2255,8 @@ def measure(self, qubit: QubitSpecifier, cbit: ClbitSpecifier) -> InstructionSet circuit.measure(qreg[1], creg[1]) """ + from .measure import Measure + return self.append(Measure(), [qubit], [cbit]) def measure_active(self, inplace: bool = True) -> Optional["QuantumCircuit"]: diff --git a/qiskit/circuit/reset.py b/qiskit/circuit/reset.py index 3243afad4010..183004dbf877 100644 --- a/qiskit/circuit/reset.py +++ b/qiskit/circuit/reset.py @@ -14,16 +14,18 @@ Qubit reset to computational zero. """ -from qiskit.circuit.instruction import Instruction +from qiskit.circuit.singleton import SingletonInstruction, stdlib_singleton_key -class Reset(Instruction): +class Reset(SingletonInstruction): """Qubit reset.""" def __init__(self, label=None, *, duration=None, unit="dt"): """Create new reset instruction.""" super().__init__("reset", 1, 0, [], label=label, duration=duration, unit=unit) + _singleton_lookup_key = stdlib_singleton_key() + def broadcast_arguments(self, qargs, cargs): for qarg in qargs[0]: yield [qarg], [] diff --git a/qiskit/qasm2/parse.py b/qiskit/qasm2/parse.py index 116c6b7c9aa0..fbd7f48390d8 100644 --- a/qiskit/qasm2/parse.py +++ b/qiskit/qasm2/parse.py @@ -234,15 +234,13 @@ def from_bytecode(bytecode, custom_instructions: Iterable[CustomInstruction]): qc._append(CircuitInstruction(Measure(), (qubits[qubit],), (clbits[clbit],))) elif opcode == OpCode.ConditionedMeasure: qubit, clbit, creg, value = op.operands - measure = Measure() - measure.condition = (qc.cregs[creg], value) + measure = Measure().c_if(qc.cregs[creg], value) qc._append(CircuitInstruction(measure, (qubits[qubit],), (clbits[clbit],))) elif opcode == OpCode.Reset: qc._append(CircuitInstruction(Reset(), (qubits[op.operands[0]],))) elif opcode == OpCode.ConditionedReset: qubit, creg, value = op.operands - reset = Reset() - reset.condition = (qc.cregs[creg], value) + reset = Reset().c_if(qc.cregs[creg], value) qc._append(CircuitInstruction(reset, (qubits[qubit],))) elif opcode == OpCode.Barrier: op_qubits = op.operands[0] diff --git a/releasenotes/notes/singletonize-instructions-78723f68cd0ac03f.yaml b/releasenotes/notes/singletonize-instructions-78723f68cd0ac03f.yaml new file mode 100644 index 000000000000..ced4a873bd6a --- /dev/null +++ b/releasenotes/notes/singletonize-instructions-78723f68cd0ac03f.yaml @@ -0,0 +1,32 @@ +--- +features: + - | + The following standard library instructions are now instances of + :class:`~.SingletonInstruction`: + + * :class:`~.Measure` + * :class:`~.Reset` + + This means that if these classes are instantiated as (e.g.) ``Measure()`` using + all the constructor defaults, they will all share a single global + instance. This results in large reduction in the memory overhead for > 1 + object of these types and significantly faster object construction time. +upgrade: + - | + The following standard library instructions: + + * :class:`~.Measure` + * :class:`~.Reset` + + are immutable, unless the attributes ``label``, ``duration`` and ``unit`` are given as keyword + arguments during class construction. + The attributes :attr:`~.Instruction.label`, :attr:`~.Instruction.duration`, :attr:`~.Instruction.unit`, + and :attr:`~.Instruction.condition` attributes are all not publicly accessible and setting these attributes + directly is not allowed and it will raise an exception. If they are needed for a particular + instance you must ensure you have a mutable instance using :meth:`.Instruction.to_mutable` + and use :meth:`.Instruction.c_if` for :attr:`~.Instruction.condition` + For the singleton variant of these instructions, there is a special attribute + :attr:`~.SingletonInstruction._singleton_lookup_key`, that when called generates a key based on the input + arguments, which can be used for identifying and indexing these instructions within the framework. + + diff --git a/test/python/circuit/test_singleton.py b/test/python/circuit/test_singleton.py index ffa3f6d076bb..ebc656f8d933 100644 --- a/test/python/circuit/test_singleton.py +++ b/test/python/circuit/test_singleton.py @@ -14,7 +14,7 @@ """ -Tests for singleton gate behavior +Tests for singleton gate and instruction behavior """ import copy @@ -36,15 +36,16 @@ XGate, C4XGate, ) +from qiskit.circuit import Measure, Reset from qiskit.circuit import Clbit, QuantumCircuit, QuantumRegister, ClassicalRegister -from qiskit.circuit.singleton import SingletonGate, SingletonInstruction +from qiskit.circuit.singleton import SingletonGate from qiskit.converters import dag_to_circuit, circuit_to_dag from qiskit.test.base import QiskitTestCase -class TestSingletonGate(QiskitTestCase): - """Qiskit SingletonGate tests.""" +class TestSingleton(QiskitTestCase): + """Qiskit SingletonGate and SingletonInstruction tests.""" def test_default_singleton(self): gate = HGate() @@ -325,20 +326,38 @@ def __init__(self, x): self.assertEqual(gate.x, 1) self.assertIsNot(MyAbstractGate(1), MyAbstractGate(1)) - def test_inherit_singleton(self): - class Measure(SingletonInstruction): - def __init__(self): - super().__init__("measure", 1, 1, []) + def test_return_type_singleton_instructions(self): + measure = Measure() + new_measure = Measure() + self.assertIs(measure, new_measure) + self.assertIs(measure.base_class, Measure) + self.assertIsInstance(measure, Measure) + + reset = Reset() + new_reset = Reset() + self.assertIs(reset, new_reset) + self.assertIs(reset.base_class, Reset) + self.assertIsInstance(reset, Reset) + + def test_singleton_instruction_integration(self): + measure = Measure() + reset = Reset() + qc = QuantumCircuit(1, 1) + qc.measure(0, 0) + qc.reset(0) + self.assertIs(qc.data[0].operation, measure) + self.assertIs(qc.data[1].operation, reset) + def test_inherit_singleton_instructions(self): class ESPMeasure(Measure): pass - base = Measure() - esp = ESPMeasure() - self.assertIs(esp, ESPMeasure()) - self.assertIsNot(esp, base) - self.assertIs(base.base_class, Measure) - self.assertIs(esp.base_class, ESPMeasure) + measure_base = Measure() + esp_measure = ESPMeasure() + self.assertIs(esp_measure, ESPMeasure()) + self.assertIsNot(esp_measure, measure_base) + self.assertIs(measure_base.base_class, Measure) + self.assertIs(esp_measure.base_class, ESPMeasure) def test_singleton_with_default(self): # Explicitly setting the label to its default. From 87fcd71e8586fb82e8d55e19d2d76d5821d2f94c Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Tue, 14 Nov 2023 15:09:30 +0100 Subject: [PATCH 02/15] small typos and spelling issues in passes docs (#11163) * small typos and spelling issues in passes docs * revert https://github.com/Qiskit/qiskit/pull/11163#discussion_r1378610584 * Update qiskit/transpiler/passes/analysis/dag_longest_path.py Co-authored-by: Jake Lishman * revert https://github.com/Qiskit/qiskit/pull/11163/files/c87ce1431714a1b5571de4b9c1b58f43b4fa4c31#r1378615081 * https://github.com/Qiskit/qiskit/pull/11163/files/c87ce1431714a1b5571de4b9c1b58f43b4fa4c31#r1378616728 * https://github.com/Qiskit/qiskit/pull/11163/files/c87ce1431714a1b5571de4b9c1b58f43b4fa4c31#r1378617566 * Update qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py Co-authored-by: Jake Lishman * https://github.com/Qiskit/qiskit/pull/11163/files/c87ce1431714a1b5571de4b9c1b58f43b4fa4c31#r1378621645 * https://github.com/Qiskit/qiskit/pull/11163/files/c87ce1431714a1b5571de4b9c1b58f43b4fa4c31#r1378621956 * https://github.com/Qiskit/qiskit/pull/11163/files/c87ce1431714a1b5571de4b9c1b58f43b4fa4c31#r1378625161 * Update qiskit/transpiler/passes/optimization/template_optimization.py Co-authored-by: Jake Lishman --------- Co-authored-by: Jake Lishman --- .../passes/analysis/count_ops_longest_path.py | 4 ++-- .../transpiler/passes/analysis/dag_longest_path.py | 5 +++-- qiskit/transpiler/passes/basis/unroll_3q_or_more.py | 2 +- .../passes/basis/unroll_custom_definitions.py | 2 +- qiskit/transpiler/passes/calibration/pulse_gate.py | 4 ++-- qiskit/transpiler/passes/layout/apply_layout.py | 6 +++--- qiskit/transpiler/passes/layout/dense_layout.py | 4 ++-- .../transpiler/passes/layout/enlarge_with_ancilla.py | 2 +- .../passes/layout/full_ancilla_allocation.py | 2 +- .../transpiler/passes/layout/layout_2q_distance.py | 2 +- qiskit/transpiler/passes/layout/sabre_layout.py | 6 +++--- qiskit/transpiler/passes/layout/vf2_layout.py | 4 ++-- qiskit/transpiler/passes/layout/vf2_post_layout.py | 6 +++--- .../passes/optimization/collect_multiqubit_blocks.py | 6 +++--- .../passes/optimization/commutation_analysis.py | 4 ++-- .../passes/optimization/commutative_cancellation.py | 2 +- .../passes/optimization/consolidate_blocks.py | 8 ++++---- .../optimization/crosstalk_adaptive_schedule.py | 6 +++--- .../passes/optimization/cx_cancellation.py | 4 ++-- .../optimization/echo_rzx_weyl_decomposition.py | 2 +- .../passes/optimization/normalize_rx_angle.py | 2 +- .../passes/optimization/optimize_1q_commutation.py | 10 +++++----- .../passes/optimization/optimize_1q_decomposition.py | 8 ++++---- .../passes/optimization/optimize_1q_gates.py | 4 ++-- .../reset_after_measure_simplification.py | 2 +- .../passes/optimization/template_optimization.py | 6 +++--- qiskit/transpiler/passes/routing/basic_swap.py | 8 ++++---- .../commuting_2q_gate_router.py | 4 ++-- .../passes/routing/layout_transformation.py | 4 ++-- qiskit/transpiler/passes/routing/lookahead_swap.py | 2 +- qiskit/transpiler/passes/routing/stochastic_swap.py | 4 ++-- .../scheduling/padding/dynamical_decoupling.py | 2 +- .../passes/scheduling/padding/pad_delay.py | 2 +- .../transpiler/passes/scheduling/scheduling/asap.py | 2 +- .../passes/scheduling/scheduling/set_io_latency.py | 2 +- .../passes/scheduling/time_unit_conversion.py | 12 ++++++------ qiskit/transpiler/passes/utils/check_map.py | 2 +- qiskit/transpiler/passes/utils/dag_fixed_point.py | 2 +- qiskit/transpiler/passes/utils/error.py | 6 +++--- qiskit/transpiler/passes/utils/fixed_point.py | 2 +- qiskit/transpiler/passes/utils/minimum_point.py | 2 +- .../passes/utils/remove_final_measurements.py | 2 +- qiskit/transpiler/passes/utils/unroll_forloops.py | 6 +++--- 43 files changed, 89 insertions(+), 88 deletions(-) diff --git a/qiskit/transpiler/passes/analysis/count_ops_longest_path.py b/qiskit/transpiler/passes/analysis/count_ops_longest_path.py index 4e06a2e6dd0a..fb3918bfed12 100644 --- a/qiskit/transpiler/passes/analysis/count_ops_longest_path.py +++ b/qiskit/transpiler/passes/analysis/count_ops_longest_path.py @@ -10,13 +10,13 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Count the operations on the longest path in a DAGcircuit.""" +"""Count the operations on the longest path in a DAGCircuit.""" from qiskit.transpiler.basepasses import AnalysisPass class CountOpsLongestPath(AnalysisPass): - """Count the operations on the longest path in a DAGcircuit. + """Count the operations on the longest path in a :class:`.DAGCircuit`. The result is saved in ``property_set['count_ops_longest_path']`` as an integer. """ diff --git a/qiskit/transpiler/passes/analysis/dag_longest_path.py b/qiskit/transpiler/passes/analysis/dag_longest_path.py index e35d86ca63ba..691910d2628f 100644 --- a/qiskit/transpiler/passes/analysis/dag_longest_path.py +++ b/qiskit/transpiler/passes/analysis/dag_longest_path.py @@ -10,13 +10,14 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Return the longest path in a DAGcircuit as a list of DAGNodes.""" +"""Return the longest path in a :class:`.DAGCircuit` as a list of DAGNodes.""" from qiskit.transpiler.basepasses import AnalysisPass class DAGLongestPath(AnalysisPass): - """Return the longest path in a DAGcircuit as a list of DAGOpNodes, DAGInNodes, and DAGOutNodes.""" + """Return the longest path in a :class:`.DAGCircuit` as a list of + :class:`.DAGOpNode`\\ s, :class:`.DAGInNode`\\ s, and :class:`.DAGOutNode`\\ s.""" def run(self, dag): """Run the DAGLongestPath pass on `dag`.""" diff --git a/qiskit/transpiler/passes/basis/unroll_3q_or_more.py b/qiskit/transpiler/passes/basis/unroll_3q_or_more.py index ed6400dce94a..36433c71d1da 100644 --- a/qiskit/transpiler/passes/basis/unroll_3q_or_more.py +++ b/qiskit/transpiler/passes/basis/unroll_3q_or_more.py @@ -27,7 +27,7 @@ def __init__(self, target=None, basis_gates=None): Args: target (Target): The target object representing the compilation - target. If specified any multiqubit instructions in the + target. If specified any multi-qubit instructions in the circuit when the pass is run that are supported by the target device will be left in place. If both this and ``basis_gates`` are specified only the target will be checked. diff --git a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py index 20e96f127d05..12e6811a2f03 100644 --- a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py +++ b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py @@ -29,7 +29,7 @@ def __init__(self, equivalence_library, basis_gates=None, target=None, min_qubit equivalence_library (EquivalenceLibrary): The equivalence library which will be used by the BasisTranslator pass. (Instructions in this library will not be unrolled by this pass.) - basis_gates (Optional[list[str]]): Target basis names to unroll to, e.g. `['u3', 'cx']`. + basis_gates (Optional[list[str]]): Target basis names to unroll to, e.g. ``['u3', 'cx']``. Ignored if ``target`` is also specified. target (Optional[Target]): The :class:`~.Target` object corresponding to the compilation target. When specified, any argument specified for ``basis_gates`` is ignored. diff --git a/qiskit/transpiler/passes/calibration/pulse_gate.py b/qiskit/transpiler/passes/calibration/pulse_gate.py index 9bfd3c544779..eacabbe89057 100644 --- a/qiskit/transpiler/passes/calibration/pulse_gate.py +++ b/qiskit/transpiler/passes/calibration/pulse_gate.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Instruction scheduel map reference pass.""" +"""Instruction schedule map reference pass.""" from typing import List, Union @@ -57,7 +57,7 @@ def __init__( Args: inst_map: Instruction schedule map that user may override. target: The :class:`~.Target` representing the target backend, if both - ``inst_map`` and this are specified then it updates instructions + ``inst_map`` and ``target`` are specified then it updates instructions in the ``target`` with ``inst_map``. """ super().__init__() diff --git a/qiskit/transpiler/passes/layout/apply_layout.py b/qiskit/transpiler/passes/layout/apply_layout.py index b6b7282e304e..8c6ed2cfec3a 100644 --- a/qiskit/transpiler/passes/layout/apply_layout.py +++ b/qiskit/transpiler/passes/layout/apply_layout.py @@ -36,7 +36,7 @@ class ApplyLayout(TransformationPass): """ def run(self, dag): - """Run the ApplyLayout pass on `dag`. + """Run the ApplyLayout pass on ``dag``. Args: dag (DAGCircuit): DAG to map. @@ -45,7 +45,7 @@ def run(self, dag): DAGCircuit: A mapped DAG (with physical qubits). Raises: - TranspilerError: if no layout is found in `property_set` or no full physical qubits. + TranspilerError: if no layout is found in ``property_set`` or no full physical qubits. """ layout = self.property_set["layout"] if not layout: @@ -77,7 +77,7 @@ def run(self, dag): new_dag.apply_operation_back(node.op, qargs, node.cargs, check=False) else: # First build a new layout object going from: - # old virtual -> old phsyical -> new virtual -> new physical + # old virtual -> old physical -> new virtual -> new physical # to: # old virtual -> new physical full_layout = Layout() diff --git a/qiskit/transpiler/passes/layout/dense_layout.py b/qiskit/transpiler/passes/layout/dense_layout.py index 973947420680..71c69739990d 100644 --- a/qiskit/transpiler/passes/layout/dense_layout.py +++ b/qiskit/transpiler/passes/layout/dense_layout.py @@ -31,9 +31,9 @@ class DenseLayout(AnalysisPass): of the circuit (Qubit). Note: - Even though a 'layout' is not strictly a property of the DAG, + Even though a ``'layout'`` is not strictly a property of the DAG, in the transpiler architecture it is best passed around between passes - by being set in `property_set`. + by being set in ``property_set``. """ def __init__(self, coupling_map=None, backend_prop=None, target=None): diff --git a/qiskit/transpiler/passes/layout/enlarge_with_ancilla.py b/qiskit/transpiler/passes/layout/enlarge_with_ancilla.py index 025f846b054b..fb0b24016f66 100644 --- a/qiskit/transpiler/passes/layout/enlarge_with_ancilla.py +++ b/qiskit/transpiler/passes/layout/enlarge_with_ancilla.py @@ -34,7 +34,7 @@ def run(self, dag): DAGCircuit: An extended DAG. Raises: - TranspilerError: If there is not layout in the property set or not set at init time. + TranspilerError: If there is no layout in the property set or not set at init time. """ layout = self.property_set["layout"] diff --git a/qiskit/transpiler/passes/layout/full_ancilla_allocation.py b/qiskit/transpiler/passes/layout/full_ancilla_allocation.py index 81ae7922806e..72d2c6f2c4d0 100644 --- a/qiskit/transpiler/passes/layout/full_ancilla_allocation.py +++ b/qiskit/transpiler/passes/layout/full_ancilla_allocation.py @@ -107,7 +107,7 @@ def run(self, dag): @staticmethod def validate_layout(layout_qubits, dag_qubits): """ - Checks if all the qregs in layout_qregs already exist in dag_qregs. Otherwise, raise. + Checks if all the qregs in ``layout_qregs`` already exist in ``dag_qregs``. Otherwise, raise. """ for qreg in layout_qubits: if qreg not in dag_qubits: diff --git a/qiskit/transpiler/passes/layout/layout_2q_distance.py b/qiskit/transpiler/passes/layout/layout_2q_distance.py index 6af709851702..fa1759a845f0 100644 --- a/qiskit/transpiler/passes/layout/layout_2q_distance.py +++ b/qiskit/transpiler/passes/layout/layout_2q_distance.py @@ -25,7 +25,7 @@ class Layout2qDistance(AnalysisPass): """Evaluate how good the layout selection was. - Saves in `property_set['layout_score']` (or the property name in property_name) + Saves in ``property_set['layout_score']`` (or the property name in property_name) the sum of distances for each circuit CX. The lower the number, the better the selection. Therefore, 0 is a perfect layout selection. No CX direction is considered. diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 0d70650e0eda..ca71ebee2777 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -60,7 +60,7 @@ class SabreLayout(TransformationPass): This method exploits the reversibility of quantum circuits, and tries to include global circuit information in the choice of initial_layout. - By default this pass will run both layout and routing and will transform the + By default, this pass will run both layout and routing and will transform the circuit so that the layout is applied to the input dag (meaning that the output circuit will have ancilla qubits allocated for unused qubits on the coupling map and the qubits will be reordered to match the mapped physical qubits) and then @@ -152,7 +152,7 @@ def __init__( will be raised if both are used. skip_routing (bool): If this is set ``True`` and ``routing_pass`` is not used then routing will not be applied to the output circuit. Only the layout - will be returned in the property set. This is a tradeoff to run custom + will be set in the property set. This is a tradeoff to run custom routing with multiple layout trials, as using this option will cause SabreLayout to run the routing stage internally but not use that result. @@ -440,7 +440,7 @@ def _compose_layouts(self, initial_layout, pass_final_layout, qregs): The routing passes internally start with a trivial layout, as the layout gets applied to the circuit prior to running them. So the - "final_layout" they report must be amended to account for the actual + ``"final_layout"`` they report must be amended to account for the actual initial_layout that was selected. """ trivial_layout = Layout.generate_trivial_layout(*qregs) diff --git a/qiskit/transpiler/passes/layout/vf2_layout.py b/qiskit/transpiler/passes/layout/vf2_layout.py index 5fd6e08ab2fb..4e3077eb1d4d 100644 --- a/qiskit/transpiler/passes/layout/vf2_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_layout.py @@ -37,7 +37,7 @@ class VF2LayoutStopReason(Enum): class VF2Layout(AnalysisPass): - """A pass for choosing a Layout of a circuit onto a Coupling graph, as a + """A pass for choosing a Layout of a circuit onto a Coupling graph, as a subgraph isomorphism problem, solved by VF2++. If a solution is found that means there is a "perfect layout" and that no @@ -52,7 +52,7 @@ class VF2Layout(AnalysisPass): * ``"nonexistent solution"``: If no perfect layout was found. * ``">2q gates in basis"``: If VF2Layout can't work with basis - By default this pass will construct a heuristic scoring map based on the + 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 and set ``vf2_avg_error_map`` in the property set with a :class:`~.ErrorMap` diff --git a/qiskit/transpiler/passes/layout/vf2_post_layout.py b/qiskit/transpiler/passes/layout/vf2_post_layout.py index 1e2d451b4774..1f574fdeed10 100644 --- a/qiskit/transpiler/passes/layout/vf2_post_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_post_layout.py @@ -41,7 +41,7 @@ class VF2PostLayoutStopReason(Enum): def _target_match(node_a, node_b): # Node A is the set of operations in the target. Node B is the count dict - # of oeprations on the node or edge in the circuit. + # of operations on the node or edge in the circuit. if isinstance(node_a, set): return node_a.issuperset(node_b.keys()) # Node A is the count dict of operations on the node or edge in the circuit @@ -77,7 +77,7 @@ class VF2PostLayout(AnalysisPass): * ``"nonexistent solution"``: If no solution was found. * ``">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 + 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 and set ``vf2_avg_error_map`` in the property set with a :class:`~.ErrorMap` @@ -182,7 +182,7 @@ def run(self, dag): 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 + # 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 diff --git a/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py b/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py index b3e3f27b89ef..51b39d7e961b 100644 --- a/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py +++ b/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py @@ -19,11 +19,11 @@ class CollectMultiQBlocks(AnalysisPass): """Collect sequences of uninterrupted gates acting on groups of qubits. - max_block_size specifies the maximum number of qubits that can be acted upon + ``max_block_size`` specifies the maximum number of qubits that can be acted upon by any single group of gates Traverse the DAG and find blocks of gates that act consecutively on - groups of qubits. Write the blocks to propert_set as a list of blocks + groups of qubits. Write the blocks to ``property_set`` as a list of blocks of the form:: [[g0, g1, g2], [g4, g5]] @@ -31,7 +31,7 @@ class CollectMultiQBlocks(AnalysisPass): Blocks are reported in a valid topological order. Further, the gates within each block are also reported in topological order Some gates may not be present in any block (e.g. if the number - of operands is greater than max_block_size) + of operands is greater than ``max_block_size``) A Disjoint Set Union data structure (DSU) is used to maintain blocks as gates are processed. This data structure points each qubit to a set at all diff --git a/qiskit/transpiler/passes/optimization/commutation_analysis.py b/qiskit/transpiler/passes/optimization/commutation_analysis.py index 0963a645c7b3..8c34c911a6ca 100644 --- a/qiskit/transpiler/passes/optimization/commutation_analysis.py +++ b/qiskit/transpiler/passes/optimization/commutation_analysis.py @@ -22,7 +22,7 @@ class CommutationAnalysis(AnalysisPass): """Analysis pass to find commutation relations between DAG nodes. - Property_set['commutation_set'] is a dictionary that describes + ``property_set['commutation_set']`` is a dictionary that describes the commutation relations on a given wire, all the gates on a wire are grouped into a set of gates that commute. """ @@ -35,7 +35,7 @@ def run(self, dag): """Run the CommutationAnalysis pass on `dag`. Run the pass on the DAG, and write the discovered commutation relations - into the property_set. + into the ``property_set``. """ # Initiate the commutation set self.property_set["commutation_set"] = defaultdict(list) diff --git a/qiskit/transpiler/passes/optimization/commutative_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_cancellation.py index 777c4ff3dc48..b0eb6bd24137 100644 --- a/qiskit/transpiler/passes/optimization/commutative_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_cancellation.py @@ -50,7 +50,7 @@ def __init__(self, basis_gates=None, target=None): the set intersection between the ``basis_gates`` parameter and the gates in the dag. target (Target): The :class:`~.Target` representing the target backend, if both - ``basis_gates`` and this are specified then this argument will take + ``basis_gates`` and ``target`` are specified then this argument will take precedence and ``basis_gates`` will be ignored. """ super().__init__() diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py index 6f973ee3fd6f..71065113bb5a 100644 --- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py +++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py @@ -55,15 +55,15 @@ def __init__( ): """ConsolidateBlocks initializer. - If `kak_basis_gate` is not `None` it will be used as the basis gate for KAK decomposition. - Otherwise, if `basis_gates` is not `None` a basis gate will be chosen from this list. - Otherwise the basis gate will be `CXGate`. + If ``kak_basis_gate`` is not ``None`` it will be used as the basis gate for KAK decomposition. + Otherwise, if ``basis_gates`` is not ``None`` a basis gate will be chosen from this list. + Otherwise, the basis gate will be :class:`.CXGate`. Args: kak_basis_gate (Gate): Basis gate for KAK decomposition. force_consolidate (bool): Force block consolidation. basis_gates (List(str)): Basis gates from which to choose a KAK gate. - approximation_degree (float): a float between [0.0, 1.0]. Lower approximates more. + approximation_degree (float): a float between $[0.0, 1.0]$. Lower approximates more. target (Target): The target object for the compilation target backend. """ super().__init__() diff --git a/qiskit/transpiler/passes/optimization/crosstalk_adaptive_schedule.py b/qiskit/transpiler/passes/optimization/crosstalk_adaptive_schedule.py index 387bdc3ba4e0..cfde23061020 100644 --- a/qiskit/transpiler/passes/optimization/crosstalk_adaptive_schedule.py +++ b/qiskit/transpiler/passes/optimization/crosstalk_adaptive_schedule.py @@ -23,8 +23,8 @@ with simultaneous two-qubit and one-qubit gates. The method ignores crosstalk between pairs of single qubit gates. -The method assumes that all qubits get measured simultaneously whether or not -they need a measurement. This assumption is based on current device properties +The method assumes that all qubits get measured simultaneously, whether +they need a measurement or not. This assumption is based on current device properties and may need to be revised for future device generations. """ @@ -89,7 +89,7 @@ def __init__( inserts the measure gates. If CrosstalkAdaptiveSchedule is made aware of those measurements, it is included in the optimization. target (Target): A target representing the target backend, if both - ``backend_prop`` and this are specified then this argument will take + ``backend_prop`` and ``target`` are specified then this argument will take precedence and ``coupling_map`` will be ignored. Raises: ImportError: if unable to import z3 solver diff --git a/qiskit/transpiler/passes/optimization/cx_cancellation.py b/qiskit/transpiler/passes/optimization/cx_cancellation.py index 6dd4a5364552..df1ffc8ebe23 100644 --- a/qiskit/transpiler/passes/optimization/cx_cancellation.py +++ b/qiskit/transpiler/passes/optimization/cx_cancellation.py @@ -10,14 +10,14 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Cancel back-to-back `cx` gates in dag.""" +"""Cancel back-to-back ``cx`` gates in dag.""" from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passes.utils import control_flow class CXCancellation(TransformationPass): - """Cancel back-to-back `cx` gates in dag.""" + """Cancel back-to-back ``cx`` gates in dag.""" @control_flow.trivial_recurse def run(self, dag): diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py index 5f4be27d220e..9352ce08f365 100644 --- a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -41,7 +41,7 @@ def __init__(self, instruction_schedule_map=None, target=None): instruction_schedule_map (InstructionScheduleMap): the mapping from circuit :class:`~.circuit.Instruction` names and arguments to :class:`.Schedule`\\ s. target (Target): The :class:`~.Target` representing the target backend, if both - ``instruction_schedule_map`` and this are specified then this argument will take + ``instruction_schedule_map`` and ``target`` are specified then this argument will take precedence and ``instruction_schedule_map`` will be ignored. """ super().__init__() diff --git a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py index c8d9619c673f..b6f36e07de36 100644 --- a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py +++ b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py @@ -57,7 +57,7 @@ def __init__(self, target=None, resolution_in_radian=0): corresponding RX gates with SX and X gates. resolution_in_radian (float): Resolution for RX rotation angle quantization. If set to zero, this pass won't modify the rotation angles in the given DAG. - (=Provides aribitary-angle RX) + (=Provides arbitrary-angle RX) """ super().__init__() self.target = target diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py b/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py index fea459b41b9e..450490734e46 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py @@ -46,10 +46,10 @@ class Optimize1qGatesSimpleCommutation(TransformationPass): """ - Optimizes 1Q gate strings interrupted by 2Q gates by commuting the components and re- - synthesizing the results. The commutation rules are stored in `commutation_table`. + Optimizes 1Q gate strings interrupted by 2Q gates by commuting the components and + resynthesizing the results. The commutation rules are stored in ``commutation_table``. - NOTE: In addition to those mentioned in `commutation_table`, this pass has some limitations: + NOTE: In addition to those mentioned in ``commutation_table``, this pass has some limitations: + Does not handle multiple commutations in a row without intermediate progress. + Can only commute into positions where there are pre-existing runs. + Does not exhaustively test all the different ways commuting gates can be assigned to @@ -58,7 +58,7 @@ class Optimize1qGatesSimpleCommutation(TransformationPass): barriers.) """ - # NOTE: A run from `dag.collect_1q_runs` is always nonempty, so we sometimes use an empty list + # NOTE: A run from dag.collect_1q_runs is always nonempty, so we sometimes use an empty list # to signify the absence of a run. def __init__(self, basis=None, run_to_completion=False, target=None): @@ -83,7 +83,7 @@ def _find_adjoining_run(dag, runs, run, front=True): Finds the run which abuts `run` from the front (or the rear if `front == False`), separated by a blocking node. - Returns a pair of the abutting multi-qubit gate and the run which it separates from this + Returns a pair of the abutting multiqubit gate and the run which it separates from this one. The next run can be the empty list `[]` if it is absent. """ edge_node = run[0] if front else run[-1] diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index 854fe8204551..f96ed999061d 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -59,9 +59,9 @@ class Optimize1qGatesDecomposition(TransformationPass): """Optimize chains of single-qubit gates by combining them into a single gate. - The decision to replace the original chain with a new resynthesis depends on: + The decision to replace the original chain with a new re-synthesis depends on: - whether the original chain was out of basis: replace - - whether the original chain was in basis but resynthesis is lower error: replace + - whether the original chain was in basis but re-synthesis is lower error: replace - whether the original chain contains a pulse gate: do not replace - whether the original chain amounts to identity: replace with null @@ -110,12 +110,12 @@ def _build_error_map(self): def _resynthesize_run(self, matrix, qubit=None): """ - Resynthesizes one 2x2 `matrix`, typically extracted via `dag.collect_1q_runs`. + Re-synthesizes one 2x2 `matrix`, typically extracted via `dag.collect_1q_runs`. Returns the newly synthesized circuit in the indicated basis, or None if no synthesis routine applied. - When multiple synthesis options are available, it prefers the one with lowest + When multiple synthesis options are available, it prefers the one with the lowest error when the circuit is applied to `qubit`. """ if self._target: diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_gates.py b/qiskit/transpiler/passes/optimization/optimize_1q_gates.py index 1dd03a738e32..3d080219274b 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_gates.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_gates.py @@ -43,7 +43,7 @@ def __init__(self, basis=None, eps=1e-15, target=None): the set `{'u1','u2','u3', 'u', 'p'}`. eps (float): EPS to check against target (Target): The :class:`~.Target` representing the target backend, if both - ``basis`` and this are specified then this argument will take + ``basis`` and ``target`` are specified then this argument will take precedence and ``basis`` will be ignored. """ super().__init__() @@ -61,7 +61,7 @@ def run(self, dag): DAGCircuit: the optimized DAG. Raises: - TranspilerError: if YZY and ZYZ angles do not give same rotation matrix. + TranspilerError: if ``YZY`` and ``ZYZ`` angles do not give same rotation matrix. """ use_u = "u" in self.basis use_p = "p" in self.basis diff --git a/qiskit/transpiler/passes/optimization/reset_after_measure_simplification.py b/qiskit/transpiler/passes/optimization/reset_after_measure_simplification.py index d49485784026..faf0ed8de6b0 100644 --- a/qiskit/transpiler/passes/optimization/reset_after_measure_simplification.py +++ b/qiskit/transpiler/passes/optimization/reset_after_measure_simplification.py @@ -26,7 +26,7 @@ class ResetAfterMeasureSimplification(TransformationPass): This optimization is suitable for use on IBM Quantum systems where the reset operation is performed by a measurement followed by a conditional - x-gate. It might not be desireable on other backends if reset is implemented + x-gate. It might not be desirable on other backends if reset is implemented differently. """ diff --git a/qiskit/transpiler/passes/optimization/template_optimization.py b/qiskit/transpiler/passes/optimization/template_optimization.py index 0aeea99ab3c6..f4274d2331ec 100644 --- a/qiskit/transpiler/passes/optimization/template_optimization.py +++ b/qiskit/transpiler/passes/optimization/template_optimization.py @@ -57,16 +57,16 @@ def __init__( heuristics_backward_param (list[int]): [length, survivor] Those are the parameters for applying heuristics on the backward part of the algorithm. This part of the algorithm creates a tree of matching scenario. This tree grows exponentially. The - heuristics evaluates which scenarios have the longest match and keep only those. + heuristics evaluate which scenarios have the longest match and keep only those. The length is the interval in the tree for cutting it and survivor is the number - of scenarios that are kept. We advice to use l=3 and s=1 to have serious time + of scenarios that are kept. We advise to use l=3 and s=1 to have serious time advantage. We remind that the heuristics implies losing a part of the maximal matches. Check reference for more details. heuristics_qubits_param (list[int]): [length] The heuristics for the qubit choice make guesses from the dag dependency of the circuit in order to limit the number of qubit configurations to explore. The length is the number of successors or not predecessors that will be explored in the dag dependency of the circuit, each - qubits of the nodes are added to the set of authorized qubits. We advice to use + qubits of the nodes are added to the set of authorized qubits. We advise to use length=1. Check reference for more details. user_cost_dict (Dict[str, int]): quantum cost dictionary passed to TemplateSubstitution to configure its behavior. This will override any default values if None diff --git a/qiskit/transpiler/passes/routing/basic_swap.py b/qiskit/transpiler/passes/routing/basic_swap.py index af721ee51c7d..c73b2fb009c6 100644 --- a/qiskit/transpiler/passes/routing/basic_swap.py +++ b/qiskit/transpiler/passes/routing/basic_swap.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Map (with minimum effort) a DAGCircuit onto a `coupling_map` adding swap gates.""" +"""Map (with minimum effort) a DAGCircuit onto a ``coupling_map`` adding swap gates.""" from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError @@ -22,7 +22,7 @@ class BasicSwap(TransformationPass): - """Map (with minimum effort) a DAGCircuit onto a `coupling_map` adding swap gates. + """Map (with minimum effort) a DAGCircuit onto a ``coupling_map`` adding swap gates. The basic mapper is a minimum effort to insert swap gates to map the DAG onto a coupling map. When a cx is not in the coupling map possibilities, it inserts @@ -34,7 +34,7 @@ def __init__(self, coupling_map, fake_run=False): Args: coupling_map (Union[CouplingMap, Target]): Directed graph represented a coupling map. - fake_run (bool): if true, it only pretend to do routing, i.e., no + fake_run (bool): if true, it will only pretend to do routing, i.e., no swap is effectively added. """ super().__init__() @@ -57,7 +57,7 @@ def run(self, dag): Raises: TranspilerError: if the coupling map or the layout are not - compatible with the DAG, or if the coupling_map=None. + compatible with the DAG, or if the ``coupling_map=None``. """ if self.fake_run: return self._fake_run(dag) diff --git a/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_gate_router.py b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_gate_router.py index 60b5cf9e3ad3..65d39abf1ab7 100644 --- a/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_gate_router.py +++ b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_gate_router.py @@ -33,7 +33,7 @@ class Commuting2qGateRouter(TransformationPass): The mapping to the coupling map is done using swap strategies, see :class:`.SwapStrategy`. The swap strategy should suit the problem and the coupling map. This transpiler pass should ideally be executed before the quantum circuit is enlarged with any idle ancilla - qubits. Otherwise we may swap qubits outside of the portion of the chip we want to use. + qubits. Otherwise, we may swap qubits outside the portion of the chip we want to use. Therefore, the swap strategy and its associated coupling map do not represent physical qubits. Instead, they represent an intermediate mapping that corresponds to the physical qubits once the initial layout is applied. The example below shows how to map a four @@ -111,7 +111,7 @@ def __init__( Args: swap_strategy: An instance of a :class:`.SwapStrategy` that holds the swap layers that are used, and the order in which to apply them, to map the instruction to - the hardware. If this field is not given if should be contained in the + the hardware. If this field is not given, it should be contained in the property set of the pass. This allows other passes to determine the most appropriate swap strategy at run-time. edge_coloring: An optional edge coloring of the coupling map (I.e. no two edges that diff --git a/qiskit/transpiler/passes/routing/layout_transformation.py b/qiskit/transpiler/passes/routing/layout_transformation.py index 218e6ca958d3..42bf44e13f2a 100644 --- a/qiskit/transpiler/passes/routing/layout_transformation.py +++ b/qiskit/transpiler/passes/routing/layout_transformation.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Map (with minimum effort) a DAGCircuit onto a `coupling_map` adding swap gates.""" +"""Map (with minimum effort) a DAGCircuit onto a ``coupling_map`` adding swap gates.""" from __future__ import annotations import numpy as np @@ -49,7 +49,7 @@ def __init__( to_layout (Union[Layout, str]): The final layout of qubits on physical qubits. - If the type is str, look up `property_set` when this pass runs. + If the type is str, look up ``property_set`` when this pass runs. seed (Union[int, np.random.default_rng]): Seed to use for random trials. diff --git a/qiskit/transpiler/passes/routing/lookahead_swap.py b/qiskit/transpiler/passes/routing/lookahead_swap.py index 0893736a2aca..a961162ab802 100644 --- a/qiskit/transpiler/passes/routing/lookahead_swap.py +++ b/qiskit/transpiler/passes/routing/lookahead_swap.py @@ -90,7 +90,7 @@ def __init__(self, coupling_map, search_depth=4, search_width=4, fake_run=False) coupling_map (Union[CouplingMap, Target]): CouplingMap of the target backend. search_depth (int): lookahead tree depth when ranking best SWAP options. search_width (int): lookahead tree width when ranking best SWAP options. - fake_run (bool): if true, it only pretend to do routing, i.e., no + fake_run (bool): if true, it will only pretend to do routing, i.e., no swap is effectively added. """ diff --git a/qiskit/transpiler/passes/routing/stochastic_swap.py b/qiskit/transpiler/passes/routing/stochastic_swap.py index 90001bb06cc8..52f0d569931d 100644 --- a/qiskit/transpiler/passes/routing/stochastic_swap.py +++ b/qiskit/transpiler/passes/routing/stochastic_swap.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Map a DAGCircuit onto a `coupling_map` adding swap gates.""" +"""Map a DAGCircuit onto a ``coupling_map`` adding swap gates.""" import itertools import logging @@ -72,7 +72,7 @@ def __init__(self, coupling_map, trials=20, seed=None, fake_run=False, initial_l map. trials (int): maximum number of iterations to attempt seed (int): seed for random number generator - fake_run (bool): if true, it only pretend to do routing, i.e., no + fake_run (bool): if true, it will only pretend to do routing, i.e., no swap is effectively added. initial_layout (Layout): starting layout at beginning of pass. """ diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 9137133567ec..97946c55df8b 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -42,7 +42,7 @@ class PadDynamicalDecoupling(BasePadding): so do not alter the logical action of the circuit, but have the effect of mitigating decoherence in those idle periods. - As a special case, the pass allows a length-1 sequence (e.g. [XGate()]). + As a special case, the pass allows a length-1 sequence (e.g. ``[XGate()]``). In this case the DD insertion happens only when the gate inverse can be absorbed into a neighboring gate in the circuit (so we would still be replacing Delay with something that is equivalent to the identity). diff --git a/qiskit/transpiler/passes/scheduling/padding/pad_delay.py b/qiskit/transpiler/passes/scheduling/padding/pad_delay.py index f1517900874f..886bb6a37974 100644 --- a/qiskit/transpiler/passes/scheduling/padding/pad_delay.py +++ b/qiskit/transpiler/passes/scheduling/padding/pad_delay.py @@ -57,7 +57,7 @@ def __init__(self, fill_very_end: bool = True, target: Target = None): Args: fill_very_end: Set ``True`` to fill the end of circuit with delay. target: The :class:`~.Target` representing the target backend. - If it supplied and it does not support delay instruction on a qubit, + If it is supplied and does not support delay instruction on a qubit, padding passes do not pad any idle time of the qubit. """ super().__init__(target=target) diff --git a/qiskit/transpiler/passes/scheduling/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/scheduling/asap.py index ce0e1d1948a0..fa07ae0c5f61 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/asap.py @@ -18,7 +18,7 @@ class ASAPScheduleAnalysis(BaseScheduler): - """ASAP Scheduling pass, which schedules the start time of instructions as early as possible.. + """ASAP Scheduling pass, which schedules the start time of instructions as early as possible. See the :ref:`scheduling_stage` section in the :mod:`qiskit.transpiler` module documentation for the detailed behavior of the control flow diff --git a/qiskit/transpiler/passes/scheduling/scheduling/set_io_latency.py b/qiskit/transpiler/passes/scheduling/scheduling/set_io_latency.py index 1116123cfa51..3b0ed1692f07 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/set_io_latency.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/set_io_latency.py @@ -19,7 +19,7 @@ class SetIOLatency(AnalysisPass): """Set IOLatency information to the input circuit. The ``clbit_write_latency`` and ``conditional_latency`` are added to - the property set of pass manager. These information can be shared among the passes + the property set of pass manager. This information can be shared among the passes that perform scheduling on instructions acting on classical registers. Once these latencies are added to the property set, this information diff --git a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py index c75e22f285b8..d53c3fc4ef6a 100644 --- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py +++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py @@ -25,15 +25,15 @@ class TimeUnitConversion(TransformationPass): """Choose a time unit to be used in the following time-aware passes, and make all circuit time units consistent with that. - This pass will add a .duration metadata to each op whose duration is known, + This pass will add a :attr:`.Instruction.duration` metadata to each op whose duration is known which will be used by subsequent scheduling passes for scheduling. - If dt (dt in seconds) is known to transpiler, the unit 'dt' is chosen. Otherwise, + If ``dt`` (in seconds) is known to transpiler, the unit ``'dt'`` is chosen. Otherwise, the unit to be selected depends on what units are used in delays and instruction durations: - * 's': if they are all in SI units. - * 'dt': if they are all in the unit 'dt'. - * raise error: if they are a mix of SI units and 'dt'. + * ``'s'``: if they are all in SI units. + * ``'dt'``: if they are all in the unit ``'dt'``. + * raise error: if they are a mix of SI units and ``'dt'``. """ def __init__(self, inst_durations: InstructionDurations = None, target: Target = None): @@ -42,7 +42,7 @@ def __init__(self, inst_durations: InstructionDurations = None, target: Target = Args: inst_durations (InstructionDurations): A dictionary of durations of instructions. target: The :class:`~.Target` representing the target backend, if both - ``inst_durations`` and this are specified then this argument will take + ``inst_durations`` and ``target`` are specified then this argument will take precedence and ``inst_durations`` will be ignored. diff --git a/qiskit/transpiler/passes/utils/check_map.py b/qiskit/transpiler/passes/utils/check_map.py index d56a709253c9..61ddc71d131e 100644 --- a/qiskit/transpiler/passes/utils/check_map.py +++ b/qiskit/transpiler/passes/utils/check_map.py @@ -21,7 +21,7 @@ class CheckMap(AnalysisPass): """Check if a DAG circuit is already mapped to a coupling map. - Check if a DAGCircuit is mapped to `coupling_map` by checking that all + Check if a DAGCircuit is mapped to ``coupling_map`` by checking that all 2-qubit interactions are laid out to be on adjacent qubits in the global coupling map of the device, setting the property set field (either specified with ``property_set_field`` or the default ``is_swap_mapped``) to ``True`` or ``False`` accordingly. Note this does not diff --git a/qiskit/transpiler/passes/utils/dag_fixed_point.py b/qiskit/transpiler/passes/utils/dag_fixed_point.py index 2f9f486545b5..60da251b3099 100644 --- a/qiskit/transpiler/passes/utils/dag_fixed_point.py +++ b/qiskit/transpiler/passes/utils/dag_fixed_point.py @@ -21,7 +21,7 @@ class DAGFixedPoint(AnalysisPass): """Check if the DAG has reached a fixed point. A dummy analysis pass that checks if the DAG a fixed point (the DAG is not - modified anymore). The results is saved in + modified anymore). The result is saved in ``property_set['dag_fixed_point']`` as a boolean. """ diff --git a/qiskit/transpiler/passes/utils/error.py b/qiskit/transpiler/passes/utils/error.py index 445b1d21d565..f2659ec052ff 100644 --- a/qiskit/transpiler/passes/utils/error.py +++ b/qiskit/transpiler/passes/utils/error.py @@ -31,9 +31,9 @@ def __init__(self, msg=None, action="raise"): will be used. This can be either a raw string, or a callback function that accepts the current ``property_set`` and returns the desired message. action (str): the action to perform. Default: 'raise'. The options are: - * 'raise': Raises a `TranspilerError` exception with msg - * 'warn': Raises a non-fatal warning with msg - * 'log': logs in `logging.getLogger(__name__)` + * ``'raise'``: Raises a ``TranspilerError`` exception with msg + * ``'warn'``: Raises a non-fatal warning with msg + * ``'log'``: logs in ``logging.getLogger(__name__)`` Raises: TranspilerError: if action is not valid. diff --git a/qiskit/transpiler/passes/utils/fixed_point.py b/qiskit/transpiler/passes/utils/fixed_point.py index 34fa1faabe3b..fbef9d0a85ef 100644 --- a/qiskit/transpiler/passes/utils/fixed_point.py +++ b/qiskit/transpiler/passes/utils/fixed_point.py @@ -21,7 +21,7 @@ class FixedPoint(AnalysisPass): """Check if a property reached a fixed point. A dummy analysis pass that checks if a property reached a fixed point. - The results is saved in ``property_set['_fixed_point']`` + The result is saved in ``property_set['_fixed_point']`` as a boolean. """ diff --git a/qiskit/transpiler/passes/utils/minimum_point.py b/qiskit/transpiler/passes/utils/minimum_point.py index 8ccfa4a7f951..1dcef0db2181 100644 --- a/qiskit/transpiler/passes/utils/minimum_point.py +++ b/qiskit/transpiler/passes/utils/minimum_point.py @@ -44,7 +44,7 @@ class MinimumPoint(TransformationPass): Fields used by this pass in the property set are (all relative to the ``prefix`` argument): - * ``{prefix}_minimum_point_state`` - Used to track the state of the minimpoint search + * ``{prefix}_minimum_point_state`` - Used to track the state of the minimum point search * ``{prefix}_minimum_point`` - This value gets set to ``True`` when either a fixed point is reached over the ``backtrack_depth`` executions, or ``backtrack_depth`` was exceeded and an earlier minimum is restored. diff --git a/qiskit/transpiler/passes/utils/remove_final_measurements.py b/qiskit/transpiler/passes/utils/remove_final_measurements.py index 3c60adeabbba..c3179e204cd4 100644 --- a/qiskit/transpiler/passes/utils/remove_final_measurements.py +++ b/qiskit/transpiler/passes/utils/remove_final_measurements.py @@ -26,7 +26,7 @@ class RemoveFinalMeasurements(TransformationPass): Classical registers are removed iff they reference at least one bit that has become unused by the circuit as a result of the operation, and all - of their other bits are also unused. Seperately, classical bits are removed + of their other bits are also unused. Separately, classical bits are removed iff they have become unused by the circuit as a result of the operation, or they appear in a removed classical register, but do not appear in a classical register that will remain. diff --git a/qiskit/transpiler/passes/utils/unroll_forloops.py b/qiskit/transpiler/passes/utils/unroll_forloops.py index 8cde3874adcb..b8ebe853f1e0 100644 --- a/qiskit/transpiler/passes/utils/unroll_forloops.py +++ b/qiskit/transpiler/passes/utils/unroll_forloops.py @@ -22,8 +22,8 @@ class UnrollForLoops(TransformationPass): """``UnrollForLoops`` transpilation pass unrolls for-loops when possible.""" def __init__(self, max_target_depth=-1): - """Things like `for x in {0, 3, 4} {rx(x) qr[1];}` will turn into - `rx(0) qr[1]; rx(3) qr[1]; rx(4) qr[1];`. + """Things like ``for x in {0, 3, 4} {rx(x) qr[1];}`` will turn into + ``rx(0) qr[1]; rx(3) qr[1]; rx(4) qr[1];``. .. note:: The ``UnrollForLoops`` unrolls only one level of block depth. No inner loop will @@ -38,7 +38,7 @@ def __init__(self, max_target_depth=-1): @control_flow.trivial_recurse def run(self, dag): - """Run the UnrollForLoops pass on `dag`. + """Run the UnrollForLoops pass on ``dag``. Args: dag (DAGCircuit): the directed acyclic graph to run on. From d418d8e240deb37e866095bef758ae593889064b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <99898527+grossardt@users.noreply.github.com> Date: Tue, 14 Nov 2023 18:11:48 +0100 Subject: [PATCH 03/15] Fix math parsing error for Pauli docstring (#11238) --- qiskit/quantum_info/operators/symplectic/pauli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/quantum_info/operators/symplectic/pauli.py b/qiskit/quantum_info/operators/symplectic/pauli.py index 295d8a97d01c..c5b93ceaf4e7 100644 --- a/qiskit/quantum_info/operators/symplectic/pauli.py +++ b/qiskit/quantum_info/operators/symplectic/pauli.py @@ -110,7 +110,7 @@ class initialization (``Pauli('-iXYZ')``). A ``Pauli`` object can be P = (-i)^{q + z\cdot x} Z^z \cdot X^x. - The :math:`k`th qubit corresponds to the :math:`k`th entry in the + The :math:`k`-th qubit corresponds to the :math:`k`-th entry in the :math:`z` and :math:`x` arrays .. math:: From d68076b8d4b9f49c08e1202b8a2f3acb1299308a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 14 Nov 2023 17:06:03 -0500 Subject: [PATCH 04/15] Add new transpiler exception class for too many qubits (#11241) * Add new transpiler exception class for too many qubits This commit adds a new exception class for when the transpiler is given a circuit too many qubits for a given backend. Previously the generic TranspilerError was raised for this, but it made it difficult for downstream users to catch as it wasn't easy to differentiate this error condition from other TranspilerError exceptions. There isn't any backwards compatibility issues with this because the new CircuitToWideForTarget class is a subclass of TranspilerError so any of the previous catches for TranspilerError will still catch this. * Fix typo in class name * Make test check more specific exception type * Replace :class: with :exc: in release note --- qiskit/compiler/transpiler.py | 4 ++-- qiskit/transpiler/__init__.py | 10 +++++++++- qiskit/transpiler/exceptions.py | 4 ++++ .../notes/new-exception-too-wide-3231c1df15952445.yaml | 9 +++++++++ test/python/compiler/test_transpiler.py | 4 ++-- 5 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/new-exception-too-wide-3231c1df15952445.yaml diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 22dfd35be3c2..a2b3971b5944 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -28,7 +28,7 @@ from qiskit.pulse import Schedule, InstructionScheduleMap from qiskit.transpiler import Layout, CouplingMap, PropertySet from qiskit.transpiler.basepasses import BasePass -from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.exceptions import TranspilerError, CircuitTooWideForTarget from qiskit.transpiler.instruction_durations import InstructionDurations, InstructionDurationsType from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager @@ -455,7 +455,7 @@ def _check_circuits_coupling_map(circuits, cmap, backend): # If coupling_map is not None or num_qubits == 1 num_qubits = len(circuit.qubits) if max_qubits is not None and (num_qubits > max_qubits): - raise TranspilerError( + raise CircuitTooWideForTarget( f"Number of qubits ({num_qubits}) in {circuit.name} " f"is greater than maximum ({max_qubits}) in the coupling_map" ) diff --git a/qiskit/transpiler/__init__.py b/qiskit/transpiler/__init__.py index 9abab3aa56a2..13ae12e09097 100644 --- a/qiskit/transpiler/__init__.py +++ b/qiskit/transpiler/__init__.py @@ -1251,6 +1251,8 @@ .. autoexception:: TranspilerAccessError .. autoexception:: CouplingError .. autoexception:: LayoutError +.. autoexception:: CircuitTooWideForTarget + """ # For backward compatibility @@ -1263,7 +1265,13 @@ from .passmanager import PassManager, StagedPassManager from .passmanager_config import PassManagerConfig from .propertyset import PropertySet # pylint: disable=no-name-in-module -from .exceptions import TranspilerError, TranspilerAccessError, CouplingError, LayoutError +from .exceptions import ( + TranspilerError, + TranspilerAccessError, + CouplingError, + LayoutError, + CircuitTooWideForTarget, +) from .fencedobjs import FencedDAGCircuit, FencedPropertySet from .basepasses import AnalysisPass, TransformationPass from .coupling import CouplingMap diff --git a/qiskit/transpiler/exceptions.py b/qiskit/transpiler/exceptions.py index ef79603bfed2..5c23cb2b3914 100644 --- a/qiskit/transpiler/exceptions.py +++ b/qiskit/transpiler/exceptions.py @@ -49,3 +49,7 @@ def __init__(self, *msg): def __str__(self): """Return the message.""" return repr(self.msg) + + +class CircuitTooWideForTarget(TranspilerError): + """Error raised if the circuit is too wide for the target.""" diff --git a/releasenotes/notes/new-exception-too-wide-3231c1df15952445.yaml b/releasenotes/notes/new-exception-too-wide-3231c1df15952445.yaml new file mode 100644 index 000000000000..d3bfa72c071e --- /dev/null +++ b/releasenotes/notes/new-exception-too-wide-3231c1df15952445.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Added a new exception class :exc:`.CircuitToWideForTarget` which + subclasses :exc:`.TranspilerError`. It's used in places where a + :exc:`.TranspilerError` was previously raised when the error was that + the number of circuit qubits was larger than the target backend's qubits. + The new class enables more differentiating between this error condition and + other :exc:`.TranspilerError`\s. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 9a54bdc7da0b..98db50a28b08 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -87,7 +87,7 @@ from qiskit.test import QiskitTestCase, slow_test from qiskit.tools import parallel from qiskit.transpiler import CouplingMap, Layout, PassManager, TransformationPass -from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.exceptions import TranspilerError, CircuitTooWideForTarget from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection, VF2PostLayout from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager, level_0_pass_manager @@ -987,7 +987,7 @@ def test_check_circuit_width(self): qc = QuantumCircuit(15, 15) - with self.assertRaises(TranspilerError): + with self.assertRaises(CircuitTooWideForTarget): transpile(qc, coupling_map=cmap) @data(0, 1, 2, 3) From 258fb234487431ab7b53c3e214d9e05e2fb3a876 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 14 Nov 2023 17:15:33 -0500 Subject: [PATCH 05/15] Speed up InverseCancellation when there's nothing to cancel (#11211) * Speed up InverseCancellation when there's nothing to cancel In the cases when there is nothing to cancel in the DAGCircuit the InverseCancellation pass would iterate over the entire circuit twice, once to search for self inverse gates and then a second time to search for other inverse pairs. This is typically fairly fast because the actual search is done in rust, but there is a python callback function that is called for each node. Depending on the size of the circuit this could add up to a significant amount of time. This commit updates the logic in the pass to first check if there are any operations being asked to cancel for either stage, and if there are then only do the iteration if there are any gates in the circuit in the set of intructions that will be cancelled, which means we'll need to do an iteration. These checks are all O(1) for any sized dag, so they're much lower overhead and will mean the pass executes much faster in the case when there isn't anything to cancel. * Speed-up when there is a partial match Building off of the previous commit this speeds up the inverse cancellation pass when only some of the inverse pairs are not present in the DAG, but others are present. In this case the previous commit would still iterate over the full dag multiple times even when we know some of the inverse pairs are not present in the DAG. This commit updates the logic to not call collect_runs() if we know it's going to be empty or there is no cancellation opportunity. * Expand test coverage * Add more tests * Fix handling of parameterized gates This commit adds a fix for an issue that was caught while tuning the performance of the pass. If a parameterized self inverse was passed in the pass would incorrectly treat all instances of that parameterized' gate as being a self inverse without checking the parameter values. This commit corrects this oversight to handle this case to only cancel gates when the optimization is correct. --- .../optimization/inverse_cancellation.py | 47 +++++-- ...terized-self-inverse-7cb2d68b273640f8.yaml | 22 ++++ .../transpiler/test_inverse_cancellation.py | 117 +++++++++++++++++- 3 files changed, 175 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/fix-parameterized-self-inverse-7cb2d68b273640f8.yaml diff --git a/qiskit/transpiler/passes/optimization/inverse_cancellation.py b/qiskit/transpiler/passes/optimization/inverse_cancellation.py index 35dae7d85f07..560cdcd707f4 100644 --- a/qiskit/transpiler/passes/optimization/inverse_cancellation.py +++ b/qiskit/transpiler/passes/optimization/inverse_cancellation.py @@ -58,12 +58,16 @@ def __init__(self, gates_to_cancel: List[Union[Gate, Tuple[Gate, Gate]]]): self.self_inverse_gates = [] self.inverse_gate_pairs = [] + self.self_inverse_gate_names = set() + self.inverse_gate_pairs_names = set() for gates in gates_to_cancel: if isinstance(gates, Gate): self.self_inverse_gates.append(gates) + self.self_inverse_gate_names.add(gates.name) else: self.inverse_gate_pairs.append(gates) + self.inverse_gate_pairs_names.update(x.name for x in gates) super().__init__() @@ -76,11 +80,13 @@ def run(self, dag: DAGCircuit): Returns: DAGCircuit: Transformed DAG. """ + if self.self_inverse_gates: + dag = self._run_on_self_inverse(dag) + if self.inverse_gate_pairs: + dag = self._run_on_inverse_pairs(dag) + return dag - dag = self._run_on_self_inverse(dag, self.self_inverse_gates) - return self._run_on_inverse_pairs(dag, self.inverse_gate_pairs) - - def _run_on_self_inverse(self, dag: DAGCircuit, self_inverse_gates: List[Gate]): + def _run_on_self_inverse(self, dag: DAGCircuit): """ Run self-inverse gates on `dag`. @@ -91,14 +97,27 @@ def _run_on_self_inverse(self, dag: DAGCircuit, self_inverse_gates: List[Gate]): Returns: DAGCircuit: Transformed DAG. """ + op_counts = dag.count_ops() + if not self.self_inverse_gate_names.intersection(op_counts): + return dag # Sets of gate runs by name, for instance: [{(H 0, H 0), (H 1, H 1)}, {(X 0, X 0}] - gate_runs_sets = [dag.collect_runs([gate.name]) for gate in self_inverse_gates] - for gate_runs in gate_runs_sets: + for gate in self.self_inverse_gates: + gate_name = gate.name + gate_count = op_counts.get(gate_name, 0) + if gate_count <= 1: + continue + gate_runs = dag.collect_runs([gate_name]) for gate_cancel_run in gate_runs: partitions = [] chunk = [] for i in range(len(gate_cancel_run) - 1): - chunk.append(gate_cancel_run[i]) + if gate_cancel_run[i].op == gate: + chunk.append(gate_cancel_run[i]) + else: + if chunk: + partitions.append(chunk) + chunk = [] + continue if gate_cancel_run[i].qargs != gate_cancel_run[i + 1].qargs: partitions.append(chunk) chunk = [] @@ -112,7 +131,7 @@ def _run_on_self_inverse(self, dag: DAGCircuit, self_inverse_gates: List[Gate]): dag.remove_op_node(node) return dag - def _run_on_inverse_pairs(self, dag: DAGCircuit, inverse_gate_pairs: List[Tuple[Gate, Gate]]): + def _run_on_inverse_pairs(self, dag: DAGCircuit): """ Run inverse gate pairs on `dag`. @@ -123,8 +142,16 @@ def _run_on_inverse_pairs(self, dag: DAGCircuit, inverse_gate_pairs: List[Tuple[ Returns: DAGCircuit: Transformed DAG. """ - for pair in inverse_gate_pairs: - gate_cancel_runs = dag.collect_runs([pair[0].name, pair[1].name]) + op_counts = dag.count_ops() + if not self.inverse_gate_pairs_names.intersection(op_counts): + return dag + + for pair in self.inverse_gate_pairs: + gate_0_name = pair[0].name + gate_1_name = pair[1].name + if gate_0_name not in op_counts or gate_1_name not in op_counts: + continue + gate_cancel_runs = dag.collect_runs([gate_0_name, gate_1_name]) for dag_nodes in gate_cancel_runs: i = 0 while i < len(dag_nodes) - 1: diff --git a/releasenotes/notes/fix-parameterized-self-inverse-7cb2d68b273640f8.yaml b/releasenotes/notes/fix-parameterized-self-inverse-7cb2d68b273640f8.yaml new file mode 100644 index 000000000000..7ca796cdd2a5 --- /dev/null +++ b/releasenotes/notes/fix-parameterized-self-inverse-7cb2d68b273640f8.yaml @@ -0,0 +1,22 @@ +--- +fixes: + - | + Fixed an issue with the :class:`~.InverseCancellation` pass where it would + incorrectly cancel gates passed in as self inverses with a parameter + value, if a run of gates had a different parameter value. For example:: + + from math import pi + + from qiskit.circuit.library import RZGate + from qiskit.circuit import QuantumCircuit + from qiskit.transpiler.passes import InverseCancellation + + inverse_pass = InverseCancellation([RZGate(0)]) + + qc = QuantumCircuit(1) + qc.rz(0, 0) + qc.rz(pi, 0) + + inverse_pass(qc) + + would previously have incorrectly cancelled the two rz gates. diff --git a/test/python/transpiler/test_inverse_cancellation.py b/test/python/transpiler/test_inverse_cancellation.py index 686b01dd5d7c..c39b531c1b0f 100644 --- a/test/python/transpiler/test_inverse_cancellation.py +++ b/test/python/transpiler/test_inverse_cancellation.py @@ -22,7 +22,17 @@ from qiskit.transpiler.passes import InverseCancellation from qiskit.transpiler import PassManager from qiskit.test import QiskitTestCase -from qiskit.circuit.library import RXGate, HGate, CXGate, PhaseGate, XGate, TGate, TdgGate +from qiskit.circuit.library import ( + RXGate, + HGate, + CXGate, + PhaseGate, + XGate, + TGate, + TdgGate, + CZGate, + RZGate, +) class TestInverseCancellation(QiskitTestCase): @@ -271,6 +281,111 @@ def test_cx_do_not_wrongly_cancel(self): self.assertIn("cx", gates_after) self.assertEqual(gates_after["cx"], 2) + def test_no_gates_to_cancel(self): + """Test when there are no gates to cancel.""" + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.cx(1, 0) + inverse_pass = InverseCancellation([HGate()]) + new_circ = inverse_pass(qc) + self.assertEqual(qc, new_circ) + + def test_some_cancel_rules_to_cancel(self): + """Test when there are some gates to cancel.""" + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.cx(1, 0) + qc.h(0) + qc.h(0) + inverse_pass = InverseCancellation([HGate(), CXGate(), CZGate()]) + new_circ = inverse_pass(qc) + self.assertNotIn("h", new_circ.count_ops()) + + def test_no_inverse_pairs(self): + """Test when there are no inverse pairs to cancel.""" + qc = QuantumCircuit(1) + qc.s(0) + qc.sdg(0) + inverse_pass = InverseCancellation([(TGate(), TdgGate())]) + new_circ = inverse_pass(qc) + self.assertEqual(qc, new_circ) + + def test_some_inverse_pairs(self): + """Test when there are some but not all inverse pairs to cancel.""" + qc = QuantumCircuit(1) + qc.s(0) + qc.sdg(0) + qc.t(0) + qc.tdg(0) + inverse_pass = InverseCancellation([(TGate(), TdgGate())]) + new_circ = inverse_pass(qc) + self.assertNotIn("t", new_circ.count_ops()) + self.assertNotIn("tdg", new_circ.count_ops()) + + def test_some_inverse_and_cancelled(self): + """Test when there are some but not all pairs to cancel.""" + qc = QuantumCircuit(2) + qc.s(0) + qc.sdg(0) + qc.t(0) + qc.tdg(0) + qc.cx(0, 1) + qc.cx(1, 0) + qc.h(0) + qc.h(0) + inverse_pass = InverseCancellation([HGate(), CXGate(), CZGate(), (TGate(), TdgGate())]) + new_circ = inverse_pass(qc) + self.assertNotIn("h", new_circ.count_ops()) + self.assertNotIn("t", new_circ.count_ops()) + self.assertNotIn("tdg", new_circ.count_ops()) + + def test_half_of_an_inverse_pair(self): + """Test that half of an inverse pair doesn't do anything.""" + qc = QuantumCircuit(1) + qc.t(0) + qc.t(0) + qc.t(0) + inverse_pass = InverseCancellation([(TGate(), TdgGate())]) + new_circ = inverse_pass(qc) + self.assertEqual(new_circ, qc) + + def test_parameterized_self_inverse(self): + """Test that a parameterized self inverse gate cancels correctly.""" + qc = QuantumCircuit(1) + qc.rz(0, 0) + qc.rz(0, 0) + inverse_pass = InverseCancellation([RZGate(0)]) + new_circ = inverse_pass(qc) + self.assertEqual(new_circ, QuantumCircuit(1)) + + def test_parameterized_self_inverse_not_equal_parameter(self): + """Test that a parameterized self inverse gate doesn't cancel incorrectly.""" + qc = QuantumCircuit(1) + qc.rz(0, 0) + qc.rz(3.14159, 0) + qc.rz(0, 0) + inverse_pass = InverseCancellation([RZGate(0)]) + new_circ = inverse_pass(qc) + self.assertEqual(new_circ, qc) + + def test_controlled_gate_open_control_does_not_cancel(self): + """Test that a controlled gate with an open control doesn't cancel.""" + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.cx(0, 1, ctrl_state=0) + inverse_pass = InverseCancellation([CXGate()]) + new_circ = inverse_pass(qc) + self.assertEqual(new_circ, qc) + + def test_backwards_pair(self): + """Test a backwards inverse pair works.""" + qc = QuantumCircuit(1) + qc.tdg(0) + qc.t(0) + inverse_pass = InverseCancellation([(TGate(), TdgGate())]) + new_circ = inverse_pass(qc) + self.assertEqual(new_circ, QuantumCircuit(1)) + if __name__ == "__main__": unittest.main() From f7f6cb1150d057249a0af04d72caac92780065c7 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 15 Nov 2023 15:42:19 +0100 Subject: [PATCH 06/15] Ensure metapackage is installed during CI and tox (#11119) * Ensure metapackage is installed during CI and tox This ensures that the local version of the metapackage is also built and installed on all CI runs (and in `tox`, where it's overridden) so that dependencies on the metapackage in our optionals (e.g. Aer) will not cause the older released version of Terra to be installed. `tox` does not like having two local packages under test simultaneously through its default configuration, so this fakes things out by putting the two packages in the run dependencies and setting `skip_install`. * Fix sdist build * Use regular installs for metapackage * Simplify build requirements install --- .azure/lint-linux.yml | 3 +++ .azure/test-linux.yml | 15 ++++++++++----- .azure/test-macos.yml | 3 +++ .azure/test-windows.yml | 3 +++ .github/workflows/coverage.yml | 2 +- .github/workflows/slow.yml | 2 +- tox.ini | 33 +++++++++++++++++---------------- 7 files changed, 38 insertions(+), 23 deletions(-) diff --git a/.azure/lint-linux.yml b/.azure/lint-linux.yml index 532c2072ed29..5855fe1e6e67 100644 --- a/.azure/lint-linux.yml +++ b/.azure/lint-linux.yml @@ -26,7 +26,10 @@ jobs: -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ + ./qiskit_pkg \ -e . + # Build and install both qiskit and qiskit-terra so that any optionals + # depending on `qiskit` will resolve correctly. displayName: 'Install dependencies' env: SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" diff --git a/.azure/test-linux.yml b/.azure/test-linux.yml index 3268f29bd4f0..eb456f8497ad 100644 --- a/.azure/test-linux.yml +++ b/.azure/test-linux.yml @@ -71,15 +71,17 @@ jobs: # Use stable Rust, rather than MSRV, to spot-check that stable builds properly. rustup override set stable source test-job/bin/activate - python -m pip install -U pip setuptools wheel - # Install setuptools-rust for building sdist - python -m pip install -U -c constraints.txt setuptools-rust - python setup.py sdist + python -m pip install -U pip + python -m pip install -U build + python -m build --sdist . + python -m build --sdist qiskit_pkg python -m pip install -U \ -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ - dist/qiskit-terra*.tar.gz + dist/qiskit*.tar.gz + # Build and install both qiskit and qiskit-terra so that any optionals + # depending on `qiskit` will resolve correctly. displayName: "Install Terra from sdist" - ${{ if eq(parameters.installFromSdist, false) }}: @@ -90,7 +92,10 @@ jobs: -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ + ./qiskit_pkg \ -e . + # Build and install both qiskit and qiskit-terra so that any optionals + # depending on `qiskit` will resolve correctly. displayName: "Install Terra directly" env: SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" diff --git a/.azure/test-macos.yml b/.azure/test-macos.yml index 2b195afbdae3..cbf2fc8b0e08 100644 --- a/.azure/test-macos.yml +++ b/.azure/test-macos.yml @@ -43,7 +43,10 @@ jobs: -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ + ./qiskit_pkg \ -e . + # Build and install both qiskit and qiskit-terra so that any optionals + # depending on `qiskit` will resolve correctly. pip check displayName: 'Install dependencies' env: diff --git a/.azure/test-windows.yml b/.azure/test-windows.yml index 30591a5dadbe..6ba2e442946c 100644 --- a/.azure/test-windows.yml +++ b/.azure/test-windows.yml @@ -42,7 +42,10 @@ jobs: -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ + ./qiskit_pkg \ -e . + # Build and install both qiskit and qiskit-terra so that any optionals + # depending on `qiskit` will resolve correctly. pip check displayName: 'Install dependencies' env: diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index de3485c5fdce..221840b4e7c6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -42,7 +42,7 @@ jobs: run: python -m pip install -c constraints.txt --upgrade pip setuptools wheel - name: Build and install qiskit-terra - run: python -m pip install -c constraints.txt -e . + run: python -m pip install -c constraints.txt -e . ./qiskit_pkg env: CARGO_INCREMENTAL: 0 RUSTFLAGS: "-Cinstrument-coverage" diff --git a/.github/workflows/slow.yml b/.github/workflows/slow.yml index 62fa9ec19c62..528305740d95 100644 --- a/.github/workflows/slow.yml +++ b/.github/workflows/slow.yml @@ -19,7 +19,7 @@ jobs: python -m pip install -U pip setuptools wheel python -m pip install -U -r requirements.txt -c constraints.txt python -m pip install -U -r requirements-dev.txt -c constraints.txt - python -m pip install -c constraints.txt -e . + python -m pip install -c constraints.txt -e . ./qiskit_pkg python -m pip install "qiskit-aer" "z3-solver" "cplex" -c constraints.txt env: SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" diff --git a/tox.ini b/tox.ini index fa6d1017d3e2..0c799d28ba9a 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,10 @@ envlist = py38, py39, py310, py311, lint-incr isolated_build = true [testenv] -usedevelop = True +# We pretend that we're not actually installing the package, because we need tox to let us have two +# packages ('qiskit' and 'qiskit-terra') under test at the same time. For that, we have to stuff +# them into 'deps'. +skip_install = true install_command = pip install -c{toxinidir}/constraints.txt -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} @@ -15,9 +18,12 @@ setenv = QISKIT_TEST_CAPTURE_STREAMS=1 QISKIT_PARALLEL=FALSE passenv = RAYON_NUM_THREADS, OMP_NUM_THREADS, QISKIT_PARALLEL, RUST_BACKTRACE, SETUPTOOLS_ENABLE_FEATURES, QISKIT_TESTS, QISKIT_IN_PARALLEL -deps = setuptools_rust # This is work around for the bug of tox 3 (see #8606 for more details.) - -r{toxinidir}/requirements.txt - -r{toxinidir}/requirements-dev.txt +deps = + setuptools_rust # This is work around for the bug of tox 3 (see #8606 for more details.) + -r{toxinidir}/requirements.txt + -r{toxinidir}/requirements-dev.txt + -e . + -e ./qiskit_pkg commands = stestr run {posargs} @@ -50,6 +56,9 @@ commands = reno lint [testenv:black] +skip_install = true +deps = + -r requirements-dev.txt commands = black {posargs} qiskit test tools examples setup.py qiskit_pkg [testenv:coverage] @@ -57,17 +66,15 @@ basepython = python3 setenv = {[testenv]setenv} PYTHON=coverage3 run --source qiskit --parallel-mode -deps = -r{toxinidir}/requirements.txt - -r{toxinidir}/requirements-dev.txt - -r{toxinidir}/requirements-optional.txt +deps = + {[testenv]deps} + -r{toxinidir}/requirements-optional.txt commands = stestr run {posargs} coverage3 combine coverage3 report [testenv:docs] -# Editable mode breaks macOS: https://github.com/sphinx-doc/sphinx/issues/10943 -usedevelop = False basepython = python3 setenv = {[testenv]setenv} @@ -75,15 +82,9 @@ setenv = RUST_DEBUG=1 # Faster to compile. passenv = {[testenv]passenv}, QISKIT_DOCS_BUILD_TUTORIALS, QISKIT_CELL_TIMEOUT, DOCS_PROD_BUILD deps = - setuptools_rust # This is work around for the bug of tox 3 (see #8606 for more details.) - -r{toxinidir}/requirements-dev.txt + {[testenv]deps} -r{toxinidir}/requirements-optional.txt -r{toxinidir}/requirements-tutorials.txt - # Some optionals depend on Terra. We want to make sure pip satisfies that requirement from a local - # installation, not from PyPI. But Tox normally doesn't install the local installation until - # after `deps` is installed. So, instead, we tell pip to do the local installation at the same - # time as the optionals. See https://github.com/Qiskit/qiskit-terra/pull/9477. - . commands = sphinx-build -W -j auto -T --keep-going -b html docs/ docs/_build/html {posargs} From 28154e6cc5b7f22a489939271939ef53c457f98e Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 15 Nov 2023 19:23:52 +0100 Subject: [PATCH 07/15] Remove `print` calls from test suite (#11253) This removes some debugging `print` statements from the test suite, to avoid noise in the test-runner output. In some cases, the `print` was only part of an assertion that the conversion to string fails, and these were converted to explicit calls to `str`. This isn't strictly necessary, it's just trying to get the test suite out of the habit of using `print`. --- .../circuit/library/test_functional_pauli_rotations.py | 6 +++--- test/python/circuit/library/test_integer_comparator.py | 4 ++-- test/python/circuit/library/test_nlocal.py | 2 +- test/python/circuit/library/test_permutation.py | 1 - test/python/circuit/library/test_piecewise_chebyshev.py | 2 +- test/python/circuit/library/test_weighted_adder.py | 4 ++-- test/python/circuit/test_piecewise_polynomial.py | 2 +- test/python/primitives/test_backend_sampler.py | 1 - test/python/primitives/test_sampler.py | 2 -- .../quantum_info/operators/symplectic/test_clifford.py | 3 ++- 10 files changed, 12 insertions(+), 15 deletions(-) diff --git a/test/python/circuit/library/test_functional_pauli_rotations.py b/test/python/circuit/library/test_functional_pauli_rotations.py index 38f6568f117a..294a2b3282f0 100644 --- a/test/python/circuit/library/test_functional_pauli_rotations.py +++ b/test/python/circuit/library/test_functional_pauli_rotations.py @@ -84,7 +84,7 @@ def test_polynomial_rotations_mutability(self): with self.subTest(msg="missing number of state qubits"): with self.assertRaises(AttributeError): # no state qubits set - print(polynomial_rotations.draw()) + _ = str(polynomial_rotations.draw()) with self.subTest(msg="default setup, just setting number of state qubits"): polynomial_rotations.num_state_qubits = 2 @@ -121,7 +121,7 @@ def test_linear_rotations_mutability(self): with self.subTest(msg="missing number of state qubits"): with self.assertRaises(AttributeError): # no state qubits set - print(linear_rotation.draw()) + _ = str(linear_rotation.draw()) with self.subTest(msg="default setup, just setting number of state qubits"): linear_rotation.num_state_qubits = 2 @@ -171,7 +171,7 @@ def test_piecewise_linear_rotations_mutability(self): with self.subTest(msg="missing number of state qubits"): with self.assertRaises(AttributeError): # no state qubits set - print(pw_linear_rotations.draw()) + _ = str(pw_linear_rotations.draw()) with self.subTest(msg="default setup, just setting number of state qubits"): pw_linear_rotations.num_state_qubits = 2 diff --git a/test/python/circuit/library/test_integer_comparator.py b/test/python/circuit/library/test_integer_comparator.py index 2c6459cf38e3..55bd2b9071af 100644 --- a/test/python/circuit/library/test_integer_comparator.py +++ b/test/python/circuit/library/test_integer_comparator.py @@ -71,13 +71,13 @@ def test_mutability(self): with self.subTest(msg="missing num state qubits and value"): with self.assertRaises(AttributeError): - print(comp.draw()) + _ = str(comp.draw()) comp.num_state_qubits = 2 with self.subTest(msg="missing value"): with self.assertRaises(AttributeError): - print(comp.draw()) + _ = str(comp.draw()) comp.value = 0 comp.geq = True diff --git a/test/python/circuit/library/test_nlocal.py b/test/python/circuit/library/test_nlocal.py index f20377368ed4..62e7912606b8 100644 --- a/test/python/circuit/library/test_nlocal.py +++ b/test/python/circuit/library/test_nlocal.py @@ -378,7 +378,7 @@ def test_pairwise_entanglement_raises(self): # pairwise entanglement is only defined if the entangling gate has 2 qubits with self.assertRaises(ValueError): - print(nlocal.draw()) + _ = str(nlocal.draw()) def test_entanglement_by_list(self): """Test setting the entanglement by list. diff --git a/test/python/circuit/library/test_permutation.py b/test/python/circuit/library/test_permutation.py index bf4da582b6ad..25bfa0bfae49 100644 --- a/test/python/circuit/library/test_permutation.py +++ b/test/python/circuit/library/test_permutation.py @@ -170,7 +170,6 @@ def test_qpy(self): circuit.cx(0, 1) circuit.append(PermutationGate([1, 2, 0]), [2, 4, 5]) circuit.h(4) - print(circuit) qpy_file = io.BytesIO() dump(circuit, qpy_file) diff --git a/test/python/circuit/library/test_piecewise_chebyshev.py b/test/python/circuit/library/test_piecewise_chebyshev.py index 4b8cb6054e1d..c721fe47ee67 100644 --- a/test/python/circuit/library/test_piecewise_chebyshev.py +++ b/test/python/circuit/library/test_piecewise_chebyshev.py @@ -103,7 +103,7 @@ def f_x_1(x): with self.subTest(msg="missing number of state qubits"): with self.assertRaises(AttributeError): # no state qubits set - print(pw_approximation.draw()) + _ = str(pw_approximation.draw()) with self.subTest(msg="default setup, just setting number of state qubits"): pw_approximation.num_state_qubits = 2 diff --git a/test/python/circuit/library/test_weighted_adder.py b/test/python/circuit/library/test_weighted_adder.py index 7bfe614e0bd6..db33156aa0a2 100644 --- a/test/python/circuit/library/test_weighted_adder.py +++ b/test/python/circuit/library/test_weighted_adder.py @@ -67,7 +67,7 @@ def test_mutability(self): with self.subTest(msg="missing number of state qubits"): with self.assertRaises(AttributeError): - print(adder.draw()) + _ = str(adder.draw()) with self.subTest(msg="default weights"): adder.num_state_qubits = 3 @@ -81,7 +81,7 @@ def test_mutability(self): with self.subTest(msg="mismatching number of state qubits and weights"): with self.assertRaises(ValueError): adder.weights = [0, 1, 2, 3] - print(adder.draw()) + _ = str(adder.draw()) with self.subTest(msg="change all attributes"): adder.num_state_qubits = 4 diff --git a/test/python/circuit/test_piecewise_polynomial.py b/test/python/circuit/test_piecewise_polynomial.py index 9bc34179ba63..e84cc35de87b 100644 --- a/test/python/circuit/test_piecewise_polynomial.py +++ b/test/python/circuit/test_piecewise_polynomial.py @@ -101,7 +101,7 @@ def pw_poly(x): with self.subTest(msg="missing number of state qubits"): with self.assertRaises(AttributeError): # no state qubits set - print(pw_polynomial_rotations.draw()) + _ = str(pw_polynomial_rotations.draw()) with self.subTest(msg="default setup, just setting number of state qubits"): pw_polynomial_rotations.num_state_qubits = 2 diff --git a/test/python/primitives/test_backend_sampler.py b/test/python/primitives/test_backend_sampler.py index 86dc709f8983..971104af9870 100644 --- a/test/python/primitives/test_backend_sampler.py +++ b/test/python/primitives/test_backend_sampler.py @@ -117,7 +117,6 @@ def test_sample_run_multiple_circuits(self, backend): bell = self._circuit[1] sampler = BackendSampler(backend=backend) result = sampler.run([bell, bell, bell]).result() - # print([q.binary_probabilities() for q in result.quasi_dists]) self._compare_probs(result.quasi_dists[0], self._target[1]) self._compare_probs(result.quasi_dists[1], self._target[1]) self._compare_probs(result.quasi_dists[2], self._target[1]) diff --git a/test/python/primitives/test_sampler.py b/test/python/primitives/test_sampler.py index 2dc2aac11098..fac9a250a9e9 100644 --- a/test/python/primitives/test_sampler.py +++ b/test/python/primitives/test_sampler.py @@ -93,7 +93,6 @@ def test_sampler_run(self): self.assertIsInstance(job, JobV1) result = job.result() self.assertIsInstance(result, SamplerResult) - # print([q.binary_probabilities() for q in result.quasi_dists]) self._compare_probs(result.quasi_dists, self._target[1]) def test_sample_run_multiple_circuits(self): @@ -103,7 +102,6 @@ def test_sample_run_multiple_circuits(self): bell = self._circuit[1] sampler = Sampler() result = sampler.run([bell, bell, bell]).result() - # print([q.binary_probabilities() for q in result.quasi_dists]) self._compare_probs(result.quasi_dists[0], self._target[1]) self._compare_probs(result.quasi_dists[1], self._target[1]) self._compare_probs(result.quasi_dists[2], self._target[1]) diff --git a/test/python/quantum_info/operators/symplectic/test_clifford.py b/test/python/quantum_info/operators/symplectic/test_clifford.py index 76cd9463f00e..615f09e6ad21 100644 --- a/test/python/quantum_info/operators/symplectic/test_clifford.py +++ b/test/python/quantum_info/operators/symplectic/test_clifford.py @@ -1044,7 +1044,8 @@ def test_visualize_does_not_throw_error(self): # An error may be thrown if visualization code calls op.condition instead # of getattr(op, "condition", None) clifford = random_clifford(3, seed=0) - print(clifford) + _ = str(clifford) + _ = repr(clifford) @combine(num_qubits=[1, 2, 3, 4]) def test_from_matrix_round_trip(self, num_qubits): From 3c1a87c48a9ff33d999aa71749bdf64a9c008a97 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 15 Nov 2023 21:28:55 +0100 Subject: [PATCH 08/15] Fix `Channel.__hash__` in multiprocessing contexts (#11251) * Fix `Channel.__hash__` in multiprocessing contexts Storing an explicit hash key is fragile in cases that a channel might be created in a different process to where it might be compared or the hash used, because the hash seeding can vary depending on how the new interpreter process was created, especially if it's not done by `fork`. In this case, transmitting the stored `_hash` over pickle meant that a `DriveChannel(0)` created in the main process of a macOS runner could compare equal to a `DriveChannel(0)` created in a separate process (standard start method `spawn`) and pickled over the wire to the main process, but have different hashes, violating the Python data model. Instead, we can just use the standard Python behaviour of creating the hash on demand when requested; this should typically be preferred unless absolutely necessary for critical performance reasons, because it will generally fail safe. * Fix `hash` and equality in other pulse objects This removes all caching of items' `hash`es. This practice is quite fraught in multiprocessing contexts, and should only be done when it is absolutely performance critical. In a couple of cases, the pulse objects were using the cached `hash` as the main component of their `__eq__` methods, which is not correct; it's totally valid to have hash collisions without implying that two objects are equal. --- qiskit/pulse/channels.py | 3 +- qiskit/pulse/instructions/instruction.py | 5 +-- qiskit/pulse/model/frames.py | 43 ++++++++----------- qiskit/pulse/model/mixed_frames.py | 3 +- qiskit/pulse/model/pulse_target.py | 14 +++--- ...x-pulse-channel-hash-549a8fb5d8738c4d.yaml | 6 +++ 6 files changed, 33 insertions(+), 41 deletions(-) create mode 100644 releasenotes/notes/fix-pulse-channel-hash-549a8fb5d8738c4d.yaml diff --git a/qiskit/pulse/channels.py b/qiskit/pulse/channels.py index 687b837700d3..e3da9b923ca4 100644 --- a/qiskit/pulse/channels.py +++ b/qiskit/pulse/channels.py @@ -96,7 +96,6 @@ def __init__(self, index: int): """ self._validate_index(index) self._index = index - self._hash = hash((self.__class__.__name__, self._index)) @property def index(self) -> Union[int, ParameterExpression]: @@ -156,7 +155,7 @@ def __eq__(self, other: "Channel") -> bool: return type(self) is type(other) and self._index == other._index def __hash__(self): - return self._hash + return hash((type(self), self._index)) class PulseChannel(Channel, metaclass=ABCMeta): diff --git a/qiskit/pulse/instructions/instruction.py b/qiskit/pulse/instructions/instruction.py index 7c6af5788e8b..559b4fb38dc7 100644 --- a/qiskit/pulse/instructions/instruction.py +++ b/qiskit/pulse/instructions/instruction.py @@ -53,7 +53,6 @@ def __init__( """ self._operands = operands self._name = name - self._hash = None self._validate() def _validate(self): @@ -301,9 +300,7 @@ def __eq__(self, other: "Instruction") -> bool: return isinstance(other, type(self)) and self.operands == other.operands def __hash__(self) -> int: - if self._hash is None: - self._hash = hash((type(self), self.operands, self.name)) - return self._hash + return hash((type(self), self.operands, self.name)) def __add__(self, other): """Return a new schedule with `other` inserted within `self` at `start_time`. diff --git a/qiskit/pulse/model/frames.py b/qiskit/pulse/model/frames.py index ae63fe3c7948..803573301320 100644 --- a/qiskit/pulse/model/frames.py +++ b/qiskit/pulse/model/frames.py @@ -33,28 +33,6 @@ class Frame(ABC): The default initial phase for every frame is 0. """ - def __init__(self, identifier): - """Create ``Frame``. - - Args: - identifier: A unique identifier used to hash the Frame. - """ - self._hash = hash((type(self), identifier)) - - def __eq__(self, other: "Frame") -> bool: - """Return True iff self and other are equal, specifically, iff they have the same type and hash. - - Args: - other: The frame to compare to this one. - - Returns: - True iff equal. - """ - return type(self) is type(other) and self._hash == other._hash - - def __hash__(self) -> int: - return self._hash - class GenericFrame(Frame): """Pulse module GenericFrame. @@ -74,7 +52,6 @@ def __init__(self, name: str): name: A unique identifier used to identify the frame. """ self._name = name - super().__init__(name) @property def name(self) -> str: @@ -84,6 +61,12 @@ def name(self) -> str: def __repr__(self) -> str: return f"GenericFrame({self._name})" + def __eq__(self, other): + return type(self) is type(other) and self._name == other._name + + def __hash__(self): + return hash((type(self), self._name)) + class QubitFrame(Frame): """A frame associated with the driving of a qubit. @@ -102,7 +85,6 @@ def __init__(self, index: int): """ self._validate_index(index) self._index = index - super().__init__("QubitFrame" + str(index)) @property def index(self) -> int: @@ -122,6 +104,12 @@ def _validate_index(self, index) -> None: def __repr__(self) -> str: return f"QubitFrame({self._index})" + def __eq__(self, other): + return type(self) is type(other) and self._index == other._index + + def __hash__(self): + return hash((type(self), self._index)) + class MeasurementFrame(Frame): """A frame associated with the measurement of a qubit. @@ -141,7 +129,6 @@ def __init__(self, index: int): """ self._validate_index(index) self._index = index - super().__init__("MeasurementFrame" + str(index)) @property def index(self) -> int: @@ -160,3 +147,9 @@ def _validate_index(self, index) -> None: def __repr__(self) -> str: return f"MeasurementFrame({self._index})" + + def __eq__(self, other): + return type(self) is type(other) and self._index == other._index + + def __hash__(self): + return hash((type(self), self._index)) diff --git a/qiskit/pulse/model/mixed_frames.py b/qiskit/pulse/model/mixed_frames.py index 50ff13c47a44..454cdbf0d2c5 100644 --- a/qiskit/pulse/model/mixed_frames.py +++ b/qiskit/pulse/model/mixed_frames.py @@ -47,7 +47,6 @@ def __init__(self, pulse_target: PulseTarget, frame: Frame): """ self._pulse_target = pulse_target self._frame = frame - self._hash = hash((self._pulse_target, self._frame, type(self))) @property def pulse_target(self) -> PulseTarget: @@ -75,4 +74,4 @@ def __eq__(self, other: "MixedFrame") -> bool: return self._pulse_target == other._pulse_target and self._frame == other._frame def __hash__(self) -> int: - return self._hash + return hash((self._pulse_target, self._frame, type(self))) diff --git a/qiskit/pulse/model/pulse_target.py b/qiskit/pulse/model/pulse_target.py index 69894780df27..bb9702ccfad4 100644 --- a/qiskit/pulse/model/pulse_target.py +++ b/qiskit/pulse/model/pulse_target.py @@ -59,7 +59,6 @@ def __init__(self, name: str): name: A string identifying the port. """ self._name = name - self._hash = hash((name, type(self))) @property def name(self) -> str: @@ -78,12 +77,12 @@ def __eq__(self, other: "Port") -> bool: """ return type(self) is type(other) and self._name == other._name + def __hash__(self) -> int: + return hash((self._name, type(self))) + def __repr__(self) -> str: return f"Port({self._name})" - def __hash__(self) -> int: - return self._hash - class LogicalElement(PulseTarget, ABC): """Base class of logical elements. @@ -104,7 +103,6 @@ def __init__(self, index: Tuple[int, ...]): """ self._validate_index(index) self._index = index - self._hash = hash((index, type(self))) @property def index(self) -> Tuple[int, ...]: @@ -132,13 +130,13 @@ def __eq__(self, other: "LogicalElement") -> bool: """ return type(self) is type(other) and self._index == other._index + def __hash__(self) -> int: + return hash((self._index, type(self))) + def __repr__(self) -> str: ind_str = str(self._index) if len(self._index) > 1 else f"({self._index[0]})" return type(self).__name__ + ind_str - def __hash__(self) -> int: - return self._hash - class Qubit(LogicalElement): """Qubit logical element. diff --git a/releasenotes/notes/fix-pulse-channel-hash-549a8fb5d8738c4d.yaml b/releasenotes/notes/fix-pulse-channel-hash-549a8fb5d8738c4d.yaml new file mode 100644 index 000000000000..19d022572963 --- /dev/null +++ b/releasenotes/notes/fix-pulse-channel-hash-549a8fb5d8738c4d.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed the :func:`hash` of Qiskit Pulse ``Channel`` objects (such as :class:`.DriveChannel`) in + cases where the channel was transferred from one Python process to another that used a different + hash seed. From 8707699280e8730a8be3c5e3643daba7ec4e9a74 Mon Sep 17 00:00:00 2001 From: SoranaAurelia <52232581+SoranaAurelia@users.noreply.github.com> Date: Thu, 16 Nov 2023 13:47:05 +0200 Subject: [PATCH 09/15] Fixed Issue 11026: SparsePauliOp.apply_layout should do nothing if given None (#11041) * fixed issue 11026 by adding condition in apply_layout and implemented test * fixed formatting * added expansion of operator if num_qubits is provided and added tests * fixed finding * added release note --- .../operators/symplectic/sparse_pauli_op.py | 16 ++++++++++++---- ...-for-sparse_pauli_op-212470d5b5b307a0.yaml | 6 ++++++ .../symplectic/test_sparse_pauli_op.py | 19 +++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/allow-none-layout-for-sparse_pauli_op-212470d5b5b307a0.yaml diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index 9eecbed2f091..65938410c28c 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -1109,17 +1109,20 @@ def assign_parameters( return None if inplace else bound def apply_layout( - self, layout: TranspileLayout | List[int], num_qubits: int | None = None + self, layout: TranspileLayout | List[int] | None, num_qubits: int | None = None ) -> SparsePauliOp: """Apply a transpiler layout to this :class:`~.SparsePauliOp` Args: - layout: Either a :class:`~.TranspileLayout` or a list of integers. + layout: Either a :class:`~.TranspileLayout`, a list of integers or None. + If both layout and num_qubits are none, a copy of the operator is + returned. num_qubits: The number of qubits to expand the operator to. If not provided then if ``layout`` is a :class:`~.TranspileLayout` the number of the transpiler output circuit qubits will be used by default. If ``layout`` is a list of integers the permutation - specified will be applied without any expansion. + specified will be applied without any expansion. If layout is + None, the operator will be expanded to the given number of qubits. Returns: @@ -1127,6 +1130,9 @@ def apply_layout( """ from qiskit.transpiler.layout import TranspileLayout + if layout is None and num_qubits is None: + return self.copy() + n_qubits = self.num_qubits if isinstance(layout, TranspileLayout): n_qubits = len(layout._output_qubit_list) @@ -1138,8 +1144,10 @@ def apply_layout( f"applied to a {n_qubits} qubit operator" ) n_qubits = num_qubits - if any(x >= n_qubits for x in layout): + if layout is not None and any(x >= n_qubits for x in layout): raise QiskitError("Provided layout contains indicies outside the number of qubits.") + if layout is None: + layout = list(range(self.num_qubits)) new_op = type(self)("I" * n_qubits) return new_op.compose(self, qargs=layout) diff --git a/releasenotes/notes/allow-none-layout-for-sparse_pauli_op-212470d5b5b307a0.yaml b/releasenotes/notes/allow-none-layout-for-sparse_pauli_op-212470d5b5b307a0.yaml new file mode 100644 index 000000000000..d40486990275 --- /dev/null +++ b/releasenotes/notes/allow-none-layout-for-sparse_pauli_op-212470d5b5b307a0.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The function :meth:`~.SparsePauliOp.apply_layout` from :class:`.SparsePauliOp` now allows for the + layout argument to also be None. That is, the method can now also be used for circuits where no transpilation/routing + took place (for example when transpiling for a simulator). diff --git a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py index 929534d26e8f..c519855faa6f 100644 --- a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py +++ b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py @@ -1088,6 +1088,25 @@ def test_apply_layout_layout_list_and_num_qubits(self): res = op.apply_layout([4, 0], 5) self.assertEqual(SparsePauliOp.from_list([("IIIIY", 2), ("IIIIX", 1)]), res) + def test_apply_layout_null_layout_no_num_qubits(self): + """Test apply_layout with a null layout""" + op = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) + res = op.apply_layout(layout=None) + self.assertEqual(op, res) + + def test_apply_layout_null_layout_and_num_qubits(self): + """Test apply_layout with a null layout a num_qubits provided""" + op = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) + res = op.apply_layout(layout=None, num_qubits=5) + # this should expand the operator + self.assertEqual(SparsePauliOp.from_list([("IIIII", 1), ("IIIIZ", 2), ("IIIXI", 3)]), res) + + def test_apply_layout_null_layout_invalid_num_qubits(self): + """Test apply_layout with a null layout and num_qubits smaller than capable""" + op = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) + with self.assertRaises(QiskitError): + op.apply_layout(layout=None, num_qubits=1) + if __name__ == "__main__": unittest.main() From 196221f2efec49de9528d4c5f108eb1b16973361 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 16 Nov 2023 14:16:43 +0100 Subject: [PATCH 10/15] Fix pickling of conditioned custom gates from OpenQASM 2 import (#11175) The condition of a gate defined by a `gate` statement in an OpenQASM 2 program was not previously exported as part of its state during pickling, causing pickle roundtrips or deepcopies to lose the condition on the new copy. --- qiskit/qasm2/parse.py | 5 ++-- ...opy-conditioned-gate-2fa75fee622c1fc0.yaml | 6 +++++ test/python/qasm2/test_structure.py | 27 +++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/fix-qasm2-deepcopy-conditioned-gate-2fa75fee622c1fc0.yaml diff --git a/qiskit/qasm2/parse.py b/qiskit/qasm2/parse.py index fbd7f48390d8..2bb4514dc2dc 100644 --- a/qiskit/qasm2/parse.py +++ b/qiskit/qasm2/parse.py @@ -322,14 +322,15 @@ def _define(self): # to pickle ourselves, we just eagerly create the definition and pickle that. def __getstate__(self): - return (self.name, self.num_qubits, self.params, self.definition) + return (self.name, self.num_qubits, self.params, self.definition, self.condition) def __setstate__(self, state): - name, num_qubits, params, definition = state + name, num_qubits, params, definition, condition = state super().__init__(name, num_qubits, params) self._gates = () self._bytecode = () self._definition = definition + self._condition = condition def _gate_builder(name, num_qubits, known_gates, bytecode): diff --git a/releasenotes/notes/fix-qasm2-deepcopy-conditioned-gate-2fa75fee622c1fc0.yaml b/releasenotes/notes/fix-qasm2-deepcopy-conditioned-gate-2fa75fee622c1fc0.yaml new file mode 100644 index 000000000000..5574dbcab368 --- /dev/null +++ b/releasenotes/notes/fix-qasm2-deepcopy-conditioned-gate-2fa75fee622c1fc0.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Conditioned custom gates imported from OpenQASM 2 will now correctly retain their conditions + when pickled and deep-copied. Previously, any conditional custom gate (defined by a ``gate`` + statement in an OpenQASM 2 file) would lose its condition when copied or pickled. diff --git a/test/python/qasm2/test_structure.py b/test/python/qasm2/test_structure.py index 805e5c3bf8f0..0b9774357a5d 100644 --- a/test/python/qasm2/test_structure.py +++ b/test/python/qasm2/test_structure.py @@ -12,6 +12,7 @@ # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring +import copy import io import math import os @@ -696,6 +697,32 @@ def test_qpy_double_call_roundtrip(self): loaded = qpy.load(fptr)[0] self.assertEqual(loaded, qc) + def test_deepcopy_conditioned_defined_gate(self): + program = """ + include "qelib1.inc"; + gate my_gate a { + x a; + } + qreg q[1]; + creg c[1]; + if (c == 1) my_gate q[0]; + """ + parsed = qiskit.qasm2.loads(program) + my_gate = parsed.data[0].operation + + self.assertEqual(my_gate.name, "my_gate") + self.assertEqual(my_gate.condition, (parsed.cregs[0], 1)) + + copied = copy.deepcopy(parsed) + copied_gate = copied.data[0].operation + self.assertEqual(copied_gate.name, "my_gate") + self.assertEqual(copied_gate.condition, (copied.cregs[0], 1)) + + pickled = pickle.loads(pickle.dumps(parsed)) + pickled_gate = pickled.data[0].operation + self.assertEqual(pickled_gate.name, "my_gate") + self.assertEqual(pickled_gate.condition, (pickled.cregs[0], 1)) + class TestOpaque(QiskitTestCase): def test_simple(self): From a54b4c0ad788ca74c34bea9db367e423e104965d Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Thu, 16 Nov 2023 15:30:04 +0200 Subject: [PATCH 11/15] Do not ignore global phases when comparing unitary gates (#11247) * fix unitary gate eq and one failing test * lint * Update releasenotes/notes/fix-unitary-equal-eb828aca3aaa5e73.yaml Co-authored-by: Jake Lishman --------- Co-authored-by: Jake Lishman --- qiskit/circuit/library/generalized_gates/unitary.py | 4 +--- .../notes/fix-unitary-equal-eb828aca3aaa5e73.yaml | 8 ++++++++ test/python/circuit/test_hamiltonian_gate.py | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/fix-unitary-equal-eb828aca3aaa5e73.yaml diff --git a/qiskit/circuit/library/generalized_gates/unitary.py b/qiskit/circuit/library/generalized_gates/unitary.py index 0f98a09c5f97..3b15e83196c6 100644 --- a/qiskit/circuit/library/generalized_gates/unitary.py +++ b/qiskit/circuit/library/generalized_gates/unitary.py @@ -111,9 +111,7 @@ def __eq__(self, other): return False if self.label != other.label: return False - # Should we match unitaries as equal if they are equal - # up to global phase? - return matrix_equal(self.params[0], other.params[0], ignore_phase=True) + return matrix_equal(self.params[0], other.params[0]) def __array__(self, dtype=None): """Return matrix for the unitary.""" diff --git a/releasenotes/notes/fix-unitary-equal-eb828aca3aaa5e73.yaml b/releasenotes/notes/fix-unitary-equal-eb828aca3aaa5e73.yaml new file mode 100644 index 000000000000..8d090ec5c356 --- /dev/null +++ b/releasenotes/notes/fix-unitary-equal-eb828aca3aaa5e73.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Previously two objects of type :class:`~.UnitaryGate` class were considered equal + even when they had different global phases. This could lead to transpiler + passes not maintaining the global phase and, in theory, to incorrect optimizations. + This commit changes the :meth:`~.UnitaryGate.__eq__` method not to ignore the global + phases in the comparison. diff --git a/test/python/circuit/test_hamiltonian_gate.py b/test/python/circuit/test_hamiltonian_gate.py index 0769d765968a..37702a6a07a9 100644 --- a/test/python/circuit/test_hamiltonian_gate.py +++ b/test/python/circuit/test_hamiltonian_gate.py @@ -18,7 +18,7 @@ import qiskit -from qiskit.circuit.library import HamiltonianGate, UnitaryGate +from qiskit.circuit.library import HamiltonianGate from qiskit.test import QiskitTestCase from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit.circuit import Parameter @@ -169,4 +169,4 @@ def test_decomposes_into_correct_unitary(self): qc.append(uni2q, [0, 1]) qc = qc.assign_parameters({theta: -np.pi / 2}).decompose() decomposed_ham = qc.data[0].operation - self.assertEqual(decomposed_ham, UnitaryGate(Operator.from_label("XY"))) + self.assertEqual(Operator(decomposed_ham), 1j * Operator.from_label("XY")) From fced4bfcc900a0c85c90cd16a861f695272bf7a3 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 16 Nov 2023 23:37:57 +0100 Subject: [PATCH 12/15] Fix phase in readme state (#11259) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c54f433b4846..739a03ca4e8e 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ qc_example.cx(0,1) # 0th-qubit-Controlled-NOT gate on 1st qubit qc_example.cx(0,2) # 0th-qubit-Controlled-NOT gate on 2nd qubit ``` -This simple example makes an entangled state known as a [GHZ state](https://en.wikipedia.org/wiki/Greenberger%E2%80%93Horne%E2%80%93Zeilinger_state) $(|000\rangle + |111\rangle)/\sqrt{2}$. It uses the standard quantum gates: Hadamard gate (`h`), Phase gate (`p`), and CNOT gate (`cx`). +This simple example makes an entangled state known as a [GHZ state](https://en.wikipedia.org/wiki/Greenberger%E2%80%93Horne%E2%80%93Zeilinger_state) $(|000\rangle + i|111\rangle)/\sqrt{2}$. It uses the standard quantum gates: Hadamard gate (`h`), Phase gate (`p`), and CNOT gate (`cx`). Once you've made your first quantum circuit, choose which primitive function you will use. Starting with `sampler`, we use `measure_all(inplace=False)` to get a copy of the circuit in which all the qubits are measured: From 252dbd5c86de44b96f99ec2b2f3ae2d871921a4d Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Sat, 18 Nov 2023 00:25:06 +0100 Subject: [PATCH 13/15] Fix `QuantumCircuit.barrier` argument conversion (#11272) * Fix `QuantumCircuit.barrier` argument conversion The manual argument conversion within `QuantumCircuit.barrier` made it inconsistent with the rest of the circuit methods, and it would silently generate invalid circuit output when given iterables outside an expected set. This switches the method to use the standard argument conversion, bringing it inline with the rest of the circuit methods. * Include link to issue Co-authored-by: Matthew Treinish --------- Co-authored-by: Matthew Treinish --- qiskit/circuit/quantumcircuit.py | 23 +++++-------------- .../fix-circuit-barrier-c696eabae1dcc6c2.yaml | 7 ++++++ .../python/circuit/test_circuit_operations.py | 21 +++++++++++++++++ 3 files changed, 34 insertions(+), 17 deletions(-) create mode 100644 releasenotes/notes/fix-circuit-barrier-c696eabae1dcc6c2.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index bc89e9740e2f..c9f9b04fa061 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2852,23 +2852,12 @@ def barrier(self, *qargs: QubitSpecifier, label=None) -> InstructionSet: """ from .barrier import Barrier - qubits: list[QubitSpecifier] = [] - - if not qargs: # None - qubits.extend(self.qubits) - - for qarg in qargs: - if isinstance(qarg, QuantumRegister): - qubits.extend([qarg[j] for j in range(qarg.size)]) - elif isinstance(qarg, list): - qubits.extend(qarg) - elif isinstance(qarg, range): - qubits.extend(list(qarg)) - elif isinstance(qarg, slice): - qubits.extend(self.qubits[qarg]) - else: - qubits.append(qarg) - + qubits = ( + # This uses a `dict` not a `set` to guarantee a deterministic order to the arguments. + list({q: None for qarg in qargs for q in self.qbit_argument_conversion(qarg)}) + if qargs + else self.qubits.copy() + ) return self.append(Barrier(len(qubits), label=label), qubits, []) def delay( diff --git a/releasenotes/notes/fix-circuit-barrier-c696eabae1dcc6c2.yaml b/releasenotes/notes/fix-circuit-barrier-c696eabae1dcc6c2.yaml new file mode 100644 index 000000000000..b16792f8fb3f --- /dev/null +++ b/releasenotes/notes/fix-circuit-barrier-c696eabae1dcc6c2.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + :meth:`.QuantumCircuit.barrier` will now generate correct output when given a :class:`set` as + one of its inputs. Previously, it would append an invalid operation onto the circuit, though in + practice this usually would not cause observable problems. + Fixed `#11208 `__ diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 11770d896bc6..3f773fb09abc 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -420,6 +420,27 @@ def test_clear_circuit(self): self.assertEqual(len(qc.data), 0) self.assertEqual(len(qc._parameter_table), 0) + def test_barrier(self): + """Test multiple argument forms of barrier.""" + qr1, qr2 = QuantumRegister(3, "qr1"), QuantumRegister(4, "qr2") + qc = QuantumCircuit(qr1, qr2) + qc.barrier() # All qubits. + qc.barrier(0, 1) + qc.barrier([4, 2]) + qc.barrier(qr1) + qc.barrier(slice(3, 5)) + qc.barrier({1, 4, 2}, range(5, 7)) + + expected = QuantumCircuit(qr1, qr2) + expected.append(Barrier(expected.num_qubits), expected.qubits.copy(), []) + expected.append(Barrier(2), [expected.qubits[0], expected.qubits[1]], []) + expected.append(Barrier(2), [expected.qubits[2], expected.qubits[4]], []) + expected.append(Barrier(3), expected.qubits[0:3], []) + expected.append(Barrier(2), [expected.qubits[3], expected.qubits[4]], []) + expected.append(Barrier(5), [expected.qubits[x] for x in [1, 2, 4, 5, 6]], []) + + self.assertEqual(qc, expected) + def test_measure_active(self): """Test measure_active Applies measurements only to non-idle qubits. Creates a ClassicalRegister of size equal to From 9111d0f9e198a87433da2d5dd8fa9427b9140194 Mon Sep 17 00:00:00 2001 From: SoranaAurelia <52232581+SoranaAurelia@users.noreply.github.com> Date: Sat, 18 Nov 2023 01:55:17 +0200 Subject: [PATCH 14/15] Allow correct retrieval of circuit with initialize instruction from qpy file (#11206) * treated cases for StatePreparation initialization when reading from qpy file * added test for state preparation from qpy * added docstring to test function * added fix release note * Fixup release note --------- Co-authored-by: Jake Lishman --- qiskit/qpy/binary_io/circuits.py | 14 +++++++++++--- ...tatepreparation-retrieval-1feea5eb74db7f1e.yaml | 5 +++++ test/python/circuit/test_circuit_load_from_qpy.py | 12 ++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/fix-qpy-statepreparation-retrieval-1feea5eb74db7f1e.yaml diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 81785bf78151..6d910746a89a 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -319,9 +319,17 @@ def _read_instruction( if condition: gate = gate.c_if(*condition) else: - if gate_name in { - "Initialize", - "StatePreparation", + if gate_name in {"Initialize", "StatePreparation"}: + if isinstance(params[0], str): + # the params are the labels of the initial state + gate = gate_class("".join(label for label in params)) + elif instruction.num_parameters == 1: + # the params is the integer indicating which qubits to initialize + gate = gate_class(int(params[0].real), instruction.num_qargs) + else: + # the params represent a list of complex amplitudes + gate = gate_class(params) + elif gate_name in { "UCRXGate", "UCRYGate", "UCRZGate", diff --git a/releasenotes/notes/fix-qpy-statepreparation-retrieval-1feea5eb74db7f1e.yaml b/releasenotes/notes/fix-qpy-statepreparation-retrieval-1feea5eb74db7f1e.yaml new file mode 100644 index 000000000000..67cdafcb463a --- /dev/null +++ b/releasenotes/notes/fix-qpy-statepreparation-retrieval-1feea5eb74db7f1e.yaml @@ -0,0 +1,5 @@ +fixes: + - | + Fixed QPY deserialization of the :class:`.StatePreparation` and :class:`.Initialize` circuit + instructions with string and integer parameters (as opposed to an explicit statevector, which + was already working). Fixed `#11158 `__. diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index f8ba8c260df9..4f8523358dbd 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -1685,6 +1685,18 @@ def test_qpy_deprecation(self): # pylint: disable=no-name-in-module, unused-import, redefined-outer-name, reimported from qiskit.circuit.qpy_serialization import dump, load + @ddt.data(0, "01", [1, 0, 0, 0]) + def test_valid_circuit_with_initialize_instruction(self, param): + """Tests that circuit that has initialize instruction can be saved and correctly retrieved""" + qc = QuantumCircuit(2) + qc.initialize(param, qc.qubits) + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_circuit = load(fptr)[0] + self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) + class TestSymengineLoadFromQPY(QiskitTestCase): """Test use of symengine in qpy set of methods.""" From 1dd4f5495e62b873327a7b8ad1c9616f6c672aa9 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 17 Nov 2023 18:55:23 -0500 Subject: [PATCH 15/15] Fix issue with ScheduleBlock qpy serialization and use_symengine (#11261) * Fix issue with ScheduleBlock qpy serialization and use_symengine This commit fixes an issue with the use_symengine flag on the qpy.dump() function. Previously in certain conditions a symengine expression that was listed in the qpy header as being encoded via symengine was incorrectly being serialized using sympy. This would cause a failure on deserialialization as the qpy payload was invalid and symengine could not parse a symengine representation. This was originally caught during the development of #10902 as that switches the default of use_symengine to ``True`` as it makes symengine a hard requirement. This commit splits it out so the isolated fix can be backported to 0.45.1. * Skip symengine test if symengine isn't installed * Fix skip syntax * Update releasenotes/notes/fix-schedule-qpy-use-symengine-05ae1dfab73e3ff8.yaml Co-authored-by: Jake Lishman --------- Co-authored-by: Jake Lishman --- qiskit/qpy/binary_io/schedules.py | 2 +- ...le-qpy-use-symengine-05ae1dfab73e3ff8.yaml | 6 ++++ test/python/qpy/test_block_load_from_qpy.py | 29 +++++++++++++++++-- 3 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/fix-schedule-qpy-use-symengine-05ae1dfab73e3ff8.yaml diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 83a023b9527b..25ca9388bb4e 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -484,7 +484,7 @@ def _dumps_operand(operand, use_symengine): def _write_element(file_obj, element, metadata_serializer, use_symengine): if isinstance(element, ScheduleBlock): common.write_type_key(file_obj, type_keys.Program.SCHEDULE_BLOCK) - write_schedule_block(file_obj, element, metadata_serializer) + write_schedule_block(file_obj, element, metadata_serializer, use_symengine) else: type_key = type_keys.ScheduleInstruction.assign(element) common.write_type_key(file_obj, type_key) diff --git a/releasenotes/notes/fix-schedule-qpy-use-symengine-05ae1dfab73e3ff8.yaml b/releasenotes/notes/fix-schedule-qpy-use-symengine-05ae1dfab73e3ff8.yaml new file mode 100644 index 000000000000..3fe71509ea7d --- /dev/null +++ b/releasenotes/notes/fix-schedule-qpy-use-symengine-05ae1dfab73e3ff8.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed an issue with :func:`.qpy.dump` which would cause the function to + potentially ignore the value of ``use_symengine`` when serializing a + :class:`.ScheduleBlock` object. diff --git a/test/python/qpy/test_block_load_from_qpy.py b/test/python/qpy/test_block_load_from_qpy.py index e68ec48d4a00..8137ef21a6bb 100644 --- a/test/python/qpy/test_block_load_from_qpy.py +++ b/test/python/qpy/test_block_load_from_qpy.py @@ -53,10 +53,10 @@ class QpyScheduleTestCase(QiskitTestCase): """QPY schedule testing platform.""" - def assert_roundtrip_equal(self, block): + def assert_roundtrip_equal(self, block, use_symengine=False): """QPY roundtrip equal test.""" qpy_file = io.BytesIO() - dump(block, qpy_file) + dump(block, qpy_file, use_symengine=use_symengine) qpy_file.seek(0) new_block = load(qpy_file)[0] @@ -278,6 +278,31 @@ def test_bell_schedule(self): self.assert_roundtrip_equal(test_sched) + @unittest.skipUnless(_optional.HAS_SYMENGINE, "Symengine required for this test") + def test_bell_schedule_use_symengine(self): + """Test complex schedule to create a Bell state.""" + with builder.build() as test_sched: + with builder.align_sequential(): + # H + builder.shift_phase(-1.57, DriveChannel(0)) + builder.play(Drag(160, 0.05, 40, 1.3), DriveChannel(0)) + builder.shift_phase(-1.57, DriveChannel(0)) + # ECR + with builder.align_left(): + builder.play(GaussianSquare(800, 0.05, 64, 544), DriveChannel(1)) + builder.play(GaussianSquare(800, 0.22, 64, 544, 2), ControlChannel(0)) + builder.play(Drag(160, 0.1, 40, 1.5), DriveChannel(0)) + with builder.align_left(): + builder.play(GaussianSquare(800, -0.05, 64, 544), DriveChannel(1)) + builder.play(GaussianSquare(800, -0.22, 64, 544, 2), ControlChannel(0)) + builder.play(Drag(160, 0.1, 40, 1.5), DriveChannel(0)) + # Measure + with builder.align_left(): + builder.play(GaussianSquare(8000, 0.2, 64, 7744), MeasureChannel(0)) + builder.acquire(8000, AcquireChannel(0), MemorySlot(0)) + + self.assert_roundtrip_equal(test_sched, True) + def test_with_acquire_instruction_with_kernel(self): """Test a schedblk with acquire instruction with kernel.""" kernel = Kernel(