Performing model-based robust optimization for the cross-resonance gate

Increasing robustness against crosstalk in a two-qubit entangling operation

BOULDER OPAL provides tools to simplify the process of control optimization even for complex multiqubit systems. Superconducting qubits in particular have been shown to benefit from the application of robust control solutions for the suppression of drift and crosstalk-induced errors endemic to real hardware. With an appropriate Hamiltonian model of the system it's possible to create noise-robust control solutions which ultimately deliver improved gate fidelity as a means to increase quantum volume.

In this application note we will demonstrate the utility of robust control to improve the performance of cross-resonance entangling gates degraded by crosstalk errors. We will cover:

  • Modelling crosstalk as a noise channel
  • Performing model-based control optimization on a pair of coupled multilevel systems
  • Validating performance by calculating susceptibility to quasistatic crosstalk errors

Ultimately this notebook demonstrates how to improve the quantum volume of superconducting quantum computers by delivering gate-level performance enhancements.

Imports and initialization

import numpy as np
from qctrl import Qctrl
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from qctrlvisualizer import plot_controls
from qctrlvisualizer import get_qctrl_style

qctrl = Qctrl()

plt.style.use(get_qctrl_style())

colors = {"Optimized": "#BF04DC", "Robust": "#680CE9", "Primitive": "k"}

Creating a Q-CTRL cross resonance gate robust to crosstalk

Consider a system of two transmon qubits coupled by a common transmission line. Qubit 1 is defined as the control qubit, and qubit 2 is the target qubit. The control qubit is driven by the in-phase and quadrature microwave drive with a frequency that matches the target qubit frequency. In a frame rotating with the drive frequency and after applying the rotating wave approximation (RWA) the Hamiltonian is of the form:

$$ \frac{1}{\hbar} H = (\Delta + \epsilon) b_1^\dagger b_1 + \frac{\delta_1}{2} (b_1^\dagger)^2 b_1^2 + \epsilon b_2^\dagger b_2+ \frac{\delta_2}{2} (b_2^\dagger)^2 b_2^2 + g \left( b_1^\dagger b_2 + b_1 b_2^\dagger \right) +\frac{1}{2}\left(\Omega(t)b_1 + \text{h.c.}\right)\,,$$

where $b_i$ is the annihilation operator of the $i$th qubit, $\Delta=\omega_1-\omega_2$ is the detuning between the two qubits where $\omega_i$ is the frequency of the $i$th qubit, $\epsilon$ is the mismatch between the drive frequency and the target qubit frequency, $g$ is the effective coupling strength between the qubits, $\Omega$ is the drive modulus and $\delta_i$ is the anharmonicity of the $i$th qubit. The above Hamiltonian is valid when the coupling strength is small compared to the detuning between the two qubits, $\Delta\gg g$.

The target operation is the $\pi/2$ cross resonance gate which operates in the space of the two lowest levels:

$$U_\mathrm{CR}\left(\frac{\pi}{2}\right)=\frac{1}{\sqrt{2}}\left(\mathbb{I}-i\sigma_1^z\sigma_2^x\right)=[ZX]^{1/2}.$$

Since the above operation is defined up to phase factors of the individual qubits, this may lead to a large infidelity. Consequently, a final (optimizable) phase shift on each qubit is introduced to overcome the infidelity inflation due to these phase factors.

This Application note addresses robustness to crosstalk. Crosstalk between the qubits leads to unwanted effects that may mimic the effect of noise. The crosstalk Hamiltonian has the form

$$ \frac{1}{\hbar} H_\mathrm{ct} = c_\mathrm{ct} \frac{\Omega(t)}{2}b_2 + \text{h.c.}\,, $$

where $c_\mathrm{ct}$ is the (complex) relative crosstalk.

In this section, we optimize the implementation of the target gate under the effect of crosstalk. This is performed within the framework of the four-level approximation, where only the first four levels of each qubit are considered. Moreover, only up to three excitations overall are permitted, which further reduces the Hilbert space from $16$ states to $10$ states.

