Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NormalizeRXAngle and RXCalibrationBuilder passes #10634

Merged
merged 19 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions qiskit/transpiler/passes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
EchoRZXWeylDecomposition
ResetAfterMeasureSimplification
OptimizeCliffords
NormalizeRXAngle

Calibration
=============
Expand All @@ -99,6 +100,7 @@
PulseGates
RZXCalibrationBuilder
RZXCalibrationBuilderNoEcho
RXCalibrationBuilder

Scheduling
=============
Expand Down Expand Up @@ -234,6 +236,7 @@
from .optimization import CollectCliffords
from .optimization import ResetAfterMeasureSimplification
from .optimization import OptimizeCliffords
from .optimization import NormalizeRXAngle

# circuit analysis
from .analysis import ResourceEstimation
Expand All @@ -258,6 +261,7 @@
from .calibration import PulseGates
from .calibration import RZXCalibrationBuilder
from .calibration import RZXCalibrationBuilderNoEcho
from .calibration import RXCalibrationBuilder

# circuit scheduling
from .scheduling import TimeUnitConversion
Expand Down
1 change: 1 addition & 0 deletions qiskit/transpiler/passes/calibration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@

from .pulse_gate import PulseGates
from .rzx_builder import RZXCalibrationBuilder, RZXCalibrationBuilderNoEcho
from .rx_builder import RXCalibrationBuilder
1 change: 1 addition & 0 deletions qiskit/transpiler/passes/calibration/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
# pylint: disable=unused-import
from .pulse_gate import PulseGates
from .rzx_builder import RZXCalibrationBuilder, RZXCalibrationBuilderNoEcho
from .rx_builder import RXCalibrationBuilder
116 changes: 116 additions & 0 deletions qiskit/transpiler/passes/calibration/rx_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2023.
#
# 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.

"""Add single-pulse RX calibrations that are bootstrapped from the SX calibration."""

from typing import Union
from functools import lru_cache
import numpy as np

from qiskit.circuit import Instruction
from qiskit.pulse import Schedule, ScheduleBlock, builder, DriveChannel
from qiskit.pulse.channels import Channel
from qiskit.pulse.library.symbolic_pulses import Drag
from qiskit.transpiler.passes.calibration.base_builder import CalibrationBuilder
from qiskit.transpiler import Target
from qiskit.circuit.library.standard_gates import RXGate
from qiskit.exceptions import QiskitError


class RXCalibrationBuilder(CalibrationBuilder):
"""Add single-pulse RX calibrations that are bootstrapped from the SX calibration.
jaeunkim marked this conversation as resolved.
Show resolved Hide resolved

.. note::
Requirement: NormalizeRXAngles pass (one of the optimization passes).

References
* [1]: Gokhale et al. (2020), Optimized Quantum Compilation for
Near-Term Algorithms with OpenPulse.
`arXiv:2004.11205 <https://arxiv.org/abs/2004.11205>`
"""

def __init__(
self,
target: Target = None,
):
"""Bootstrap single-pulse RX gate calibrations from the
(hardware-calibrated) SX gate calibration.

Args:
target (Target): Should contain a SX calibration that will be
used for bootstrapping RX calibrations.
"""
from qiskit.transpiler.passes.optimization import NormalizeRXAngle

super().__init__()
self.target = target
self.already_generated = {}
self.requires = [NormalizeRXAngle(self.target)]

if self.target.instruction_schedule_map() is None:
jaeunkim marked this conversation as resolved.
Show resolved Hide resolved
raise QiskitError("Calibrations can only be added to Pulse-enabled backends")

def supported(self, node_op: Instruction, qubits: list) -> bool:
"""
Check if the calibration for SX gate exists.
"""
return isinstance(node_op, RXGate) and self.target.has_calibration("sx", tuple(qubits))

def get_calibration(self, node_op: Instruction, qubits: list) -> Union[Schedule, ScheduleBlock]:
"""
Generate RX calibration for the rotation angle specified in node_op.
"""
# already within [0, pi] by NormalizeRXAngles pass
angle = node_op.params[0]

try:
angle = float(angle)
except TypeError as ex:
raise QiskitError("Target rotation angle is not assigned.") from ex

params = (
self.target.get_calibration("sx", tuple(qubits))
.instructions[0][1]
.pulse.parameters.copy()
)
new_rx_sched = _create_rx_sched(
rx_angle=angle,
channel_identifier=DriveChannel(qubits[0]),
jaeunkim marked this conversation as resolved.
Show resolved Hide resolved
duration=params["duration"],
amp=params["amp"],
sigma=params["sigma"],
beta=params["beta"],
)

