From f39c9070692e3955ada776e1c3fc537fe00fefd1 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Mon, 6 May 2024 08:49:40 -0400 Subject: [PATCH 01/17] Improve header hierarchy for API module pages (#12325) * Improve header hierarchy for API module pages * Tweaks * Review feedback --- qiskit/assembler/__init__.py | 15 +-- qiskit/circuit/__init__.py | 4 +- qiskit/circuit/classical/types/__init__.py | 9 +- qiskit/circuit/classicalfunction/__init__.py | 1 + qiskit/circuit/library/__init__.py | 22 +--- qiskit/converters/__init__.py | 19 ++- qiskit/providers/__init__.py | 29 ++--- qiskit/providers/basic_provider/__init__.py | 25 +--- qiskit/providers/fake_provider/__init__.py | 2 +- qiskit/providers/models/__init__.py | 4 +- qiskit/qpy/__init__.py | 118 +++++++++---------- qiskit/result/__init__.py | 6 + qiskit/scheduler/__init__.py | 11 +- qiskit/scheduler/methods/__init__.py | 9 +- qiskit/transpiler/passes/__init__.py | 6 +- 15 files changed, 129 insertions(+), 151 deletions(-) diff --git a/qiskit/assembler/__init__.py b/qiskit/assembler/__init__.py index a356501a263c..45798084ea62 100644 --- a/qiskit/assembler/__init__.py +++ b/qiskit/assembler/__init__.py @@ -17,23 +17,18 @@ .. currentmodule:: qiskit.assembler -Circuit Assembler -================= +Functions +========= -.. autofunction:: assemble_circuits -Schedule Assembler -================== +.. autofunction:: assemble_circuits .. autofunction:: assemble_schedules -Disassembler -============ - .. autofunction:: disassemble -RunConfig -========= +Classes +======= .. autosummary:: :toctree: ../stubs/ diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index 9fbefb4c5d9f..ff36550967d8 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -203,8 +203,8 @@ .. _circuit-module-api: -API overview of :mod:`qiskit.circuit` -===================================== +API overview of qiskit.circuit +============================== All objects here are described in more detail, and in their greater context in the following sections. This section provides an overview of the API elements documented here. diff --git a/qiskit/circuit/classical/types/__init__.py b/qiskit/circuit/classical/types/__init__.py index 93ab90e32166..14365fd32a6f 100644 --- a/qiskit/circuit/classical/types/__init__.py +++ b/qiskit/circuit/classical/types/__init__.py @@ -47,13 +47,14 @@ Working with types ================== -There are some functions on these types exposed here as well. These are mostly expected to be used -only in manipulations of the expression tree; users who are building expressions using the +There are some additional functions on these types documented in the subsequent sections. +These are mostly expected to be used only in manipulations of the expression tree; +users who are building expressions using the :ref:`user-facing construction interface ` should not need to use these. Partial ordering of types -------------------------- +========================= The type system is equipped with a partial ordering, where :math:`a < b` is interpreted as ":math:`a` is a strict subtype of :math:`b`". Note that the partial ordering is a subset of the @@ -78,7 +79,7 @@ Casting between types ---------------------- +===================== It is common to need to cast values of one type to another type. The casting rules for this are embedded into the :mod:`types` module. You can query the casting kinds using :func:`cast_kind`: diff --git a/qiskit/circuit/classicalfunction/__init__.py b/qiskit/circuit/classicalfunction/__init__.py index a2268acfe2db..a072d910f97a 100644 --- a/qiskit/circuit/classicalfunction/__init__.py +++ b/qiskit/circuit/classicalfunction/__init__.py @@ -81,6 +81,7 @@ def grover_oracle(a: Int1, b: Int1, c: Int1, d: Int1) -> Int1: Decorator for a classical function that returns a `ClassicalFunction` object. +.. autofunction:: classical_function ClassicalFunction ----------------- diff --git a/qiskit/circuit/library/__init__.py b/qiskit/circuit/library/__init__.py index 5f21967e4828..a9ae005d982d 100644 --- a/qiskit/circuit/library/__init__.py +++ b/qiskit/circuit/library/__init__.py @@ -129,35 +129,19 @@ Standard Directives =================== -.. - This summary table deliberately does not generate toctree entries; these directives are "owned" - by ``qiskit.circuit``. - Directives are operations to the quantum stack that are meant to be interpreted by the backend or the transpiler. In general, the transpiler or backend might optionally ignore them if there is no implementation for them. -.. - This summary table deliberately does not generate toctree entries; these directives are "owned" - by ``qiskit.circuit``. - -.. autosummary:: - - Barrier +* :class:`qiskit.circuit.Barrier` Standard Operations =================== Operations are non-reversible changes in the quantum state of the circuit. -.. - This summary table deliberately does not generate toctree entries; these directives are "owned" - by ``qiskit.circuit``. - -.. autosummary:: - - Measure - Reset +* :class:`qiskit.circuit.Measure` +* :class:`qiskit.circuit.Reset` Generalized Gates ================= diff --git a/qiskit/converters/__init__.py b/qiskit/converters/__init__.py index 459b739ee011..f3d3edb5b778 100644 --- a/qiskit/converters/__init__.py +++ b/qiskit/converters/__init__.py @@ -17,12 +17,27 @@ .. currentmodule:: qiskit.converters -.. autofunction:: circuit_to_dag -.. autofunction:: dag_to_circuit +QuantumCircuit -> circuit components +==================================== + .. autofunction:: circuit_to_instruction .. autofunction:: circuit_to_gate + +QuantumCircuit <-> DagCircuit +============================= + +.. autofunction:: circuit_to_dag +.. autofunction:: dag_to_circuit + +QuantumCircuit <-> DagDependency +================================ + .. autofunction:: dagdependency_to_circuit .. autofunction:: circuit_to_dagdependency + +DagCircuit <-> DagDependency +============================ + .. autofunction:: dag_to_dagdependency .. autofunction:: dagdependency_to_dag """ diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index 19d300c9bbf6..b0ebc942523c 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -131,7 +131,6 @@ .. autoexception:: JobTimeoutError .. autoexception:: BackendConfigurationError -===================== Writing a New Backend ===================== @@ -164,7 +163,7 @@ `qiskit-aqt-provider `__ Provider -======== +-------- A provider class serves a single purpose: to get backend objects that enable executing circuits on a device or simulator. The expectation is that any @@ -195,7 +194,7 @@ def backends(self, name=None, **kwargs): method matches the required interface. The rest is up to the specific provider on how to implement. Backend -======= +------- The backend classes are the core to the provider. These classes are what provide the interface between Qiskit and the hardware or simulator that will @@ -276,8 +275,8 @@ def run(circuits, **kwargs): return MyJob(self. job_handle, job_json, circuit) -Transpiler Interface --------------------- +Backend's Transpiler Interface +------------------------------ The key piece of the :class:`~qiskit.providers.Backend` object is how it describes itself to the compiler. This is handled with the :class:`~qiskit.transpiler.Target` class which defines @@ -453,8 +452,8 @@ def get_translation_stage_plugin(self): efficient output on ``Mybackend`` the transpiler will be able to perform these custom steps without any manual user input. -Run Method ----------- +Backend.run Method +-------------------- Of key importance is the :meth:`~qiskit.providers.BackendV2.run` method, which is used to actually submit circuits to a device or simulator. The run method @@ -484,8 +483,8 @@ def run(self, circuits. **kwargs): job_handle = submit_to_backend(job_jsonb) return MyJob(self. job_handle, job_json, circuit) -Options -------- +Backend Options +--------------- There are often several options for a backend that control how a circuit is run. The typical example of this is something like the number of ``shots`` which is @@ -515,7 +514,7 @@ def _default_options(cls): Job -=== +--- The output from the :obj:`~qiskit.providers.BackendV2.run` method is a :class:`~qiskit.providers.JobV1` object. Each provider is expected to implement a custom job subclass that @@ -612,7 +611,7 @@ def status(self): return JobStatus.DONE Primitives -========== +---------- While not directly part of the provider interface, the :mod:`qiskit.primitives` module is tightly coupled with providers. Specifically the primitive @@ -640,12 +639,8 @@ def status(self): :class:`~.Estimator`, :class:`~.BackendSampler`, and :class:`~.BackendEstimator` can serve as references/models on how to implement these as well. -====================================== -Migrating between Backend API Versions -====================================== - -BackendV1 -> BackendV2 -====================== +Migrating from BackendV1 to BackendV2 +===================================== The :obj:`~BackendV2` class re-defined user access for most properties of a backend to make them work with native Qiskit data structures and have flatter diff --git a/qiskit/providers/basic_provider/__init__.py b/qiskit/providers/basic_provider/__init__.py index 48427c73fca0..4fc0f06d76af 100644 --- a/qiskit/providers/basic_provider/__init__.py +++ b/qiskit/providers/basic_provider/__init__.py @@ -27,36 +27,15 @@ backend = BasicProvider().get_backend('basic_simulator') -Simulators -========== +Classes +======= .. autosummary:: :toctree: ../stubs/ BasicSimulator - -Provider -======== - -.. autosummary:: - :toctree: ../stubs/ - BasicProvider - -Job Class -========= - -.. autosummary:: - :toctree: ../stubs/ - BasicProviderJob - -Exceptions -========== - -.. autosummary:: - :toctree: ../stubs/ - BasicProviderError """ diff --git a/qiskit/providers/fake_provider/__init__.py b/qiskit/providers/fake_provider/__init__.py index 00dadd2ad254..9526793f0e10 100644 --- a/qiskit/providers/fake_provider/__init__.py +++ b/qiskit/providers/fake_provider/__init__.py @@ -24,7 +24,7 @@ useful for testing the transpiler and other backend-facing functionality. Example Usage -============= +------------- Here is an example of using a simulated backend for transpilation and running. diff --git a/qiskit/providers/models/__init__.py b/qiskit/providers/models/__init__.py index a69038eb78c7..bf90a9d16c0e 100644 --- a/qiskit/providers/models/__init__.py +++ b/qiskit/providers/models/__init__.py @@ -19,8 +19,8 @@ Qiskit schema-conformant objects used by the backends and providers. -Backend Objects -=============== +Classes +======= .. autosummary:: :toctree: ../stubs/ diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index d7275bcd62f4..4e7769106fc6 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -11,9 +11,9 @@ # that they have been altered from the originals. """ -########################################################### +===================================== QPY serialization (:mod:`qiskit.qpy`) -########################################################### +===================================== .. currentmodule:: qiskit.qpy @@ -32,9 +32,8 @@ version (it is also `potentially insecure `__). -********* -Using QPY -********* +Basic Usage +=========== Using QPY is defined to be straightforward and mirror the user API of the serializers in Python's standard library, ``pickle`` and ``json``. There are @@ -248,9 +247,8 @@ .. _qpy_format: -********** QPY Format -********** +========== The QPY serialization format is a portable cross-platform binary serialization format for :class:`~qiskit.circuit.QuantumCircuit` objects in Qiskit. The basic @@ -303,14 +301,14 @@ .. _qpy_version_12: Version 12 -========== +---------- Version 12 adds support for: * circuits containing memory-owning :class:`.expr.Var` variables. Changes to HEADER ------------------ +~~~~~~~~~~~~~~~~~ The HEADER struct for an individual circuit has added three ``uint32_t`` counts of the input, captured and locally declared variables in the circuit. The new form looks like: @@ -336,7 +334,7 @@ EXPR_VAR_DECLARATION --------------------- +~~~~~~~~~~~~~~~~~~~~ An ``EXPR_VAR_DECLARATION`` defines an :class:`.expr.Var` instance that is standalone; that is, it represents a self-owned memory location rather than wrapping a :class:`.Clbit` or @@ -367,7 +365,7 @@ Changes to EXPR_VAR -------------------- +~~~~~~~~~~~~~~~~~~~ The EXPR_VAR variable has gained a new type code and payload, in addition to the pre-existing ones: @@ -400,7 +398,7 @@ .. _qpy_version_11: Version 11 -========== +---------- Version 11 is identical to Version 10 except for the following. First, the names in the CUSTOM_INSTRUCTION blocks @@ -418,7 +416,7 @@ .. _modifier_qpy: MODIFIER --------- +~~~~~~~~ This represents :class:`~qiskit.circuit.annotated_operation.Modifier` @@ -441,7 +439,7 @@ .. _qpy_version_10: Version 10 -========== +---------- Version 10 adds support for: @@ -454,7 +452,7 @@ encoding and ``e`` refers to symengine encoding. Changes to FILE_HEADER ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ The contents of FILE_HEADER after V10 are defined as a C struct as: @@ -470,7 +468,7 @@ } FILE_HEADER_V10; Changes to LAYOUT ------------------ +~~~~~~~~~~~~~~~~~ The ``LAYOUT`` struct is updated to have an additional ``input_qubit_count`` field. With version 10 the ``LAYOUT`` struct is now: @@ -493,14 +491,14 @@ .. _qpy_version_9: Version 9 -========= +--------- Version 9 adds support for classical :class:`~.expr.Expr` nodes and their associated :class:`~.types.Type`\\ s. EXPRESSION ----------- +~~~~~~~~~~ An :class:`~.expr.Expr` node is represented by a stream of variable-width data. A node itself is represented by (in order in the byte stream): @@ -532,7 +530,7 @@ EXPR_TYPE ---------- +~~~~~~~~~ A :class:`~.types.Type` is encoded by a single-byte ASCII ``char`` that encodes the kind of type, followed by a payload that varies depending on the type. The defined codes are: @@ -547,7 +545,7 @@ EXPR_VAR --------- +~~~~~~~~ This represents a runtime variable of a :class:`~.expr.Var` node. These are a type code, followed by a type-code-specific payload: @@ -564,7 +562,7 @@ EXPR_VALUE ----------- +~~~~~~~~~~ This represents a literal object in the classical type system, such as an integer. Currently there are very few such literals. These are encoded as a type code, followed by a type-code-specific @@ -582,7 +580,7 @@ Changes to INSTRUCTION ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ To support the use of :class:`~.expr.Expr` nodes in the fields :attr:`.IfElseOp.condition`, :attr:`.WhileLoopOp.condition` and :attr:`.SwitchCaseOp.target`, the INSTRUCTION struct is changed @@ -629,7 +627,7 @@ Changes to INSTRUCTION_PARAM ----------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A new type code ``x`` is added that defines an EXPRESSION parameter. @@ -637,7 +635,7 @@ .. _qpy_version_8: Version 8 -========= +--------- Version 8 adds support for handling a :class:`~.TranspileLayout` stored in the :attr:`.QuantumCircuit.layout` attribute. In version 8 immediately following the @@ -646,7 +644,7 @@ :class:`~.TranspileLayout` class. LAYOUT ------- +~~~~~~ .. code-block:: c @@ -668,7 +666,7 @@ :attr:`.TranspileLayout.initial_layout` attribute. INITIAL_LAYOUT_BIT ------------------- +~~~~~~~~~~~~~~~~~~ .. code-block:: c @@ -694,7 +692,7 @@ .. _qpy_version_7: Version 7 -========= +--------- Version 7 adds support for :class:`.~Reference` instruction and serialization of a :class:`.~ScheduleBlock` program while keeping its reference to subroutines:: @@ -740,7 +738,7 @@ .. _qpy_version_6: Version 6 -========= +--------- Version 6 adds support for :class:`.~ScalableSymbolicPulse`. These objects are saved and read like `SymbolicPulse` objects, and the class name is added to the data to correctly handle @@ -767,7 +765,7 @@ .. _qpy_version_5: Version 5 -========= +--------- Version 5 changes from :ref:`qpy_version_4` by adding support for :class:`.~ScheduleBlock` and changing two payloads the INSTRUCTION metadata payload and the CUSTOM_INSTRUCTION block. @@ -802,7 +800,7 @@ .. _qpy_schedule_block: SCHEDULE_BLOCK --------------- +~~~~~~~~~~~~~~ :class:`~.ScheduleBlock` is first supported in QPY Version 5. This allows users to save pulse programs in the QPY binary format as follows: @@ -827,7 +825,7 @@ .. _qpy_schedule_block_header: SCHEDULE_BLOCK_HEADER ---------------------- +~~~~~~~~~~~~~~~~~~~~~ :class:`~.ScheduleBlock` block starts with the following header: @@ -846,7 +844,7 @@ .. _qpy_schedule_alignments: SCHEDULE_BLOCK_ALIGNMENTS -------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~ Then, alignment context of the schedule block starts with ``char`` representing the supported context type followed by the :ref:`qpy_sequence` block representing @@ -864,7 +862,7 @@ .. _qpy_schedule_instructions: SCHEDULE_BLOCK_INSTRUCTIONS ---------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~ This alignment block is further followed by ``num_element`` length of block elements which may consist of nested schedule blocks and schedule instructions. @@ -889,7 +887,7 @@ .. _qpy_schedule_operands: SCHEDULE_BLOCK_OPERANDS ------------------------ +~~~~~~~~~~~~~~~~~~~~~~~ The operands of these instances can be serialized through the standard QPY value serialization mechanism, however there are special object types that only appear in the schedule operands. @@ -906,7 +904,7 @@ .. _qpy_schedule_channel: CHANNEL -------- +~~~~~~~ Channel block starts with channel subtype ``char`` that maps an object data to :class:`~qiskit.pulse.channels.Channel` subclass. Mapping is defined as follows: @@ -923,7 +921,7 @@ .. _qpy_schedule_waveform: Waveform --------- +~~~~~~~~ Waveform block starts with WAVEFORM header: @@ -945,7 +943,7 @@ .. _qpy_schedule_symbolic_pulse: SymbolicPulse -------------- +~~~~~~~~~~~~~ SymbolicPulse block starts with SYMBOLIC_PULSE header: @@ -979,7 +977,7 @@ .. _qpy_mapping: MAPPING -------- +~~~~~~~ The MAPPING is a representation for arbitrary mapping object. This is a fixed length :ref:`qpy_sequence` of key-value pair represented by the MAP_ITEM payload. @@ -1001,7 +999,7 @@ .. _qpy_circuit_calibrations: CIRCUIT_CALIBRATIONS --------------------- +~~~~~~~~~~~~~~~~~~~~ The CIRCUIT_CALIBRATIONS block is a dictionary to define pulse calibrations of the custom instruction set. This block starts with the following CALIBRATION header: @@ -1036,7 +1034,7 @@ .. _qpy_instruction_v5: INSTRUCTION ------------ +~~~~~~~~~~~ The INSTRUCTION block was modified to add two new fields ``num_ctrl_qubits`` and ``ctrl_state`` which are used to model the :attr:`.ControlledGate.num_ctrl_qubits` and @@ -1062,7 +1060,7 @@ :ref:`qpy_instructions` for the details of the full payload. CUSTOM_INSTRUCTION ------------------- +~~~~~~~~~~~~~~~~~~ The CUSTOM_INSTRUCTION block in QPY version 5 adds a new field ``base_gate_size`` which is used to define the size of the @@ -1105,7 +1103,7 @@ .. _qpy_version_4: Version 4 -========= +--------- Version 4 is identical to :ref:`qpy_version_3` except that it adds 2 new type strings to the INSTRUCTION_PARAM struct, ``z`` to represent ``None`` (which is encoded as @@ -1135,7 +1133,7 @@ .. _qpy_range_pack: RANGE ------ +~~~~~ A RANGE is a representation of a ``range`` object. It is defined as: @@ -1150,7 +1148,7 @@ .. _qpy_sequence: SEQUENCE --------- +~~~~~~~~ A SEQUENCE is a representation of an arbitrary sequence object. As sequence are just fixed length containers of arbitrary python objects their QPY can't fully represent any sequence, @@ -1172,7 +1170,7 @@ .. _qpy_version_3: Version 3 -========= +--------- Version 3 of the QPY format is identical to :ref:`qpy_version_2` except that it defines a struct format to represent a :class:`~qiskit.circuit.library.PauliEvolutionGate` @@ -1187,7 +1185,7 @@ .. _pauli_evo_qpy: PAULI_EVOLUTION ---------------- +~~~~~~~~~~~~~~~ This represents the high level :class:`~qiskit.circuit.library.PauliEvolutionGate` @@ -1215,7 +1213,7 @@ .. _qpy_pauli_sum_op: SPARSE_PAULI_OP_LIST_ELEM -------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~ This represents an instance of :class:`.SparsePauliOp`. @@ -1239,7 +1237,7 @@ .. _qpy_param_vector: PARAMETER_VECTOR_ELEMENT ------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~ A PARAMETER_VECTOR_ELEMENT represents a :class:`~qiskit.circuit.ParameterVectorElement` object the data for a INSTRUCTION_PARAM. The contents of the PARAMETER_VECTOR_ELEMENT are @@ -1261,7 +1259,7 @@ PARAMETER_EXPR --------------- +~~~~~~~~~~~~~~ Additionally, since QPY format version v3 distinguishes between a :class:`~qiskit.circuit.Parameter` and :class:`~qiskit.circuit.ParameterVectorElement` @@ -1315,14 +1313,14 @@ .. _qpy_version_2: Version 2 -========= +--------- Version 2 of the QPY format is identical to version 1 except for the HEADER section is slightly different. You can refer to the :ref:`qpy_version_1` section for the details on the rest of the payload format. HEADER ------- +~~~~~~ The contents of HEADER are defined as a C struct are: @@ -1352,10 +1350,10 @@ .. _qpy_version_1: Version 1 -========= +--------- HEADER ------- +~~~~~~ The contents of HEADER as defined as a C struct are: @@ -1375,7 +1373,7 @@ of the circuit. METADATA --------- +~~~~~~~~ The METADATA field is a UTF8 encoded JSON string. After reading the HEADER (which is a fixed size at the start of the QPY file) and the ``name`` string @@ -1385,7 +1383,7 @@ .. _qpy_registers: REGISTERS ---------- +~~~~~~~~~ The contents of REGISTERS is a number of REGISTER object. If num_registers is > 0 then after reading METADATA you read that number of REGISTER structs defined @@ -1435,7 +1433,7 @@ .. _qpy_custom_definition: CUSTOM_DEFINITIONS ------------------- +~~~~~~~~~~~~~~~~~~ This section specifies custom definitions for any of the instructions in the circuit. @@ -1475,7 +1473,7 @@ .. _qpy_instructions: INSTRUCTIONS ------------- +~~~~~~~~~~~~ The contents of INSTRUCTIONS is a list of INSTRUCTION metadata objects @@ -1551,7 +1549,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom .. _qpy_param_struct: PARAMETER ---------- +~~~~~~~~~ A PARAMETER represents a :class:`~qiskit.circuit.Parameter` object the data for a INSTRUCTION_PARAM. The contents of the PARAMETER are defined as: @@ -1569,7 +1567,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom .. _qpy_param_expr: PARAMETER_EXPR --------------- +~~~~~~~~~~~~~~ A PARAMETER_EXPR represents a :class:`~qiskit.circuit.ParameterExpression` object that the data for an INSTRUCTION_PARAM. The contents of a PARAMETER_EXPR @@ -1608,7 +1606,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom .. _qpy_complex: COMPLEX -------- +~~~~~~~ When representing a double precision complex value in QPY the following struct is used: diff --git a/qiskit/result/__init__.py b/qiskit/result/__init__.py index 08b43f704939..2eaa7803c5fd 100644 --- a/qiskit/result/__init__.py +++ b/qiskit/result/__init__.py @@ -17,6 +17,9 @@ .. currentmodule:: qiskit.result +Core classes +============ + .. autosummary:: :toctree: ../stubs/ @@ -24,6 +27,9 @@ ResultError Counts +Marginalization +=============== + .. autofunction:: marginal_counts .. autofunction:: marginal_distribution .. autofunction:: marginal_memory diff --git a/qiskit/scheduler/__init__.py b/qiskit/scheduler/__init__.py index b33ececf5d65..7062e01a941e 100644 --- a/qiskit/scheduler/__init__.py +++ b/qiskit/scheduler/__init__.py @@ -19,13 +19,22 @@ A circuit scheduler compiles a circuit program to a pulse program. +Core API +======== + .. autoclass:: ScheduleConfig .. currentmodule:: qiskit.scheduler.schedule_circuit .. autofunction:: schedule_circuit .. currentmodule:: qiskit.scheduler -.. automodule:: qiskit.scheduler.methods +Pulse scheduling methods +======================== + +.. currentmodule:: qiskit.scheduler.methods +.. autofunction:: as_soon_as_possible +.. autofunction:: as_late_as_possible +.. currentmodule:: qiskit.scheduler """ from qiskit.scheduler import schedule_circuit from qiskit.scheduler.config import ScheduleConfig diff --git a/qiskit/scheduler/methods/__init__.py b/qiskit/scheduler/methods/__init__.py index 1fe4b301b7ab..6df887d54995 100644 --- a/qiskit/scheduler/methods/__init__.py +++ b/qiskit/scheduler/methods/__init__.py @@ -10,13 +10,6 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -""" -.. currentmodule:: qiskit.scheduler.methods - -Pulse scheduling methods. - -.. autofunction:: as_soon_as_possible -.. autofunction:: as_late_as_possible -""" +"""Scheduling methods.""" from qiskit.scheduler.methods.basic import as_soon_as_possible, as_late_as_possible diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 54599e00b9ae..400d98304951 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -154,8 +154,10 @@ HLSConfig SolovayKitaev -Post Layout (Post transpile qubit selection) -============================================ +Post Layout +=========== + +These are post qubit selection. .. autosummary:: :toctree: ../stubs/ From 2c418aa9b359645642230c1cecb5122e9d01de56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 13:27:12 +0000 Subject: [PATCH 02/17] Bump num-traits from 0.2.18 to 0.2.19 (#12349) Bumps [num-traits](https://github.com/rust-num/num-traits) from 0.2.18 to 0.2.19. - [Changelog](https://github.com/rust-num/num-traits/blob/master/RELEASES.md) - [Commits](https://github.com/rust-num/num-traits/compare/num-traits-0.2.18...num-traits-0.2.19) --- updated-dependencies: - dependency-name: num-traits dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 84e0e0090663..c8b22ed79172 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -754,9 +754,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", From c062dd6cf22558f630903d530728f2e1b60dacc3 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 6 May 2024 15:08:19 -0400 Subject: [PATCH 03/17] Fix Rustfmt and clippy with latest rust stable (#12339) * Fix Rustfmt and clippy with latest rust stable Running `cargo fmt` with a newer version of rust than what we use in CI (1.77.2 and 1.78.0 locally for me) is triggering the formatting updates in this commit. To ensure developers don't have to worry about commiting an accidental formatting change in their commits this commit proactively makes the change. Similarly the recent Rust 1.78 release included new clippy rules which are flagging some small issues that our MSRV of clippy doesn't have. This commit also fixes these as the suggestions are good and are compatible with our MSRV of 1.70. * Rename deprecated config file name * Remove dead code --- .cargo/{config => config.toml} | 0 crates/accelerate/src/sparse_pauli_op.rs | 4 +++- crates/circuit/src/circuit_data.rs | 2 +- crates/qasm3/src/circuit.rs | 10 ---------- 4 files changed, 4 insertions(+), 12 deletions(-) rename .cargo/{config => config.toml} (100%) diff --git a/.cargo/config b/.cargo/config.toml similarity index 100% rename from .cargo/config rename to .cargo/config.toml diff --git a/crates/accelerate/src/sparse_pauli_op.rs b/crates/accelerate/src/sparse_pauli_op.rs index 5d6a82df7940..808269d8ab90 100644 --- a/crates/accelerate/src/sparse_pauli_op.rs +++ b/crates/accelerate/src/sparse_pauli_op.rs @@ -141,7 +141,9 @@ impl ZXPaulis { phases: &Bound>, coeffs: &Bound>, ) -> PyResult { - let &[num_ops, num_qubits] = x.shape() else { unreachable!("PyArray2 must be 2D") }; + let &[num_ops, num_qubits] = x.shape() else { + unreachable!("PyArray2 must be 2D") + }; if z.shape() != [num_ops, num_qubits] { return Err(PyValueError::new_err(format!( "'x' and 'z' have different shapes: {:?} and {:?}", diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 590fc07e8f8b..944565cf36d8 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -324,7 +324,7 @@ impl CircuitData { 0, )?; res.intern_context = self.intern_context.clone(); - res.data = self.data.clone(); + res.data.clone_from(&self.data); Ok(res) } diff --git a/crates/qasm3/src/circuit.rs b/crates/qasm3/src/circuit.rs index 747980819a0e..330805fa2f86 100644 --- a/crates/qasm3/src/circuit.rs +++ b/crates/qasm3/src/circuit.rs @@ -16,7 +16,6 @@ use pyo3::types::{PyList, PyString, PyTuple, PyType}; use crate::error::QASM3ImporterError; pub trait PyRegister { - fn bit(&self, py: Python, index: usize) -> PyResult>; // This really should be // fn iter<'a>(&'a self, py: Python<'a>) -> impl Iterator; // or at a minimum @@ -39,15 +38,6 @@ macro_rules! register_type { } impl PyRegister for $name { - /// Get an individual bit from the register. - fn bit(&self, py: Python, index: usize) -> PyResult> { - // Unfortunately, `PyList::get_item_unchecked` isn't usable with the stable ABI. - self.items - .bind(py) - .get_item(index) - .map(|item| item.into_py(py)) - } - fn bit_list<'a>(&'a self, py: Python<'a>) -> &Bound<'a, PyList> { self.items.bind(py) } From 6ff0eb54f88cdbe98275872b8ec9d65f76948bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Tue, 7 May 2024 11:49:31 +0200 Subject: [PATCH 04/17] Add standalone test file for Clifford synthesis functions (#12347) * Convert synthesis imports to runtime imports to avoid cyclic import errors * Set copy=False in append * Move clifford synthesis tests to separate file in test/synthesis. * Add random_clifford_circuit to qiskit.circuit.random * Remove pylint disable * Fix reno --- .../generalized_gates/linear_function.py | 5 +- .../circuit/library/generalized_gates/uc.py | 2 +- .../library/generalized_gates/unitary.py | 18 +- .../n_local/evolved_operator_ansatz.py | 5 +- qiskit/circuit/library/pauli_evolution.py | 10 +- qiskit/circuit/random/__init__.py | 2 +- qiskit/circuit/random/utils.py | 71 ++++++- .../quantum_info/operators/dihedral/random.py | 6 +- .../clifford/clifford_decompose_bm.py | 6 +- .../clifford/clifford_decompose_layers.py | 18 +- ...random-clifford-util-5358041208729988.yaml | 14 ++ .../operators/symplectic/test_clifford.py | 187 +----------------- .../synthesis/test_clifford_sythesis.py | 118 +++++++++++ 13 files changed, 248 insertions(+), 214 deletions(-) create mode 100644 releasenotes/notes/add-random-clifford-util-5358041208729988.yaml create mode 100644 test/python/synthesis/test_clifford_sythesis.py diff --git a/qiskit/circuit/library/generalized_gates/linear_function.py b/qiskit/circuit/library/generalized_gates/linear_function.py index 68deaddd7327..519a306c357e 100644 --- a/qiskit/circuit/library/generalized_gates/linear_function.py +++ b/qiskit/circuit/library/generalized_gates/linear_function.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2021. +# (C) Copyright IBM 2017, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -16,7 +16,6 @@ import numpy as np from qiskit.circuit.quantumcircuit import QuantumCircuit, Gate from qiskit.circuit.exceptions import CircuitError -from qiskit.synthesis.linear import check_invertible_binary_matrix from qiskit.circuit.library.generalized_gates.permutation import PermutationGate # pylint: disable=cyclic-import @@ -115,6 +114,8 @@ def __init__( # Optionally, check that the matrix is invertible if validate_input: + from qiskit.synthesis.linear import check_invertible_binary_matrix + if not check_invertible_binary_matrix(linear): raise CircuitError( "A linear function must be represented by an invertible matrix." diff --git a/qiskit/circuit/library/generalized_gates/uc.py b/qiskit/circuit/library/generalized_gates/uc.py index f54567123e02..6e6a1db95ca3 100644 --- a/qiskit/circuit/library/generalized_gates/uc.py +++ b/qiskit/circuit/library/generalized_gates/uc.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit/circuit/library/generalized_gates/unitary.py b/qiskit/circuit/library/generalized_gates/unitary.py index 1fd36e52e0c0..6a6623ffce5d 100644 --- a/qiskit/circuit/library/generalized_gates/unitary.py +++ b/qiskit/circuit/library/generalized_gates/unitary.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2019. +# (C) Copyright IBM 2017, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -30,14 +30,8 @@ from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.quantum_info.operators.predicates import is_unitary_matrix -# pylint: disable=cyclic-import -from qiskit.synthesis.one_qubit.one_qubit_decompose import OneQubitEulerDecomposer -from qiskit.synthesis.two_qubit.two_qubit_decompose import two_qubit_cnot_decompose - from .isometry import Isometry -_DECOMPOSER1Q = OneQubitEulerDecomposer("U") - if typing.TYPE_CHECKING: from qiskit.quantum_info.operators.base_operator import BaseOperator @@ -143,13 +137,21 @@ def transpose(self): def _define(self): """Calculate a subcircuit that implements this unitary.""" if self.num_qubits == 1: + from qiskit.synthesis.one_qubit.one_qubit_decompose import OneQubitEulerDecomposer + q = QuantumRegister(1, "q") qc = QuantumCircuit(q, name=self.name) - theta, phi, lam, global_phase = _DECOMPOSER1Q.angles_and_phase(self.to_matrix()) + theta, phi, lam, global_phase = OneQubitEulerDecomposer("U").angles_and_phase( + self.to_matrix() + ) qc._append(UGate(theta, phi, lam), [q[0]], []) qc.global_phase = global_phase self.definition = qc elif self.num_qubits == 2: + from qiskit.synthesis.two_qubit.two_qubit_decompose import ( # pylint: disable=cyclic-import + two_qubit_cnot_decompose, + ) + self.definition = two_qubit_cnot_decompose(self.to_matrix()) else: from qiskit.synthesis.unitary.qsd import ( # pylint: disable=cyclic-import diff --git a/qiskit/circuit/library/n_local/evolved_operator_ansatz.py b/qiskit/circuit/library/n_local/evolved_operator_ansatz.py index a50b48ce488e..4bc6bcc58a13 100644 --- a/qiskit/circuit/library/n_local/evolved_operator_ansatz.py +++ b/qiskit/circuit/library/n_local/evolved_operator_ansatz.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -22,7 +22,6 @@ from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.quantum_info import Operator, Pauli, SparsePauliOp -from qiskit.synthesis.evolution import LieTrotter from .n_local import NLocal @@ -185,6 +184,8 @@ def _evolve_operator(self, operator, time): gate = HamiltonianGate(operator, time) # otherwise, use the PauliEvolutionGate else: + from qiskit.synthesis.evolution import LieTrotter + evolution = LieTrotter() if self._evolution is None else self._evolution gate = PauliEvolutionGate(operator, time, synthesis=evolution) diff --git a/qiskit/circuit/library/pauli_evolution.py b/qiskit/circuit/library/pauli_evolution.py index c6d69789bae6..b0af3fbe4163 100644 --- a/qiskit/circuit/library/pauli_evolution.py +++ b/qiskit/circuit/library/pauli_evolution.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -14,14 +14,16 @@ from __future__ import annotations -from typing import Union, Optional +from typing import Union, Optional, TYPE_CHECKING import numpy as np from qiskit.circuit.gate import Gate from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.synthesis.evolution import EvolutionSynthesis, LieTrotter from qiskit.quantum_info import Pauli, SparsePauliOp +if TYPE_CHECKING: + from qiskit.synthesis.evolution import EvolutionSynthesis + class PauliEvolutionGate(Gate): r"""Time-evolution of an operator consisting of Paulis. @@ -107,6 +109,8 @@ class docstring for an example. operator = _to_sparse_pauli_op(operator) if synthesis is None: + from qiskit.synthesis.evolution import LieTrotter + synthesis = LieTrotter() if label is None: diff --git a/qiskit/circuit/random/__init__.py b/qiskit/circuit/random/__init__.py index 3e3dc752d5a1..06e817bb4de8 100644 --- a/qiskit/circuit/random/__init__.py +++ b/qiskit/circuit/random/__init__.py @@ -12,4 +12,4 @@ """Method for generating random circuits.""" -from .utils import random_circuit +from .utils import random_circuit, random_clifford_circuit diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index 71809735aa8e..fc497a300cba 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017. +# (C) Copyright IBM 2017, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -207,3 +207,72 @@ def random_circuit( qc.measure(qc.qubits, cr) return qc + + +def random_clifford_circuit(num_qubits, num_gates, gates="all", seed=None): + """Generate a pseudo-random Clifford circuit. + + This function will generate a Clifford circuit by randomly selecting the chosen amount of Clifford + gates from the set of standard gates in :mod:`qiskit.circuit.library.standard_gates`. For example: + + .. plot:: + :include-source: + + from qiskit.circuit.random import random_clifford_circuit + + circ = random_clifford_circuit(num_qubits=2, num_gates=6) + circ.draw(output='mpl') + + Args: + num_qubits (int): number of quantum wires. + num_gates (int): number of gates in the circuit. + gates (list[str]): optional list of Clifford gate names to randomly sample from. + If ``"all"`` (default), use all Clifford gates in the standard library. + seed (int | np.random.Generator): sets random seed/generator (optional). + + Returns: + QuantumCircuit: constructed circuit + """ + + gates_1q = ["i", "x", "y", "z", "h", "s", "sdg", "sx", "sxdg"] + gates_2q = ["cx", "cz", "cy", "swap", "iswap", "ecr", "dcx"] + if gates == "all": + if num_qubits == 1: + gates = gates_1q + else: + gates = gates_1q + gates_2q + + instructions = { + "i": (standard_gates.IGate(), 1), + "x": (standard_gates.XGate(), 1), + "y": (standard_gates.YGate(), 1), + "z": (standard_gates.ZGate(), 1), + "h": (standard_gates.HGate(), 1), + "s": (standard_gates.SGate(), 1), + "sdg": (standard_gates.SdgGate(), 1), + "sx": (standard_gates.SXGate(), 1), + "sxdg": (standard_gates.SXdgGate(), 1), + "cx": (standard_gates.CXGate(), 2), + "cy": (standard_gates.CYGate(), 2), + "cz": (standard_gates.CZGate(), 2), + "swap": (standard_gates.SwapGate(), 2), + "iswap": (standard_gates.iSwapGate(), 2), + "ecr": (standard_gates.ECRGate(), 2), + "dcx": (standard_gates.DCXGate(), 2), + } + + if isinstance(seed, np.random.Generator): + rng = seed + else: + rng = np.random.default_rng(seed) + + samples = rng.choice(gates, num_gates) + + circ = QuantumCircuit(num_qubits) + + for name in samples: + gate, nqargs = instructions[name] + qargs = rng.choice(range(num_qubits), nqargs, replace=False).tolist() + circ.append(gate, qargs, copy=False) + + return circ diff --git a/qiskit/quantum_info/operators/dihedral/random.py b/qiskit/quantum_info/operators/dihedral/random.py index f339cf983771..4331d618d73d 100644 --- a/qiskit/quantum_info/operators/dihedral/random.py +++ b/qiskit/quantum_info/operators/dihedral/random.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019, 2021. +# (C) Copyright IBM 2019, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -49,7 +49,9 @@ def random_cnotdihedral(num_qubits, seed=None): # Random affine function # Random invertible binary matrix - from qiskit.synthesis.linear import random_invertible_binary_matrix + from qiskit.synthesis.linear import ( # pylint: disable=cyclic-import + random_invertible_binary_matrix, + ) linear = random_invertible_binary_matrix(num_qubits, seed=rng) elem.linear = linear diff --git a/qiskit/synthesis/clifford/clifford_decompose_bm.py b/qiskit/synthesis/clifford/clifford_decompose_bm.py index cbc54f16bb0c..50ffcb743162 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_bm.py +++ b/qiskit/synthesis/clifford/clifford_decompose_bm.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -76,11 +76,11 @@ def synth_clifford_bm(clifford: Clifford) -> QuantumCircuit: pos = [qubit, qubit + num_qubits] circ = _decompose_clifford_1q(clifford.tableau[pos][:, pos + [-1]]) if len(circ) > 0: - ret_circ.append(circ, [qubit]) + ret_circ.append(circ, [qubit], copy=False) # Add the inverse of the 2-qubit reductions circuit if len(inv_circuit) > 0: - ret_circ.append(inv_circuit.inverse(), range(num_qubits)) + ret_circ.append(inv_circuit.inverse(), range(num_qubits), copy=False) return ret_circ.decompose() diff --git a/qiskit/synthesis/clifford/clifford_decompose_layers.py b/qiskit/synthesis/clifford/clifford_decompose_layers.py index f1a7c5cce137..2fc9ca5bdb29 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_layers.py +++ b/qiskit/synthesis/clifford/clifford_decompose_layers.py @@ -137,32 +137,32 @@ def synth_clifford_layers( cz_func_reverse_qubits=cz_func_reverse_qubits, ) - layeredCircuit.append(S2_circ, qubit_list) + layeredCircuit.append(S2_circ, qubit_list, copy=False) if cx_cz_synth_func is None: - layeredCircuit.append(CZ2_circ, qubit_list) + layeredCircuit.append(CZ2_circ, qubit_list, copy=False) CXinv = CX_circ.copy().inverse() - layeredCircuit.append(CXinv, qubit_list) + layeredCircuit.append(CXinv, qubit_list, copy=False) else: # note that CZ2_circ is None and built into the CX_circ when # cx_cz_synth_func is not None - layeredCircuit.append(CX_circ, qubit_list) + layeredCircuit.append(CX_circ, qubit_list, copy=False) - layeredCircuit.append(H2_circ, qubit_list) - layeredCircuit.append(S1_circ, qubit_list) - layeredCircuit.append(CZ1_circ, qubit_list) + layeredCircuit.append(H2_circ, qubit_list, copy=False) + layeredCircuit.append(S1_circ, qubit_list, copy=False) + layeredCircuit.append(CZ1_circ, qubit_list, copy=False) if cz_func_reverse_qubits: H1_circ = H1_circ.reverse_bits() - layeredCircuit.append(H1_circ, qubit_list) + layeredCircuit.append(H1_circ, qubit_list, copy=False) # Add Pauli layer to fix the Clifford phase signs clifford_target = Clifford(layeredCircuit) pauli_circ = _calc_pauli_diff(cliff, clifford_target) - layeredCircuit.append(pauli_circ, qubit_list) + layeredCircuit.append(pauli_circ, qubit_list, copy=False) return layeredCircuit diff --git a/releasenotes/notes/add-random-clifford-util-5358041208729988.yaml b/releasenotes/notes/add-random-clifford-util-5358041208729988.yaml new file mode 100644 index 000000000000..7f2e20db6522 --- /dev/null +++ b/releasenotes/notes/add-random-clifford-util-5358041208729988.yaml @@ -0,0 +1,14 @@ +--- +features_circuits: + - | + Added a new function to ``qiskit.circuit.random`` that allows to generate a pseudo-random + Clifford circuit with gates from the standard library: :func:`.random_clifford_circuit`. + Example usage: + + .. plot:: + :include-source: + + from qiskit.circuit.random import random_clifford_circuit + + circ = random_clifford_circuit(num_qubits=2, num_gates=6) + circ.draw(output='mpl') diff --git a/test/python/quantum_info/operators/symplectic/test_clifford.py b/test/python/quantum_info/operators/symplectic/test_clifford.py index f23c0155bc6d..3585efb9f646 100644 --- a/test/python/quantum_info/operators/symplectic/test_clifford.py +++ b/test/python/quantum_info/operators/symplectic/test_clifford.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2023. +# (C) Copyright IBM 2017, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -17,7 +17,9 @@ import numpy as np from ddt import ddt -from qiskit.circuit import Gate, QuantumCircuit, QuantumRegister +from qiskit.circuit import Gate, QuantumCircuit +from qiskit.circuit.random import random_clifford_circuit + from qiskit.circuit.library import ( CPhaseGate, CRXGate, @@ -26,7 +28,6 @@ CXGate, CYGate, CZGate, - DCXGate, ECRGate, HGate, IGate, @@ -37,10 +38,7 @@ RYYGate, RZZGate, RZXGate, - SdgGate, SGate, - SXGate, - SXdgGate, SwapGate, XGate, XXMinusYYGate, @@ -57,98 +55,11 @@ from qiskit.quantum_info.operators import Clifford, Operator from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.quantum_info.operators.symplectic.clifford_circuits import _append_operation -from qiskit.synthesis.clifford import ( - synth_clifford_full, - synth_clifford_ag, - synth_clifford_bm, - synth_clifford_greedy, -) from qiskit.synthesis.linear import random_invertible_binary_matrix from test import QiskitTestCase # pylint: disable=wrong-import-order from test import combine # pylint: disable=wrong-import-order -class VGate(Gate): - """V Gate used in Clifford synthesis.""" - - def __init__(self): - """Create new V Gate.""" - super().__init__("v", 1, []) - - def _define(self): - """V Gate definition.""" - q = QuantumRegister(1, "q") - qc = QuantumCircuit(q) - qc.sdg(0) - qc.h(0) - self.definition = qc - - -class WGate(Gate): - """W Gate used in Clifford synthesis.""" - - def __init__(self): - """Create new W Gate.""" - super().__init__("w", 1, []) - - def _define(self): - """W Gate definition.""" - q = QuantumRegister(1, "q") - qc = QuantumCircuit(q) - qc.append(VGate(), [q[0]], []) - qc.append(VGate(), [q[0]], []) - self.definition = qc - - -def random_clifford_circuit(num_qubits, num_gates, gates="all", seed=None): - """Generate a pseudo random Clifford circuit.""" - - qubits_1_gates = ["i", "x", "y", "z", "h", "s", "sdg", "sx", "sxdg", "v", "w"] - qubits_2_gates = ["cx", "cz", "cy", "swap", "iswap", "ecr", "dcx"] - if gates == "all": - if num_qubits == 1: - gates = qubits_1_gates - else: - gates = qubits_1_gates + qubits_2_gates - - instructions = { - "i": (IGate(), 1), - "x": (XGate(), 1), - "y": (YGate(), 1), - "z": (ZGate(), 1), - "h": (HGate(), 1), - "s": (SGate(), 1), - "sdg": (SdgGate(), 1), - "sx": (SXGate(), 1), - "sxdg": (SXdgGate(), 1), - "v": (VGate(), 1), - "w": (WGate(), 1), - "cx": (CXGate(), 2), - "cy": (CYGate(), 2), - "cz": (CZGate(), 2), - "swap": (SwapGate(), 2), - "iswap": (iSwapGate(), 2), - "ecr": (ECRGate(), 2), - "dcx": (DCXGate(), 2), - } - - if isinstance(seed, np.random.Generator): - rng = seed - else: - rng = np.random.default_rng(seed) - - samples = rng.choice(gates, num_gates) - - circ = QuantumCircuit(num_qubits) - - for name in samples: - gate, nqargs = instructions[name] - qargs = rng.choice(range(num_qubits), nqargs, replace=False).tolist() - circ.append(gate, qargs) - - return circ - - @ddt class TestCliffordGates(QiskitTestCase): """Tests for clifford append gate functions.""" @@ -588,92 +499,6 @@ def test_from_circuit_with_all_types(self): self.assertEqual(combined_clifford, expected_clifford) -@ddt -class TestCliffordSynthesis(QiskitTestCase): - """Test Clifford synthesis methods.""" - - @staticmethod - def _cliffords_1q(): - clifford_dicts = [ - {"stabilizer": ["+Z"], "destabilizer": ["-X"]}, - {"stabilizer": ["-Z"], "destabilizer": ["+X"]}, - {"stabilizer": ["-Z"], "destabilizer": ["-X"]}, - {"stabilizer": ["+Z"], "destabilizer": ["+Y"]}, - {"stabilizer": ["+Z"], "destabilizer": ["-Y"]}, - {"stabilizer": ["-Z"], "destabilizer": ["+Y"]}, - {"stabilizer": ["-Z"], "destabilizer": ["-Y"]}, - {"stabilizer": ["+X"], "destabilizer": ["+Z"]}, - {"stabilizer": ["+X"], "destabilizer": ["-Z"]}, - {"stabilizer": ["-X"], "destabilizer": ["+Z"]}, - {"stabilizer": ["-X"], "destabilizer": ["-Z"]}, - {"stabilizer": ["+X"], "destabilizer": ["+Y"]}, - {"stabilizer": ["+X"], "destabilizer": ["-Y"]}, - {"stabilizer": ["-X"], "destabilizer": ["+Y"]}, - {"stabilizer": ["-X"], "destabilizer": ["-Y"]}, - {"stabilizer": ["+Y"], "destabilizer": ["+X"]}, - {"stabilizer": ["+Y"], "destabilizer": ["-X"]}, - {"stabilizer": ["-Y"], "destabilizer": ["+X"]}, - {"stabilizer": ["-Y"], "destabilizer": ["-X"]}, - {"stabilizer": ["+Y"], "destabilizer": ["+Z"]}, - {"stabilizer": ["+Y"], "destabilizer": ["-Z"]}, - {"stabilizer": ["-Y"], "destabilizer": ["+Z"]}, - {"stabilizer": ["-Y"], "destabilizer": ["-Z"]}, - ] - return [Clifford.from_dict(i) for i in clifford_dicts] - - def test_decompose_1q(self): - """Test synthesis for all 1-qubit Cliffords""" - for cliff in self._cliffords_1q(): - with self.subTest(msg=f"Test circuit {cliff}"): - target = cliff - value = Clifford(cliff.to_circuit()) - self.assertEqual(target, value) - - @combine(num_qubits=[2, 3]) - def test_synth_bm(self, num_qubits): - """Test B&M synthesis for set of {num_qubits}-qubit Cliffords""" - rng = np.random.default_rng(1234) - samples = 50 - for _ in range(samples): - circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) - target = Clifford(circ) - value = Clifford(synth_clifford_bm(target)) - self.assertEqual(value, target) - - @combine(num_qubits=[2, 3, 4, 5]) - def test_synth_ag(self, num_qubits): - """Test A&G synthesis for set of {num_qubits}-qubit Cliffords""" - rng = np.random.default_rng(1234) - samples = 50 - for _ in range(samples): - circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) - target = Clifford(circ) - value = Clifford(synth_clifford_ag(target)) - self.assertEqual(value, target) - - @combine(num_qubits=[1, 2, 3, 4, 5]) - def test_synth_greedy(self, num_qubits): - """Test greedy synthesis for set of {num_qubits}-qubit Cliffords""" - rng = np.random.default_rng(1234) - samples = 50 - for _ in range(samples): - circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) - target = Clifford(circ) - value = Clifford(synth_clifford_greedy(target)) - self.assertEqual(value, target) - - @combine(num_qubits=[1, 2, 3, 4, 5]) - def test_synth_full(self, num_qubits): - """Test synthesis for set of {num_qubits}-qubit Cliffords""" - rng = np.random.default_rng(1234) - samples = 50 - for _ in range(samples): - circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) - target = Clifford(circ) - value = Clifford(synth_clifford_full(target)) - self.assertEqual(value, target) - - @ddt class TestCliffordDecomposition(QiskitTestCase): """Test Clifford decompositions.""" @@ -683,11 +508,9 @@ class TestCliffordDecomposition(QiskitTestCase): ["h", "s"], ["h", "s", "i", "x", "y", "z"], ["h", "s", "sdg"], - ["h", "s", "v"], - ["h", "s", "w"], ["h", "sx", "sxdg"], ["s", "sx", "sxdg"], - ["h", "s", "sdg", "i", "x", "y", "z", "v", "w", "sx", "sxdg"], + ["h", "s", "sdg", "i", "x", "y", "z", "sx", "sxdg"], ] ) def test_to_operator_1qubit_gates(self, gates): diff --git a/test/python/synthesis/test_clifford_sythesis.py b/test/python/synthesis/test_clifford_sythesis.py new file mode 100644 index 000000000000..887f1af5ad99 --- /dev/null +++ b/test/python/synthesis/test_clifford_sythesis.py @@ -0,0 +1,118 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# pylint: disable=invalid-name +"""Tests for Clifford synthesis functions.""" + +import numpy as np +from ddt import ddt +from qiskit.circuit.random import random_clifford_circuit +from qiskit.quantum_info.operators import Clifford +from qiskit.synthesis.clifford import ( + synth_clifford_full, + synth_clifford_ag, + synth_clifford_bm, + synth_clifford_greedy, +) + +from test import QiskitTestCase # pylint: disable=wrong-import-order +from test import combine # pylint: disable=wrong-import-order + + +@ddt +class TestCliffordSynthesis(QiskitTestCase): + """Tests for clifford synthesis functions.""" + + @staticmethod + def _cliffords_1q(): + clifford_dicts = [ + {"stabilizer": ["+Z"], "destabilizer": ["-X"]}, + {"stabilizer": ["-Z"], "destabilizer": ["+X"]}, + {"stabilizer": ["-Z"], "destabilizer": ["-X"]}, + {"stabilizer": ["+Z"], "destabilizer": ["+Y"]}, + {"stabilizer": ["+Z"], "destabilizer": ["-Y"]}, + {"stabilizer": ["-Z"], "destabilizer": ["+Y"]}, + {"stabilizer": ["-Z"], "destabilizer": ["-Y"]}, + {"stabilizer": ["+X"], "destabilizer": ["+Z"]}, + {"stabilizer": ["+X"], "destabilizer": ["-Z"]}, + {"stabilizer": ["-X"], "destabilizer": ["+Z"]}, + {"stabilizer": ["-X"], "destabilizer": ["-Z"]}, + {"stabilizer": ["+X"], "destabilizer": ["+Y"]}, + {"stabilizer": ["+X"], "destabilizer": ["-Y"]}, + {"stabilizer": ["-X"], "destabilizer": ["+Y"]}, + {"stabilizer": ["-X"], "destabilizer": ["-Y"]}, + {"stabilizer": ["+Y"], "destabilizer": ["+X"]}, + {"stabilizer": ["+Y"], "destabilizer": ["-X"]}, + {"stabilizer": ["-Y"], "destabilizer": ["+X"]}, + {"stabilizer": ["-Y"], "destabilizer": ["-X"]}, + {"stabilizer": ["+Y"], "destabilizer": ["+Z"]}, + {"stabilizer": ["+Y"], "destabilizer": ["-Z"]}, + {"stabilizer": ["-Y"], "destabilizer": ["+Z"]}, + {"stabilizer": ["-Y"], "destabilizer": ["-Z"]}, + ] + return [Clifford.from_dict(i) for i in clifford_dicts] + + def test_decompose_1q(self): + """Test synthesis for all 1-qubit Cliffords""" + for cliff in self._cliffords_1q(): + with self.subTest(msg=f"Test circuit {cliff}"): + target = cliff + value = Clifford(cliff.to_circuit()) + self.assertEqual(target, value) + + @combine(num_qubits=[2, 3]) + def test_synth_bm(self, num_qubits): + """Test B&M synthesis for set of {num_qubits}-qubit Cliffords""" + rng = np.random.default_rng(1234) + samples = 50 + for _ in range(samples): + circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) + target = Clifford(circ) + synth_circ = synth_clifford_bm(target) + value = Clifford(synth_circ) + self.assertEqual(value, target) + + @combine(num_qubits=[2, 3, 4, 5]) + def test_synth_ag(self, num_qubits): + """Test A&G synthesis for set of {num_qubits}-qubit Cliffords""" + rng = np.random.default_rng(1234) + samples = 1 + for _ in range(samples): + circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) + target = Clifford(circ) + synth_circ = synth_clifford_ag(target) + value = Clifford(synth_circ) + self.assertEqual(value, target) + + @combine(num_qubits=[1, 2, 3, 4, 5]) + def test_synth_greedy(self, num_qubits): + """Test greedy synthesis for set of {num_qubits}-qubit Cliffords""" + rng = np.random.default_rng(1234) + samples = 50 + for _ in range(samples): + circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) + target = Clifford(circ) + synth_circ = synth_clifford_greedy(target) + value = Clifford(synth_circ) + self.assertEqual(value, target) + + @combine(num_qubits=[1, 2, 3, 4, 5]) + def test_synth_full(self, num_qubits): + """Test synthesis for set of {num_qubits}-qubit Cliffords""" + rng = np.random.default_rng(1234) + samples = 50 + for _ in range(samples): + circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) + target = Clifford(circ) + synth_circ = synth_clifford_full(target) + value = Clifford(synth_circ) + self.assertEqual(value, target) From 6a872a8e48e642631c73ee37ec4b949d98386d16 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 7 May 2024 17:59:41 +0300 Subject: [PATCH 05/17] docstring fixes (#12358) --- qiskit/quantum_info/operators/symplectic/pauli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/quantum_info/operators/symplectic/pauli.py b/qiskit/quantum_info/operators/symplectic/pauli.py index e1bcfa29ebcb..1ccecc04a6c0 100644 --- a/qiskit/quantum_info/operators/symplectic/pauli.py +++ b/qiskit/quantum_info/operators/symplectic/pauli.py @@ -144,13 +144,13 @@ class initialization (``Pauli('-iXYZ')``). A ``Pauli`` object can be .. code-block:: python - p = Pauli('-iXYZ') + P = Pauli('-iXYZ') print('P[0] =', repr(P[0])) print('P[1] =', repr(P[1])) print('P[2] =', repr(P[2])) print('P[:] =', repr(P[:])) - print('P[::-1] =, repr(P[::-1])) + print('P[::-1] =', repr(P[::-1])) """ # Set the max Pauli string size before truncation From 0f7424cb144cf985107273780b809091ceff0432 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 00:31:25 +0100 Subject: [PATCH 06/17] Bump num-bigint from 0.4.4 to 0.4.5 (#12357) Bumps [num-bigint](https://github.com/rust-num/num-bigint) from 0.4.4 to 0.4.5. - [Changelog](https://github.com/rust-num/num-bigint/blob/master/RELEASES.md) - [Commits](https://github.com/rust-num/num-bigint/compare/num-bigint-0.4.4...num-bigint-0.4.5) --- updated-dependencies: - dependency-name: num-bigint dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c8b22ed79172..33114124e1ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -724,11 +724,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", ] From b5c5179a0da209911dae69916bc1454def851a3e Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Wed, 8 May 2024 01:37:22 -0400 Subject: [PATCH 07/17] fix indentation in XXPlusYYGate docstring (#12365) * fix indentation in XXPlusYYGate docstring * break long lines --- .../library/standard_gates/xx_plus_yy.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/qiskit/circuit/library/standard_gates/xx_plus_yy.py b/qiskit/circuit/library/standard_gates/xx_plus_yy.py index a7b62175f207..a82316ed7b09 100644 --- a/qiskit/circuit/library/standard_gates/xx_plus_yy.py +++ b/qiskit/circuit/library/standard_gates/xx_plus_yy.py @@ -71,18 +71,20 @@ class XXPlusYYGate(Gate): q_1: ┤0 ├ └───────────────┘ - .. math:: - - \newcommand{\rotationangle}{\frac{\theta}{2}} - - R_{XX+YY}(\theta, \beta)\ q_0, q_1 = - RZ_1(-\beta) \cdot \exp\left(-i \frac{\theta}{2} \frac{XX+YY}{2}\right) \cdot RZ_1(\beta) = - \begin{pmatrix} - 1 & 0 & 0 & 0 \\ - 0 & \cos\left(\rotationangle\right) & -i\sin\left(\rotationangle\right)e^{i\beta} & 0 \\ - 0 & -i\sin\left(\rotationangle\right)e^{-i\beta} & \cos\left(\rotationangle\right) & 0 \\ - 0 & 0 & 0 & 1 - \end{pmatrix} + .. math:: + + \newcommand{\rotationangle}{\frac{\theta}{2}} + + R_{XX+YY}(\theta, \beta)\ q_0, q_1 = + RZ_1(-\beta) \cdot \exp\left(-i \frac{\theta}{2} \frac{XX+YY}{2}\right) \cdot RZ_1(\beta) = + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & \cos\left(\rotationangle\right) & + -i\sin\left(\rotationangle\right)e^{i\beta} & 0 \\ + 0 & -i\sin\left(\rotationangle\right)e^{-i\beta} & + \cos\left(\rotationangle\right) & 0 \\ + 0 & 0 & 0 & 1 + \end{pmatrix} """ def __init__( From 8b94fc36df05a9f5b2150e7a8ac1c36dca3286d1 Mon Sep 17 00:00:00 2001 From: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> Date: Wed, 8 May 2024 12:13:16 +0300 Subject: [PATCH 08/17] Replace initialization method by Isometry in StatePreparation (#12178) * replace initializetion method by Isometry in StatePreparation * add names to QuantumRegister and QuantumCircuit * update docs to the new reference * remove old initialization code based on Shende et al * fix reference in docs * add release notes * fix lint errors * fix references in docs * add a benchmark for state preparation * update circuit name following review --- .../library/data_preparation/initializer.py | 8 + .../data_preparation/state_preparation.py | 208 ++---------------- .../library/generalized_gates/isometry.py | 16 +- .../generalized_gates/mcg_up_to_diagonal.py | 8 +- .../circuit/library/generalized_gates/uc.py | 4 +- .../library/generalized_gates/uc_pauli_rot.py | 4 +- ...lgorithm-by-isometry-41f9ffa58f72ece5.yaml | 7 + test/benchmarks/statepreparation.py | 66 ++++++ 8 files changed, 114 insertions(+), 207 deletions(-) create mode 100644 releasenotes/notes/replace-initialization-algorithm-by-isometry-41f9ffa58f72ece5.yaml create mode 100644 test/benchmarks/statepreparation.py diff --git a/qiskit/circuit/library/data_preparation/initializer.py b/qiskit/circuit/library/data_preparation/initializer.py index 394f863191d5..0e38f067403c 100644 --- a/qiskit/circuit/library/data_preparation/initializer.py +++ b/qiskit/circuit/library/data_preparation/initializer.py @@ -36,6 +36,14 @@ class Initialize(Instruction): the :class:`~.library.StatePreparation` class. Note that ``Initialize`` is an :class:`~.circuit.Instruction` and not a :class:`.Gate` since it contains a reset instruction, which is not unitary. + + The initial state is prepared based on the :class:`~.library.Isometry` synthesis described in [1]. + + References: + 1. Iten et al., Quantum circuits for isometries (2016). + `Phys. Rev. A 93, 032318 + `__. + """ def __init__( diff --git a/qiskit/circuit/library/data_preparation/state_preparation.py b/qiskit/circuit/library/data_preparation/state_preparation.py index 2d48e5cd0776..43e80ead8836 100644 --- a/qiskit/circuit/library/data_preparation/state_preparation.py +++ b/qiskit/circuit/library/data_preparation/state_preparation.py @@ -11,7 +11,6 @@ # that they have been altered from the originals. """Prepare a quantum state from the state where all qubits are 0.""" -import cmath from typing import Union, Optional import math @@ -21,11 +20,10 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.gate import Gate -from qiskit.circuit.library.standard_gates.x import CXGate, XGate +from qiskit.circuit.library.standard_gates.x import XGate from qiskit.circuit.library.standard_gates.h import HGate from qiskit.circuit.library.standard_gates.s import SGate, SdgGate -from qiskit.circuit.library.standard_gates.ry import RYGate -from qiskit.circuit.library.standard_gates.rz import RZGate +from qiskit.circuit.library.generalized_gates import Isometry from qiskit.circuit.exceptions import CircuitError from qiskit.quantum_info.states.statevector import Statevector # pylint: disable=cyclic-import @@ -71,13 +69,13 @@ def __init__( Raises: QiskitError: ``num_qubits`` parameter used when ``params`` is not an integer - When a Statevector argument is passed the state is prepared using a recursive - initialization algorithm, including optimizations, from [1], as well - as some additional optimizations including removing zero rotations and double cnots. + When a Statevector argument is passed the state is prepared based on the + :class:`~.library.Isometry` synthesis described in [1]. - **References:** - [1] Shende, Bullock, Markov. Synthesis of Quantum Logic Circuits (2004) - [`https://arxiv.org/abs/quant-ph/0406176v5`] + References: + 1. Iten et al., Quantum circuits for isometries (2016). + `Phys. Rev. A 93, 032318 + `__. """ self._params_arg = params @@ -119,7 +117,7 @@ def _define(self): elif self._from_int: self.definition = self._define_from_int() else: - self.definition = self._define_synthesis() + self.definition = self._define_synthesis_isom() def _define_from_label(self): q = QuantumRegister(self.num_qubits, "q") @@ -168,29 +166,18 @@ def _define_from_int(self): # we don't need to invert anything return initialize_circuit - def _define_synthesis(self): - """Calculate a subcircuit that implements this initialization - - Implements a recursive initialization algorithm, including optimizations, - from "Synthesis of Quantum Logic Circuits" Shende, Bullock, Markov - https://arxiv.org/abs/quant-ph/0406176v5 + def _define_synthesis_isom(self): + """Calculate a subcircuit that implements this initialization via isometry""" + q = QuantumRegister(self.num_qubits, "q") + initialize_circuit = QuantumCircuit(q, name="init_def") - Additionally implements some extra optimizations: remove zero rotations and - double cnots. - """ - # call to generate the circuit that takes the desired vector to zero - disentangling_circuit = self._gates_to_uncompute() + isom = Isometry(self._params_arg, 0, 0) + initialize_circuit.append(isom, q[:]) # invert the circuit to create the desired vector from zero (assuming # the qubits are in the zero state) - if self._inverse is False: - initialize_instr = disentangling_circuit.to_instruction().inverse() - else: - initialize_instr = disentangling_circuit.to_instruction() - - q = QuantumRegister(self.num_qubits, "q") - initialize_circuit = QuantumCircuit(q, name="init_def") - initialize_circuit.append(initialize_instr, q[:]) + if self._inverse is True: + return initialize_circuit.inverse() return initialize_circuit @@ -253,164 +240,3 @@ def validate_parameter(self, parameter): def _return_repeat(self, exponent: float) -> "Gate": return Gate(name=f"{self.name}*{exponent}", num_qubits=self.num_qubits, params=[]) - - def _gates_to_uncompute(self): - """Call to create a circuit with gates that take the desired vector to zero. - - Returns: - QuantumCircuit: circuit to take self.params vector to :math:`|{00\\ldots0}\\rangle` - """ - q = QuantumRegister(self.num_qubits) - circuit = QuantumCircuit(q, name="disentangler") - - # kick start the peeling loop, and disentangle one-by-one from LSB to MSB - remaining_param = self.params - - for i in range(self.num_qubits): - # work out which rotations must be done to disentangle the LSB - # qubit (we peel away one qubit at a time) - (remaining_param, thetas, phis) = StatePreparation._rotations_to_disentangle( - remaining_param - ) - - # perform the required rotations to decouple the LSB qubit (so that - # it can be "factored" out, leaving a shorter amplitude vector to peel away) - - add_last_cnot = True - if np.linalg.norm(phis) != 0 and np.linalg.norm(thetas) != 0: - add_last_cnot = False - - if np.linalg.norm(phis) != 0: - rz_mult = self._multiplex(RZGate, phis, last_cnot=add_last_cnot) - circuit.append(rz_mult.to_instruction(), q[i : self.num_qubits]) - - if np.linalg.norm(thetas) != 0: - ry_mult = self._multiplex(RYGate, thetas, last_cnot=add_last_cnot) - circuit.append(ry_mult.to_instruction().reverse_ops(), q[i : self.num_qubits]) - circuit.global_phase -= np.angle(sum(remaining_param)) - return circuit - - @staticmethod - def _rotations_to_disentangle(local_param): - """ - Static internal method to work out Ry and Rz rotation angles used - to disentangle the LSB qubit. - These rotations make up the block diagonal matrix U (i.e. multiplexor) - that disentangles the LSB. - - [[Ry(theta_1).Rz(phi_1) 0 . . 0], - [0 Ry(theta_2).Rz(phi_2) . 0], - . - . - 0 0 Ry(theta_2^n).Rz(phi_2^n)]] - """ - remaining_vector = [] - thetas = [] - phis = [] - - param_len = len(local_param) - - for i in range(param_len // 2): - # Ry and Rz rotations to move bloch vector from 0 to "imaginary" - # qubit - # (imagine a qubit state signified by the amplitudes at index 2*i - # and 2*(i+1), corresponding to the select qubits of the - # multiplexor being in state |i>) - (remains, add_theta, add_phi) = StatePreparation._bloch_angles( - local_param[2 * i : 2 * (i + 1)] - ) - - remaining_vector.append(remains) - - # rotations for all imaginary qubits of the full vector - # to move from where it is to zero, hence the negative sign - thetas.append(-add_theta) - phis.append(-add_phi) - - return remaining_vector, thetas, phis - - @staticmethod - def _bloch_angles(pair_of_complex): - """ - Static internal method to work out rotation to create the passed-in - qubit from the zero vector. - """ - [a_complex, b_complex] = pair_of_complex - # Force a and b to be complex, as otherwise numpy.angle might fail. - a_complex = complex(a_complex) - b_complex = complex(b_complex) - mag_a = abs(a_complex) - final_r = math.sqrt(mag_a**2 + abs(b_complex) ** 2) - if final_r < _EPS: - theta = 0 - phi = 0 - final_r = 0 - final_t = 0 - else: - theta = 2 * math.acos(mag_a / final_r) - a_arg = cmath.phase(a_complex) - b_arg = cmath.phase(b_complex) - final_t = a_arg + b_arg - phi = b_arg - a_arg - - return final_r * cmath.exp(1.0j * final_t / 2), theta, phi - - def _multiplex(self, target_gate, list_of_angles, last_cnot=True): - """ - Return a recursive implementation of a multiplexor circuit, - where each instruction itself has a decomposition based on - smaller multiplexors. - - The LSB is the multiplexor "data" and the other bits are multiplexor "select". - - Args: - target_gate (Gate): Ry or Rz gate to apply to target qubit, multiplexed - over all other "select" qubits - list_of_angles (list[float]): list of rotation angles to apply Ry and Rz - last_cnot (bool): add the last cnot if last_cnot = True - - Returns: - DAGCircuit: the circuit implementing the multiplexor's action - """ - list_len = len(list_of_angles) - local_num_qubits = int(math.log2(list_len)) + 1 - - q = QuantumRegister(local_num_qubits) - circuit = QuantumCircuit(q, name="multiplex" + str(local_num_qubits)) - - lsb = q[0] - msb = q[local_num_qubits - 1] - - # case of no multiplexing: base case for recursion - if local_num_qubits == 1: - circuit.append(target_gate(list_of_angles[0]), [q[0]]) - return circuit - - # calc angle weights, assuming recursion (that is the lower-level - # requested angles have been correctly implemented by recursion - angle_weight = np.kron([[0.5, 0.5], [0.5, -0.5]], np.identity(2 ** (local_num_qubits - 2))) - - # calc the combo angles - list_of_angles = angle_weight.dot(np.array(list_of_angles)).tolist() - - # recursive step on half the angles fulfilling the above assumption - multiplex_1 = self._multiplex(target_gate, list_of_angles[0 : (list_len // 2)], False) - circuit.append(multiplex_1.to_instruction(), q[0:-1]) - - # attach CNOT as follows, thereby flipping the LSB qubit - circuit.append(CXGate(), [msb, lsb]) - - # implement extra efficiency from the paper of cancelling adjacent - # CNOTs (by leaving out last CNOT and reversing (NOT inverting) the - # second lower-level multiplex) - multiplex_2 = self._multiplex(target_gate, list_of_angles[(list_len // 2) :], False) - if list_len > 1: - circuit.append(multiplex_2.to_instruction().reverse_ops(), q[0:-1]) - else: - circuit.append(multiplex_2.to_instruction(), q[0:-1]) - - # attach a final CNOT - if last_cnot: - circuit.append(CXGate(), [msb, lsb]) - - return circuit diff --git a/qiskit/circuit/library/generalized_gates/isometry.py b/qiskit/circuit/library/generalized_gates/isometry.py index c180e7a14484..e6b4f6fb21cc 100644 --- a/qiskit/circuit/library/generalized_gates/isometry.py +++ b/qiskit/circuit/library/generalized_gates/isometry.py @@ -45,10 +45,10 @@ class Isometry(Instruction): The decomposition is based on [1]. - **References:** - - [1] Iten et al., Quantum circuits for isometries (2016). - `Phys. Rev. A 93, 032318 `__. + References: + 1. Iten et al., Quantum circuits for isometries (2016). + `Phys. Rev. A 93, 032318 + `__. """ @@ -123,8 +123,8 @@ def _define(self): # later here instead. gate = self.inv_gate() gate = gate.inverse() - q = QuantumRegister(self.num_qubits) - iso_circuit = QuantumCircuit(q) + q = QuantumRegister(self.num_qubits, "q") + iso_circuit = QuantumCircuit(q, name="isometry") iso_circuit.append(gate, q[:]) self.definition = iso_circuit @@ -139,8 +139,8 @@ def _gates_to_uncompute(self): Call to create a circuit with gates that take the desired isometry to the first 2^m columns of the 2^n*2^n identity matrix (see https://arxiv.org/abs/1501.06911) """ - q = QuantumRegister(self.num_qubits) - circuit = QuantumCircuit(q) + q = QuantumRegister(self.num_qubits, "q") + circuit = QuantumCircuit(q, name="isometry_to_uncompute") ( q_input, q_ancillas_for_output, diff --git a/qiskit/circuit/library/generalized_gates/mcg_up_to_diagonal.py b/qiskit/circuit/library/generalized_gates/mcg_up_to_diagonal.py index 49d7dc36958a..b95ec6f63e35 100644 --- a/qiskit/circuit/library/generalized_gates/mcg_up_to_diagonal.py +++ b/qiskit/circuit/library/generalized_gates/mcg_up_to_diagonal.py @@ -68,8 +68,8 @@ def __init__( def _define(self): mcg_up_diag_circuit, _ = self._dec_mcg_up_diag() gate = mcg_up_diag_circuit.to_instruction() - q = QuantumRegister(self.num_qubits) - mcg_up_diag_circuit = QuantumCircuit(q) + q = QuantumRegister(self.num_qubits, "q") + mcg_up_diag_circuit = QuantumCircuit(q, name="mcg_up_to_diagonal") mcg_up_diag_circuit.append(gate, q[:]) self.definition = mcg_up_diag_circuit @@ -108,8 +108,8 @@ def _dec_mcg_up_diag(self): q=[q_target,q_controls,q_ancilla_zero,q_ancilla_dirty] """ diag = np.ones(2 ** (self.num_controls + 1)).tolist() - q = QuantumRegister(self.num_qubits) - circuit = QuantumCircuit(q) + q = QuantumRegister(self.num_qubits, "q") + circuit = QuantumCircuit(q, name="mcg_up_to_diagonal") (q_target, q_controls, q_ancillas_zero, q_ancillas_dirty) = self._define_qubit_role(q) # ToDo: Keep this threshold updated such that the lowest gate count is achieved: # ToDo: we implement the MCG with a UCGate up to diagonal if the number of controls is diff --git a/qiskit/circuit/library/generalized_gates/uc.py b/qiskit/circuit/library/generalized_gates/uc.py index 6e6a1db95ca3..c81494da3eec 100644 --- a/qiskit/circuit/library/generalized_gates/uc.py +++ b/qiskit/circuit/library/generalized_gates/uc.py @@ -148,10 +148,10 @@ def _dec_ucg(self): the diagonal gate is also returned. """ diag = np.ones(2**self.num_qubits).tolist() - q = QuantumRegister(self.num_qubits) + q = QuantumRegister(self.num_qubits, "q") q_controls = q[1:] q_target = q[0] - circuit = QuantumCircuit(q) + circuit = QuantumCircuit(q, name="uc") # If there is no control, we use the ZYZ decomposition if not q_controls: circuit.unitary(self.params[0], [q]) diff --git a/qiskit/circuit/library/generalized_gates/uc_pauli_rot.py b/qiskit/circuit/library/generalized_gates/uc_pauli_rot.py index 5b5633ec423f..6b637f7d2b2a 100644 --- a/qiskit/circuit/library/generalized_gates/uc_pauli_rot.py +++ b/qiskit/circuit/library/generalized_gates/uc_pauli_rot.py @@ -69,7 +69,7 @@ def __init__(self, angle_list: list[float], rot_axis: str) -> None: def _define(self): ucr_circuit = self._dec_ucrot() gate = ucr_circuit.to_instruction() - q = QuantumRegister(self.num_qubits) + q = QuantumRegister(self.num_qubits, "q") ucr_circuit = QuantumCircuit(q) ucr_circuit.append(gate, q[:]) self.definition = ucr_circuit @@ -79,7 +79,7 @@ def _dec_ucrot(self): Finds a decomposition of a UC rotation gate into elementary gates (C-NOTs and single-qubit rotations). """ - q = QuantumRegister(self.num_qubits) + q = QuantumRegister(self.num_qubits, "q") circuit = QuantumCircuit(q) q_target = q[0] q_controls = q[1:] diff --git a/releasenotes/notes/replace-initialization-algorithm-by-isometry-41f9ffa58f72ece5.yaml b/releasenotes/notes/replace-initialization-algorithm-by-isometry-41f9ffa58f72ece5.yaml new file mode 100644 index 000000000000..5bf8e7a80b48 --- /dev/null +++ b/releasenotes/notes/replace-initialization-algorithm-by-isometry-41f9ffa58f72ece5.yaml @@ -0,0 +1,7 @@ +--- +features_circuits: + - | + Replacing the internal synthesis algorithm of :class:`~.library.StatePreparation` + and :class:`~.library.Initialize` of Shende et al. by the algorithm given in + :class:`~.library.Isometry` of Iten et al. + The new algorithm reduces the number of CX gates and the circuit depth by a factor of 2. diff --git a/test/benchmarks/statepreparation.py b/test/benchmarks/statepreparation.py new file mode 100644 index 000000000000..67dc1178fc24 --- /dev/null +++ b/test/benchmarks/statepreparation.py @@ -0,0 +1,66 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# pylint: disable=missing-docstring,invalid-name,no-member +# pylint: disable=attribute-defined-outside-init +# pylint: disable=unused-argument + +import numpy as np +from qiskit import QuantumRegister, QuantumCircuit +from qiskit.compiler import transpile +from qiskit.circuit.library.data_preparation import StatePreparation + + +class StatePreparationTranspileBench: + params = [4, 5, 6, 7, 8] + param_names = ["number of qubits in state"] + + def setup(self, n): + q = QuantumRegister(n) + qc = QuantumCircuit(q) + state = np.random.rand(2**n) + np.random.rand(2**n) * 1j + state = state / np.linalg.norm(state) + state_gate = StatePreparation(state) + qc.append(state_gate, q) + + self.circuit = qc + + def track_cnot_counts_after_mapping_to_ibmq_16_melbourne(self, *unused): + coupling = [ + [1, 0], + [1, 2], + [2, 3], + [4, 3], + [4, 10], + [5, 4], + [5, 6], + [5, 9], + [6, 8], + [7, 8], + [9, 8], + [9, 10], + [11, 3], + [11, 10], + [11, 12], + [12, 2], + [13, 1], + [13, 12], + ] + circuit = transpile( + self.circuit, + basis_gates=["u1", "u3", "u2", "cx"], + coupling_map=coupling, + seed_transpiler=0, + ) + counts = circuit.count_ops() + cnot_count = counts.get("cx", 0) + return cnot_count From be1d24a739b3e78316ef592ebd06188bb9824d2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 12:24:10 +0000 Subject: [PATCH 09/17] Bump num-complex from 0.4.5 to 0.4.6 (#12368) Bumps [num-complex](https://github.com/rust-num/num-complex) from 0.4.5 to 0.4.6. - [Changelog](https://github.com/rust-num/num-complex/blob/master/RELEASES.md) - [Commits](https://github.com/rust-num/num-complex/compare/num-complex-0.4.5...num-complex-0.4.6) --- updated-dependencies: - dependency-name: num-complex dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33114124e1ed..b6915167f9c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -734,9 +734,9 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "bytemuck", "num-traits", From 87c14cb2bf9cfd9ae12985de994b6c3a31edfa61 Mon Sep 17 00:00:00 2001 From: Derek Miner Date: Wed, 8 May 2024 09:42:18 -0500 Subject: [PATCH 10/17] Add new method for single bitstring target when measuring stabilizerstate probabilities_dict (#12147) * added new method, deterministic helper unction added new method probabilities_dict_from_bitstrings, moved logic from probabilities_dict to probabilities_dict_from_bitstrings with ability to pass target, probabilities_dict will pass a target of None. * Adding test timing, fixed algorithm for determining if should get probability for qubit Added some timing of the tests to determine if the version with a specfic target runs faster then the non-targetted version (which must calculate all 2^n possibilities for stabilizer state test_probablities_dict_single_qubit. When given a target it can skip branches that were not wanting to be calculated * Adding more tests, fixing deterministic issues with bad target * Prob of targets with 0.0 probability * target calc changes * Simpler way to get all targets * Need to improve performance with multiple targets * Simplified the probabilities_dict_from_bitstrings method simplified the method probabilities_dict_from_bitstrings, less checking was needed, simpler handling of no target value passed in * Adding tests for targets * Added tests to test_probablities_dict_qubits * Add performance boost check when using targets for test_probablities_dict_qubits * Added Caching to prevent duplicate branch calculations In stabilizerstate, added the ability to cache previously calculated branches so they do not get recalculated when attempting to get target values, gives a better starting point to calculate from instead of starting from an unknown probability value of 1.0 * improve performance by performing expensive operations only when needed when creating an array for the outcome in probability, it is only built if the cache key is not found * Optimization for caching, deterministic calcs for branches, enabling/disabling cache added more optimization for performance when performing calculations for branches using caching and targets, it will now also store the deterministic node values which gives a very slight performance increase, migrated to most efficient ways to store and retrieve cache values. The performance increase when using a large amount of qubits is huge, when using a very small amount of qubits the very slight over head of caching makes it about the same. Increasing test coverage for using targets with probabilities * improved tests, checking of caching vs not caching implemented more accurate tests that verify probability caching * added more tests and variations for stabilizerstate * Corrected deterministic probability calculations * Fixed bug with cache in wrong place for some calculations for some calculations the cached results were in the wrong place which could cause false results for certain test cases with 3+ qubits * added more test cases * Probabilities update for moved method call * Added test for random test_probs_random_subsystem * fixed single probabilities tests lost in previous merge * Removed caching of deterministic values overhead was too high compared to calculating * Fixed failing test with test_probs_random_subsystem Fixed failing test that would randomly occur, the stabilizer state was not being cached as well, which would sometimes produce a failure. After caching the object to restore when calculating starting at a node down the branch, this failure no longer occurs. Need to optimize and clean up the code a bit, but functioning * Adding tests back that were not test_probs_random_subsystem, removed to test * Created ProbabilityCache object for dealing with caching * Finished adding ProbabilityCache object and commenting * Added more documentation for stabilizer state * Fixing style with tox -eblack * commenting fixes in stabilizerstate * Added release note * Updated release notes * added __future__ to probabilitiescache, removed unused import stabilizerstate * run tox -eblack to correct probabilitiescache * Corrected lint issues in multiple classes fixed lint issues in test_probabilitiycache, test_stabilizerstate, stabilizerstate, and probabilitycache * fixed more pylint lint issues pylint -rn qiskit test tools Corrected in probabilitycache, stabilizerstate, test_probabilitycach, test_stabilizerstate * fixed more lint issues fixed with: tox -eblack verified with: black --check qiskit test tools examples setup.py pylint -rn qiskit test tools tox -elint * added a bit more variance allowance for performance tests renamed test_probabilitycache.py file to correct spelling added a bit more performance_varability_percent percent time allowed from 0.001 to 0.005 * Corrected Lint issues, added test for probabilitycache added test_probabilitycache test, fixed lint issues and type issues in stabilizerstate, test_stabilizerstate * Fixed remaining import ordering issue * readded ddt decorator for test_probabilitycache * Changed to using process_time_ns from monotonic for performance measuring * Added output when performance times fail to aid in debugging in test_stabilizerstate * moved performance end time out of same method to calculate times * changed method name in test_stabilizerstate changed probability_percent_of_calculated_branches to _probability_percent_of_calculated_branches * Added GC disabling between timed tests * Attempting using perf_counter_ns instead of process_time_ns * Updated commenting, moved 'X' outcome check moved 'X' in outcome out of the for loop for StabilizerState._get_probabilities to help performance * more comment updating * Changed performance timer based on OS type * Changing to time.thread_time_ns(), and raising performance varabilitiy by 0.005 using time.thread_time_ns() for benchmarking instead of time.perf_counter_ns() and time.process_time_ns() based on the OS in test_stabilizerstate. It seems that time is being counted that shouldn't be with the other methods that causes false reports of the work taking longer then it does. Raising the performance_varability_percent in test_stablilizerstate by 0.005 to 0.01 for more headroom for performance checking * changing time.thread_time() vs time.thread_time_ns() to see if compatible with windows also added one more target for test_4 to differentiate no caching against caching in test_stabilizerstate * Changing performance timer based on OS for test_stablizerstate perf_counter_ns for win32 and thread_time for all other OS * fixed small issue in probabilitycache, improved probabilitycache test fixed issue when checking the cache key that was not operating correctly, internally the key is always a str, but when a list[str] was passed in the check was not correct, so changed to check if an instance of a list, and will join the outcome val to a str for the internal cache key. Improved the probabilitycache to test having alternating keys of str and list[str] to test this internal key checking * Added correct type hinting to probabilitycache * removed typing hint module removed the use of typing module for type hinting, looks like python wants to get away from using this module for type hinting, also fixed some commenting issues in stabilizerstate, test_probabilitycache, test_stabilizerstate, and probabilitycache files * Fix test pylint failure of Value 'list' is unsubscriptable (unsubscriptable-object) fixing tests failing with pylint when lint checking is performed. This worked locally and wasn't failing, but when it runs in the CI/CD process it fails, so adding from __future__ import annotations to avoid this failure for test_probabilitycache and test_stabilizerstate as new tests include type hinting * Fix small comment error in test, Added use_caching to tests, simplified cache calls Fixed a small error in my commenting for the test_stabilizerstate.py and forced use_caching parm in the call of probabilities_dict_from_bitstrings in the tests, even tho it is used as default, to make sure it is clear in the tests. Simplfied the adding values to Probabilitycache, and simplified the code in StabilizerState._get_probabilities() setting values in cache and retrieving the state when using cache, made cache_key in probabilitycache "private" and changed method to _cache_key * Significant code refactoring in StabiizerState Significant code refactoring in stabilizerstate. I was able to condense the use the cache to a smaller area in the _get_probabilities helper method so it is easier to work with and more clear. I removed methods I created that were single use helper methods that were able to be condensed down into simpler code such as _branches_to_measure, _retrieve_deterministic_probability, _branches_to_measure, _is_qubit_deterministic. Since these are 1 time use methods it didn't make sense to keep them around. The _get_probabilities helper method is much simpler and clearer, closer to the original with less tweaks and functioning with the same performance, and more clarity. Most of the code changed at this point is adding tests, most of this commit was removing unnecessary code and refactoring * Fixed stabilizerstate cache key outcome not found there was a check to make sure a key could be found before that was removed in the refactoring, once and awhile you get a target that can't be found in the cache because of the algorithm of looking for a key by increasing the number of X from the left to the right. There are situations where the X would be in the middle such as 0X1 that won't be found. The algorithm in the probability cache could be changed to use BST which would very rarely help performance, the fix is to make sure a key can be found and then only use the cache if that is the case * Moved all caching retrieval outside of _get_probabilities for performance there is a slight overhead when recursively iterating through the _get_probabilities helper when having to check if caching should be retrieved. Realistically when you use caching it is just to get you a better starting point, so it only needs to be retrieved once. The cache inserting remains in the _get_probabilities helper to build up the cache, but the logic was significantly simplified in the _get_probabilities helper, and now only a 1 time cache retrieval is performed for each target via the probabilities_dict_from_bitstrings method * Simplified Bitstring method for probability cache, removed unnecessary classes Removed Probabilitycache class, test_probabilitiycache.py. Simplified test_stabilizerstate.py for new bitstring probability testing methods. changed method probabilities_dict_from_bitstrings to probabilities_dict_from_bitstring, and simplified by removing cache, and only allowing a single string value to be passed in * Commenting update for _probabilities_bitstring_verify updating the commenting for _probabilities_bitstring_verify, move to top of file * black lint formatter ran * Uncomment out line causing circular import when running locally I get a circular import for two_qubit_decompose.py on line 154 "_specializations = two_qubit_decompose.Specialization", I need to comment out to test locally, but did not mean to commit this. Simplified the stablizierstate self._get_probabilities method but removing an unnecessary check * Remove type hints from test_stabilizerstate * Updated test_stabilizerstate, removed helper test method, new release note update the tests in test_stabilizerstate to no longer use a helper method for checking all the bitstring measurements in. Created a _get_probabilities_dict helper method in stabilizer state to be used for the probabilities_dict and probabilities_dict_with_bitstring methods. This was needed so that they could share the logic of the measurements, but also so that you could enforce in the probabilities_dict_with_bitstring method that outcome_bitstring must be provided. Previouslly this was optional and the user could avoid passing in a value, but this would essentially make it function the same as probabilities_dict. Now with the helper it enforces they must provide a bitstring * Added large qubit test with h gates, reorder imports for test_stabilizerstate added new test method test_probabilities_dict_large_num_qubits with large amount of qubit tests using an outcome bitstring target, num_qubits=[25, 50, 100, 200, 300, 400, 500, 600, 750], normally get any results for 750 qubits would require ~5.922387e+225 full bitstring calculations, but if you want one target bitstring you can perform ~750 measurements for a single probability for one result and get the value quickly * moved methods out of loop that are unnecessary in test_stabilizerstate moded target dict calculation out of loop that is not necessary to recalculate through each sample loop calculation * broke out medium hgate test to own test method, issue with assertTrue type in test testing a variety of hgates that also do the full probabilities_dict calculations up to 9 qubits called test_probabilities_dict_hgate_medium_num_qubits, also changed a assertTrue typo to assertDictEqual in the test_probabilities_dict_large_num_qubits method * fixed commenting in stabilizerstate, removed unnecessary outcome_bitstring array access syntax fixed some commenting for probabilities_dict_from_bitstring, and removed unnecessary syntax for accessing the array, was outcome_bitstring[i : i + 1] now outcome_bitstring[i] in _get_probabilities * Condensed tests in test_stabilizerstate, added helper test class, more test coverage added class StabilizerStateTestingTools in file test_stabilizerstate, these methods needed to be moved outside of the TestStabilizerState class because they are also needed in the TestStabilizerStateExpectationValue class within the file. It did not make sense to put these helper methods in the QiskitTestCase class, or the BaseQiskitTestCase class as these helpers are specific to helping test a new method in the stabilizerstate class. I condensed redudnant code for checking the individual bitstrings into helper method _verify_individual_bitstrings which takes a target dict, uses each entry in the dict to attempt to get the single probability, and verify each, which drastically raises the test coverage of this new method probabilities_dict_from_bitstring. added helper method StabilizerStateTestingTools._bitstring_product which builds a dict of the product of 0, 1 for a lenth passed in, used in several test cases for testing the zero probabilities. In the test cases you will also notice cases where I update the target dict target.update(StabilizerStateTestingTools._bitstring_product( num_qubits, target)), which builds the rest of the bitstring values that will have a 0 probability, which is then passed into the _verify_individual_bitstrings method to verify they get the correct 0 value I reduced the number of qubits for the test_probabilities_dict_hgate_large_num_qubits from tests of num_qubits=[25, 50, 100, 200, 300, 400, 500, 600, 750] to num_qubits=[25, 50, 100, 200] due to memory issues on windows, and the largest use case I have received so far is 100 qubits, so going above that by 2x Overall test coverage should be dramatically increased now testing for 0 probabilities, checking every bitstring individually has a probability, and simplifying the code, removing a lot of repetative logic, moved to the helper methods * Fixed isues in release notes https://github.com/Qiskit/qiskit/pull/12147#discussion_r1578906579 https://github.com/Qiskit/qiskit/pull/12147#discussion_r1578907028 * Corrected Targetted to targeted in stablizerstate * Updated commenting about target https://github.com/Qiskit/qiskit/pull/12147#discussion_r1579358105 https://github.com/Qiskit/qiskit/pull/12147#discussion_r1579356207 * Moved helper function _get_probabilities_dict moved the helper function _get_probabilities_dict to the helper function area next to _get_probabilities https://github.com/Qiskit/qiskit/pull/12147#discussion_r1579365690 * differentiated description for probabilities https://github.com/Qiskit/qiskit/pull/12147#discussion_r1579193692 * Added test to test_stabilizerstate added a test that does not have all equal probabilities and is not a GHZ state https://github.com/Qiskit/qiskit/pull/12147#discussion_r1579395521 * combined 2 test methods combine the large and medium tests into one due to taking too long to run the tests and lowered the amount of qubits * removed unnecessary test removed redundant test in test_probabilities_dict_medium_num_qubits, as this was combined with the large test * fixed lint issues, renamed test method renamed test_probabilities_dict_medium_num_qubits in test_stabilizerstate to test_probabilities_dict_from_bitstring * fixed type for targetting in stabilizerstate typo for description for word targetting, changed to targeting --------- Co-authored-by: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> --- qiskit/quantum_info/states/stabilizerstate.py | 161 +++++++++++--- ...r_probabilities_dict-e53f524d115bbcfc.yaml | 13 ++ .../states/test_stabilizerstate.py | 203 ++++++++++++++++-- 3 files changed, 329 insertions(+), 48 deletions(-) create mode 100644 releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml diff --git a/qiskit/quantum_info/states/stabilizerstate.py b/qiskit/quantum_info/states/stabilizerstate.py index 7f616bcff79f..4ae16c32bf54 100644 --- a/qiskit/quantum_info/states/stabilizerstate.py +++ b/qiskit/quantum_info/states/stabilizerstate.py @@ -386,8 +386,17 @@ def probabilities(self, qargs: None | list = None, decimals: None | int = None) return probs - def probabilities_dict(self, qargs: None | list = None, decimals: None | int = None) -> dict: - """Return the subsystem measurement probability dictionary. + def probabilities_dict_from_bitstring( + self, + outcome_bitstring: str, + qargs: None | list = None, + decimals: None | int = None, + ) -> dict[str, float]: + """Return the subsystem measurement probability dictionary utilizing + a targeted outcome_bitstring to perform the measurement for. This + will calculate a probability for only a single targeted + outcome_bitstring value, giving a performance boost over calculating + all possible outcomes. Measurement probabilities are with respect to measurement in the computation (diagonal) basis. @@ -398,30 +407,44 @@ def probabilities_dict(self, qargs: None | list = None, decimals: None | int = N inserted between integers so that subsystems can be distinguished. Args: + outcome_bitstring (None or str): targeted outcome bitstring + to perform a measurement calculation for, this will significantly + reduce the number of calculation performed (Default: None) qargs (None or list): subsystems to return probabilities for, - if None return for all subsystems (Default: None). + if None return for all subsystems (Default: None). decimals (None or int): the number of decimal places to round - values. If None no rounding is done (Default: None). + values. If None no rounding is done (Default: None) Returns: - dict: The measurement probabilities in dict (ket) form. + dict[str, float]: The measurement probabilities in dict (ket) form. """ - if qargs is None: - qubits = range(self.clifford.num_qubits) - else: - qubits = qargs + return self._get_probabilities_dict( + outcome_bitstring=outcome_bitstring, qargs=qargs, decimals=decimals + ) - outcome = ["X"] * len(qubits) - outcome_prob = 1.0 - probs = {} # probabilities dictionary + def probabilities_dict( + self, qargs: None | list = None, decimals: None | int = None + ) -> dict[str, float]: + """Return the subsystem measurement probability dictionary. - self._get_probabilities(qubits, outcome, outcome_prob, probs) + Measurement probabilities are with respect to measurement in the + computation (diagonal) basis. - if decimals is not None: - for key, value in probs.items(): - probs[key] = round(value, decimals) + This dictionary representation uses a Ket-like notation where the + dictionary keys are qudit strings for the subsystem basis vectors. + If any subsystem has a dimension greater than 10 comma delimiters are + inserted between integers so that subsystems can be distinguished. - return probs + Args: + qargs (None or list): subsystems to return probabilities for, + if None return for all subsystems (Default: None). + decimals (None or int): the number of decimal places to round + values. If None no rounding is done (Default: None). + + Returns: + dict: The measurement probabilities in dict (key) form. + """ + return self._get_probabilities_dict(outcome_bitstring=None, qargs=qargs, decimals=decimals) def reset(self, qargs: list | None = None) -> StabilizerState: """Reset state or subsystems to the 0-state. @@ -644,22 +667,48 @@ def _rowsum_deterministic(clifford, aux_pauli, row): # ----------------------------------------------------------------------- # Helper functions for calculating the probabilities # ----------------------------------------------------------------------- - def _get_probabilities(self, qubits, outcome, outcome_prob, probs): - """Recursive helper function for calculating the probabilities""" + def _get_probabilities( + self, + qubits: range, + outcome: list[str], + outcome_prob: float, + probs: dict[str, float], + outcome_bitstring: str = None, + ): + """Recursive helper function for calculating the probabilities - qubit_for_branching = -1 - ret = self.copy() + Args: + qubits (range): range of qubits + outcome (list[str]): outcome being built + outcome_prob (float): probabilitiy of the outcome + probs (dict[str, float]): holds the outcomes and probabilitiy results + outcome_bitstring (str): target outcome to measure which reduces measurements, None + if not targeting a specific target + """ + qubit_for_branching: int = -1 + ret: StabilizerState = self.copy() + + # Find outcomes for each qubit for i in range(len(qubits)): - qubit = qubits[len(qubits) - i - 1] if outcome[i] == "X": - is_deterministic = not any(ret.clifford.stab_x[:, qubit]) - if is_deterministic: - single_qubit_outcome = ret._measure_and_update(qubit, 0) - if single_qubit_outcome: - outcome[i] = "1" + # Retrieve the qubit for the current measurement + qubit = qubits[(len(qubits) - i - 1)] + # Determine if the probabilitiy is deterministic + if not any(ret.clifford.stab_x[:, qubit]): + single_qubit_outcome: np.int64 = ret._measure_and_update(qubit, 0) + if outcome_bitstring is None or ( + int(outcome_bitstring[i]) == single_qubit_outcome + ): + # No outcome_bitstring target, or using outcome_bitstring target and + # the single_qubit_outcome equals the desired outcome_bitstring target value, + # then use current outcome_prob value + outcome[i] = str(single_qubit_outcome) else: - outcome[i] = "0" + # If the single_qubit_outcome does not equal the outcome_bitsring target + # then we know that the probability will be 0 + outcome[i] = str(outcome_bitstring[i]) + outcome_prob = 0 else: qubit_for_branching = i @@ -668,15 +717,57 @@ def _get_probabilities(self, qubits, outcome, outcome_prob, probs): probs[str_outcome] = outcome_prob return - for single_qubit_outcome in range(0, 2): + for single_qubit_outcome in ( + range(0, 2) + if (outcome_bitstring is None) + else [int(outcome_bitstring[qubit_for_branching])] + ): new_outcome = outcome.copy() - if single_qubit_outcome: - new_outcome[qubit_for_branching] = "1" - else: - new_outcome[qubit_for_branching] = "0" + new_outcome[qubit_for_branching] = str(single_qubit_outcome) stab_cpy = ret.copy() stab_cpy._measure_and_update( - qubits[len(qubits) - qubit_for_branching - 1], single_qubit_outcome + qubits[(len(qubits) - qubit_for_branching - 1)], single_qubit_outcome + ) + stab_cpy._get_probabilities( + qubits, new_outcome, (0.5 * outcome_prob), probs, outcome_bitstring ) - stab_cpy._get_probabilities(qubits, new_outcome, 0.5 * outcome_prob, probs) + + def _get_probabilities_dict( + self, + outcome_bitstring: None | str = None, + qargs: None | list = None, + decimals: None | int = None, + ) -> dict[str, float]: + """Helper Function for calculating the subsystem measurement probability dictionary. + When the targeted outcome_bitstring value is set, then only the single outcome_bitstring + probability will be calculated. + + Args: + outcome_bitstring (None or str): targeted outcome bitstring + to perform a measurement calculation for, this will significantly + reduce the number of calculation performed (Default: None) + qargs (None or list): subsystems to return probabilities for, + if None return for all subsystems (Default: None). + decimals (None or int): the number of decimal places to round + values. If None no rounding is done (Default: None). + + Returns: + dict: The measurement probabilities in dict (key) form. + """ + if qargs is None: + qubits = range(self.clifford.num_qubits) + else: + qubits = qargs + + outcome = ["X"] * len(qubits) + outcome_prob = 1.0 + probs: dict[str, float] = {} # Probabilities dict to return with the measured values + + self._get_probabilities(qubits, outcome, outcome_prob, probs, outcome_bitstring) + + if decimals is not None: + for key, value in probs.items(): + probs[key] = round(value, decimals) + + return probs diff --git a/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml b/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml new file mode 100644 index 000000000000..73da8e6b7ad3 --- /dev/null +++ b/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + The :class:'.StabilizerState' class now has a new method + :meth:'~.StabilizerState.probabilities_dict_from_bitstring' allowing the + user to pass single bitstring to measure an outcome for. Previouslly the + :meth:'~.StabilizerState.probabilities_dict' would be utilized and would + at worst case calculate (2^n) number of probabilbity calculations (depending + on the state), even if a user wanted a single result. With this new method + the user can calculate just the single outcome bitstring value a user passes + to measure the probability for. As the number of qubits increases, the more + prevelant the performance enhancement may be (depending on the state) as only + 1 bitstring result is measured. diff --git a/test/python/quantum_info/states/test_stabilizerstate.py b/test/python/quantum_info/states/test_stabilizerstate.py index 56fecafbe58b..4e1659ff6999 100644 --- a/test/python/quantum_info/states/test_stabilizerstate.py +++ b/test/python/quantum_info/states/test_stabilizerstate.py @@ -13,6 +13,7 @@ """Tests for Stabilizerstate quantum state class.""" +from itertools import product import unittest import logging from ddt import ddt, data, unpack @@ -32,6 +33,61 @@ logger = logging.getLogger(__name__) +class StabilizerStateTestingTools: + """Test tools for verifying test cases in StabilizerState""" + + @staticmethod + def _bitstring_product_dict(bitstring_length: int, skip_entries: dict = None) -> dict: + """Retrieves a dict of every possible product of '0', '1' for length bitstring_length + pass in a dict to use the keys as entries to skip adding to the dict + + Args: + bitstring_length (int): length of the bitstring product + skip_entries (dict[str, float], optional): dict entries to skip adding to the dict based + on existing keys in the dict passed in. Defaults to {}. + + Returns: + dict[str, float]: dict with entries, all set to 0 + """ + if skip_entries is None: + skip_entries = {} + return { + result: 0 + for result in ["".join(x) for x in product(["0", "1"], repeat=bitstring_length)] + if result not in skip_entries + } + + @staticmethod + def _verify_individual_bitstrings( + testcase: QiskitTestCase, + target_dict: dict, + stab: StabilizerState, + qargs: list = None, + decimals: int = None, + dict_almost_equal: bool = False, + ) -> None: + """Helper that iterates through the target_dict and checks all probabilities by + running the value through the probabilities_dict_from_bitstring method for + retrieving a single measurement + + Args: + target_dict (dict[str, float]): dict to check probabilities for + stab (StabilizerState): stabilizerstate object to run probabilities_dict_from_bitstring on + qargs (None or list): subsystems to return probabilities for, + if None return for all subsystems (Default: None). + decimals (None or int): the number of decimal places to round + values. If None no rounding is done (Default: None) + dict_almost_equal (bool): utilize assertDictAlmostEqual when true, assertDictEqual when false + """ + for outcome_bitstring in target_dict: + (testcase.assertDictAlmostEqual if (dict_almost_equal) else testcase.assertDictEqual)( + stab.probabilities_dict_from_bitstring( + outcome_bitstring=outcome_bitstring, qargs=qargs, decimals=decimals + ), + {outcome_bitstring: target_dict[outcome_bitstring]}, + ) + + @ddt class TestStabilizerState(QiskitTestCase): """Tests for StabilizerState class.""" @@ -315,6 +371,8 @@ def test_probabilities_dict_single_qubit(self): value = stab.probabilities_dict() target = {"0": 1} self.assertEqual(value, target) + target.update({"1": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([1, 0]) self.assertTrue(np.allclose(probs, target)) @@ -326,6 +384,8 @@ def test_probabilities_dict_single_qubit(self): value = stab.probabilities_dict() target = {"1": 1} self.assertEqual(value, target) + target.update({"0": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([0, 1]) self.assertTrue(np.allclose(probs, target)) @@ -338,6 +398,7 @@ def test_probabilities_dict_single_qubit(self): value = stab.probabilities_dict() target = {"0": 0.5, "1": 0.5} self.assertEqual(value, target) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([0.5, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -355,43 +416,56 @@ def test_probabilities_dict_two_qubits(self): value = stab.probabilities_dict() target = {"00": 0.5, "01": 0.5} self.assertEqual(value, target) + target.update({"10": 0.0, "11": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([0.5, 0.5, 0, 0]) self.assertTrue(np.allclose(probs, target)) + qargs: list = [0, 1] for _ in range(self.samples): with self.subTest(msg="P([0, 1])"): - value = stab.probabilities_dict([0, 1]) + value = stab.probabilities_dict(qargs) target = {"00": 0.5, "01": 0.5} self.assertEqual(value, target) - probs = stab.probabilities([0, 1]) + target.update({"10": 0.0, "11": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) + probs = stab.probabilities(qargs) target = np.array([0.5, 0.5, 0, 0]) self.assertTrue(np.allclose(probs, target)) + qargs: list = [1, 0] for _ in range(self.samples): with self.subTest(msg="P([1, 0])"): - value = stab.probabilities_dict([1, 0]) + value = stab.probabilities_dict(qargs) target = {"00": 0.5, "10": 0.5} self.assertEqual(value, target) - probs = stab.probabilities([1, 0]) + target.update({"01": 0.0, "11": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) + probs = stab.probabilities(qargs) target = np.array([0.5, 0, 0.5, 0]) self.assertTrue(np.allclose(probs, target)) + qargs: list = [0] for _ in range(self.samples): with self.subTest(msg="P[0]"): - value = stab.probabilities_dict([0]) + value = stab.probabilities_dict(qargs) target = {"0": 0.5, "1": 0.5} self.assertEqual(value, target) - probs = stab.probabilities([0]) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) + probs = stab.probabilities(qargs) target = np.array([0.5, 0.5]) self.assertTrue(np.allclose(probs, target)) + qargs: list = [1] for _ in range(self.samples): with self.subTest(msg="P([1])"): - value = stab.probabilities_dict([1]) + value = stab.probabilities_dict(qargs) target = {"0": 1.0} self.assertEqual(value, target) - probs = stab.probabilities([1]) + target.update({"1": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) + probs = stab.probabilities(qargs) target = np.array([1, 0]) self.assertTrue(np.allclose(probs, target)) @@ -405,9 +479,10 @@ def test_probabilities_dict_qubits(self): qc.h(2) stab = StabilizerState(qc) + decimals: int = 1 for _ in range(self.samples): with self.subTest(msg="P(None), decimals=1"): - value = stab.probabilities_dict(decimals=1) + value = stab.probabilities_dict(decimals=decimals) target = { "000": 0.1, "001": 0.1, @@ -419,13 +494,17 @@ def test_probabilities_dict_qubits(self): "111": 0.1, } self.assertEqual(value, target) - probs = stab.probabilities(decimals=1) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, decimals=decimals + ) + probs = stab.probabilities(decimals=decimals) target = np.array([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]) self.assertTrue(np.allclose(probs, target)) + decimals: int = 2 for _ in range(self.samples): with self.subTest(msg="P(None), decimals=2"): - value = stab.probabilities_dict(decimals=2) + value = stab.probabilities_dict(decimals=decimals) target = { "000": 0.12, "001": 0.12, @@ -437,13 +516,17 @@ def test_probabilities_dict_qubits(self): "111": 0.12, } self.assertEqual(value, target) - probs = stab.probabilities(decimals=2) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, decimals=decimals + ) + probs = stab.probabilities(decimals=decimals) target = np.array([0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12]) self.assertTrue(np.allclose(probs, target)) + decimals: int = 3 for _ in range(self.samples): with self.subTest(msg="P(None), decimals=3"): - value = stab.probabilities_dict(decimals=3) + value = stab.probabilities_dict(decimals=decimals) target = { "000": 0.125, "001": 0.125, @@ -455,10 +538,72 @@ def test_probabilities_dict_qubits(self): "111": 0.125, } self.assertEqual(value, target) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, decimals=decimals + ) probs = stab.probabilities(decimals=3) target = np.array([0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125]) self.assertTrue(np.allclose(probs, target)) + @combine(num_qubits=[5, 6, 7, 8, 9]) + def test_probabilities_dict_from_bitstring(self, num_qubits): + """Test probabilities_dict_from_bitstring methods with medium number of qubits that are still + reasonable to calculate the full dict with probabilities_dict of all possible outcomes""" + + qc: QuantumCircuit = QuantumCircuit(num_qubits) + for qubit_num in range(0, num_qubits): + qc.h(qubit_num) + stab = StabilizerState(qc) + + expected_result: float = float(1 / (2**num_qubits)) + target_dict: dict = StabilizerStateTestingTools._bitstring_product_dict(num_qubits) + target_dict.update((k, expected_result) for k in target_dict) + + for _ in range(self.samples): + with self.subTest(msg="P(None)"): + value = stab.probabilities_dict() + self.assertDictEqual(value, target_dict) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target_dict, stab) + probs = stab.probabilities() + target = np.array(([expected_result] * (2**num_qubits))) + self.assertTrue(np.allclose(probs, target)) + + # H gate at qubit 0, Every gate after is an X gate + # will result in 2 outcomes with 0.5 + qc = QuantumCircuit(num_qubits) + qc.h(0) + for qubit_num in range(1, num_qubits): + qc.x(qubit_num) + stab = StabilizerState(qc) + + # Build the 2 expected outcome bitstrings for + # 0.5 probability based on h and x gates + target_1: str = "".join(["1" * (num_qubits - 1)] + ["0"]) + target_2: str = "".join(["1" * num_qubits]) + target: dict = {target_1: 0.5, target_2: 0.5} + target_all_bitstrings: dict = StabilizerStateTestingTools._bitstring_product_dict( + num_qubits, target + ) + target_all_bitstrings.update(target_all_bitstrings) + + # Numpy Array to verify stab.probabilities() + target_np_dict: dict = StabilizerStateTestingTools._bitstring_product_dict( + num_qubits, [target_1, target_2] + ) + target_np_dict.update(target) + target_np_array: np.ndarray = np.array(list(target_np_dict.values())) + + for _ in range(self.samples): + with self.subTest(msg="P(None)"): + stab = StabilizerState(qc) + value = stab.probabilities_dict() + self.assertEqual(value, target) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target_all_bitstrings, stab + ) + probs = stab.probabilities() + self.assertTrue(np.allclose(probs, target_np_array)) + def test_probabilities_dict_ghz(self): """Test probabilities and probabilities_dict method of a subsystem of qubits""" @@ -473,6 +618,8 @@ def test_probabilities_dict_ghz(self): value = stab.probabilities_dict() target = {"000": 0.5, "111": 0.5} self.assertEqual(value, target) + target.update(StabilizerStateTestingTools._bitstring_product_dict(num_qubits, target)) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([0.5, 0, 0, 0, 0, 0, 0, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -483,6 +630,10 @@ def test_probabilities_dict_ghz(self): probs = stab.probabilities_dict(qargs) target = {"000": 0.5, "111": 0.5} self.assertDictAlmostEqual(probs, target) + target.update( + StabilizerStateTestingTools._bitstring_product_dict(num_qubits, target) + ) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) probs = stab.probabilities(qargs) target = np.array([0.5, 0, 0, 0, 0, 0, 0, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -493,6 +644,10 @@ def test_probabilities_dict_ghz(self): probs = stab.probabilities_dict(qargs) target = {"00": 0.5, "11": 0.5} self.assertDictAlmostEqual(probs, target) + target.update(StabilizerStateTestingTools._bitstring_product_dict(2, target)) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, qargs, dict_almost_equal=True + ) probs = stab.probabilities(qargs) target = np.array([0.5, 0, 0, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -503,6 +658,9 @@ def test_probabilities_dict_ghz(self): probs = stab.probabilities_dict(qargs) target = {"0": 0.5, "1": 0.5} self.assertDictAlmostEqual(probs, target) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, qargs, dict_almost_equal=True + ) probs = stab.probabilities(qargs) target = np.array([0.5, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -520,10 +678,17 @@ def test_probs_random_subsystem(self, num_qubits): stab = StabilizerState(cliff) probs = stab.probabilities(qargs) probs_dict = stab.probabilities_dict(qargs) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, probs_dict, stab, qargs + ) target = Statevector(qc).probabilities(qargs) target_dict = Statevector(qc).probabilities_dict(qargs) + Statevector(qc).probabilities_dict() self.assertTrue(np.allclose(probs, target)) self.assertDictAlmostEqual(probs_dict, target_dict) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target_dict, stab, qargs, dict_almost_equal=True + ) @combine(num_qubits=[2, 3, 4, 5]) def test_expval_from_random_clifford(self, num_qubits): @@ -972,10 +1137,22 @@ def test_stabilizer_bell_equiv(self): # [XX, -ZZ] and [XX, YY] both generate the stabilizer group {II, XX, YY, -ZZ} self.assertTrue(cliff1.equiv(cliff2)) self.assertEqual(cliff1.probabilities_dict(), cliff2.probabilities_dict()) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, cliff1.probabilities_dict(), cliff2 + ) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, cliff2.probabilities_dict(), cliff1 + ) # [XX, ZZ] and [XX, -YY] both generate the stabilizer group {II, XX, -YY, ZZ} self.assertTrue(cliff3.equiv(cliff4)) self.assertEqual(cliff3.probabilities_dict(), cliff4.probabilities_dict()) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, cliff3.probabilities_dict(), cliff4 + ) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, cliff4.probabilities_dict(), cliff3 + ) self.assertFalse(cliff1.equiv(cliff3)) self.assertFalse(cliff2.equiv(cliff4)) From 70b36d061c5423dd0630c24df2a16be499a08c12 Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Thu, 9 May 2024 12:51:58 -0400 Subject: [PATCH 11/17] improve docstring of plot_circuit_layout (#12370) * improve docstring of plot_circuit_layout * "name" -> "index" --- qiskit/visualization/gate_map.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/qiskit/visualization/gate_map.py b/qiskit/visualization/gate_map.py index d8ffb6e10388..c7cc0727a6c4 100644 --- a/qiskit/visualization/gate_map.py +++ b/qiskit/visualization/gate_map.py @@ -1122,7 +1122,13 @@ def plot_circuit_layout(circuit, backend, view="virtual", qubit_coordinates=None Args: circuit (QuantumCircuit): Input quantum circuit. backend (Backend): Target backend. - view (str): Layout view: either 'virtual' or 'physical'. + view (str): How to label qubits in the layout. Options: + + - ``"virtual"``: Label each qubit with the index of the virtual qubit that + mapped to it. + - ``"physical"``: Label each qubit with the index of the physical qubit that it + corresponds to on the device. + qubit_coordinates (Sequence): An optional sequence input (list or array being the most common) of 2d coordinates for each qubit. The length of the sequence must match the number of qubits on the backend. The sequence From 24f1436fdb5d4061fde3a6fab6dd5139556f7e10 Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Thu, 9 May 2024 14:19:07 -0400 Subject: [PATCH 12/17] fix edge coloring bug in plot_coupling_map (#12369) * fix edge coloring bug in plot circuit layout * add release note --- qiskit/visualization/gate_map.py | 2 ++ releasenotes/notes/plot-circuit-layout-5935646107893c12.yaml | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/notes/plot-circuit-layout-5935646107893c12.yaml diff --git a/qiskit/visualization/gate_map.py b/qiskit/visualization/gate_map.py index c7cc0727a6c4..b950c84c902a 100644 --- a/qiskit/visualization/gate_map.py +++ b/qiskit/visualization/gate_map.py @@ -1039,7 +1039,9 @@ def plot_coupling_map( graph = CouplingMap(coupling_map).graph if not plot_directed: + line_color_map = dict(zip(graph.edge_list(), line_color)) graph = graph.to_undirected(multigraph=False) + line_color = [line_color_map[edge] for edge in graph.edge_list()] for node in graph.node_indices(): graph[node] = node diff --git a/releasenotes/notes/plot-circuit-layout-5935646107893c12.yaml b/releasenotes/notes/plot-circuit-layout-5935646107893c12.yaml new file mode 100644 index 000000000000..72f2c95962a5 --- /dev/null +++ b/releasenotes/notes/plot-circuit-layout-5935646107893c12.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed a bug in :func:`plot_coupling_map` that caused the edges of the coupling map to be colored incorrectly. + See https://github.com/Qiskit/qiskit/pull/12369 for details. From 8c8c78a345caf35c85e30387b200565fd7f970de Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Thu, 9 May 2024 15:11:30 -0400 Subject: [PATCH 13/17] Reorganize API index page into sections (#12333) * Reorganize API index page into sections * Sort alphabetically within each section * Clarify ordering expectation --- docs/apidoc/index.rst | 85 +++++++++++++++++++++++++++++++++---------- 1 file changed, 66 insertions(+), 19 deletions(-) diff --git a/docs/apidoc/index.rst b/docs/apidoc/index.rst index 3a6c1b04cfdf..30bb20998d6b 100644 --- a/docs/apidoc/index.rst +++ b/docs/apidoc/index.rst @@ -1,42 +1,89 @@ .. module:: qiskit +.. + Within each section, the modules should be ordered alphabetically by + module name (not RST filename). ============= API Reference ============= +Circuit construction: + .. toctree:: :maxdepth: 1 circuit - circuit_library circuit_classical - circuit_singleton - compiler - visualization classicalfunction + circuit_library + circuit_singleton + +Quantum information: + +.. toctree:: + :maxdepth: 1 + + quantum_info + +Transpilation: + +.. toctree:: + :maxdepth: 1 + converters - assembler dagcircuit passmanager + synthesis + qiskit.synthesis.unitary.aqc + transpiler + transpiler_passes + transpiler_synthesis_plugins + transpiler_preset + transpiler_plugins + +Primitives and providers: + +.. toctree:: + :maxdepth: 1 + + primitives providers providers_basic_provider providers_fake_provider providers_models - pulse - scheduler - synthesis - qiskit.synthesis.unitary.aqc - primitives + +Results and visualizations: + +.. toctree:: + :maxdepth: 1 + + result + visualization + +Serialization: + +.. toctree:: + :maxdepth: 1 + qasm2 qasm3 - qobj qpy - quantum_info - result - transpiler - transpiler_passes - transpiler_preset - transpiler_plugins - transpiler_synthesis_plugins - utils + +Pulse-level programming: + +.. toctree:: + :maxdepth: 1 + + pulse + scheduler + +Other: + +.. toctree:: + :maxdepth: 1 + + assembler + compiler exceptions + qobj + utils From 4a6c57044f35f4fab62b3f10764c6463a2b75f03 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 9 May 2024 15:31:06 -0400 Subject: [PATCH 14/17] Fix runtime scaling of StarPreRouting pass (#12376) This commit fixes a runtime performance scaling issue with the new StarPreRouting pass. If there are any stars identified by the pass when the pass goes to pre-route those star connectivity blocks it specifies a custom lexicographical topological sort key to ensure the stars are kept together in the sort. However the mechanism by which this sort key was generated scaled quadratically with the number of DAG nodes in the identified stars. This ended up being a large runtime performance bottleneck. This commit fixes this issue by pre-computing the sort key for all nodes in the stars and putting that in a dictionary so that when we call rustworkx to perform the topological sort the sort key callback does not become the bottleneck for the entire pass. --- qiskit/transpiler/passes/routing/star_prerouting.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit/transpiler/passes/routing/star_prerouting.py b/qiskit/transpiler/passes/routing/star_prerouting.py index 4b27749c6dad..b79a298ad595 100644 --- a/qiskit/transpiler/passes/routing/star_prerouting.py +++ b/qiskit/transpiler/passes/routing/star_prerouting.py @@ -329,13 +329,13 @@ def _apply_mapping(qargs, qubit_mapping, qubits): last_2q_gate = None int_digits = floor(log10(len(processing_order))) + 1 - processing_order_s = set(processing_order) + processing_order_index_map = { + node: f"a{str(index).zfill(int(int_digits))}" + for index, node in enumerate(processing_order) + } def tie_breaker_key(node): - if node in processing_order_s: - return "a" + str(processing_order.index(node)).zfill(int(int_digits)) - else: - return node.sort_key + return processing_order_index_map.get(node, node.sort_key) for node in dag.topological_op_nodes(key=tie_breaker_key): block_id = node_to_block_id.get(node, None) From 48709afe0489febf965c237696e4f3376936287f Mon Sep 17 00:00:00 2001 From: Joe Schulte Date: Thu, 9 May 2024 15:59:11 -0400 Subject: [PATCH 15/17] Removing unnecessary-dict-index-lookup lint rule and updates (#12373) --- pyproject.toml | 1 - qiskit/transpiler/target.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9c7094827c8a..303192e75cf9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -226,7 +226,6 @@ disable = [ "no-value-for-parameter", "not-context-manager", "unexpected-keyword-arg", - "unnecessary-dict-index-lookup", "unnecessary-dunder-call", "unnecessary-lambda-assignment", "unspecified-encoding", diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 8d609ce3b8a3..466d93fc89e8 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -814,7 +814,7 @@ def check_obj_params(parameters, obj): if qargs in self._gate_map[op_name]: return True if self._gate_map[op_name] is None or None in self._gate_map[op_name]: - return self._gate_name_map[op_name].num_qubits == len(qargs) and all( + return obj.num_qubits == len(qargs) and all( x < self.num_qubits for x in qargs ) return False From 4ede4701d1b245f6ce35aa184a938e3be41711b2 Mon Sep 17 00:00:00 2001 From: Joe Schulte Date: Thu, 9 May 2024 16:02:40 -0400 Subject: [PATCH 16/17] Removing consider-iterating-dictionary lint rule and updates (#12366) --- pyproject.toml | 1 - qiskit/circuit/library/n_local/pauli_two_design.py | 2 +- qiskit/pulse/parser.py | 4 ++-- .../synthesis/discrete_basis/generate_basis_approximations.py | 2 +- qiskit/synthesis/discrete_basis/solovay_kitaev.py | 2 +- qiskit/transpiler/passes/synthesis/plugin.py | 4 ++-- qiskit/visualization/circuit/matplotlib.py | 2 +- qiskit/visualization/circuit/qcstyle.py | 4 ++-- test/python/transpiler/test_high_level_synthesis.py | 4 ++-- 9 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 303192e75cf9..45725f51bb48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -218,7 +218,6 @@ disable = [ # with the rationale "arguments-renamed", "broad-exception-raised", - "consider-iterating-dictionary", "consider-using-dict-items", "consider-using-enumerate", "consider-using-f-string", diff --git a/qiskit/circuit/library/n_local/pauli_two_design.py b/qiskit/circuit/library/n_local/pauli_two_design.py index b79f08889387..71b090d08848 100644 --- a/qiskit/circuit/library/n_local/pauli_two_design.py +++ b/qiskit/circuit/library/n_local/pauli_two_design.py @@ -118,7 +118,7 @@ def _build_rotation_layer(self, circuit, param_iter, i): qubits = range(self.num_qubits) # if no gates for this layer were generated, generate them - if i not in self._gates.keys(): + if i not in self._gates: self._gates[i] = list(self._rng.choice(["rx", "ry", "rz"], self.num_qubits)) # if not enough gates exist, add more elif len(self._gates[i]) < self.num_qubits: diff --git a/qiskit/pulse/parser.py b/qiskit/pulse/parser.py index a9e752f562e5..8e31faebf77a 100644 --- a/qiskit/pulse/parser.py +++ b/qiskit/pulse/parser.py @@ -120,7 +120,7 @@ def __call__(self, *args, **kwargs) -> complex | ast.Expression | PulseExpressio if kwargs: for key, val in kwargs.items(): if key in self.params: - if key not in self._locals_dict.keys(): + if key not in self._locals_dict: self._locals_dict[key] = val else: raise PulseError( @@ -272,7 +272,7 @@ def visit_Call(self, node: ast.Call) -> ast.Call | ast.Constant: node = copy.copy(node) node.args = [self.visit(arg) for arg in node.args] if all(isinstance(arg, ast.Constant) for arg in node.args): - if node.func.id not in self._math_ops.keys(): + if node.func.id not in self._math_ops: raise PulseError("Function %s is not supported." % node.func.id) _args = [arg.value for arg in node.args] _val = self._math_ops[node.func.id](*_args) diff --git a/qiskit/synthesis/discrete_basis/generate_basis_approximations.py b/qiskit/synthesis/discrete_basis/generate_basis_approximations.py index 07139b223b1d..672d0eb9e8ef 100644 --- a/qiskit/synthesis/discrete_basis/generate_basis_approximations.py +++ b/qiskit/synthesis/discrete_basis/generate_basis_approximations.py @@ -137,7 +137,7 @@ def generate_basic_approximations( basis = [] for gate in basis_gates: if isinstance(gate, str): - if gate not in _1q_gates.keys(): + if gate not in _1q_gates: raise ValueError(f"Invalid gate identifier: {gate}") basis.append(gate) else: # gate is a qiskit.circuit.Gate diff --git a/qiskit/synthesis/discrete_basis/solovay_kitaev.py b/qiskit/synthesis/discrete_basis/solovay_kitaev.py index 62ad50582d40..e1db47beaeff 100644 --- a/qiskit/synthesis/discrete_basis/solovay_kitaev.py +++ b/qiskit/synthesis/discrete_basis/solovay_kitaev.py @@ -180,7 +180,7 @@ def _remove_inverse_follows_gate(sequence): while index < len(sequence.gates) - 1: curr_gate = sequence.gates[index] next_gate = sequence.gates[index + 1] - if curr_gate.name in _1q_inverses.keys(): + if curr_gate.name in _1q_inverses: remove = _1q_inverses[curr_gate.name] == next_gate.name else: remove = curr_gate.inverse() == next_gate diff --git a/qiskit/transpiler/passes/synthesis/plugin.py b/qiskit/transpiler/passes/synthesis/plugin.py index f2485bfee530..c57c6d76f9fb 100644 --- a/qiskit/transpiler/passes/synthesis/plugin.py +++ b/qiskit/transpiler/passes/synthesis/plugin.py @@ -698,13 +698,13 @@ def __init__(self): self.plugins_by_op = {} for plugin_name in self.plugins.names(): op_name, method_name = plugin_name.split(".") - if op_name not in self.plugins_by_op.keys(): + if op_name not in self.plugins_by_op: self.plugins_by_op[op_name] = [] self.plugins_by_op[op_name].append(method_name) def method_names(self, op_name): """Returns plugin methods for op_name.""" - if op_name in self.plugins_by_op.keys(): + if op_name in self.plugins_by_op: return self.plugins_by_op[op_name] else: return [] diff --git a/qiskit/visualization/circuit/matplotlib.py b/qiskit/visualization/circuit/matplotlib.py index c547846acc5b..b4252065006c 100644 --- a/qiskit/visualization/circuit/matplotlib.py +++ b/qiskit/visualization/circuit/matplotlib.py @@ -893,7 +893,7 @@ def _draw_regs_wires(self, num_folds, xmax, max_x_index, qubits_dict, clbits_dic this_clbit_dict = {} for clbit in clbits_dict.values(): y = clbit["y"] - fold_num * (glob_data["n_lines"] + 1) - if y not in this_clbit_dict.keys(): + if y not in this_clbit_dict: this_clbit_dict[y] = { "val": 1, "wire_label": clbit["wire_label"], diff --git a/qiskit/visualization/circuit/qcstyle.py b/qiskit/visualization/circuit/qcstyle.py index a8432ca86a9e..67ae9faaf24b 100644 --- a/qiskit/visualization/circuit/qcstyle.py +++ b/qiskit/visualization/circuit/qcstyle.py @@ -72,7 +72,7 @@ class StyleDict(dict): def __setitem__(self, key: Any, value: Any) -> None: # allow using field abbreviations - if key in self.ABBREVIATIONS.keys(): + if key in self.ABBREVIATIONS: key = self.ABBREVIATIONS[key] if key not in self.VALID_FIELDS: @@ -85,7 +85,7 @@ def __setitem__(self, key: Any, value: Any) -> None: def __getitem__(self, key: Any) -> Any: # allow using field abbreviations - if key in self.ABBREVIATIONS.keys(): + if key in self.ABBREVIATIONS: key = self.ABBREVIATIONS[key] return super().__getitem__(key) diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 9a2432b82f9c..f20b102d1838 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -126,7 +126,7 @@ class OpARepeatSynthesisPlugin(HighLevelSynthesisPlugin): """The repeat synthesis for opA""" def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - if "n" not in options.keys(): + if "n" not in options: return None qc = QuantumCircuit(1) @@ -206,7 +206,7 @@ def __init__(self): def method_names(self, op_name): """Returns plugin methods for op_name.""" - if op_name in self.plugins_by_op.keys(): + if op_name in self.plugins_by_op: return self.plugins_by_op[op_name] else: return [] From b80885d1d617bd96ab5314db1b50d959258c2f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Thu, 9 May 2024 22:15:36 +0200 Subject: [PATCH 17/17] Fix missing layout in `Commuting2qGateRouter` (#12137) * Add layout to property set, add test * Apply comments from code review * Add virtual permutation instead of final layout. Test --- .../commuting_2q_gate_router.py | 9 +++- ...x-swap-router-layout-f28cf0a2de7976a8.yaml | 7 +++ .../transpiler/test_swap_strategy_router.py | 44 ++++++++++++++++++- 3 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/fix-swap-router-layout-f28cf0a2de7976a8.yaml 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 402aa9146f0a..501400f70ced 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 @@ -160,8 +160,13 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if len(dag.qubits) != next(iter(dag.qregs.values())).size: raise TranspilerError("Circuit has qubits not contained in the qubit register.") - new_dag = dag.copy_empty_like() + # Fix output permutation -- copied from ElidePermutations + input_qubit_mapping = {qubit: index for index, qubit in enumerate(dag.qubits)} + self.property_set["original_layout"] = Layout(input_qubit_mapping) + if self.property_set["original_qubit_indices"] is None: + self.property_set["original_qubit_indices"] = input_qubit_mapping + new_dag = dag.copy_empty_like() current_layout = Layout.generate_trivial_layout(*dag.qregs.values()) # Used to keep track of nodes that do not decompose using swap strategies. @@ -183,6 +188,8 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: self._compose_non_swap_nodes(accumulator, current_layout, new_dag) + self.property_set["virtual_permutation_layout"] = current_layout + return new_dag def _compose_non_swap_nodes( diff --git a/releasenotes/notes/fix-swap-router-layout-f28cf0a2de7976a8.yaml b/releasenotes/notes/fix-swap-router-layout-f28cf0a2de7976a8.yaml new file mode 100644 index 000000000000..834d7986ab85 --- /dev/null +++ b/releasenotes/notes/fix-swap-router-layout-f28cf0a2de7976a8.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed an oversight in the :class:`.Commuting2qGateRouter` transpiler pass where the qreg permutations + were not added to the pass property set, so they would have to be tracked manually by the user. Now it's + possible to access the permutation through the output circuit's ``layout`` property and plug the pass + into any transpilation pipeline without loss of information. diff --git a/test/python/transpiler/test_swap_strategy_router.py b/test/python/transpiler/test_swap_strategy_router.py index 4a46efd57b2c..d6ca1bde53dd 100644 --- a/test/python/transpiler/test_swap_strategy_router.py +++ b/test/python/transpiler/test_swap_strategy_router.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -15,12 +15,14 @@ from ddt import ddt, data from qiskit.circuit import QuantumCircuit, Qubit, QuantumRegister +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.transpiler import PassManager, CouplingMap, Layout, TranspilerError from qiskit.circuit.library import PauliEvolutionGate, CXGate from qiskit.circuit.library.n_local import QAOAAnsatz from qiskit.converters import circuit_to_dag from qiskit.exceptions import QiskitError from qiskit.quantum_info import Pauli, SparsePauliOp +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.transpiler.passes import FullAncillaAllocation from qiskit.transpiler.passes import EnlargeWithAncilla from qiskit.transpiler.passes import ApplyLayout @@ -562,9 +564,47 @@ def test_edge_coloring(self, edge_coloring): self.assertEqual(pm_.run(circ), expected) + def test_permutation_tracking(self): + """Test that circuit layout permutations are properly tracked in the pass property + set and returned with the output circuit.""" + + # We use the same scenario as the QAOA test above + mixer = QuantumCircuit(4) + for idx in range(4): + mixer.ry(-idx, idx) + + op = SparsePauliOp.from_list([("IZZI", 1), ("ZIIZ", 2), ("ZIZI", 3)]) + circ = QAOAAnsatz(op, reps=2, mixer_operator=mixer) + + expected_swap_permutation = [3, 1, 2, 0] + expected_full_permutation = [1, 3, 2, 0] + + cmap = CouplingMap(couplinglist=[(0, 1), (1, 2), (2, 3)]) + swap_strat = SwapStrategy(cmap, swap_layers=[[(0, 1), (2, 3)], [(1, 2)]]) + + # test standalone + swap_pm = PassManager( + [ + FindCommutingPauliEvolutions(), + Commuting2qGateRouter(swap_strat), + ] + ) + swapped = swap_pm.run(circ.decompose()) + + # test as pre-routing step + backend = GenericBackendV2(num_qubits=4, coupling_map=[[0, 1], [0, 2], [0, 3]], seed=42) + pm = generate_preset_pass_manager( + optimization_level=3, target=backend.target, seed_transpiler=40 + ) + pm.pre_routing = swap_pm + full = pm.run(circ.decompose()) + + self.assertEqual(swapped.layout.routing_permutation(), expected_swap_permutation) + self.assertEqual(full.layout.routing_permutation(), expected_full_permutation) + class TestSwapRouterExceptions(QiskitTestCase): - """Test that exceptions are properly raises.""" + """Test that exceptions are properly raised.""" def setUp(self): """Setup useful variables."""