To set up the optimization, first define operators and constants.

# Constants in the lab frame
omega_1 = 5.114 * 2 * np.pi * 1e9  # Hz
omega_2 = 4.914 * 2 * np.pi * 1e9  # Hz
delta_1 = -330 * 2 * np.pi * 1e6  # Hz
delta_2 = -330 * 2 * np.pi * 1e6  # Hz
g = 3.8 * 2 * np.pi * 1e6  # Hz
Delta = omega_1 - omega_2
epsilon = 0

# Limits for drive amplitudes
epsilon_1_min = -2 * np.pi * 200 * 1e6  # Hz
epsilon_1_max = 2 * np.pi * 200 * 1e6  # Hz

Next, define the operators in the Hamiltonian and the target gate.

# Lowering operator
b = np.diag([1, np.sqrt(2), np.sqrt(3)], 1)
# Number operator
n = np.diag([0, 1, 2, 3])
# Projector onto the two lowest states
qubit_proj = np.diag([1, 1, 0, 0])

# Pauli operators
sigma_x = np.zeros(n.shape)
sigma_x[:2, :2] = np.array([[0, 1], [1, 0]])
sigma_z = np.zeros(n.shape)
sigma_z[:2, :2] = np.array([[1, 0], [0, -1]])

# Embedding the single-qubit number operators into the two-qubit system of dimension 16
n1 = np.kron(n, np.eye(4))
n2 = np.kron(np.eye(4), n)

proj = np.kron(qubit_proj, qubit_proj)

# Block-diagonal permutations. We find the indices of the 10 two-qutrit
# states with the fewest total excitations. This truncates the
# Hilbert space to a total of three excitations or less.
perm = np.argsort(np.diag(n1 + n2))
perm_trunc = perm[:10]
block_n = [np.where(np.diag(n1 + n2) == n)[0].tolist() for n in range(4)]
truncate = np.ix_(perm_trunc, perm_trunc)

# Define truncated operators
b_1 = np.kron(b, np.eye(4))[truncate]
bd_1 = b_1.T.conj()
b_2 = np.kron(np.eye(4), b)[truncate]
bd_2 = b_2.T.conj()
bdb_1 = n1[truncate]
bdb_2 = n2[truncate]
bdb_12 = np.kron(b.T.conj(), b)[truncate]
bdb_21 = np.kron(b, b.T.conj())[truncate]
bd2b2_1 = (n1 @ n1 - n1)[truncate]
bd2b2_2 = (n2 @ n2 - n2)[truncate]

sigma_z_1 = np.kron(sigma_z, np.eye(4))[truncate]
sigma_z_2 = np.kron(np.eye(4), sigma_z)[truncate]

# Frame transformation HS = SD
hamiltonian_with_no_drives = (
    (Delta + epsilon) * bdb_1
    + epsilon * bdb_2
    + delta_1 * bd2b2_1 / 2
    + delta_2 * bd2b2_2 / 2
    + g * (bdb_12 + bdb_21)
)
S = np.eye(10)
base_index = 0
for i in block_n:
    inds = np.arange(base_index, base_index + len(i))
    eigenvalues, eigenvectors = np.linalg.eigh(
        hamiltonian_with_no_drives[:, inds][inds, :]
    )
    for k, j in enumerate(eigenvectors.T):
        ind = np.abs(np.abs(j) - 1.0).argmin()
        S[:, base_index + ind][inds] = j * np.sign(j[ind])
    base_index += len(i)

# The phase shift operators in the simulation/optimization frame
phase_shift_operator1 = S @ (sigma_z_1 / 2) @ S.T.conj()
phase_shift_operator2 = S @ (sigma_z_2 / 2) @ S.T.conj()

# Target in the simulation/optimization frame
zx_half = S.dot(((proj - 1j * np.kron(sigma_z, sigma_x)) / np.sqrt(2))[truncate]).dot(
    S.T.conj()
)