return new_rx_sched


@lru_cache
def _create_rx_sched(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably fine, but it makes me a little nervous. I would do more checking to confirm that the default calibration is a single Drag pulse.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added two tests in the supported() method to check if the default calibration is a single pulse and it's a DRAG pulse.

 def supported(self, node_op: Instruction, qubits: list) -> bool:
        """
        Check if the calibration for SX gate exists and it's a single DRAG pulse.
        """
        return (
            isinstance(node_op, RXGate)
            and self.target.has_calibration("sx", tuple(qubits))
            and (len(self.target.get_calibration("sx", tuple(qubits)).instructions) == 1)
            and isinstance(
                self.target.get_calibration("sx", tuple(qubits)).instructions[0][1].pulse, Drag
            )
        )

However, I can't decide whether we should allow non-DRAG pulses too or not.

  • Pros for allowing non-DRAG pulses: Flexibility. This pass simply scales the amplitude of the default calibration, based on preliminary experiments that indicated no need to alter the DRAG coefficient. Since all other types of pulses (Gaussian, square, etc) have the amplitude parameter, it's technically possible to scale the amplitude of any type of pulses.

  • Cons for allowing non-DRAG pulses: The goal of this pass is to maximize the gate fidelity by decreasing gate time. The gate fidelity might not be optimal with non-DRAG pulses. Therefore, the user should probably try using DRAG pulses first, instead of using this pass.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be okay to check for ScalableSymbolicPulse and scale the amp parameter in that. Since this is not part of a preset passmanager and is a somewhat advanced concept, I think it is okay to assume that the user has some understanding of the pulse to be scaled. I was more concerned about a using doing something unusual like using a schedule with two pulses or with a variant of Drag and not realizing the schedule was being replaced with a single Drag.

Also, isinstance(pulse, Drag) is going to be deprecated. The recommendation is to use isinstance(pulse, SymoblicPulse) and pulse.pulse_type == "Drag" because Drag and the other symbolic pulses are being converted into factory functions that produce SymbolicPulses instead of classes themselves. If you switch to checking for ScalableSymbolicPulse and scaling the amp this won't matter.

Copy link
Contributor Author

@jaeunkim jaeunkim Sep 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @wshanks, I'm trying to cover the case where the original sx calibration is a ScheduleBlock with multiple types of instructions and multiple types of pulses, as you pointed out.
Could you help me building a new ScheduleBlock where everything else is the same but only the amplitude is halved?
I'm having trouble copying the alignment constraints and non-Play type of instructions.

This is an example of an unusually complicated sx schedule:

d0 = pulse.DriveChannel(0)
with pulse.build() as complicated_sx_cal:
    pulse.play(Drag(duration=100, amp=0.1, sigma=40, beta=0.2, angle=10), d0)
    pulse.delay(50, d0)
    pulse.play(
        GaussianSquare(duration=130, amp=0.3, sigma=30, risefall_sigma_ratio=2, angle=30), d0
    )

스크린샷 2023-09-20 오전 11 39 59

This is my attempt:

# goal: copy a ScheduleBlock, but halve the amplitude.

with pulse.build() as rx_cal:
    for i in range(len(complicated_sx_cal.instructions)):
        if isinstance(complicated_sx_cal.instructions[i][1], Play) and isinstance(
            complicated_sx_cal.instructions[i][1].pulse, ScalableSymbolicPulse
        ):
            # caveat: assumed that all the params are immutable (checked that Parameter is immutable)
            params = complicated_sx_cal.instructions[i][1].pulse.parameters.copy()
            halved_amp = params.pop("amp") * 0.5
            duration = params.pop("duration")
            angle = params.pop("angle", 0)
            pulse.play(
                ScalableSymbolicPulse(
                    complicated_sx_cal.instructions[i][1].pulse.pulse_type,
                    duration=duration,
                    amp=halved_amp,
                    angle=angle,
                    parameters=params,
                ),
                channel=complicated_sx_cal.channels[0]
            )  
        else:
            complicated_sx_cal.instructions[i][1]  #??

Questions:

  • How do you copy a non-Play (ex. delay) instruction?
  • How do you copy the alignment constraints in a ScheduleBlock? Or, would it be OK to assume that there won't be any alignment constraints for a sx gate calibration?

Copy link
Contributor Author

@jaeunkim jaeunkim Sep 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I print out the above rx_cal, the pulses seem to have the correct types (ex. Drag and GaussianSquare).
ScheduleBlock(Play(Drag(duration=100, sigma=40, beta=0.2, amp=0.05, angle=10), DriveChannel(0)), Play(GaussianSquare(duration=130, sigma=30, width=10.0, amp=0.15, angle=30), DriveChannel(0)), name="block25", transform=AlignLeft())

However, I get Pulse envelope expression is not assigned errors when I try to draw the schedule.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, we can see what @nkanazawa1989 thinks. One option is not to try to handle such a complicated case, but to choose either to give an error or ignore it. A complicated schedule seems not too likely to come up, and either giving an error or ignoring the gate would avoid the case of assigning the wrong schedule (i.e. assigning a Drag with amplitude based on the first pulse amplitude if the schedule really has multiple instructions).

I think it's pretty tricky to walk through a ScheduleBlock and modify the pulses. I think I would not use the builder interface to do it. There is a ScheduleBlock.replace method that you could use like:

new_sched = copy.deepcopy(sched)
for block in sched.blocks:
    if isinstance(block, Play) and isinstance(block.pulse, ScalableSymbolicPulse):
        new = Play(ScalableSymbolicPulse(pulse_type=block.pulse.pulse_type, ...<a lot of options to copy>), block.channel)
        new_sched.replace(block, new)

but that only covers the top level blocks. You would also need to check isinstance(block, ScheduleBlock) and recurse into the nested ScheduleBlocks and replace pulses in them and call replace(block, new_scheduleblock).

Copy link
Contributor Author

@jaeunkim jaeunkim Sep 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, I agree that it's reasonable to reject a schedule with multiple pulses.
The current implementation of supported() checks that the sx cal is a single pulse AND it's a Drag.
https://github.com/Qiskit/qiskit/pull/10634/files#diff-5c2525ef082142486a945b9138611abe282cc048803c89059207112f8db192b8R103-R106

To deal with any type of ScalableSymbolicPulse, I need to figure out why I get Pulse envelope expression is not assigned errors after instantiating a ScalableSymbolicPulse(pulse_type=some-string,...)
Thank you for the example on replace().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copying a ScalableSymbolicPulse is a bit awkward. It should work if you copy all the input parameters:

p = pulse.Drag(100, 0.5, 10, 0.1)
p2 = pulse.ScalableSymbolicPulse(
    pulse_type=p.pulse_type,
    duration=p.duration,
    amp=p.amp,
    angle=p.angle,
    parameters={k: v for k, v in p.parameters.items() if k not in ("amp", "angle", "duration")},
    limit_amplitude=p.limit_amplitude,
    envelope=p.envelope,
    constraints=p.constraints,
    valid_amp_conditions=p.valid_amp_conditions,
)

It would be nice to be able to copy the pulse and mutate the amp, but mutation is not part of the design. A SymbolicPulse.copy_with_new_parameters() method might be nice to have.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I agree that the current version is fine if you don't want to support ScalableSymbolicPulse but I would change the isinstance(<pulse>, Drag) to <pulse>.pulse_type == "Drag" to avoid the deprecation of isinstance support for the library pulses.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the discussion 😊 I updated the Drag check according to your suggestion. (In other words, the current version supports only the calibrations with a single Drag pulse)
As a next step, I will support calibrations with a single ScalableSymbolicPulse.
The final step would be to support calibrations with multiple instructions (nested ScheduleBlock, non-Play instructions, etc). However, such calibrations seem unlikely to occur. Moreover, I may need to introduce some arbitrary rules to reject edge cases. Therefore, I would like to require in supported() that sx calibration should consist of a single ScalableSymbolicPulse.

rx_angle: float,
duration: int,
amp: float,
sigma: float,
beta: float,
channel_identifier: Channel,
):
"""Generates (and caches) pulse calibrations for RX gates.
Assumes that the rotation angle is in [0, pi].
"""
new_amp = rx_angle / (np.pi / 2) * amp
with builder.build() as new_rx_sched:
builder.play(
Drag(duration=duration, amp=new_amp, sigma=sigma, beta=beta, angle=0),
channel=channel_identifier,
)

