qiskit 2.0.1__cp39-abi3-macosx_11_0_arm64.whl → 2.1.0rc1__cp39-abi3-macosx_11_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qiskit/VERSION.txt +1 -1
- qiskit/__init__.py +19 -1
- qiskit/_accelerate.abi3.so +0 -0
- qiskit/circuit/__init__.py +13 -21
- qiskit/circuit/_add_control.py +57 -31
- qiskit/circuit/_classical_resource_map.py +4 -0
- qiskit/circuit/annotation.py +404 -0
- qiskit/circuit/classical/expr/__init__.py +1 -1
- qiskit/circuit/classical/expr/expr.py +104 -446
- qiskit/circuit/classical/expr/visitors.py +6 -0
- qiskit/circuit/classical/types/types.py +7 -130
- qiskit/circuit/controlflow/box.py +32 -7
- qiskit/circuit/delay.py +11 -9
- qiskit/circuit/library/arithmetic/adders/adder.py +5 -5
- qiskit/circuit/library/arithmetic/multipliers/multiplier.py +3 -3
- qiskit/circuit/library/arithmetic/piecewise_chebyshev.py +7 -3
- qiskit/circuit/library/arithmetic/piecewise_linear_pauli_rotations.py +23 -15
- qiskit/circuit/library/arithmetic/piecewise_polynomial_pauli_rotations.py +22 -14
- qiskit/circuit/library/arithmetic/quadratic_form.py +6 -0
- qiskit/circuit/library/arithmetic/weighted_adder.py +43 -24
- qiskit/circuit/library/basis_change/qft.py +2 -2
- qiskit/circuit/library/blueprintcircuit.py +6 -0
- qiskit/circuit/library/boolean_logic/inner_product.py +2 -2
- qiskit/circuit/library/boolean_logic/quantum_and.py +2 -2
- qiskit/circuit/library/boolean_logic/quantum_or.py +5 -5
- qiskit/circuit/library/boolean_logic/quantum_xor.py +2 -2
- qiskit/circuit/library/data_preparation/_z_feature_map.py +2 -2
- qiskit/circuit/library/data_preparation/_zz_feature_map.py +2 -2
- qiskit/circuit/library/data_preparation/pauli_feature_map.py +2 -2
- qiskit/circuit/library/fourier_checking.py +2 -2
- qiskit/circuit/library/generalized_gates/diagonal.py +5 -1
- qiskit/circuit/library/generalized_gates/gms.py +5 -1
- qiskit/circuit/library/generalized_gates/linear_function.py +2 -2
- qiskit/circuit/library/generalized_gates/permutation.py +5 -1
- qiskit/circuit/library/generalized_gates/uc.py +1 -1
- qiskit/circuit/library/generalized_gates/unitary.py +21 -2
- qiskit/circuit/library/graph_state.py +2 -2
- qiskit/circuit/library/grover_operator.py +2 -2
- qiskit/circuit/library/hidden_linear_function.py +2 -2
- qiskit/circuit/library/iqp.py +2 -2
- qiskit/circuit/library/n_local/efficient_su2.py +2 -2
- qiskit/circuit/library/n_local/evolved_operator_ansatz.py +4 -2
- qiskit/circuit/library/n_local/excitation_preserving.py +7 -9
- qiskit/circuit/library/n_local/n_local.py +4 -3
- qiskit/circuit/library/n_local/pauli_two_design.py +2 -2
- qiskit/circuit/library/n_local/real_amplitudes.py +2 -2
- qiskit/circuit/library/n_local/two_local.py +2 -2
- qiskit/circuit/library/overlap.py +2 -2
- qiskit/circuit/library/pauli_evolution.py +3 -2
- qiskit/circuit/library/phase_estimation.py +2 -2
- qiskit/circuit/library/standard_gates/dcx.py +11 -12
- qiskit/circuit/library/standard_gates/ecr.py +21 -24
- qiskit/circuit/library/standard_gates/equivalence_library.py +232 -96
- qiskit/circuit/library/standard_gates/global_phase.py +5 -6
- qiskit/circuit/library/standard_gates/h.py +22 -45
- qiskit/circuit/library/standard_gates/i.py +1 -1
- qiskit/circuit/library/standard_gates/iswap.py +13 -31
- qiskit/circuit/library/standard_gates/p.py +19 -26
- qiskit/circuit/library/standard_gates/r.py +11 -17
- qiskit/circuit/library/standard_gates/rx.py +21 -45
- qiskit/circuit/library/standard_gates/rxx.py +7 -22
- qiskit/circuit/library/standard_gates/ry.py +21 -39
- qiskit/circuit/library/standard_gates/ryy.py +13 -28
- qiskit/circuit/library/standard_gates/rz.py +18 -35
- qiskit/circuit/library/standard_gates/rzx.py +7 -22
- qiskit/circuit/library/standard_gates/rzz.py +7 -19
- qiskit/circuit/library/standard_gates/s.py +44 -39
- qiskit/circuit/library/standard_gates/swap.py +25 -38
- qiskit/circuit/library/standard_gates/sx.py +34 -41
- qiskit/circuit/library/standard_gates/t.py +18 -27
- qiskit/circuit/library/standard_gates/u.py +8 -24
- qiskit/circuit/library/standard_gates/u1.py +28 -52
- qiskit/circuit/library/standard_gates/u2.py +9 -9
- qiskit/circuit/library/standard_gates/u3.py +24 -40
- qiskit/circuit/library/standard_gates/x.py +190 -336
- qiskit/circuit/library/standard_gates/xx_minus_yy.py +12 -50
- qiskit/circuit/library/standard_gates/xx_plus_yy.py +13 -52
- qiskit/circuit/library/standard_gates/y.py +19 -23
- qiskit/circuit/library/standard_gates/z.py +31 -38
- qiskit/circuit/parameter.py +14 -5
- qiskit/circuit/parameterexpression.py +109 -75
- qiskit/circuit/quantumcircuit.py +168 -98
- qiskit/circuit/quantumcircuitdata.py +1 -0
- qiskit/circuit/random/__init__.py +37 -2
- qiskit/circuit/random/utils.py +445 -56
- qiskit/circuit/tools/pi_check.py +5 -13
- qiskit/compiler/transpiler.py +1 -1
- qiskit/converters/circuit_to_instruction.py +2 -2
- qiskit/dagcircuit/dagnode.py +8 -3
- qiskit/primitives/__init__.py +2 -2
- qiskit/primitives/base/base_estimator.py +2 -2
- qiskit/primitives/containers/data_bin.py +0 -3
- qiskit/primitives/containers/observables_array.py +192 -108
- qiskit/primitives/primitive_job.py +29 -10
- qiskit/providers/fake_provider/generic_backend_v2.py +2 -0
- qiskit/qasm3/__init__.py +106 -12
- qiskit/qasm3/ast.py +15 -1
- qiskit/qasm3/exporter.py +59 -36
- qiskit/qasm3/printer.py +12 -0
- qiskit/qpy/__init__.py +183 -7
- qiskit/qpy/binary_io/circuits.py +256 -24
- qiskit/qpy/binary_io/parse_sympy_repr.py +5 -0
- qiskit/qpy/binary_io/schedules.py +12 -32
- qiskit/qpy/binary_io/value.py +36 -18
- qiskit/qpy/common.py +11 -3
- qiskit/qpy/formats.py +17 -1
- qiskit/qpy/interface.py +52 -12
- qiskit/qpy/type_keys.py +7 -1
- qiskit/quantum_info/__init__.py +10 -0
- qiskit/quantum_info/operators/__init__.py +1 -0
- qiskit/quantum_info/operators/symplectic/__init__.py +1 -0
- qiskit/quantum_info/operators/symplectic/clifford_circuits.py +26 -0
- qiskit/quantum_info/operators/symplectic/pauli.py +2 -2
- qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +1 -1
- qiskit/result/sampled_expval.py +3 -1
- qiskit/synthesis/__init__.py +10 -0
- qiskit/synthesis/arithmetic/__init__.py +1 -1
- qiskit/synthesis/arithmetic/adders/__init__.py +1 -0
- qiskit/synthesis/arithmetic/adders/draper_qft_adder.py +6 -2
- qiskit/synthesis/arithmetic/adders/rv_ripple_carry_adder.py +156 -0
- qiskit/synthesis/discrete_basis/generate_basis_approximations.py +14 -126
- qiskit/synthesis/discrete_basis/solovay_kitaev.py +161 -121
- qiskit/synthesis/evolution/lie_trotter.py +10 -7
- qiskit/synthesis/evolution/product_formula.py +10 -7
- qiskit/synthesis/evolution/qdrift.py +10 -7
- qiskit/synthesis/evolution/suzuki_trotter.py +10 -7
- qiskit/synthesis/multi_controlled/__init__.py +4 -0
- qiskit/synthesis/multi_controlled/mcx_synthesis.py +402 -178
- qiskit/synthesis/multi_controlled/multi_control_rotation_gates.py +14 -15
- qiskit/synthesis/qft/qft_decompose_lnn.py +7 -25
- qiskit/synthesis/unitary/qsd.py +80 -9
- qiskit/transpiler/__init__.py +19 -8
- qiskit/transpiler/instruction_durations.py +2 -20
- qiskit/transpiler/passes/__init__.py +4 -2
- qiskit/transpiler/passes/layout/dense_layout.py +26 -6
- qiskit/transpiler/passes/layout/disjoint_utils.py +1 -166
- qiskit/transpiler/passes/layout/sabre_layout.py +22 -3
- qiskit/transpiler/passes/layout/sabre_pre_layout.py +1 -1
- qiskit/transpiler/passes/layout/vf2_layout.py +49 -13
- qiskit/transpiler/passes/layout/vf2_utils.py +13 -1
- qiskit/transpiler/passes/optimization/__init__.py +1 -1
- qiskit/transpiler/passes/optimization/consolidate_blocks.py +6 -1
- qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +2 -1
- qiskit/transpiler/passes/optimization/optimize_clifford_t.py +68 -0
- qiskit/transpiler/passes/optimization/template_matching/template_substitution.py +3 -9
- qiskit/transpiler/passes/routing/sabre_swap.py +12 -2
- qiskit/transpiler/passes/routing/star_prerouting.py +106 -81
- qiskit/transpiler/passes/scheduling/__init__.py +1 -1
- qiskit/transpiler/passes/scheduling/alignments/check_durations.py +1 -1
- qiskit/transpiler/passes/scheduling/padding/__init__.py +1 -0
- qiskit/transpiler/passes/scheduling/padding/context_aware_dynamical_decoupling.py +876 -0
- qiskit/transpiler/passes/synthesis/__init__.py +1 -0
- qiskit/transpiler/passes/synthesis/clifford_unitary_synth_plugin.py +123 -0
- qiskit/transpiler/passes/synthesis/hls_plugins.py +472 -92
- qiskit/transpiler/passes/synthesis/solovay_kitaev_synthesis.py +27 -22
- qiskit/transpiler/passmanager_config.py +3 -0
- qiskit/transpiler/preset_passmanagers/builtin_plugins.py +149 -28
- qiskit/transpiler/preset_passmanagers/common.py +101 -0
- qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py +6 -0
- qiskit/transpiler/preset_passmanagers/level3.py +2 -2
- qiskit/utils/optionals.py +6 -5
- qiskit/visualization/circuit/_utils.py +5 -3
- qiskit/visualization/circuit/latex.py +9 -2
- qiskit/visualization/circuit/matplotlib.py +26 -4
- qiskit/visualization/circuit/qcstyle.py +9 -157
- qiskit/visualization/dag/__init__.py +13 -0
- qiskit/visualization/dag/dagstyle.py +103 -0
- qiskit/visualization/dag/styles/__init__.py +13 -0
- qiskit/visualization/dag/styles/color.json +10 -0
- qiskit/visualization/dag/styles/plain.json +5 -0
- qiskit/visualization/dag_visualization.py +169 -98
- qiskit/visualization/style.py +223 -0
- {qiskit-2.0.1.dist-info → qiskit-2.1.0rc1.dist-info}/METADATA +14 -13
- {qiskit-2.0.1.dist-info → qiskit-2.1.0rc1.dist-info}/RECORD +178 -169
- {qiskit-2.0.1.dist-info → qiskit-2.1.0rc1.dist-info}/WHEEL +1 -1
- {qiskit-2.0.1.dist-info → qiskit-2.1.0rc1.dist-info}/entry_points.txt +6 -0
- qiskit/synthesis/discrete_basis/commutator_decompose.py +0 -265
- qiskit/synthesis/discrete_basis/gate_sequence.py +0 -421
- {qiskit-2.0.1.dist-info → qiskit-2.1.0rc1.dist-info}/licenses/LICENSE.txt +0 -0
- {qiskit-2.0.1.dist-info → qiskit-2.1.0rc1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,876 @@
|
|
1
|
+
# This code is part of Qiskit.
|
2
|
+
#
|
3
|
+
# (C) Copyright IBM 2024.
|
4
|
+
#
|
5
|
+
# This code is licensed under the Apache License, Version 2.0. You may
|
6
|
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
7
|
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
8
|
+
#
|
9
|
+
# Any modifications or derivative works of this code must retain this
|
10
|
+
# copyright notice, and modified files need to carry a notice indicating
|
11
|
+
# that they have been altered from the originals.
|
12
|
+
|
13
|
+
"""Context-aware dynamical decoupling."""
|
14
|
+
|
15
|
+
from __future__ import annotations
|
16
|
+
from enum import Enum
|
17
|
+
|
18
|
+
import itertools
|
19
|
+
import logging
|
20
|
+
|
21
|
+
from pprint import pformat
|
22
|
+
from dataclasses import dataclass, field
|
23
|
+
from collections import defaultdict
|
24
|
+
import numpy as np
|
25
|
+
import rustworkx as rx
|
26
|
+
|
27
|
+
from qiskit.circuit import QuantumCircuit, Qubit, Reset, Gate
|
28
|
+
from qiskit.circuit.library import CXGate, ECRGate, XGate
|
29
|
+
from qiskit.dagcircuit import DAGCircuit
|
30
|
+
from qiskit.converters import circuit_to_dag
|
31
|
+
from qiskit.dagcircuit import DAGOpNode, DAGInNode
|
32
|
+
from qiskit.transpiler.target import Target
|
33
|
+
from qiskit.transpiler.basepasses import TransformationPass
|
34
|
+
from qiskit.transpiler.coupling import CouplingMap
|
35
|
+
from qiskit.transpiler.exceptions import TranspilerError
|
36
|
+
|
37
|
+
from .pad_delay import PadDelay
|
38
|
+
|
39
|
+
logger = logging.getLogger(__name__)
|
40
|
+
|
41
|
+
|
42
|
+
class ContextAwareDynamicalDecoupling(TransformationPass):
|
43
|
+
"""Implement an X-sequence dynamical decoupling considering the gate- and qubit-context.
|
44
|
+
|
45
|
+
This pass implements a context-aware dynamical decoupling (DD) [1], which ensures that
|
46
|
+
|
47
|
+
(1) simultaneously occurring DD sequences on device-adjacent qubits are mutually orthogonal, and
|
48
|
+
(2) DD sequences on spectator qubits of ECR and CX gates are orthogonal to the echo
|
49
|
+
pulses on the neighboring target/control qubits.
|
50
|
+
|
51
|
+
The mutually orthogonal DD sequences are currently Walsh-Hadamard sequences, consisting of only
|
52
|
+
X gates. In some cases it might therefore be beneficial to use :class:`.PadDynamicalDecoupling`
|
53
|
+
with more generic sequences, such as XY4.
|
54
|
+
|
55
|
+
This pass performs best if the two-qubit interactions have the same durations on the
|
56
|
+
device, as it allows to align delay sequences and take into account potential control and target
|
57
|
+
operations on neighboring qubits. However, it is still valid if this is not the case.
|
58
|
+
|
59
|
+
.. note::
|
60
|
+
|
61
|
+
If this pass is run within a pass manager (as in the example below), it will
|
62
|
+
automatically run :class:`.PadDelay` to allocate the delays. If instead it is run as
|
63
|
+
standalone (not inside a :class:`.PassManager`), the delays must already be inserted.
|
64
|
+
|
65
|
+
|
66
|
+
Example::
|
67
|
+
|
68
|
+
from qiskit.circuit import QuantumCircuit
|
69
|
+
from qiskit.circuit.library import QFTGate
|
70
|
+
from qiskit.transpiler import PassManager
|
71
|
+
from qiskit.transpiler.passes import ALAPScheduleAnalysis, ContextAwareDynamicalDecoupling
|
72
|
+
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
|
73
|
+
from qiskit_ibm_runtime.fake_provider import FakeSherbrooke
|
74
|
+
|
75
|
+
num_qubits = 10
|
76
|
+
circuit = QuantumCircuit(num_qubits)
|
77
|
+
circuit.append(QFTGate(num_qubits), circuit.qubits)
|
78
|
+
circuit.measure_all()
|
79
|
+
|
80
|
+
target = FakeSherbrooke().target
|
81
|
+
|
82
|
+
pm = generate_preset_pass_manager(optimization_level=2, target=target)
|
83
|
+
dd = PassManager([
|
84
|
+
ALAPScheduleAnalysis(target=target),
|
85
|
+
ContextAwareDynamicalDecoupling(target=target),
|
86
|
+
])
|
87
|
+
|
88
|
+
transpiled = pm.run(circuit)
|
89
|
+
with_dd = dd.run(transpiled)
|
90
|
+
|
91
|
+
print(with_dd.draw(idle_wires=False))
|
92
|
+
|
93
|
+
References:
|
94
|
+
|
95
|
+
[1] A. Seif et al. (2024). Suppressing Correlated Noise in Quantum Computers via
|
96
|
+
Context-Aware Compiling, `arXiv:2403.06852 <https://arxiv.org/abs/2403.06852>`_.
|
97
|
+
|
98
|
+
"""
|
99
|
+
|
100
|
+
def __init__(
|
101
|
+
self,
|
102
|
+
target: Target,
|
103
|
+
*,
|
104
|
+
min_duration: int | None = None,
|
105
|
+
skip_reset_qubits: bool = True,
|
106
|
+
skip_dd_threshold: float = 1.0,
|
107
|
+
pulse_alignment: int | None = None,
|
108
|
+
coloring_strategy: rx.ColoringStrategy = rx.ColoringStrategy.Saturation,
|
109
|
+
) -> None:
|
110
|
+
"""
|
111
|
+
Args:
|
112
|
+
target: The :class:`.Target` of the device to run the circuit.
|
113
|
+
min_duration: Minimal delay duration (in ``dt``) to insert a DD sequence. This
|
114
|
+
can be useful, e.g. if a big delay block would be interrupted and split into
|
115
|
+
smaller blocks due to a very short, adjacent delay. If ``None``, this is set
|
116
|
+
to be at least twice the difference of the longest/shortest CX or ECR gate.
|
117
|
+
skip_reset_qubits: Skip initial delays and delays after a reset.
|
118
|
+
skip_dd_threshold: Skip dynamical decoupling on an idle qubit, if the duration of
|
119
|
+
the decoupling sequence exceeds this fraction of the idle window. For example, to
|
120
|
+
skip a DD sequence if it would take up more than 95% of the idle time, set this
|
121
|
+
value to 0.95. A value of 1. means that the DD sequence is applied if it fits into
|
122
|
+
the window.
|
123
|
+
pulse_alignment: The hardware constraints (in ``dt``) for gate timing allocation.
|
124
|
+
If provided, the inserted gates will only be executed on integer multiples of
|
125
|
+
this value. This is usually provided on ``backend.configuration().timing_constraints``.
|
126
|
+
If ``None``, this is extracted from the ``target``.
|
127
|
+
coloring_strategy: The coloring strategy used for ``rx.greedy_graph_color``.
|
128
|
+
Defaults to a saturation strategy, which is optimal on bipartite graphs,
|
129
|
+
see Section 1.2.2.8 of [2].
|
130
|
+
|
131
|
+
References:
|
132
|
+
|
133
|
+
[2] A. Kosowski, and K. Manuszewski, Classical Coloring of Graphs, Graph Colorings,
|
134
|
+
2-19, 2004. ISBN 0-8218-3458-4.
|
135
|
+
|
136
|
+
"""
|
137
|
+
super().__init__()
|
138
|
+
|
139
|
+
if not 0 <= skip_dd_threshold <= 1:
|
140
|
+
raise ValueError(f"skip_dd_threshold must be in [0, 1], but is {skip_dd_threshold}")
|
141
|
+
|
142
|
+
if min_duration is None:
|
143
|
+
if target.dt is None:
|
144
|
+
min_duration = 0
|
145
|
+
else:
|
146
|
+
min_duration = 2 * _gate_length_variance(target) / target.dt
|
147
|
+
|
148
|
+
self._min_duration = min_duration
|
149
|
+
self._skip_reset_qubits = skip_reset_qubits
|
150
|
+
self._skip_dd_threshold = skip_dd_threshold
|
151
|
+
self._target = target
|
152
|
+
self._coupling_map = target.build_coupling_map() # build once and re-use for performance
|
153
|
+
self._pulse_alignment = (
|
154
|
+
target.pulse_alignment if pulse_alignment is None else pulse_alignment
|
155
|
+
)
|
156
|
+
self._coloring_strategy = coloring_strategy
|
157
|
+
self._sequence_generator = WalshHadamardSequence()
|
158
|
+
|
159
|
+
# Use PadDelay to insert Delay operations into the DAG before running this pass.
|
160
|
+
# This could be integrated into this pass as well, saving one iteration over the DAG,
|
161
|
+
# but this would repeat logic and is currently no bottleneck.
|
162
|
+
self.requires = [PadDelay(target=target)]
|
163
|
+
|
164
|
+
def run(self, dag: DAGCircuit) -> DAGCircuit:
|
165
|
+
# check the schedule analysis was run
|
166
|
+
if "node_start_time" not in self.property_set:
|
167
|
+
raise RuntimeError(
|
168
|
+
"node_start_time not found in the pass manager's property set. "
|
169
|
+
"Please run a scheduling pass before calling ContextAwareDynamicalDecoupling."
|
170
|
+
)
|
171
|
+
|
172
|
+
# 1) Find all delay instructions that qualify. We split them into explicit
|
173
|
+
# "begin" and "end" events and sort them.
|
174
|
+
sorted_delay_events = self._get_sorted_delays(dag)
|
175
|
+
|
176
|
+
# 2) Identify adjacent delays by overlap in time and adjacency on device.
|
177
|
+
qubit_map = {bit: idx for idx, bit in enumerate(dag.qubits)} # map qubits to their indices
|
178
|
+
adjacent_delay_blocks = self._collect_adjacent_delay_blocks(sorted_delay_events, qubit_map)
|
179
|
+
logger.debug("adjacent delay blocks: %s", pformat(adjacent_delay_blocks))
|
180
|
+
|
181
|
+
# 3) Split the blocks into multi-qubit delay layers
|
182
|
+
merged_delays = []
|
183
|
+
for block in adjacent_delay_blocks:
|
184
|
+
merged_delays_in_block = self._split_adjacent_block(block, qubit_map)
|
185
|
+
merged_delays += merged_delays_in_block
|
186
|
+
|
187
|
+
# 4) For each multi-qubit delay, find a coloring (that takes into account neighboring
|
188
|
+
# CX or ECR gates) and get the DD sequence
|
189
|
+
all_delays = set() # keep a list of all delays to iterate over them easily later
|
190
|
+
|
191
|
+
for merged_delay in merged_delays:
|
192
|
+
all_delays.update(merged_delay.ops)
|
193
|
+
|
194
|
+
# get coloring inside the n-qubit delay
|
195
|
+
coloring = self._get_wire_coloring(dag, merged_delay)
|
196
|
+
|
197
|
+
# get the DD sequence on the qubit
|
198
|
+
duration = merged_delay.end - merged_delay.start
|
199
|
+
checked_durations_cache = set()
|
200
|
+
for op in merged_delay.ops:
|
201
|
+
dd_circuit, start_times = self._get_dd_sequence(
|
202
|
+
coloring[op.index],
|
203
|
+
op.index,
|
204
|
+
duration,
|
205
|
+
checked_durations_cache,
|
206
|
+
)
|
207
|
+
op.replacement.compose(dd_circuit, inplace=True, copy=False)
|
208
|
+
if len(op.start_times) == 0:
|
209
|
+
op.start_times += start_times
|
210
|
+
else:
|
211
|
+
start_times = [op.start_times[-1] + time for time in start_times]
|
212
|
+
|
213
|
+
op.start_times += start_times
|
214
|
+
|
215
|
+
# 5) Replace each delay operation with its individual DD sequence
|
216
|
+
qubit_map = dict(enumerate(dag.qubits))
|
217
|
+
for delay in all_delays:
|
218
|
+
# replace it with its stored replacement
|
219
|
+
as_dag = circuit_to_dag(delay.replacement)
|
220
|
+
node_ids = [node._node_id for node in as_dag.topological_op_nodes()]
|
221
|
+
id_map = dag.substitute_node_with_dag(
|
222
|
+
delay.op, as_dag, {as_dag.qubits[0]: qubit_map[delay.index]}
|
223
|
+
)
|
224
|
+
|
225
|
+
# update node start times
|
226
|
+
for node_id, start_time in zip(node_ids, delay.start_times):
|
227
|
+
self.property_set["node_start_time"][id_map[node_id]] = start_time
|
228
|
+
|
229
|
+
return dag
|
230
|
+
|
231
|
+
def get_orthogonal_sequence(self, order: int) -> tuple[list[float], list[Gate]]:
|
232
|
+
"""Return a DD sequence of given order, where different orders are orthogonal."""
|
233
|
+
spacing = self._sequence_generator.get_sequence(order)
|
234
|
+
return spacing, [XGate() for _ in spacing[:-1]]
|
235
|
+
|
236
|
+
def _get_sorted_delays(
|
237
|
+
self,
|
238
|
+
dag: DAGCircuit,
|
239
|
+
) -> list[DelayEvent]:
|
240
|
+
"""Get sorted DelayEvent objects for all eligible delay operations."""
|
241
|
+
|
242
|
+
def is_after_reset(node):
|
243
|
+
if not self._skip_reset_qubits:
|
244
|
+
return False # if we do not skip reset qubits, we always want to use the delay
|
245
|
+
|
246
|
+
predecessor = next(dag.predecessors(node)) # a delay has one 1 predecessor
|
247
|
+
return isinstance(predecessor, DAGInNode) or isinstance(predecessor.op, Reset)
|
248
|
+
|
249
|
+
qubit_map = {bit: index for index, bit in enumerate(dag.qubits)}
|
250
|
+
|
251
|
+
eligible_delays = [
|
252
|
+
DelayEvent(
|
253
|
+
event_type,
|
254
|
+
(
|
255
|
+
start_time
|
256
|
+
if event_type == EventType.BEGIN
|
257
|
+
else start_time + self._duration(node, qubit_map)
|
258
|
+
),
|
259
|
+
node,
|
260
|
+
)
|
261
|
+
for node, start_time in self.property_set["node_start_time"].items()
|
262
|
+
if (
|
263
|
+
node.op.name == "delay"
|
264
|
+
and not is_after_reset(node)
|
265
|
+
and self._duration(node, qubit_map) > self._min_duration
|
266
|
+
)
|
267
|
+
for event_type in (EventType.BEGIN, EventType.END)
|
268
|
+
]
|
269
|
+
|
270
|
+
sorted_events = sorted(eligible_delays, key=DelayEvent.sort_key)
|
271
|
+
logger.debug("Sorted delay events: %s", sorted_events)
|
272
|
+
|
273
|
+
return sorted_events
|
274
|
+
|
275
|
+
def _get_wire_coloring(self, dag: DAGCircuit, merged_delay: MultiDelayOp) -> dict[int, int]:
|
276
|
+
"""Find a wire coloring for a multi-delay operation.
|
277
|
+
|
278
|
+
This function returns a dictionary that includes the coloring (as int) for the indices in the
|
279
|
+
``merged_delay`` as ``{index: color}`` pairs. Spectator qubits are handled by assigning
|
280
|
+
neighboring qubits with a CX or ECR a color (0 if control, 1 if target) and including them
|
281
|
+
in the coloring problem.
|
282
|
+
"""
|
283
|
+
# get neighboring wires, for which we will give initial colors
|
284
|
+
neighbors = set()
|
285
|
+
index_map = dict(enumerate(dag.qubits))
|
286
|
+
qubit_map = {bit: index for index, bit in enumerate(dag.qubits)}
|
287
|
+
|
288
|
+
for delay_op in merged_delay.ops:
|
289
|
+
# use coupling_map.graph.neighbors_undirected once Qiskit/rustworkx#1254 is in a release
|
290
|
+
new_neighbors = {
|
291
|
+
i
|
292
|
+
for i in range(dag.num_qubits())
|
293
|
+
if self._coupling_map.distance(i, delay_op.index) == 1
|
294
|
+
}
|
295
|
+
# do not add indices that are already in the merged delay
|
296
|
+
neighbors.update(new_neighbors.difference(merged_delay.indices))
|
297
|
+
|
298
|
+
# build a subgraph we will apply the coloring function on
|
299
|
+
wires = sorted(neighbors.union(merged_delay.indices))
|
300
|
+
subgraph = self._coupling_map.graph.subgraph(list(wires)).to_undirected()
|
301
|
+
glob2loc = dict(zip(wires, subgraph.node_indices()))
|
302
|
+
preset_coloring = {index: None for index in subgraph.node_indices()}
|
303
|
+
|
304
|
+
# find the neighbor wires and check if ctrl/tgt spectator
|
305
|
+
for wire in neighbors:
|
306
|
+
for op_node in dag.nodes_on_wire(dag.qubits[wire], only_ops=True):
|
307
|
+
# check if the operation occurs during the delay
|
308
|
+
op_start = self.property_set["node_start_time"][op_node]
|
309
|
+
op_end = op_start + self._duration(op_node, qubit_map)
|
310
|
+
if (
|
311
|
+
isinstance(op_node.op, (CXGate, ECRGate))
|
312
|
+
and op_start < merged_delay.end
|
313
|
+
and op_end > merged_delay.start
|
314
|
+
):
|
315
|
+
# set coloring to 0 if ctrl, and to 1 if tgt
|
316
|
+
ctrl, tgt = op_node.qargs
|
317
|
+
if index_map[wire] == ctrl:
|
318
|
+
preset_coloring[glob2loc[wire]] = 0
|
319
|
+
if index_map[wire] == tgt:
|
320
|
+
preset_coloring[glob2loc[wire]] = 1
|
321
|
+
|
322
|
+
local_coloring = rx.graph_greedy_color(
|
323
|
+
subgraph,
|
324
|
+
preset_color_fn=lambda loc: preset_coloring[loc],
|
325
|
+
strategy=self._coloring_strategy,
|
326
|
+
)
|
327
|
+
|
328
|
+
# map the local indices of the subgraph back to our DAG indices
|
329
|
+
loc2glob = dict(zip(subgraph.node_indices(), wires))
|
330
|
+
coloring = {loc2glob[loc]: color for loc, color in local_coloring.items()}
|
331
|
+
|
332
|
+
# for debugging purposes, print the coloring of each block
|
333
|
+
logger.debug("Coloring for block %s: %s", merged_delay, coloring)
|
334
|
+
|
335
|
+
return coloring
|
336
|
+
|
337
|
+
def _get_dd_sequence(
|
338
|
+
self,
|
339
|
+
order: int,
|
340
|
+
index: int,
|
341
|
+
duration: int,
|
342
|
+
checked_durations_cache: set[int],
|
343
|
+
) -> tuple[QuantumCircuit, list[int]]:
|
344
|
+
"""Get a DD sequence of specified order on qubit with a given index.
|
345
|
+
|
346
|
+
Takes the gate durations, the pulse alignment and a set to cache, which gate lengths
|
347
|
+
we've already checked to match the pulse alignment. Returns the DD sequence and the
|
348
|
+
node start times as list.
|
349
|
+
"""
|
350
|
+
instruction_durations = self._target.durations()
|
351
|
+
# check the X gate on the active qubit is compatible with pulse alignment
|
352
|
+
if index not in checked_durations_cache:
|
353
|
+
x_duration = instruction_durations.get("x", index)
|
354
|
+
if x_duration % self._pulse_alignment != 0:
|
355
|
+
raise TranspilerError(
|
356
|
+
f"X gate length on qubit with index {index} is {x_duration} which is not "
|
357
|
+
f"an integer multiple of the pulse alignment {self._pulse_alignment}."
|
358
|
+
)
|
359
|
+
checked_durations_cache.add(index)
|
360
|
+
|
361
|
+
spacing, dd_sequence = self.get_orthogonal_sequence(order=order)
|
362
|
+
|
363
|
+
# check if DD can be applied or if there is not enough time
|
364
|
+
dd_sequence_duration = sum(instruction_durations.get(gate, index) for gate in dd_sequence)
|
365
|
+
slack = duration - dd_sequence_duration
|
366
|
+
slack_fraction = slack / duration
|
367
|
+
if 1 - slack_fraction >= self._skip_dd_threshold: # dd doesn't fit
|
368
|
+
seq = QuantumCircuit(1)
|
369
|
+
seq.delay(duration, 0)
|
370
|
+
return seq, [0]
|
371
|
+
|
372
|
+
# compute actual spacings in between the delays, taking into account
|
373
|
+
# the pulse alignment restriction of the hardware
|
374
|
+
taus = self._constrain_spacing(spacing, slack)
|
375
|
+
|
376
|
+
# apply the DD gates
|
377
|
+
# tau has one more entry than the gate sequence
|
378
|
+
start_times = []
|
379
|
+
time = 0 # track the node start time
|
380
|
+
seq = QuantumCircuit(1) # if the DD sequence has a global phase, add it here
|
381
|
+
for tau, gate in itertools.zip_longest(taus, dd_sequence):
|
382
|
+
if tau > 0:
|
383
|
+
seq.delay(tau, 0)
|
384
|
+
start_times.append(time)
|
385
|
+
time += tau
|
386
|
+
if gate is not None:
|
387
|
+
seq.append(gate, [0])
|
388
|
+
start_times.append(time)
|
389
|
+
time += instruction_durations.get(gate, index)
|
390
|
+
|
391
|
+
return seq, start_times
|
392
|
+
|
393
|
+
def _constrain_spacing(self, spacing, slack):
|
394
|
+
def _constrained_length(values):
|
395
|
+
return self._pulse_alignment * np.floor(values / self._pulse_alignment)
|
396
|
+
|
397
|
+
taus = _constrained_length(slack * np.asarray(spacing))
|
398
|
+
unused_slack = slack - sum(taus) # unused, due to rounding to int multiples of dt
|
399
|
+
middle_index = int((len(taus) - 1) / 2) # arbitrary: redistribute to middle
|
400
|
+
to_middle = _constrained_length(unused_slack)
|
401
|
+
taus[middle_index] += to_middle # now we add up to original delay duration
|
402
|
+
if unused_slack - to_middle:
|
403
|
+
taus[-1] += unused_slack - to_middle
|
404
|
+
|
405
|
+
return taus
|
406
|
+
|
407
|
+
def _collect_adjacent_delay_blocks(
|
408
|
+
self,
|
409
|
+
sorted_delay_events: list[DelayEvent],
|
410
|
+
qubit_map: dict[Qubit, int],
|
411
|
+
) -> list[AdjacentDelayBlock]:
|
412
|
+
"""Collect delay events into adjacent blocks.
|
413
|
+
|
414
|
+
Events in an adjacent block are overlapping in time and adjacent on the device.
|
415
|
+
See also the dataclass ``AdjacentDelayBlock`` for more information.
|
416
|
+
|
417
|
+
Args:
|
418
|
+
sorted_delay_events: All eligible delay operations, sorted by time and type.
|
419
|
+
qubit_map: A map from qubit instance to qubit index.
|
420
|
+
|
421
|
+
Returns:
|
422
|
+
A list of adjacent delay blocks.
|
423
|
+
"""
|
424
|
+
open_delay_blocks = []
|
425
|
+
closed_delay_blocks = []
|
426
|
+
|
427
|
+
def _open_delay_block(delay_event):
|
428
|
+
open_delay_blocks.append(
|
429
|
+
AdjacentDelayBlock(
|
430
|
+
events=[delay_event], active_qubits=set(delay_event.op_node.qargs)
|
431
|
+
)
|
432
|
+
)
|
433
|
+
return open_delay_blocks[-1]
|
434
|
+
|
435
|
+
def _update_delay_block(open_delay_block, delay_event):
|
436
|
+
"""Add another delay event to an existing block to either extend or close it."""
|
437
|
+
open_delay_block.events.append(delay_event)
|
438
|
+
|
439
|
+
# at this point we know that delay_event.op_node.qargs is active
|
440
|
+
open_delay_block.active_qubits -= set(delay_event.op_node.qargs)
|
441
|
+
|
442
|
+
if not open_delay_block.active_qubits:
|
443
|
+
open_delay_blocks.remove(open_delay_block)
|
444
|
+
closed_delay_blocks.append(open_delay_block)
|
445
|
+
|
446
|
+
def _combine_delay_blocks(delay_blocks):
|
447
|
+
survivor, *doomed = delay_blocks
|
448
|
+
|
449
|
+
for doomed_delay_group in doomed:
|
450
|
+
# Add events and qubits from doomed block to survivor.
|
451
|
+
if logger.isEnabledFor(logging.DEBUG):
|
452
|
+
if survivor.active_qubits.intersection(doomed_delay_group.active_qubits):
|
453
|
+
logger.debug("More than one open delay on a qubit?")
|
454
|
+
|
455
|
+
survivor.events.extend(doomed_delay_group.events)
|
456
|
+
survivor.active_qubits.update(doomed_delay_group.active_qubits)
|
457
|
+
|
458
|
+
open_delay_blocks.remove(doomed_delay_group)
|
459
|
+
survivor.events.sort(key=DelayEvent.sort_key) # Maintain sorted event order
|
460
|
+
|
461
|
+
for delay_event in sorted_delay_events:
|
462
|
+
# This could be avoided by keeping a map of device qubit to open
|
463
|
+
# block and only considering neighbors of current event.
|
464
|
+
# use coupling_map.graph.neighbors_undirected once Qiskit/rustworkx#1254 is in a release
|
465
|
+
adjacent_open_delay_blocks = [
|
466
|
+
open_delay
|
467
|
+
for open_delay in open_delay_blocks
|
468
|
+
if any(
|
469
|
+
self._coupling_map.distance(
|
470
|
+
qubit_map[delay_event.op_node.qargs[0]], qubit_map[open_delay_qubit]
|
471
|
+
)
|
472
|
+
<= 1
|
473
|
+
for open_delay_qubit in open_delay.active_qubits
|
474
|
+
)
|
475
|
+
]
|
476
|
+
|
477
|
+
if delay_event.type == EventType.BEGIN:
|
478
|
+
# If crossing a begin edge, check if there are any open delays that are adjacent.
|
479
|
+
# If so, add current event to that group.
|
480
|
+
|
481
|
+
if len(adjacent_open_delay_blocks) == 0:
|
482
|
+
# Make a new delay block
|
483
|
+
_open_delay_block(delay_event)
|
484
|
+
else:
|
485
|
+
# Make a new block and combine that with adjacent open blocks
|
486
|
+
new_block = _open_delay_block(delay_event)
|
487
|
+
_combine_delay_blocks(adjacent_open_delay_blocks + [new_block])
|
488
|
+
|
489
|
+
else:
|
490
|
+
if logger.isEnabledFor(logging.DEBUG):
|
491
|
+
# If crossing a end edge, remove this qubit from the actively delaying qubits"
|
492
|
+
if len(adjacent_open_delay_blocks) != 1:
|
493
|
+
logger.debug("Closing edge w/o an open delay?")
|
494
|
+
|
495
|
+
_update_delay_block(adjacent_open_delay_blocks[0], delay_event)
|
496
|
+
|
497
|
+
# log the open delays and the number of closed delays
|
498
|
+
logger.debug("-- post collect")
|
499
|
+
logger.debug("open delays: %s", open_delay_blocks)
|
500
|
+
logger.debug("len(closed delays): %s", len(closed_delay_blocks))
|
501
|
+
|
502
|
+
# validate the results, there should be no open delays and all active qubits
|
503
|
+
# should be accounted for
|
504
|
+
if logger.isEnabledFor(logging.DEBUG):
|
505
|
+
if len(open_delay_blocks) > 0:
|
506
|
+
logger.debug("Failed to close all open delays.")
|
507
|
+
|
508
|
+
for closed_delay in closed_delay_blocks:
|
509
|
+
if len(closed_delay.active_qubits) > 0:
|
510
|
+
logger.debug("Failed to remove active qubits on closed delay %s.", closed_delay)
|
511
|
+
|
512
|
+
closed_delay.validate()
|
513
|
+
|
514
|
+
return closed_delay_blocks
|
515
|
+
|
516
|
+
def _split_adjacent_block(
|
517
|
+
self,
|
518
|
+
adjacent_block: AdjacentDelayBlock,
|
519
|
+
qubit_map: dict[Qubit, int],
|
520
|
+
) -> list[MultiDelayOp]:
|
521
|
+
"""Split adjacent delay blocks in concurrent layers of maximum width.
|
522
|
+
|
523
|
+
This code performs the following steps:
|
524
|
+
|
525
|
+
1) Find the atomic time windows where delays can begin and end.
|
526
|
+
2) Find which connected qubit components are jointly idle during the windows.
|
527
|
+
3) Merge time-adjacent qubit components.
|
528
|
+
|
529
|
+
"""
|
530
|
+
# 1) Find times at which a new delay event is happening and we might have to break up
|
531
|
+
# the adjacent delays
|
532
|
+
breakpoints = []
|
533
|
+
# a dictionary of {qubit_index: [delay_op1, delay_op2, ...]}
|
534
|
+
all_delays = defaultdict(list)
|
535
|
+
|
536
|
+
for event in adjacent_block.events:
|
537
|
+
index = qubit_map[event.op_node.qargs[0]] # delays are 1q ops
|
538
|
+
if event.type == EventType.BEGIN:
|
539
|
+
delay = DelayOp(start=event.time, index=index, op=event.op_node)
|
540
|
+
all_delays[index].append(delay)
|
541
|
+
else: # find which delay to close
|
542
|
+
all_delays[index][-1].end = event.time
|
543
|
+
|
544
|
+
if len(breakpoints) == 0 or event.time > breakpoints[-1]:
|
545
|
+
breakpoints.append(event.time)
|
546
|
+
|
547
|
+
# 2) Find which delays are active during which time windows, where a time window
|
548
|
+
# is defined as (breakpoints[i], breakpoints[i + 1])
|
549
|
+
active_delays = {} # {window: [group1, group2]} where each group = (index1, index2, ..)
|
550
|
+
op_map = {} # {(qubit_index, window): delay operation as DAGOpNode}
|
551
|
+
windows = list(zip(breakpoints[:-1], breakpoints[1:]))
|
552
|
+
for window in windows:
|
553
|
+
active = []
|
554
|
+
|
555
|
+
# check which delays are active during the window
|
556
|
+
# this could be e.g. [0, 1, 2, 5, 6, 9]
|
557
|
+
for index, delays in all_delays.items():
|
558
|
+
for delay in delays:
|
559
|
+
# since the windows are atomic, we have three cases:
|
560
|
+
# (1) the delay starts at the time window, (2) it ends with it, (3) it contains it
|
561
|
+
if delay.start <= window[0] and delay.end >= window[1]:
|
562
|
+
active.append(index)
|
563
|
+
delay.add_window(window)
|
564
|
+
op_map[(index, window)] = delay
|
565
|
+
|
566
|
+
# check which are adjacent
|
567
|
+
# on a linear topology, we would get [[0, 1, 2], [5, 6], [9]]
|
568
|
+
visited = defaultdict(lambda: False)
|
569
|
+
grouped = []
|
570
|
+
for start_index in active:
|
571
|
+
if visited[start_index]:
|
572
|
+
continue
|
573
|
+
|
574
|
+
active_neighbors = {start_index}
|
575
|
+
_dfs(start_index, self._coupling_map, active_neighbors, active)
|
576
|
+
|
577
|
+
for index in active_neighbors:
|
578
|
+
visited[index] = True
|
579
|
+
|
580
|
+
group = tuple(sorted(active_neighbors)) # must be sorted to merge later
|
581
|
+
grouped.append(group)
|
582
|
+
|
583
|
+
# sanity check: groups must be disjoint
|
584
|
+
if logger.isEnabledFor(logging.DEBUG):
|
585
|
+
for i, g1 in enumerate(grouped):
|
586
|
+
for g2 in grouped[i + 1 :]:
|
587
|
+
if len(set(g1).intersection(g2)) > 0:
|
588
|
+
logger.debug("Groups not disjoint: %s and %s.", g1, g2)
|
589
|
+
|
590
|
+
active_delays[window] = grouped
|
591
|
+
|
592
|
+
# 3) Merge time-adjacent active delays
|
593
|
+
merged_delays = []
|
594
|
+
open_groups = {}
|
595
|
+
for window in windows:
|
596
|
+
next_groups = active_delays[window]
|
597
|
+
|
598
|
+
# check which opened groups are still active in this window
|
599
|
+
next_open_groups = {}
|
600
|
+
for open_group, delay in open_groups.items():
|
601
|
+
if open_group in next_groups:
|
602
|
+
# extend the delay operation and remove a breakpoint
|
603
|
+
delay.end = window[1]
|
604
|
+
for op in delay.ops:
|
605
|
+
op.breakpoints.remove(window[0])
|
606
|
+
|
607
|
+
if logger.isEnabledFor(logging.DEBUG):
|
608
|
+
involved_ops = {op_map[(index, window)] for index in open_group}
|
609
|
+
if len(involved_ops.difference(delay.ops)) > 0:
|
610
|
+
logger.debug("Not all involved operations are part of the joint delay.")
|
611
|
+
|
612
|
+
next_open_groups[open_group] = delay
|
613
|
+
else:
|
614
|
+
# the group has not been extended, close it!
|
615
|
+
merged_delays.append(delay)
|
616
|
+
|
617
|
+
# add the new open groups
|
618
|
+
for group in next_groups:
|
619
|
+
if group not in open_groups:
|
620
|
+
involved_ops = {op_map[(index, window)] for index in group}
|
621
|
+
next_open_groups[group] = MultiDelayOp(
|
622
|
+
start=window[0], end=window[1], indices=group, ops=involved_ops
|
623
|
+
)
|
624
|
+
|
625
|
+
open_groups = next_open_groups
|
626
|
+
|
627
|
+
# add all from the final layer
|
628
|
+
for open_group, delay in open_groups.items():
|
629
|
+
merged_delays.append(delay)
|
630
|
+
|
631
|
+
logger.debug("Split adjacent delay block into %s", "\n".join(map(str, merged_delays)))
|
632
|
+
|
633
|
+
return merged_delays
|
634
|
+
|
635
|
+
def _duration(self, node: DAGOpNode, qubit_map: dict[Qubit, int]) -> float:
|
636
|
+
# this is cached on the target, so we can repeatedly call it w/o penalty
|
637
|
+
instruction_durations = self._target.durations()
|
638
|
+
indices = [qubit_map[bit] for bit in node.qargs]
|
639
|
+
|
640
|
+
return instruction_durations.get(node.op, indices)
|
641
|
+
|
642
|
+
|
643
|
+
class EventType(Enum):
|
644
|
+
"""Delay event type, which is either begin or end."""
|
645
|
+
|
646
|
+
BEGIN = 0
|
647
|
+
END = 1
|
648
|
+
|
649
|
+
|
650
|
+
@dataclass
|
651
|
+
class DelayEvent:
|
652
|
+
"""Represent a single-qubit delay event, which is either begin or end of a delay instruction."""
|
653
|
+
|
654
|
+
type: EventType
|
655
|
+
time: int # Time in this circuit, in dt
|
656
|
+
op_node: DAGOpNode # The node for the circuit delay
|
657
|
+
|
658
|
+
@staticmethod
|
659
|
+
def sort_key(event: DelayEvent) -> tuple(int, int):
|
660
|
+
"""Sort events, first by time then by type ('end' events come before 'begin' events)."""
|
661
|
+
return (
|
662
|
+
event.time, # Sort by event time
|
663
|
+
0 if event.type == EventType.END else 1, # With 'end' events before 'begin'
|
664
|
+
)
|
665
|
+
|
666
|
+
|
667
|
+
@dataclass
|
668
|
+
class DelayOp:
|
669
|
+
"""Represent a delay operation."""
|
670
|
+
|
671
|
+
start: int
|
672
|
+
index: int
|
673
|
+
op: DAGOpNode # the circuit op node this represents
|
674
|
+
end: int | None = None # None means currently unknown
|
675
|
+
breakpoints: list[int] = field(
|
676
|
+
default_factory=list
|
677
|
+
) # timepoints at which the delay op is split
|
678
|
+
# circuit with which we will replace this delay
|
679
|
+
start_times: list[int] = field(default_factory=list)
|
680
|
+
replacement: QuantumCircuit = field(default_factory=lambda: QuantumCircuit(1))
|
681
|
+
|
682
|
+
def __hash__(self):
|
683
|
+
return hash(self.op)
|
684
|
+
|
685
|
+
def add_window(self, window: tuple[int, int]):
|
686
|
+
"""Add a time window to the delay op.
|
687
|
+
|
688
|
+
This means the delay is active during this window and we add a potential breakpoint.
|
689
|
+
"""
|
690
|
+
if self.end is None:
|
691
|
+
raise ValueError("Cannot add a window if DelayOp.end is None. Please set it.")
|
692
|
+
|
693
|
+
start, end = window
|
694
|
+
if self.start < start and start not in self.breakpoints:
|
695
|
+
self.breakpoints.append(start)
|
696
|
+
if self.end > end and end not in self.breakpoints:
|
697
|
+
self.breakpoints.append(end)
|
698
|
+
|
699
|
+
self.breakpoints = sorted(self.breakpoints)
|
700
|
+
|
701
|
+
|
702
|
+
@dataclass
|
703
|
+
class MultiDelayOp:
|
704
|
+
"""A multi-qubit delay operation."""
|
705
|
+
|
706
|
+
start: int
|
707
|
+
end: int
|
708
|
+
indices: list[int]
|
709
|
+
ops: set[DelayOp]
|
710
|
+
replacing: set[DAGOpNode] = field(default_factory=set)
|
711
|
+
|
712
|
+
def __str__(self) -> str:
|
713
|
+
return f"MultiDelay({self.start}:{self.end} on {self.indices})"
|
714
|
+
|
715
|
+
|
716
|
+
@dataclass
|
717
|
+
class AdjacentDelayBlock:
|
718
|
+
"""Group of circuit delays which are collectively adjacent in time and on device.
|
719
|
+
|
720
|
+
For example, here the 3 delay operations on q0, q1 and q2 form an adjacent delay block.
|
721
|
+
|
722
|
+
q0: -██████--------- | qubits q0,q1,q2 have adjacent delay
|
723
|
+
q1: ------███████--- | operations, since the delay operations
|
724
|
+
q2: --█████████----- | all overlap
|
725
|
+
q3: -----------████- -> this delay starts when delay on q2 ends, so they have no overlap
|
726
|
+
q4: ----████-------- -> this clearly has no overlap with something else
|
727
|
+
|
728
|
+
"""
|
729
|
+
|
730
|
+
events: list[DelayEvent]
|
731
|
+
active_qubits: set[Qubit]
|
732
|
+
|
733
|
+
def validate(self, log: bool = True) -> None:
|
734
|
+
"""Validate the list of delay events in the adjacent block.
|
735
|
+
|
736
|
+
Args:
|
737
|
+
log: If ``True`` log invalid blocks on DEBUG level. Otherwise raise an error if the
|
738
|
+
block is invalid.
|
739
|
+
|
740
|
+
Raises:
|
741
|
+
RuntimeError: If the blocks are not ordered by time and event type.
|
742
|
+
"""
|
743
|
+
|
744
|
+
def notify(msg, *args):
|
745
|
+
if log:
|
746
|
+
logger.debug(msg, *args)
|
747
|
+
else:
|
748
|
+
raise RuntimeError(msg.format(*args))
|
749
|
+
|
750
|
+
for idx, event in enumerate(self.events[:-1]):
|
751
|
+
if event.time > self.events[idx + 1].time:
|
752
|
+
notify("adjacent_delay_block.events not ordered by time")
|
753
|
+
|
754
|
+
if event.time == self.events[idx + 1].time:
|
755
|
+
# At same time, can either be ('begin', 'begin'), ('end', 'begin') or ('end', 'end')
|
756
|
+
if (event.type, self.events[idx + 1].type) == (EventType.BEGIN, EventType.END):
|
757
|
+
notify(
|
758
|
+
"Events in the AdjacentDelayBlock are not correctly sorted by "
|
759
|
+
"event type. At same time, we can have either of (begin, begin), "
|
760
|
+
"(end, begin) or (end, end). This happened at time %s.",
|
761
|
+
event.time,
|
762
|
+
)
|
763
|
+
|
764
|
+
|
765
|
+
def _dfs(qubit, cmap: CouplingMap, visited, active_qubits):
|
766
|
+
"""Depth-first search to get the widest group of idle qubits during a given time frame."""
|
767
|
+
# use coupling_map.graph.neighbors_undirected once Qiskit/rustworkx#1254 is in a release
|
768
|
+
neighbors = {other for other in active_qubits if cmap.distance(qubit, other) == 1}
|
769
|
+
for neighbor in neighbors:
|
770
|
+
if neighbor in active_qubits and neighbor not in visited:
|
771
|
+
visited.add(neighbor)
|
772
|
+
_dfs(neighbor, cmap, visited, active_qubits)
|
773
|
+
|
774
|
+
|
775
|
+
class WalshHadamardSequence:
|
776
|
+
"""Get Walsh-Hadamard sequences for DD up to arbitrary order."""
|
777
|
+
|
778
|
+
def __init__(self, max_order: int = 5):
|
779
|
+
"""
|
780
|
+
Args:
|
781
|
+
max_order: The maximal order for which the sequences are computed.
|
782
|
+
"""
|
783
|
+
# these are set in set_max_order
|
784
|
+
self.sequences = None
|
785
|
+
self.max_order = None
|
786
|
+
|
787
|
+
self.set_max_order(max_order)
|
788
|
+
|
789
|
+
def set_max_order(self, max_order: int) -> None:
|
790
|
+
"""Set the maximal available order."""
|
791
|
+
if self.max_order is not None:
|
792
|
+
if max_order <= self.max_order:
|
793
|
+
return
|
794
|
+
|
795
|
+
# get the dimension of the transformation matrix we need,
|
796
|
+
# this is given by the smallest power of 2 that includes max_order
|
797
|
+
num_krons = int(np.ceil(np.log2(max_order + 1)))
|
798
|
+
self.max_order = 2**num_krons - 1
|
799
|
+
|
800
|
+
rows = _get_transformation_matrix(num_krons).tolist()
|
801
|
+
distances = [_bitflips_to_timings(row) for row in rows]
|
802
|
+
num_flips = [len(distance) - 1 for distance in distances]
|
803
|
+
|
804
|
+
# sort by the number of flips and throw out the first one,
|
805
|
+
# which corresponds to the 000... bit-sequence, i.e., no flip
|
806
|
+
indices = np.argsort(num_flips)[1:]
|
807
|
+
|
808
|
+
self.sequences = [distances[i] for i in indices]
|
809
|
+
|
810
|
+
def get_sequence(self, order: int) -> list[float]:
|
811
|
+
"""Get the Walsh-Hadamard sequence of given order (starts at 0)."""
|
812
|
+
if order > self.max_order:
|
813
|
+
self.set_max_order(order)
|
814
|
+
|
815
|
+
return self.sequences[order]
|
816
|
+
|
817
|
+
|
818
|
+
def _get_transformation_matrix(n):
|
819
|
+
"""Get a 2^n x 2^n Walsh-Hadamard matrix with elements in [0, 1]."""
|
820
|
+
|
821
|
+
from qiskit.circuit.library import HGate
|
822
|
+
|
823
|
+
had = np.array(HGate()).real * np.sqrt(2)
|
824
|
+
|
825
|
+
def recurse(matrix, m):
|
826
|
+
# we build the matrix recursively, adding one Hadamard kronecker product per recursion
|
827
|
+
if m == 1:
|
828
|
+
# finally, map to [0, 1] by the mapping (1 - H) / 2
|
829
|
+
return ((1 - matrix) / 2).astype(int)
|
830
|
+
|
831
|
+
return recurse(np.kron(had, matrix), m - 1)
|
832
|
+
|
833
|
+
return recurse(had, n)
|
834
|
+
|
835
|
+
|
836
|
+
def _bitflips_to_timings(row):
|
837
|
+
num = len(row)
|
838
|
+
distances = []
|
839
|
+
count = 0
|
840
|
+
last = 0 # start in no flip state
|
841
|
+
for el in row:
|
842
|
+
if el == last:
|
843
|
+
count += 1
|
844
|
+
else:
|
845
|
+
distances.append(count / num)
|
846
|
+
last = el
|
847
|
+
count = 1
|
848
|
+
|
849
|
+
distances.append(count / num)
|
850
|
+
|
851
|
+
if len(distances) % 2 == 0:
|
852
|
+
return distances + [0]
|
853
|
+
|
854
|
+
return distances
|
855
|
+
|
856
|
+
|
857
|
+
def _gate_length_variance(target: Target) -> float:
|
858
|
+
max_length, min_length = None, None
|
859
|
+
|
860
|
+
for gate, properties in target.items():
|
861
|
+
if gate not in ["cx", "cz", "ecr"]:
|
862
|
+
continue
|
863
|
+
|
864
|
+
for prop in properties.values():
|
865
|
+
duration = prop.duration
|
866
|
+
if max_length is None or max_length < duration:
|
867
|
+
max_length = duration
|
868
|
+
if min_length is None or min_length > duration:
|
869
|
+
min_length = duration
|
870
|
+
|
871
|
+
# it could be that there are no 2q gates available, in this
|
872
|
+
# case we just return 0, which will mean we join all idle times
|
873
|
+
if max_length is None or min_length is None:
|
874
|
+
return 0
|
875
|
+
|
876
|
+
return max_length - min_length
|