Creating control pulses

Next, define the optimization configuration and the control schemes: Primitive, Optimized and Robust.

# Optimization constraints
duration = 0.4 * 1e-6  # s
crosstalk_constant = 0.1
segment_count_primitive = 1
segment_count_optimized = 200

pulse_durations_primitive = (
    np.ones([segment_count_primitive]) * duration / segment_count_primitive
)
pulse_durations_optimized = (
    np.ones([segment_count_optimized]) * duration / segment_count_optimized
)

# Gate schemes
gate_schemes = ["Primitive", "Optimized", "Robust"]

The optimization specification in the next cell involves several steps:

  • create the (noiseless) Hamiltonian,
  • create the noise operators,
  • define the cost function to be minimized by the optimizer, which is the infidelity of the system's evolution,
  • introduce the final (optimizable) phase shift on each qubit,
  • compute the infidelity of the Hamiltonian with optimizable phase shifts.

The outputs are the optimized values of $\Omega(t)$ ($x$ and $y$ components) across the gate duration, the optimized phase shifts, and the infidelities with and without the phase shifts.

def scheme_optimization(primitive_flag, noise_flag, segment_count, optimization_count):
    with qctrl.create_graph() as graph:

        # Construct drive terms
        moduli = qctrl.operations.optimization_variable(
            count=segment_count,
            lower_bound=epsilon_1_min,
            upper_bound=epsilon_1_max,
        )
        if primitive_flag:
            phases = np.zeros(segment_count)
        else:
            phases = qctrl.operations.optimization_variable(
                count=segment_count,
                lower_bound=-np.pi,
                upper_bound=np.pi,
            )
        drive_1 = qctrl.operations.complex_pwc_signal(
            moduli=moduli,
            phases=phases,
            duration=duration,
            name="drive_1",
        )
        drive_1_term = qctrl.operations.pwc_operator_hermitian_part(drive_1 * b_1)

        # Construct remaining Hamiltonian term
        fixed_hamiltonian_term = (
            (Delta + epsilon) * bdb_1
            + epsilon * bdb_2
            + delta_1 * bd2b2_1 / 2
            + delta_2 * bd2b2_2 / 2
            + g * (bdb_12 + bdb_21)
        )

        hamiltonian = drive_1_term + fixed_hamiltonian_term

        # Create noise operators, they should have the same numeric type
        crosstalk_1x = (
            crosstalk_constant * qctrl.operations.real(drive_1) * (b_2 + bd_2) / 2
        )
        crosstalk_1y = (
            crosstalk_constant * qctrl.operations.imag(drive_1) * 1j * (b_2 - bd_2) / 2
        )

        if noise_flag:
            noise_list = [crosstalk_1x, crosstalk_1y]
        else:
            noise_list = []

        # Create infidelity
        target_operator = qctrl.operations.target(operator=zx_half)
        infidelity = qctrl.operations.infidelity_pwc(
            hamiltonian=hamiltonian,
            noise_operators=noise_list,
            target=target_operator,
            name="infidelity",
        )

        # Introduce optimizable phase shifts on each qubit
        phase_shift_1 = qctrl.operations.pwc_signal(
            values=qctrl.operations.optimization_variable(
                count=1,
                lower_bound=-np.pi,
                upper_bound=np.pi,
                is_lower_unbounded=True,
                is_upper_unbounded=True,
            ),
            duration=1.0,
            name="phase1",
        )
        phase_shift_2 = qctrl.operations.pwc_signal(
            values=qctrl.operations.optimization_variable(
                count=1,
                lower_bound=-np.pi,
                upper_bound=np.pi,
                is_lower_unbounded=True,
                is_upper_unbounded=True,
            ),
            duration=1.0,
            name="phase2",
        )
        phase_shift_hamiltonian = (
            phase_shift_1 * phase_shift_operator1
            + phase_shift_2 * phase_shift_operator2
        )

        u_phase = qctrl.operations.time_evolution_operators_pwc(
            hamiltonian=phase_shift_hamiltonian, sample_times=np.array([1.0])
        )
        target_operator_with_phase_shift = qctrl.operations.target(
            operator=u_phase[0] @ zx_half
        )

        # Create infidelity with phase shifts
        infidelity_with_phase_shift = qctrl.operations.infidelity_pwc(
            hamiltonian=hamiltonian,
            noise_operators=noise_list,
            target=target_operator_with_phase_shift,
        )
        cost = infidelity_with_phase_shift
        cost.name = "infidelity_with_phase_shift"

    return qctrl.functions.calculate_optimization(
        graph=graph,
        optimization_count=optimization_count,
        cost_node_name="infidelity_with_phase_shift",
        output_node_names=[
            "infidelity",
            "infidelity_with_phase_shift",
            "drive_1",
            "phase1",
            "phase2",
        ],
    )