return new_rx_sched
1 change: 1 addition & 0 deletions qiskit/transpiler/passes/optimization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@
from .reset_after_measure_simplification import ResetAfterMeasureSimplification
from .optimize_cliffords import OptimizeCliffords
from .collect_cliffords import CollectCliffords
from .normalize_rx_angle import NormalizeRXAngle
149 changes: 149 additions & 0 deletions qiskit/transpiler/passes/optimization/normalize_rx_angle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2023.
#
# 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.

"""Performs three optimizations to reduce the number of pulse calibrations for
the single-pulse RX gates:
Wrap RX Gate rotation angles into [0, pi] by sandwiching them with RZ gates.
Convert RX(pi/2) to SX, and RX(pi) to X if the calibrations exist in the target.
Quantize the RX rotation angles using a resolution provided by the user.
wshanks marked this conversation as resolved.
Show resolved Hide resolved
"""

import numpy as np

from qiskit.transpiler.basepasses import TransformationPass
from qiskit.dagcircuit import DAGCircuit
from qiskit.circuit.library.standard_gates import RXGate, RZGate, SXGate, XGate


class NormalizeRXAngle(TransformationPass):
"""Wrap RX Gate rotation angles into [0, pi] by sandwiching them with RZ gates.
This will help reduce the size of calibration data,
as we won't have to keep separate, phase-flipped calibrations for negative rotation angles.
Moreover, if the calibrations exist in the target, convert RX(pi/2) to SX, and RX(pi) to X.
This will allow us to exploit the more accurate, hardware-calibrated pulses.
Lastly, quantize the RX rotation angles using a resolution provided by the user.
"""
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved

def __init__(self, target=None, resolution_in_radian=0):
"""NormalizeRXAngle initializer.

Args:
target (Target): The :class:`~.Target` representing the target backend.
If the target contains SX and X calibrations, this pass will replace the
corresponding RX gates with SX and X gates.
resolution_in_radian (float): Resolution for RX rotation angle quantization.
If set to zero, this pass won't modify the rotation angles in the given DAG.
(=Provides aribitary-angle RX)
"""
super().__init__()
self.target = target
self.resolution_in_radian = resolution_in_radian
self.already_generated = {}

def quantize_angles(self, qubit, original_angle):
"""Quantize the RX rotation angles using a resolution provided by the user.

Args:
qubit (Qubit): This will be the dict key to access the list of quantized rotation angles.
original_angle (float): Original rotation angle, before quantization.

Returns:
float: Quantized angle.
"""

# check if there is already a calibration for a simliar angle
try:
angles = self.already_generated[qubit] # 1d ndarray of already generated angles
quantized_angle = float(
angles[np.where(np.abs(angles - original_angle) < (self.resolution_in_radian / 2))]
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved
)
except KeyError:
quantized_angle = original_angle
self.already_generated[qubit] = np.array([quantized_angle])
except TypeError:
quantized_angle = original_angle
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess current logic causes an edge case. For example,

pass_ = NormalizeRXAngle(target, resolution_in_radian=0.1)
pass_.quantize_angles(0, 1.23)
pass_.quantize_angles(0, 1.24)
pass_.quantize_angles(0, 1.235)  # What happens here?

Copy link
Contributor Author

@jaeunkim jaeunkim Aug 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, more than one value will return True from np.isclose. In such a case, I will take only the first value.

similar_angle = angles[np.isclose(angles, original_angle, atol=self.resolution_in_radian/2)]
quantized_angle = float(similar_angle[0]) if len(similar_angle) > 1 else float(similar_angle)

(Note: the np.isclose test for 1.235 will return at least one True, because rtol is set to a small, nonzero default value. I will retain this setting because setting rtol to zero can lead to the np.isclose returning no True at all, due to uncertainties related to floating-point arithmetic.)

self.already_generated[qubit] = np.append(
self.already_generated[qubit], quantized_angle
)

return quantized_angle

def run(self, dag):
"""Run the NormalizeRXAngle pass on ``dag``. This pass consists of three parts:
normalize_rx_angles(), convert_to_hardware_sx_x(), quantize_rx_angles().
jaeunkim marked this conversation as resolved.
Show resolved Hide resolved

Args:
dag (DAGCircuit): The DAG to be optimized.

Returns:
DAGCircuit: A DAG where all RX rotation angles are within [0, pi].
jaeunkim marked this conversation as resolved.
Show resolved Hide resolved
"""

# Iterate over all op_nodes and replace RX if eligible for modification.
for op_node in dag.op_nodes():
if not (op_node.op.name == "rx"):
jaeunkim marked this conversation as resolved.
Show resolved Hide resolved
continue

raw_theta = op_node.op.params[0]
wrapped_theta = np.arctan2(np.sin(raw_theta), np.cos(raw_theta)) # [-pi, pi]

