From 5a086cae855ac27fd1df51a0dee7d2d69467f47c Mon Sep 17 00:00:00 2001 From: nick bronn Date: Mon, 17 Jul 2023 16:48:27 -0400 Subject: [PATCH 1/4] added strategic urdd passes --- qiskit_research/utils/dynamical_decoupling.py | 263 +++++++++++++++++- 1 file changed, 259 insertions(+), 4 deletions(-) diff --git a/qiskit_research/utils/dynamical_decoupling.py b/qiskit_research/utils/dynamical_decoupling.py index df39898d..a9120516 100644 --- a/qiskit_research/utils/dynamical_decoupling.py +++ b/qiskit_research/utils/dynamical_decoupling.py @@ -17,16 +17,21 @@ from typing import Iterable, List, Optional, Sequence, Union from qiskit import QuantumCircuit, pulse -from qiskit.circuit import Gate -from qiskit.circuit.library import XGate, YGate +from qiskit.circuit import Gate, Qubit +from qiskit.circuit.delay import Delay +from qiskit.circuit.library import XGate, YGate, UGate, U3Gate +from qiskit.circuit.reset import Reset from qiskit.converters import circuit_to_dag +from qiskit.dagcircuit import DAGCircuit, DAGInNode, DAGNode, DAGOpNode from qiskit.providers.backend import Backend from qiskit.pulse import Drag, Waveform from qiskit.qasm import pi -from qiskit.transpiler import InstructionDurations +from qiskit.quantum_info import OneQubitEulerDecomposer +from qiskit.transpiler import InstructionDurations, Target from qiskit.transpiler.basepasses import BasePass +from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.instruction_durations import InstructionDurationsType -from qiskit.transpiler.passes import PadDynamicalDecoupling +from qiskit.transpiler.passes import PadDynamicalDecoupling, Optimize1qGates from qiskit.transpiler.passes.scheduling import ALAPScheduleAnalysis from qiskit.transpiler.passes.scheduling.scheduling.base_scheduler import BaseScheduler @@ -122,6 +127,32 @@ def periodic_dynamical_decoupling( pulse_alignment=pulse_alignment, ) +def urdd_strategy_passes( + backend: Backend, + pulse_nums: List[int] = [4], + min_delay_times: List[int] = [0], + scheduler: BaseScheduler = ALAPScheduleAnalysis, +) -> Iterable[BasePass]: + """ + Yield transpilation passes for URDD timing strategy. + + Args: + backend (Backend): Backend to run on; gate timing is required for this method. + pulse_nums (List[int]): Numbers of pulses to use for each minimum delay time, in decreasing order. + min_delay_times: (List[int]): Min delay times for each sequence of pulses, in decreasing order. + scheduler (BaseScheduler, optional): Scheduler, defaults to ALAPScheduleAnalysis. + + Yields: + Iterator[Iterable[BasePass]]: Transpiler passes used for adding DD sequences. + """ + durations = get_instruction_durations(backend) + pulse_alignment = backend.configuration().timing_constraints["pulse_alignment"] + + yield scheduler(durations) + for pparams in zip(pulse_nums, min_delay_times): + yield URDDSequenceStrategy( + durations, pulse_alignment=pulse_alignment, num_pulses=pparams[0], min_delay_time=pparams[1] + ) # TODO this should take instruction schedule map instead of backend def get_instruction_durations(backend: Backend) -> InstructionDurations: @@ -314,3 +345,227 @@ def get_urdd_angles(num_pulses: int = 4) -> Sequence[float]: phis.append(unique_phi[idx]) return phis + + +class URDDSequenceStrategy(PadDynamicalDecoupling): + """URDD strategic timing dynamical decoupling insertion pass. + + This pass acts the same as PadDynamicalDecoupling, but only inserts + a number of URDD pulses specified by num_pulses only when there + is a delay greater than min_delay_time. By combining these passes + in decreasing order of delay time, a multiple-pulse-number DD + strategy is achieved. + """ + def __init__( + self, + durations: InstructionDurations = None, + # dd_sequence: List[Gate] = None, + num_pulses: int = 4, + min_delay_time: int = 0, + qubits: Optional[List[int]] = None, + spacing: Optional[List[float]] = None, + skip_reset_qubits: bool = True, + pulse_alignment: int = 1, + extra_slack_distribution: str = "middle", + ): + """URDD strategic timing initializer. + + Args: + durations: Durations of instructions to be used in scheduling. + num_pulses (int, optional): _description_. Defaults to 4. + min_delay_time (int, optional): _description_. Defaults to 0. + qubits: Physical qubits on which to apply DD. + If None, all qubits will undergo DD (when possible). + spacing: A list of spacings between the DD gates. + The available slack will be divided according to this. + The list length must be one more than the length of dd_sequence, + and the elements must sum to 1. If None, a balanced spacing + will be used [d/2, d, d, ..., d, d, d/2]. + skip_reset_qubits: If True, does not insert DD on idle periods that + immediately follow initialized/reset qubits + (as qubits in the ground state are less susceptile to decoherence). + pulse_alignment: The hardware constraints for gate timing allocation. + This is usually provided from ``backend.configuration().timing_constraints``. + If provided, the delay length, i.e. ``spacing``, is implicitly adjusted to + satisfy this constraint. + extra_slack_distribution: The option to control the behavior of DD sequence generation. + The duration of the DD sequence should be identical to an idle time in the + scheduled quantum circuit, however, the delay in between gates comprising the sequence + should be integer number in units of dt, and it might be further truncated + when ``pulse_alignment`` is specified. This sometimes results in the duration of + the created sequence being shorter than the idle time + that you want to fill with the sequence, i.e. `extra slack`. + This option takes following values. + + - "middle": Put the extra slack to the interval at the middle of the sequence. + - "edges": Divide the extra slack as evenly as possible into + intervals at beginning and end of the sequence. + + + Raises: + TranspilerError: When invalid DD sequence is specified. + TranspilerError: When pulse gate with the duration which is + non-multiple of the alignment constraint value is found. + """ + phis = get_urdd_angles(num_pulses) + dd_sequence = tuple(PiPhiGate(phi) for phi in phis) + + super().__init__( + durations=durations, + dd_sequence=dd_sequence, + qubits=qubits, + spacing=spacing, + skip_reset_qubits=skip_reset_qubits, + pulse_alignment=pulse_alignment, + extra_slack_distribution=extra_slack_distribution, + target=None, # TODO: check whether this is needed + ) + + self._min_delay_time = min_delay_time + + def __gate_supported(self, gate: Gate, qarg: int) -> bool: + """A gate is supported on the qubit (qarg) or not.""" + if self.target is None or self.target.instruction_supported(gate.name, qargs=(qarg,)): + return True + return False + + def __is_dd_qubit(self, qubit_index: int) -> bool: + """DD can be inserted in the qubit or not.""" + if (qubit_index in self._no_dd_qubits) or ( + self._qubits and qubit_index not in self._qubits + ): + return False + return True + + def _pad( + self, + dag: DAGCircuit, + qubit: Qubit, + t_start: int, + t_end: int, + next_node: DAGNode, + prev_node: DAGNode, + ): + # This routine takes care of the pulse alignment constraint for the DD sequence. + # Note that the alignment constraint acts on the t0 of the DAGOpNode. + # Now this constrained scheduling problem is simplified to the problem of + # finding a delay amount which is a multiple of the constraint value by assuming + # that the duration of every DAGOpNode is also a multiple of the constraint value. + # + # For example, given the constraint value of 16 and XY4 with 160 dt gates. + # Here we assume current interval is 992 dt. + # + # relative spacing := [0.125, 0.25, 0.25, 0.25, 0.125] + # slack = 992 dt - 4 x 160 dt = 352 dt + # + # unconstraind sequence: 44dt-X1-88dt-Y2-88dt-X3-88dt-Y4-44dt + # constraind sequence : 32dt-X1-80dt-Y2-80dt-X3-80dt-Y4-32dt + extra slack 48 dt + # + # Now we evenly split extra slack into start and end of the sequence. + # The distributed slack should be multiple of 16. + # Start = +16, End += 32 + # + # final sequence : 48dt-X1-80dt-Y2-80dt-X3-80dt-Y4-64dt / in total 992 dt + # + # Now we verify t0 of every node starts from multiple of 16 dt. + # + # X1: 48 dt (3 x 16 dt) + # Y2: 48 dt + 160 dt + 80 dt = 288 dt (18 x 16 dt) + # Y3: 288 dt + 160 dt + 80 dt = 528 dt (33 x 16 dt) + # Y4: 368 dt + 160 dt + 80 dt = 768 dt (48 x 16 dt) + # + # As you can see, constraints on t0 are all satisfied without explicit scheduling. + time_interval = t_end - t_start + if time_interval > self._min_delay_time: + if time_interval % self._alignment != 0: + raise TranspilerError( + f"Time interval {time_interval} is not divisible by alignment {self._alignment} " + f"between DAGNode {prev_node.name} on qargs {prev_node.qargs} and {next_node.name} " + f"on qargs {next_node.qargs}." + ) + + if not self.__is_dd_qubit(dag.qubits.index(qubit)): + # Target physical qubit is not the target of this DD sequence. + self._apply_scheduled_op(dag, t_start, Delay(time_interval, dag.unit), qubit) + return + + if self._skip_reset_qubits and ( + isinstance(prev_node, DAGInNode) or isinstance(prev_node.op, Reset) + ): + # Previous node is the start edge or reset, i.e. qubit is ground state. + self._apply_scheduled_op(dag, t_start, Delay(time_interval, dag.unit), qubit) + return + + slack = time_interval - np.sum(self._dd_sequence_lengths[qubit]) + sequence_gphase = self._sequence_phase + + if slack <= 0: + # Interval too short. + self._apply_scheduled_op(dag, t_start, Delay(time_interval, dag.unit), qubit) + return + + if len(self._dd_sequence) == 1: + # Special case of using a single gate for DD + u_inv = self._dd_sequence[0].inverse().to_matrix() + theta, phi, lam, phase = OneQubitEulerDecomposer().angles_and_phase(u_inv) + if isinstance(next_node, DAGOpNode) and isinstance(next_node.op, (UGate, U3Gate)): + # Absorb the inverse into the successor (from left in circuit) + theta_r, phi_r, lam_r = next_node.op.params + next_node.op.params = Optimize1qGates.compose_u3( + theta_r, phi_r, lam_r, theta, phi, lam + ) + sequence_gphase += phase + elif isinstance(prev_node, DAGOpNode) and isinstance(prev_node.op, (UGate, U3Gate)): + # Absorb the inverse into the predecessor (from right in circuit) + theta_l, phi_l, lam_l = prev_node.op.params + prev_node.op.params = Optimize1qGates.compose_u3( + theta, phi, lam, theta_l, phi_l, lam_l + ) + sequence_gphase += phase + else: + # Don't do anything if there's no single-qubit gate to absorb the inverse + self._apply_scheduled_op(dag, t_start, Delay(time_interval, dag.unit), qubit) + return + + def _constrained_length(values): + return self._alignment * np.floor(values / self._alignment) + + # (1) Compute DD intervals satisfying the constraint + taus = _constrained_length(slack * np.asarray(self._spacing)) + extra_slack = slack - np.sum(taus) + + # (2) Distribute extra slack + if self._extra_slack_distribution == "middle": + mid_ind = int((len(taus) - 1) / 2) + to_middle = _constrained_length(extra_slack) + taus[mid_ind] += to_middle + if extra_slack - to_middle: + # If to_middle is not a multiple value of the pulse alignment, + # it is truncated to the nearlest multiple value and + # the rest of slack is added to the end. + taus[-1] += extra_slack - to_middle + elif self._extra_slack_distribution == "edges": + to_begin_edge = _constrained_length(extra_slack / 2) + taus[0] += to_begin_edge + taus[-1] += extra_slack - to_begin_edge + else: + raise TranspilerError( + f"Option extra_slack_distribution = {self._extra_slack_distribution} is invalid." + ) + + # (3) Construct DD sequence with delays + num_elements = max(len(self._dd_sequence), len(taus)) + idle_after = t_start + for dd_ind in range(num_elements): + if dd_ind < len(taus): + tau = taus[dd_ind] + if tau > 0: + self._apply_scheduled_op(dag, idle_after, Delay(tau, dag.unit), qubit) + idle_after += tau + if dd_ind < len(self._dd_sequence): + gate = self._dd_sequence[dd_ind] + gate_length = self._dd_sequence_lengths[qubit][dd_ind] + self._apply_scheduled_op(dag, idle_after, gate, qubit) + idle_after += gate_length + + dag.global_phase = self._mod_2pi(dag.global_phase + sequence_gphase) From 5972e55c899dc4ab6d3d32726287d351b567a597 Mon Sep 17 00:00:00 2001 From: nick bronn Date: Mon, 17 Jul 2023 17:12:09 -0400 Subject: [PATCH 2/4] fixed tox/linting problems --- qiskit_research/utils/dynamical_decoupling.py | 115 ++++++++---------- 1 file changed, 52 insertions(+), 63 deletions(-) diff --git a/qiskit_research/utils/dynamical_decoupling.py b/qiskit_research/utils/dynamical_decoupling.py index a9120516..a4334c4c 100644 --- a/qiskit_research/utils/dynamical_decoupling.py +++ b/qiskit_research/utils/dynamical_decoupling.py @@ -27,7 +27,7 @@ from qiskit.pulse import Drag, Waveform from qiskit.qasm import pi from qiskit.quantum_info import OneQubitEulerDecomposer -from qiskit.transpiler import InstructionDurations, Target +from qiskit.transpiler import InstructionDurations from qiskit.transpiler.basepasses import BasePass from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.instruction_durations import InstructionDurationsType @@ -127,10 +127,11 @@ def periodic_dynamical_decoupling( pulse_alignment=pulse_alignment, ) + def urdd_strategy_passes( backend: Backend, - pulse_nums: List[int] = [4], - min_delay_times: List[int] = [0], + pulse_nums: List[int] = None, + min_delay_times: List[int] = None, scheduler: BaseScheduler = ALAPScheduleAnalysis, ) -> Iterable[BasePass]: """ @@ -138,8 +139,10 @@ def urdd_strategy_passes( Args: backend (Backend): Backend to run on; gate timing is required for this method. - pulse_nums (List[int]): Numbers of pulses to use for each minimum delay time, in decreasing order. - min_delay_times: (List[int]): Min delay times for each sequence of pulses, in decreasing order. + pulse_nums (List[int]): Numbers of pulses to use for each minimum delay time, + in decreasing order. + min_delay_times: (List[int]): Min delay times for each sequence of pulses, + in decreasing order. scheduler (BaseScheduler, optional): Scheduler, defaults to ALAPScheduleAnalysis. Yields: @@ -151,9 +154,13 @@ def urdd_strategy_passes( yield scheduler(durations) for pparams in zip(pulse_nums, min_delay_times): yield URDDSequenceStrategy( - durations, pulse_alignment=pulse_alignment, num_pulses=pparams[0], min_delay_time=pparams[1] + durations, + pulse_alignment=pulse_alignment, + num_pulses=pparams[0], + min_delay_time=pparams[1], ) + # TODO this should take instruction schedule map instead of backend def get_instruction_durations(backend: Backend) -> InstructionDurations: """ @@ -356,10 +363,10 @@ class URDDSequenceStrategy(PadDynamicalDecoupling): in decreasing order of delay time, a multiple-pulse-number DD strategy is achieved. """ + def __init__( self, durations: InstructionDurations = None, - # dd_sequence: List[Gate] = None, num_pulses: int = 4, min_delay_time: int = 0, qubits: Optional[List[int]] = None, @@ -390,10 +397,10 @@ def __init__( satisfy this constraint. extra_slack_distribution: The option to control the behavior of DD sequence generation. The duration of the DD sequence should be identical to an idle time in the - scheduled quantum circuit, however, the delay in between gates comprising the sequence - should be integer number in units of dt, and it might be further truncated - when ``pulse_alignment`` is specified. This sometimes results in the duration of - the created sequence being shorter than the idle time + scheduled quantum circuit, however, the delay in between gates comprising the + sequence should be integer number in units of dt, and it might be further + truncated when ``pulse_alignment`` is specified. This sometimes results in + the duration of the created sequence being shorter than the idle time that you want to fill with the sequence, i.e. `extra slack`. This option takes following values. @@ -401,7 +408,6 @@ def __init__( - "edges": Divide the extra slack as evenly as possible into intervals at beginning and end of the sequence. - Raises: TranspilerError: When invalid DD sequence is specified. TranspilerError: When pulse gate with the duration which is @@ -418,22 +424,13 @@ def __init__( skip_reset_qubits=skip_reset_qubits, pulse_alignment=pulse_alignment, extra_slack_distribution=extra_slack_distribution, - target=None, # TODO: check whether this is needed ) self._min_delay_time = min_delay_time - def __gate_supported(self, gate: Gate, qarg: int) -> bool: - """A gate is supported on the qubit (qarg) or not.""" - if self.target is None or self.target.instruction_supported(gate.name, qargs=(qarg,)): - return True - return False - def __is_dd_qubit(self, qubit_index: int) -> bool: """DD can be inserted in the qubit or not.""" - if (qubit_index in self._no_dd_qubits) or ( - self._qubits and qubit_index not in self._qubits - ): + if self._qubits and qubit_index not in self._qubits: return False return True @@ -446,54 +443,33 @@ def _pad( next_node: DAGNode, prev_node: DAGNode, ): - # This routine takes care of the pulse alignment constraint for the DD sequence. - # Note that the alignment constraint acts on the t0 of the DAGOpNode. - # Now this constrained scheduling problem is simplified to the problem of - # finding a delay amount which is a multiple of the constraint value by assuming - # that the duration of every DAGOpNode is also a multiple of the constraint value. - # - # For example, given the constraint value of 16 and XY4 with 160 dt gates. - # Here we assume current interval is 992 dt. - # - # relative spacing := [0.125, 0.25, 0.25, 0.25, 0.125] - # slack = 992 dt - 4 x 160 dt = 352 dt - # - # unconstraind sequence: 44dt-X1-88dt-Y2-88dt-X3-88dt-Y4-44dt - # constraind sequence : 32dt-X1-80dt-Y2-80dt-X3-80dt-Y4-32dt + extra slack 48 dt - # - # Now we evenly split extra slack into start and end of the sequence. - # The distributed slack should be multiple of 16. - # Start = +16, End += 32 - # - # final sequence : 48dt-X1-80dt-Y2-80dt-X3-80dt-Y4-64dt / in total 992 dt - # - # Now we verify t0 of every node starts from multiple of 16 dt. - # - # X1: 48 dt (3 x 16 dt) - # Y2: 48 dt + 160 dt + 80 dt = 288 dt (18 x 16 dt) - # Y3: 288 dt + 160 dt + 80 dt = 528 dt (33 x 16 dt) - # Y4: 368 dt + 160 dt + 80 dt = 768 dt (48 x 16 dt) - # - # As you can see, constraints on t0 are all satisfied without explicit scheduling. + # This routine takes care of the pulse alignment constraint for the URDD sequence. + # The only difference is that it will only execute for time_intervals larger than + # those specified by the internal property self._min_delay_time which is defined + # at initialization. time_interval = t_end - t_start if time_interval > self._min_delay_time: if time_interval % self._alignment != 0: raise TranspilerError( - f"Time interval {time_interval} is not divisible by alignment {self._alignment} " - f"between DAGNode {prev_node.name} on qargs {prev_node.qargs} and {next_node.name} " - f"on qargs {next_node.qargs}." + f"Time interval {time_interval} is not divisible by alignment " + f"{self._alignment} between DAGNode {prev_node.name} on qargs " + f"{prev_node.qargs} and {next_node.name} on qargs {next_node.qargs}." ) if not self.__is_dd_qubit(dag.qubits.index(qubit)): # Target physical qubit is not the target of this DD sequence. - self._apply_scheduled_op(dag, t_start, Delay(time_interval, dag.unit), qubit) + self._apply_scheduled_op( + dag, t_start, Delay(time_interval, dag.unit), qubit + ) return if self._skip_reset_qubits and ( isinstance(prev_node, DAGInNode) or isinstance(prev_node.op, Reset) ): # Previous node is the start edge or reset, i.e. qubit is ground state. - self._apply_scheduled_op(dag, t_start, Delay(time_interval, dag.unit), qubit) + self._apply_scheduled_op( + dag, t_start, Delay(time_interval, dag.unit), qubit + ) return slack = time_interval - np.sum(self._dd_sequence_lengths[qubit]) @@ -501,21 +477,29 @@ def _pad( if slack <= 0: # Interval too short. - self._apply_scheduled_op(dag, t_start, Delay(time_interval, dag.unit), qubit) + self._apply_scheduled_op( + dag, t_start, Delay(time_interval, dag.unit), qubit + ) return if len(self._dd_sequence) == 1: # Special case of using a single gate for DD u_inv = self._dd_sequence[0].inverse().to_matrix() - theta, phi, lam, phase = OneQubitEulerDecomposer().angles_and_phase(u_inv) - if isinstance(next_node, DAGOpNode) and isinstance(next_node.op, (UGate, U3Gate)): + theta, phi, lam, phase = OneQubitEulerDecomposer().angles_and_phase( + u_inv + ) + if isinstance(next_node, DAGOpNode) and isinstance( + next_node.op, (UGate, U3Gate) + ): # Absorb the inverse into the successor (from left in circuit) theta_r, phi_r, lam_r = next_node.op.params next_node.op.params = Optimize1qGates.compose_u3( theta_r, phi_r, lam_r, theta, phi, lam ) sequence_gphase += phase - elif isinstance(prev_node, DAGOpNode) and isinstance(prev_node.op, (UGate, U3Gate)): + elif isinstance(prev_node, DAGOpNode) and isinstance( + prev_node.op, (UGate, U3Gate) + ): # Absorb the inverse into the predecessor (from right in circuit) theta_l, phi_l, lam_l = prev_node.op.params prev_node.op.params = Optimize1qGates.compose_u3( @@ -524,7 +508,9 @@ def _pad( sequence_gphase += phase else: # Don't do anything if there's no single-qubit gate to absorb the inverse - self._apply_scheduled_op(dag, t_start, Delay(time_interval, dag.unit), qubit) + self._apply_scheduled_op( + dag, t_start, Delay(time_interval, dag.unit), qubit + ) return def _constrained_length(values): @@ -550,7 +536,8 @@ def _constrained_length(values): taus[-1] += extra_slack - to_begin_edge else: raise TranspilerError( - f"Option extra_slack_distribution = {self._extra_slack_distribution} is invalid." + f"Option extra_slack_distribution = {self._extra_slack_distribution} " + f"is invalid." ) # (3) Construct DD sequence with delays @@ -560,7 +547,9 @@ def _constrained_length(values): if dd_ind < len(taus): tau = taus[dd_ind] if tau > 0: - self._apply_scheduled_op(dag, idle_after, Delay(tau, dag.unit), qubit) + self._apply_scheduled_op( + dag, idle_after, Delay(tau, dag.unit), qubit + ) idle_after += tau if dd_ind < len(self._dd_sequence): gate = self._dd_sequence[dd_ind] From 16703374462c62ce9818deff1b66258520a90ffa Mon Sep 17 00:00:00 2001 From: nick bronn Date: Fri, 28 Jul 2023 11:29:29 -0400 Subject: [PATCH 3/4] modified class URDDSequenceStrategy to take sequences of pulses --- qiskit_research/utils/dynamical_decoupling.py | 116 +++++++++++------- 1 file changed, 71 insertions(+), 45 deletions(-) diff --git a/qiskit_research/utils/dynamical_decoupling.py b/qiskit_research/utils/dynamical_decoupling.py index a4334c4c..f9ae284d 100644 --- a/qiskit_research/utils/dynamical_decoupling.py +++ b/qiskit_research/utils/dynamical_decoupling.py @@ -128,39 +128,6 @@ def periodic_dynamical_decoupling( ) -def urdd_strategy_passes( - backend: Backend, - pulse_nums: List[int] = None, - min_delay_times: List[int] = None, - scheduler: BaseScheduler = ALAPScheduleAnalysis, -) -> Iterable[BasePass]: - """ - Yield transpilation passes for URDD timing strategy. - - Args: - backend (Backend): Backend to run on; gate timing is required for this method. - pulse_nums (List[int]): Numbers of pulses to use for each minimum delay time, - in decreasing order. - min_delay_times: (List[int]): Min delay times for each sequence of pulses, - in decreasing order. - scheduler (BaseScheduler, optional): Scheduler, defaults to ALAPScheduleAnalysis. - - Yields: - Iterator[Iterable[BasePass]]: Transpiler passes used for adding DD sequences. - """ - durations = get_instruction_durations(backend) - pulse_alignment = backend.configuration().timing_constraints["pulse_alignment"] - - yield scheduler(durations) - for pparams in zip(pulse_nums, min_delay_times): - yield URDDSequenceStrategy( - durations, - pulse_alignment=pulse_alignment, - num_pulses=pparams[0], - min_delay_time=pparams[1], - ) - - # TODO this should take instruction schedule map instead of backend def get_instruction_durations(backend: Backend) -> InstructionDurations: """ @@ -367,8 +334,8 @@ class URDDSequenceStrategy(PadDynamicalDecoupling): def __init__( self, durations: InstructionDurations = None, - num_pulses: int = 4, - min_delay_time: int = 0, + num_pulses: List[int] = None, + min_delay_times: List[int] = None, qubits: Optional[List[int]] = None, spacing: Optional[List[float]] = None, skip_reset_qubits: bool = True, @@ -413,7 +380,12 @@ def __init__( TranspilerError: When pulse gate with the duration which is non-multiple of the alignment constraint value is found. """ - phis = get_urdd_angles(num_pulses) + if num_pulses is None: + num_pulses = [4] + if min_delay_times is None: + min_delay_times = [0] + + phis = get_urdd_angles(min(num_pulses)) dd_sequence = tuple(PiPhiGate(phi) for phi in phis) super().__init__( @@ -426,7 +398,8 @@ def __init__( extra_slack_distribution=extra_slack_distribution, ) - self._min_delay_time = min_delay_time + self._num_pulses = num_pulses + self._min_delay_times = np.array(min_delay_times) def __is_dd_qubit(self, qubit_index: int) -> bool: """DD can be inserted in the qubit or not.""" @@ -434,6 +407,48 @@ def __is_dd_qubit(self, qubit_index: int) -> bool: return False return True + def _compute_spacing(self, num_pulses): + mid = 1 / num_pulses + end = mid / 2 + self._spacing = [end] + [mid] * (num_pulses - 1) + [end] + + def _compute_dd_sequence_lengths(self, dd_sequence, dag: DAGCircuit) -> dict: + # Precompute qubit-wise DD sequence length for performance + dd_sequence_lengths = {} + for physical_index, qubit in enumerate(dag.qubits): + if not self.__is_dd_qubit(physical_index): + continue + + sequence_lengths = [] + for gate in dd_sequence: + try: + # Check calibration. + gate_length = dag.calibrations[gate.name][ + (physical_index, gate.params) + ] + 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 + # the same value, however, we can intentionally violate this pattern + # at the gate level. For example, we can create a schedule consisting of + # a pi-pulse of 32 dt followed by a post buffer, i.e. delay, of 4 dt + # on the device with 16 dt constraint. Note that the pi-pulse length + # is multiple of 16 dt but the gate length of 36 is not multiple of it. + # Such pulse gate should be excluded. + raise TranspilerError( + f"Pulse gate {gate.name} with length non-multiple of {self._alignment} " + f"is not acceptable in {self.__class__.__name__} pass." + ) + except KeyError: + gate_length = self._durations.get(gate, physical_index) + sequence_lengths.append(gate_length) + # Update gate duration. This is necessary for current timeline drawer, + # i.e. scheduled. + gate.duration = gate_length + dd_sequence_lengths[qubit] = sequence_lengths + + return dd_sequence_lengths + def _pad( self, dag: DAGCircuit, @@ -448,7 +463,18 @@ def _pad( # those specified by the internal property self._min_delay_time which is defined # at initialization. time_interval = t_end - t_start - if time_interval > self._min_delay_time: + dd_indices = np.where(self._min_delay_times < time_interval)[0] + if len(dd_indices) > 0: + dd_idx = np.where( + self._min_delay_times[dd_indices] + == max(self._min_delay_times[dd_indices]) + )[0][0] + urdd_num = self._num_pulses[dd_idx] + phis = get_urdd_angles(urdd_num) + dd_sequence = tuple(PiPhiGate(phi) for phi in phis) + dd_sequence_lengths = self._compute_dd_sequence_lengths(dd_sequence, dag) + self._compute_spacing(urdd_num) + if time_interval % self._alignment != 0: raise TranspilerError( f"Time interval {time_interval} is not divisible by alignment " @@ -472,7 +498,7 @@ def _pad( ) return - slack = time_interval - np.sum(self._dd_sequence_lengths[qubit]) + slack = time_interval - np.sum(dd_sequence_lengths[qubit]) sequence_gphase = self._sequence_phase if slack <= 0: @@ -482,9 +508,9 @@ def _pad( ) return - if len(self._dd_sequence) == 1: + if len(dd_sequence) == 1: # Special case of using a single gate for DD - u_inv = self._dd_sequence[0].inverse().to_matrix() + u_inv = dd_sequence[0].inverse().to_matrix() theta, phi, lam, phase = OneQubitEulerDecomposer().angles_and_phase( u_inv ) @@ -541,7 +567,7 @@ def _constrained_length(values): ) # (3) Construct DD sequence with delays - num_elements = max(len(self._dd_sequence), len(taus)) + num_elements = max(len(dd_sequence), len(taus)) idle_after = t_start for dd_ind in range(num_elements): if dd_ind < len(taus): @@ -551,9 +577,9 @@ def _constrained_length(values): dag, idle_after, Delay(tau, dag.unit), qubit ) idle_after += tau - if dd_ind < len(self._dd_sequence): - gate = self._dd_sequence[dd_ind] - gate_length = self._dd_sequence_lengths[qubit][dd_ind] + if dd_ind < len(dd_sequence): + gate = dd_sequence[dd_ind] + gate_length = dd_sequence_lengths[qubit][dd_ind] self._apply_scheduled_op(dag, idle_after, gate, qubit) idle_after += gate_length From ba69018b63bb10a6507011880ea8328640026886 Mon Sep 17 00:00:00 2001 From: nick bronn Date: Fri, 28 Jul 2023 16:50:06 -0400 Subject: [PATCH 4/4] fixed some minor docs issues --- qiskit_research/utils/dynamical_decoupling.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/qiskit_research/utils/dynamical_decoupling.py b/qiskit_research/utils/dynamical_decoupling.py index f9ae284d..c71d10fc 100644 --- a/qiskit_research/utils/dynamical_decoupling.py +++ b/qiskit_research/utils/dynamical_decoupling.py @@ -325,10 +325,10 @@ class URDDSequenceStrategy(PadDynamicalDecoupling): """URDD strategic timing dynamical decoupling insertion pass. This pass acts the same as PadDynamicalDecoupling, but only inserts - a number of URDD pulses specified by num_pulses only when there - is a delay greater than min_delay_time. By combining these passes - in decreasing order of delay time, a multiple-pulse-number DD - strategy is achieved. + a number of URDD pulses specified by a sequence of num_pulses only when there + is a delay greater than a sequence of min_delay_time. Each delay will be + considered and the large number of sequence will be given by that delay + specificied max(min_delay_time) < delay. """ def __init__( @@ -346,8 +346,9 @@ def __init__( Args: durations: Durations of instructions to be used in scheduling. - num_pulses (int, optional): _description_. Defaults to 4. - min_delay_time (int, optional): _description_. Defaults to 0. + num_pulses Sequence[int]: Number of pulses to use corresponding + to min_delay_time. Defaults to 4. + min_delay_time Sequence[int]: Minimum delay for a given sequence length. In units of dt. qubits: Physical qubits on which to apply DD. If None, all qubits will undergo DD (when possible). spacing: A list of spacings between the DD gates.