Next, perform the optimization. The following cell optimizes a primitive pulse as a benchmark, along with the optimized and the robust controls. The infidelities for each control scheme are presented with and without the phase shift.

noise_flag = False
primitive_flag = True
segment_count = segment_count_primitive
optimization_count = 40
result_primitive = scheme_optimization(
    primitive_flag, noise_flag, segment_count, optimization_count
)
print("Primitive")
print("Infidelity:", result_primitive.output["infidelity"]["value"])
print(
    "Infidelity with phase shift:",
    result_primitive.output["infidelity_with_phase_shift"]["value"],
)

noise_flag = False
primitive_flag = False
segment_count = segment_count_optimized
optimization_count = 5
result_optimized = scheme_optimization(
    primitive_flag, noise_flag, segment_count, optimization_count
)
print("Optimized")
print("Infidelity:", result_optimized.output["infidelity"]["value"])
print(
    "Infidelity with phase shift:",
    result_optimized.output["infidelity_with_phase_shift"]["value"],
)

noise_flag = True
segment_count_cr = segment_count_optimized
optimization_count = 5
result_robust = scheme_optimization(
    primitive_flag, noise_flag, segment_count, optimization_count
)
print("Robust")
print("Infidelity:", result_robust.output["infidelity"]["value"])
print(
    "Infidelity with phase shift:",
    result_robust.output["infidelity_with_phase_shift"]["value"],
)
Your task calculate_optimization has completed.
Primitive
Infidelity: 0.970658351975714
Infidelity with phase shift: 0.12458284456393776
Your task calculate_optimization has started.
Your task calculate_optimization has completed.
Optimized
Infidelity: 0.9494065972515537
Infidelity with phase shift: 1.2367626811560228e-07
Your task calculate_optimization has started.
Your task calculate_optimization has completed.
Robust
Infidelity: 0.9175745236333452
Infidelity with phase shift: 3.286813668706549e-05

Even in the presence of crosstalk, the infidelity arising from the robust pulse (with phase shifts) is extremely low. Visualize the control schemes using the cell below.

# Primitive
omega_1_primitive = [
    {"duration": d["duration"], "value": d["value"]}
    for d in result_primitive.output["drive_1"]
]

plot_controls(
    plt.figure(),
    {
        "$\\Omega$": omega_1_primitive,
    },
)
plt.suptitle("Primitive", fontsize=14)
plt.show()

# Optimized
omega_1_optimized = [
    {"duration": d["duration"], "value": d["value"]}
    for d in result_optimized.output["drive_1"]
]
plot_controls(
    plt.figure(),
    {
        "$\\Omega$": omega_1_optimized,
    },
)
plt.suptitle("Optimized", fontsize=14)
plt.show()

# Robust
omega_1_robust = [
    {"duration": d["duration"], "value": d["value"]}
    for d in result_robust.output["drive_1"]
]
plot_controls(
    plt.figure(),
    {
        "$\\Omega$": omega_1_robust,
    },
)
plt.suptitle("Robust", fontsize=14)
plt.show()

