Skip to content

Commit

Permalink
Allow dynamical decoupling pass to work on circuits with pulse gates (Q…
Browse files Browse the repository at this point in the history
…iskit#10834)

The pass was accessing the circuit calibrations incorrectly, resulting in
an error when one of the gates in the dynamical decoupling sequence had
a pulse gate calibration.
  • Loading branch information
wshanks authored Sep 15, 2023
1 parent 72f27aa commit 4e49a56
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import logging
import numpy as np

from qiskit.circuit import Qubit, Gate
from qiskit.circuit import Gate, ParameterExpression, Qubit
from qiskit.circuit.delay import Delay
from qiskit.circuit.library.standard_gates import IGate, UGate, U3Gate
from qiskit.circuit.reset import Reset
Expand Down Expand Up @@ -227,7 +227,8 @@ def _pre_runhook(self, dag: DAGCircuit):
for gate in self._dd_sequence:
try:
# Check calibration.
gate_length = dag.calibrations[gate.name][(physical_index, gate.params)]
params = self._resolve_params(gate)
gate_length = dag.calibrations[gate.name][((physical_index,), params)].duration
if gate_length % self._alignment != 0:
# This is necessary to implement lightweight scheduling logic for this pass.
# Usually the pulse alignment constraint and pulse data chunk size take
Expand Down Expand Up @@ -394,6 +395,17 @@ def _constrained_length(values):

dag.global_phase = self._mod_2pi(dag.global_phase + sequence_gphase)

@staticmethod
def _resolve_params(gate: Gate) -> tuple:
"""Return gate params with any bound parameters replaced with floats"""
params = []
for p in gate.params:
if isinstance(p, ParameterExpression) and not p.parameters:
params.append(float(p))
else:
params.append(p)
return tuple(params)

@staticmethod
def _mod_2pi(angle: float, atol: float = 0):
"""Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π"""
Expand Down
7 changes: 7 additions & 0 deletions releasenotes/notes/dd-pg-10833-ddddee68ffd913c4.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
fixes:
- |
Fixed a bug in the :class:`.PadDynamicalDecoupling` transpiler pass which
would cause the pass to fail if a circuit contained a pulse gate
calibration for one of the gates in the decoupling sequence. Fixed `#10833
<https://github.com/Qiskit/qiskit/issues/10833>`_.
152 changes: 150 additions & 2 deletions test/python/transpiler/test_dynamical_decoupling.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
from numpy import pi
from ddt import ddt, data

from qiskit.circuit import QuantumCircuit, Delay, Measure, Reset, Parameter
from qiskit import pulse
from qiskit.circuit import Gate, QuantumCircuit, Delay, Measure, Reset, Parameter
from qiskit.circuit.library import XGate, YGate, RXGate, UGate, CXGate, HGate
from qiskit.quantum_info import Operator
from qiskit.transpiler.instruction_durations import InstructionDurations
Expand All @@ -29,7 +30,6 @@
from qiskit.transpiler.passmanager import PassManager
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.target import Target, InstructionProperties
from qiskit import pulse

from qiskit.test import QiskitTestCase

Expand Down Expand Up @@ -339,6 +339,154 @@ def test_insert_dd_ghz_everywhere(self):

self.assertEqual(ghz4_dd, expected)

def test_insert_dd_with_pulse_gate_calibrations(self):
"""Test DD gates are inserted without error when circuit calibrations are used
┌───┐ ┌───────────────┐ ┌───┐ »
q_0: ──────┤ H ├─────────■──┤ Delay(75[dt]) ├──────┤ X ├───────»
┌─────┴───┴─────┐ ┌─┴─┐└───────────────┘┌─────┴───┴──────┐»
q_1: ┤ Delay(50[dt]) ├─┤ X ├────────■────────┤ Delay(300[dt]) ├»
├───────────────┴┐└───┘ ┌─┴─┐ └────────────────┘»
q_2: ┤ Delay(750[dt]) ├───────────┤ X ├──────────────■─────────»
├────────────────┤ └───┘ ┌─┴─┐ »
q_3: ┤ Delay(950[dt]) ├────────────────────────────┤ X ├───────»
└────────────────┘ └───┘ »
meas: 4/══════════════════════════════════════════════════════════»
»
« ┌────────────────┐┌───┐┌───────────────┐ ░ ┌─┐
« q_0: ┤ Delay(150[dt]) ├┤ X ├┤ Delay(75[dt]) ├─░─┤M├─────────
« └────────────────┘└───┘└───────────────┘ ░ └╥┘┌─┐
« q_1: ─────────────────────────────────────────░──╫─┤M├──────
« ░ ║ └╥┘┌─┐
« q_2: ─────────────────────────────────────────░──╫──╫─┤M├───
« ░ ║ ║ └╥┘┌─┐
« q_3: ─────────────────────────────────────────░──╫──╫──╫─┤M├
« ░ ║ ║ ║ └╥┘
«meas: 4/════════════════════════════════════════════╩══╩══╩══╩═
« 0 1 2 3
"""
dd_sequence = [XGate(), XGate()]
pm = PassManager(
[
ALAPScheduleAnalysis(self.durations),
PadDynamicalDecoupling(self.durations, dd_sequence, qubits=[0]),
]
)

# Change duration to 100 from the 50 in self.durations to make sure
# gate duration is used correctly.
with pulse.builder.build() as x_sched:
pulse.builder.delay(100, pulse.DriveChannel(0))

circ_in = self.ghz4.measure_all(inplace=False)
circ_in.add_calibration(XGate(), (0,), x_sched)

ghz4_dd = pm.run(circ_in)

expected = self.ghz4.copy()
expected = expected.compose(Delay(50), [1], front=True)
expected = expected.compose(Delay(750), [2], front=True)
expected = expected.compose(Delay(950), [3], front=True)

# Delays different from those of the default case using self.durations
expected = expected.compose(Delay(75), [0])
expected = expected.compose(XGate(), [0])
expected = expected.compose(Delay(150), [0])
expected = expected.compose(XGate(), [0])
expected = expected.compose(Delay(75), [0])

expected = expected.compose(Delay(300), [1])

expected.measure_all()
expected.add_calibration(XGate(), (0,), x_sched)

self.assertEqual(ghz4_dd, expected)

def test_insert_dd_with_pulse_gate_calibrations_with_parmas(self):
"""Test DD gates are inserted without error when parameterized circuit calibrations are used
┌───┐ ┌───────────────┐ ┌───┐ »
q_0: ──────┤ H ├─────────■──┤ Delay(75[dt]) ├──────┤ X ├───────»
┌─────┴───┴─────┐ ┌─┴─┐└───────────────┘┌─────┴───┴──────┐»
q_1: ┤ Delay(50[dt]) ├─┤ X ├────────■────────┤ Delay(300[dt]) ├»
├───────────────┴┐└───┘ ┌─┴─┐ └────────────────┘»
q_2: ┤ Delay(750[dt]) ├───────────┤ X ├──────────────■─────────»
├────────────────┤ └───┘ ┌─┴─┐ »
q_3: ┤ Delay(950[dt]) ├────────────────────────────┤ X ├───────»
└────────────────┘ └───┘ »
meas: 4/══════════════════════════════════════════════════════════»
»
« ┌────────────────┐┌───┐┌───────────────┐ ░ ┌─┐
« q_0: ┤ Delay(150[dt]) ├┤ X ├┤ Delay(75[dt]) ├─░─┤M├─────────
« └────────────────┘└───┘└───────────────┘ ░ └╥┘┌─┐
« q_1: ─────────────────────────────────────────░──╫─┤M├──────
« ░ ║ └╥┘┌─┐
« q_2: ─────────────────────────────────────────░──╫──╫─┤M├───
« ░ ║ ║ └╥┘┌─┐
« q_3: ─────────────────────────────────────────░──╫──╫──╫─┤M├
« ░ ║ ║ ║ └╥┘
«meas: 4/════════════════════════════════════════════╩══╩══╩══╩═
« 0 1 2 3
"""
# Change duration to 100 from the 50 in self.durations to make sure
# gate duration is used correctly.
amp = Parameter("amp")
with pulse.builder.build() as sched:
pulse.builder.play(
pulse.Gaussian(100, amp=amp, sigma=10.0),
pulse.DriveChannel(0),
)

class Echo(Gate):
"""Dummy Gate subclass for testing
In this test, we use a non-standard gate so we can add parameters
to it, in order to test the handling of parameters by
PadDynamicalDecoupling. PadDynamicalDecoupling checks that the DD
sequence is equivalent to the identity, so we can not use Gate
directly. Here we subclass Gate and add the identity as its matrix
representation to satisfy PadDynamicalDecoupling's check.
"""

def __array__(self, dtype=None):
return np.eye(2, dtype=dtype)

# A gate with one unbound and one bound parameter to leave in the final
# circuit.
echo = Echo("echo", 1, [amp, 10.0])

circ_in = self.ghz4.measure_all(inplace=False)
circ_in.add_calibration(echo, (0,), sched)

dd_sequence = [echo, echo]
pm = PassManager(
[
ALAPScheduleAnalysis(self.durations),
PadDynamicalDecoupling(self.durations, dd_sequence, qubits=[0]),
]
)

ghz4_dd = pm.run(circ_in)

expected = self.ghz4.copy()
expected = expected.compose(Delay(50), [1], front=True)
expected = expected.compose(Delay(750), [2], front=True)
expected = expected.compose(Delay(950), [3], front=True)

# Delays different from those of the default case using self.durations
expected = expected.compose(Delay(75), [0])
expected = expected.compose(echo, [0])
expected = expected.compose(Delay(150), [0])
expected = expected.compose(echo, [0])
expected = expected.compose(Delay(75), [0])

expected = expected.compose(Delay(300), [1])

expected.measure_all()
expected.add_calibration(echo, (0,), sched)

self.assertEqual(ghz4_dd, expected)

def test_insert_dd_ghz_xy4(self):
"""Test XY4 sequence of DD gates.
Expand Down

0 comments on commit 4e49a56

Please sign in to comment.