if self.resolution_in_radian:
wrapped_theta = self.quantize_angles(op_node.qargs[0], wrapped_theta)

half_pi_rotation = np.isclose(abs(wrapped_theta), np.pi / 2)
pi_rotation = np.isclose(abs(wrapped_theta), np.pi)
jaeunkim marked this conversation as resolved.
Show resolved Hide resolved

# get the physical qubit index to look up the SX or X calibrations
qubit = dag.find_bit(op_node.qargs[0]).index if half_pi_rotation | pi_rotation else None
try:
qubit = int(qubit)
find_bit_succeeded = True
except TypeError:
find_bit_succeeded = False
jaeunkim marked this conversation as resolved.
Show resolved Hide resolved

should_modify_node = (
(wrapped_theta != raw_theta)
or (wrapped_theta < 0)
or half_pi_rotation
or pi_rotation
)

if should_modify_node:
mini_dag = DAGCircuit()
mini_dag.add_qubits(op_node.qargs)

# new X-rotation gate with angle in [0, pi]
if (
half_pi_rotation
and find_bit_succeeded
and self.target.has_calibration("sx", (qubit,))
jaeunkim marked this conversation as resolved.
Show resolved Hide resolved
):
mini_dag.apply_operation_back(SXGate(), qargs=op_node.qargs)
elif (
pi_rotation
and find_bit_succeeded
and self.target.has_calibration("x", (qubit,))
):
mini_dag.apply_operation_back(XGate(), qargs=op_node.qargs)
else:
mini_dag.apply_operation_back(
RXGate(np.abs(wrapped_theta)), qargs=op_node.qargs
)

# sandwich with RZ if the intended rotation angle was negative
if wrapped_theta < 0:
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved
mini_dag.apply_operation_front(RZGate(np.pi), qargs=op_node.qargs)
mini_dag.apply_operation_back(RZGate(-np.pi), qargs=op_node.qargs)

dag.substitute_node_with_dag(node=op_node, input_dag=mini_dag, wires=op_node.qargs)

return dag
30 changes: 30 additions & 0 deletions releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
features:
- |
Two new transpiler passes are added to generate single-pulse RX gate calibrations on the fly.
These single-pulse RX calibrations will reduce the gate time in half, as described in
P.Gokhale et al, Optimized Quantum Compilation for Near-Term Algorithms with OpenPulse
(2020), `arXiv:2004.11205 <https://arxiv.org/abs/2004.11205>`.

To reduce the amount of RX calibration data that needs to be generated,
:class:`~qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle`
performs three optimizations: wrapping RX gate rotation angles to [0, pi],
replacing RX(pi/2) and RX(pi) with SX and X gates, and quantizing the rotation angles.
This pass is required to be run before
:class:`~qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder`,
which generates RX calibrations on the fly.

The details of the transpiler passes are as follows:
:class:`~qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle` wraps
RX gate rotation angles to [0, pi] by replacing an RX gate with negative rotation angle, RX(-theta),
with a sequence: RZ(pi)-RX(theta)-RZ(-pi). Moreover, the pass replaces RX(pi/2) with SX gate,
and RX(pi) with X gate. This will enable us to exploit the more accurate, hardware-calibrated
pulses. Lastly, the pass quantizes the rotation angles using a user-provided resolution.
If the resolution is set to 0, this pass will not perform any quantization.
:class:`~qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder`
generates RX calibrations on the fly. The pulse calibrations are bootstrapped from
the SX gate calibration in the target.
The amplitude is linearly scaled to achieve the desired arbitrary rotation angle.
jaeunkim marked this conversation as resolved.
Show resolved Hide resolved
Such single-pulse calibrations reduces the gate time in half, compared to the
conventional sequence that consists of two SX pulses.
There could be an improvement in fidelity due to this reduction in gate time.
4 changes: 2 additions & 2 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ ddt>=1.2.0,!=1.4.0,!=1.4.3
# components of Terra use some of its optional dependencies in order to document
# themselves. These are the requirements that are _only_ required for the docs
# build, and are not used by Terra itself.
Sphinx>=6.0
qiskit-sphinx-theme~=1.14.0
Sphinx>=6.0,<7.2
qiskit-sphinx-theme~=1.15.0
jaeunkim marked this conversation as resolved.
Show resolved Hide resolved
sphinx-design>=0.2.0
nbsphinx~=0.9.2
nbconvert~=7.7.1
Expand Down
Loading