The above plots display the control pulses ($\Omega_x$ and $\Omega_y$) for the primitive, optimized and robust schemes, respectively.

Verifying accuracy and robustness of Q-CTRL cross resonance gate

Use the controls obtained above to calculate the exact time evolution determined by the Hamiltonian in the four-level approximation. In the cell below, the evolution dynamics is calculated for each scheme with a batch of crosstalk amplitudes. We then extract both the noise-free and the noisy dynamics from the result.

First, verify the noise-free operations: compare the noise-free operational infidelities and examine the evolution of an equal superposition of the four lowest 2-qubit states, $|\psi_0\rangle =\frac{1}{2}\left(|00\rangle-i|01\rangle+i|10\rangle+|11\rangle\right)$.

schemes_results = {
    "Primitive": result_primitive,
    "Optimized": result_optimized,
    "Robust": result_robust,
}
initial_state = (np.array([[1, -1j, 1j, 0, 1, 0, 0, 0, 0, 0]]) / 2).T
crosstalks_count = 101
crosstalk_array = np.linspace(-1, 1, crosstalks_count)
infidelities = []
probabilities = []

for scheme in gate_schemes:

    with qctrl.create_graph() as graph:

        # Construct drive terms
        drive_1_values = np.array(
            [d["value"] for d in schemes_results[scheme].output["drive_1"]]
        )
        drive_1 = qctrl.operations.pwc_signal(
            values=drive_1_values,
            duration=duration,
        )
        drive_1_term = qctrl.operations.pwc_operator_hermitian_part(drive_1 * b_1)

        # Construct crosstalk terms
        drive_1_batched = qctrl.operations.pwc_signal(
            values=drive_1_values * crosstalk_array[:, None], duration=duration
        )
        crosstalk_1x = (
            crosstalk_constant
            * qctrl.operations.real(drive_1_batched)
            * (b_2 + bd_2)
            / 2
        )
        crosstalk_1y = (
            crosstalk_constant
            * qctrl.operations.imag(drive_1_batched)
            * 1j
            * (b_2 - bd_2)
            / 2
        )

        # Construct remaining Hamiltonian terms
        fixed_hamiltonian_term = (
            (Delta + epsilon) * bdb_1
            + epsilon * bdb_2
            + delta_1 * bd2b2_1 / 2
            + delta_2 * bd2b2_2 / 2
            + g * (bdb_12 + bdb_21)
        )

        # Generate Hamiltonian and evolution operator.
        hamiltonian = (
            crosstalk_1x + crosstalk_1y + drive_1_term + fixed_hamiltonian_term
        )

        u_H = qctrl.operations.time_evolution_operators_pwc(
            hamiltonian=hamiltonian,
            sample_times=np.array([duration]),
        )

        # Construct phase correction terms
        phase_shift_hamiltonian = qctrl.operations.constant_pwc_operator(
            1.0,
            schemes_results[scheme].output["phase1"][0]["value"] * phase_shift_operator1
            + schemes_results[scheme].output["phase2"][0]["value"]
            * phase_shift_operator2,
        )
        u_phase = qctrl.operations.time_evolution_operators_pwc(
            hamiltonian=phase_shift_hamiltonian,
            sample_times=np.array([1.0]),
        )

        # Obtain infidelity
        target_operator_with_phase_shift = qctrl.operations.target(
            operator=u_phase[0] @ zx_half
        )
        infidelity_with_phase_shift = qctrl.operations.infidelity_pwc(
            hamiltonian=hamiltonian,
            noise_operators=[],
            target=target_operator_with_phase_shift,
            name="infidelity",
        )

        # Obtain noise-free state evolution
        final_unitary = qctrl.operations.conjugate(u_phase) @ u_H
        final_state = final_unitary[int(crosstalks_count / 2), 0] @ initial_state
        final_state.name = "final_state"

    # Calculate simulation
    graph_result = qctrl.functions.calculate_graph(
        graph=graph,
        output_node_names=[
            "infidelity",
            "final_state",
        ],
    )

    infidelities.append(graph_result.output["infidelity"]["value"])
    probabilities.append(np.abs(graph_result.output["final_state"]["value"]).T[0] ** 2)

print("Noise-free operational infidelities")
print("Primitive\t\t Optimized\t\t\t Robust")
print(
    infidelities[0][int(crosstalks_count / 2)],
    "\t",
    infidelities[1][int(crosstalks_count / 2)],
    "\t",
    infidelities[2][int(crosstalks_count / 2)],
)
Your task calculate_graph has completed.
Your task calculate_graph has completed.
Your task calculate_graph has completed.
Noise-free operational infidelities
Primitive		 Optimized			 Robust
0.12458284456393776 	 1.2367626811560228e-07 	 1.4023340177971022e-07
gs = gridspec.GridSpec(1, 3, wspace=0.1)
fig = plt.figure()
fig.set_figheight(5)
fig.set_figwidth(3.0 * 5)
fig.suptitle("Final populations", fontsize=16, y=1.01)

ax = fig.add_subplot(gs[0])
ax.tick_params(labelsize=14)
langs = ["|01>", "|10>", "Rest"]
population_primitive = [
    probabilities[0][1],
    probabilities[0][2],
    1 - probabilities[0][1] - probabilities[0][2],
]
ax.bar(langs, population_primitive, color=colors["Primitive"])
ax.set_ylim(0.0, 0.55)
ax.set_title("Primitive", fontsize=14)
ax.set_xlabel("States", fontsize=14)
ax.set_ylabel("Probability", fontsize=14)

ax = fig.add_subplot(gs[1])
ax.tick_params(labelsize=14)
population_optimized = [
    probabilities[1][1],
    probabilities[1][2],
    1 - probabilities[1][1] - probabilities[1][2],
]
ax.bar(langs, population_optimized, color=colors["Optimized"])
ax.set_ylim(0.0, 0.55)
ax.yaxis.set_ticklabels([])
ax.set_title("Optimized", fontsize=14)

ax = fig.add_subplot(gs[2])
ax.tick_params(labelsize=14)
population_robust = [
    probabilities[2][1],
    probabilities[2][2],
    1 - probabilities[2][1] - probabilities[2][2],
]
ax.bar(langs, population_robust, color=colors["Robust"])
ax.set_ylim(0.0, 0.55)
ax.yaxis.set_ticklabels([])
ax.set_title("Robust", fontsize=14)

plt.show()

The figure shows the final state after operating the respective gates on the initial state $|\psi_0\rangle =\frac{1}{2}\left(|00\rangle-i|01\rangle+i|10\rangle+|11\rangle\right)$. The final state after an ideal operation of the CR gate should be the Bell-state $|\psi_f\rangle =\frac{1}{2}\left(|01\rangle+|10\rangle\right)$. The populations are displayed to assess each scheme. Clearly, there is non-negligible population leakage in the primitive scheme, while both the Optimized and Robust schemes perform almost perfectly (up to a small asymmetry).

Next, assess the robustness of the different control solutions to crosstalk noise.

fig, ax = plt.subplots()
fig.set_figheight(5)
fig.set_figwidth(15)
plt.title("Scheme robustness", fontsize=16, y=1)
ax.tick_params(labelsize=14)

for ind, infidelity in enumerate(infidelities):
    name = gate_schemes[ind]
    ax.plot(crosstalk_array, infidelity, label=name, color=colors[name], linewidth=2)
    ax.set_xlabel("Relative crosstalk", fontsize=14)
    ax.set_ylabel("Infidelities", fontsize=14)
    ax.legend(loc="best", bbox_to_anchor=(0.16, 0.5), fontsize=14)
plt.show()

The figure demonstrates the different noise (crosstalk) susceptibilities (in terms of operational infidelity) of the Primitive, Optimized and Robust controls. The Robust scheme has a much broader low-infidelity region.