seQuencing logo

Controlling a Transmon coupled to a Cavity

In this tutorial, we will start to write some serious sequences, starting with basic calibration of single Modes, and moving on to explore the fundamental toolkit for manipulating a dispersively coupled qubit and cavity.

[1]:
%config InlineBackend.figure_formats = ['svg']
%matplotlib inline
import inspect
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm
import qutip

from sequencing import Transmon, Cavity, System, get_sequence, sync, delay
from sequencing.calibration import fit_sine, fit_displacement, tune_rabi, tune_displacement, tune_drag
[2]:
qubit = Transmon('qubit', levels=2)
qubit.gaussian_pulse.sigma = 5 # ns
qubit.gaussian_pulse.chop = 4 # sigma
system = System('system', modes=[qubit])

Basic calibration

Rabi

Let’s write an “amplitude Rabi” sequence to tune the amplitude of our qubit’s GaussianPulse.

[3]:
ax = qubit.gaussian_pulse.plot()
_ = ax.set_xlabel('Time [ns]')
../_images/notebooks_06-transmon-cavity-control_5_0.svg
[4]:
pulse_amps = np.linspace(-2, 2, 51)
init_state = qubit.fock(0) # start in |g>
e_ops = [qubit.fock(1)] # measure P(|e>)
[5]:
e_population = []
for amp in tqdm(pulse_amps, desc='Pulse amp'):
    with qubit.pulse_scale(amp):
        seq = get_sequence(system)
        qubit.rotate_x(np.pi)
        result = seq.run(init_state, e_ops=e_ops)
        e_population.append(result.expect[0][-1])
Pulse amp: 100%|██████████| 51/51 [00:00<00:00, 118.22it/s]
[6]:
fit_result = fit_sine(pulse_amps, e_population)
optimal_amp = 1 / (2 * fit_result.params['f0'].value)
print('Initial amplitude: 1')
print(f'Optimal scale factor: {optimal_amp:.5f}')
Initial amplitude: 1
Optimal scale factor: 1.05253
[7]:
qubit.gaussian_pulse.amp = optimal_amp

e_population_final = []
for amp in tqdm(pulse_amps, desc='Pulse amp (after update)'):
    with qubit.pulse_scale(amp):
        seq = get_sequence(system)
        qubit.rotate_x(np.pi)
        result = seq.run(init_state, e_ops=e_ops)
        e_population_final.append(result.expect[0][-1])
Pulse amp (after update): 100%|██████████| 51/51 [00:00<00:00, 118.46it/s]
[8]:
fit_result_final = fit_sine(pulse_amps, e_population_final)
optimal_amp_final = 1 / (2 * fit_result_final.params["f0"].value)
print('Initial amplitude: 1', flush=True)
print(f'Optimal scale factor: {optimal_amp_final:.5f}')
Initial amplitude: 1
Optimal scale factor: 1.00000
[9]:
fig, ax = plt.subplots()
ax.plot(
    pulse_amps, e_population, 'C0o', label='Initial data'
)
ax.plot(
    pulse_amps, fit_result.best_fit, 'C0-', label=f'Fit (scale: {optimal_amp:.5f})'
)
ax.plot(
    pulse_amps, e_population_final, 'C1o', label='Final data'
)
ax.plot(
    pulse_amps, fit_result_final.best_fit, 'C1-',
    label=f'Fit (scale: {optimal_amp_final:.5f})'
)
ax.set_xlabel('Pulse amplitude scale')
ax.set_ylabel('|e> population')
ax.set_title('Amplitude Rabi')
ax.grid(True)
_ = ax.legend(loc=0)
../_images/notebooks_06-transmon-cavity-control_11_0.svg

Let’s check how well we have tuned up our \(\pi\)-pulse by looking at state fidelities:

[10]:
seq = get_sequence(system)
qubit.rotate_x(np.pi)
result = seq.run(init_state)

ideal_state = qubit.rotate_x(np.pi, unitary=True) * init_state
fid_g_to_e = qutip.fidelity(result.states[-1], ideal_state)**2
print(f'State fidelity |g> -> |e>: {fid_g_to_e:.5f}')

init_state = qubit.fock(1) # start in |e>
result = seq.run(init_state)
ideal_state = qubit.rotate_x(np.pi, unitary=True) * init_state
fid_e_to_g = qutip.fidelity(result.states[-1], ideal_state)**2
print(f'State fidelity |e> -> |g>: {fid_e_to_g:.5f}')
State fidelity |g> -> |e>: 1.00000
State fidelity |e> -> |g>: 1.00000

This is such a common and useful tuneup sequence that sequencing.calibration has a built-in function to handle it:

[11]:
print('sequencing.calibration.tune_rabi():')
print('\n\t'.join([''] + inspect.getdoc(tune_rabi).splitlines()))
sequencing.calibration.tune_rabi():

        Tune the amplitude of a Transmon pulse using
        an amplitude-Rabi experiment.

        Args:
            system (System): System containing the Transmon whose
                pulse you want to tune.
            init_state (qutip.Qobj): Initial state of the system.
            e_ops (optional, list[qutip.Qobj]): List of expectation
                operators. If none, defaults to init_state. Default: None.
            mode_name (optional, str): Name of the Transmon mode. Default: 'qubit'.
            pulse_name (optional, str): Name of the pulse to tune. If None,
                will use transmon.default_pulse. Default: None.
            amp_range (optional, tuple[float, float, int]): Range over which to
                sweep the pulse amplitude, specified by (start, stop, num_steps).
                The units are such that, if the pulse is tuned up, amplitude of 1
                generates a rotation by pi. Default: (-2, 2, 51).
            progbar (optional, bool): Whether to display a tqdm progress bar.
                Default: True.
            plot (optional, bool): Whether to plot the results: Default: True.
            ax (optional, matplotlib axis): Axis on which to plot results. If None,
                one is automatically created. Default: None.
            ylabel (optional, str): ylabel for the plot. Default: None.
            update (optional, bool): Whether to update the pulse amplitude based on
                the fit result. Default: True.
            verify (optional, bool): Whether to re-run the Rabi sequence with the
                newly-determined amplitude to verify correctness. Default: True.

        Returns:
            tuple[tuple, float, float]: (fig, ax), old_amp, new_amp

DRAG

For weakly anharmonic systems like transmon qubits, the existence of levels above \(|g\rangle\) and \(|e\rangle\) can lead to errors for fast gates. Here, we demonstrate the use of DRAG (Derivative Removal by Adiabatic Gate) to reduce the magnitude of these errors.

First, we demonstrate the problem by adding a third level to our transmon, and adding an experimentally reasonable anharmonicity of 200 MHz.

[12]:
qubit.levels = 3
qubit.anharmonicity = -200e-3 # GHz
init_state = qubit.fock(0)
_, old_amp, new_amp = tune_rabi(
    system, init_state, mode_name='qubit',
    e_ops=[qubit.fock(1)], verify=False
)
100%|██████████| 51/51 [00:00<00:00, 103.04it/s]
../_images/notebooks_06-transmon-cavity-control_17_1.svg
Updating qubit unit amp from 1.05e+00 to 1.06e+00.
[13]:
seq = get_sequence(system)
qubit.rotate_x(np.pi)
result = seq.run(init_state)
ideal_state = qubit.rotate_x(np.pi, unitary=True) * init_state
fid_g_to_e = qutip.fidelity(result.states[-1], ideal_state)**2
print(f'State fidelity |g> -> |e>: {fid_g_to_e:.5f}')
State fidelity |g> -> |e>: 0.98873

To regain our excellent gate fidelity with the three-level transmon, we can adjust the DRAG coefficient of our pulse: qubit.gaussian_pulse.drag. We can do this with the built-in function sequencing.calibration.tune-drag.

[14]:
print('sequencing.calibration.tune_drag():')
print('\n\t'.join([''] + inspect.getdoc(tune_drag).splitlines()))
sequencing.calibration.tune_drag():

        Tune the DRAG coefficient for a Transmon pulse by executing
        Rx(pi) - Ry(pi/2) and Ry(pi) - Rx(pi/2) using different DRAG
        values.

        Args:
            system (System): System containing the Transmon whose
                pulse you want to tune.
            init_state (qutip.Qobj): Initial state of the system.
            e_ops (optional, list[qutip.Qobj]): List of expectation
                operators. If None, defaults to init_state. Default: None.
            mode_name (optional, str): Name of the Cavity mode. Default: 'qubit'.
            pulse_name (optional, str): Name of the pulse to tune. If None,
                will use qubit.default_pulse. Default: None.
            drag_range (optional, tuple[float, float, int]): Range over which to
                sweep the DRAG value, specified by (start, stop, num_steps).
                Default: (-5, 5, 21)
            progbar (optional, bool): Whether to display a tqdm progress bar.
                Default: True.
            plot (optional, bool): Whether to plot the results: Default: True.
            ax (optional, matplotlib axis): Axis on which to plot results. If None,
                one is automatically created. Default: None.
            ylabel (optional, str): ylabel for the plot. Default: None.
            update (optional, bool): Whether to update the DRAG value based on
                the fit result. Default: True.

        Returns:
            tuple[tuple, float, float]: (fig, ax), old_drag, new_drag
[15]:
_, old_drag, new_drag = tune_drag(
    system, init_state, mode_name='qubit', e_ops=[qubit.fock(1)]
)
100%|██████████| 21/21 [00:00<00:00, 38.12it/s]
../_images/notebooks_06-transmon-cavity-control_21_1.svg
Updating gaussian_pulse.drag from 0.00e+00 to -1.60e+00.
[16]:
ax = qubit.gaussian_pulse.plot()
ax.set_xlabel('Time [ns]')
_ = ax.set_title(f'Gaussian pulse with DRAG = {new_drag:.3f}')
../_images/notebooks_06-transmon-cavity-control_22_0.svg
[17]:
seq = get_sequence(system)
qubit.rotate_x(np.pi)
result = seq.run(init_state)
ideal_state = qubit.rotate_x(np.pi, unitary=True) * init_state
fid_g_to_e = qutip.fidelity(result.states[-1], ideal_state)**2
print(f'With DRAG - state fidelity |g> -> |e>: {fid_g_to_e:.5f}')
With DRAG - state fidelity |g> -> |e>: 0.99996

Displacement

[18]:
cavity = Cavity('cavity', levels=10)
system.modes = [qubit, cavity]
system.active_modes = [qubit, cavity]
system.set_cross_kerr(qubit, cavity, -2e-3) # set dispersive coupling to 2 MHz

The analogous tuneup sequence for a multilevel Cavity involves displacing the cavity with a gaussian pulse and then–because this is an experimentally convenient thing to do–measuring the probablity of the cavity being in fock state \(|0\rangle\). Of course, because we have access to the full state of any quantum system we create using sequencing, we can directly calculate the probability of the cavity being in \(|0\rangle\), \(|1\rangle\), or any state we wish. This is what we do in the built-in function sequencing.calibration.tune_displacement, which is used to calibrate the cavity’s GaussianPulse.

Note that, assuming the displacement pulse is correctly tuned, the x axis of the below plot is displacement amplitude \(\alpha\), which has units of \(\sqrt{\text{photons}}\), so at a pulse scale of 3, the cavity state is a coherent state with \(\bar{n} = |\alpha|^2 = 9\).

[19]:
init_state = system.ground_state()
_ = tune_displacement(system, init_state, verify=False)
100%|██████████| 51/51 [00:01<00:00, 26.58it/s]
../_images/notebooks_06-transmon-cavity-control_27_1.svg
Updating cavity unit amp from 1.00e+00 to 1.03e+00.

Experimentally, it’s not quite so easy - typically a cavity is measured by inferring its state based on its dispersive coupling to an ancilla qubit whose state can be measured with high fidelity. Below we will explore two pulse sequences that will allow us to infer the state of the cavity by measuring the state of the ancilla qubit. But first, we must desribe in more detail the operations that are available to us to control and measure a coupled qubit and cavity.

Quantum control in the dispersive regime

There are five basic operations we can perform on a system composed of a qubit coupled to a cavity, assuming that we are in the number-resolved regime, i.e. the dispersive shift between cavity and qubit is large compared to their linewidths. Below, \(n_c\) (\(n_q\)) represents the number of excitations in the cavity (qubit) and \(I_c\) (\(I_q\)) represents the identity operator for the cavity (qubit).

  1. Transmon frequency *unselective* qubit rotations: \(R_\phi(\theta)\), implemented in sequencing by qubit.rotate(theta, phi) when the qubit pulse has a bandwidth \(\gg\chi/2\pi\). This will rotate the qubit (approximately) independently of the state of the cavity. The wider the bandwidth (shorter the duration) of the qubit pulse, the better this approximation will be. This is the operation we tuned up in the Rabi sequence above.

  2. Transmon frequency *selective* qubit rotations: \(R_{\phi,n_c}(\theta) = |n_c\rangle\langle n_c|R_\phi(\theta) + (I_c-|n_c\rangle\langle n_c|)I_q\), implemented in sequencing by qubit.rotate(theta, phi) when the qubit pulse has a bandwidth \(\ll\chi/2\pi\). This will rotate the qubit conditioned on the state of the cavity. Applying a selective qubit pulse at frequency \(\omega_q-n_c|\chi|\) rotates the qubit if and only if there are \(n_c\) photons in the cavity. As we will see below, we can use this operation followed by measuring the state of the qubit to answer the question “are there zero photons in the cavity?”

  3. Cavity frequency *unselective* displacements: \(D(\alpha)\), implemented in sequencing by cavity.displace(alpha) when the cavity pulse has a bandwidth \(\gg\chi/2\pi\). This is the operation we tuned up in the displacement sequence above.

  4. Cavity frequency *selective* displacements: \(D_{n_q}(\alpha) = |n_q\rangle\langle n_q|D(\alpha) + (I_q-|n_q\rangle\langle n_q|)I_c\), implemented in sequencing by cavity.displace(alpha) when the cavity pulse has a bandwidth \(\ll\chi/2\pi\). (Treating the qubit as a true two-level system, we have \(|n_q\rangle\in\{|g\rangle,|e\rangle\}=\{|0\rangle,|1\rangle\}\).) Applying a selective cavity pulse at frequency \(\omega_c-n_q|\chi|\) displaces the cavity if and only if the qubit is in state \(|n_q\rangle\).

  5. Conditional phase: \(C_\phi=\exp(i\phi\,a^\dagger a\,|e\rangle\langle e|)\), where \(a\) is the cavity annihillation operator. This is an entangling operation that can be used to impart a phase on the qubit conditioned on the state of the cavity (or vice-versa, of course). We can enact the operation \(C_\phi\) by evolving the state of the system for a time \(t=\phi/\chi\). As we will see below, we can use this operation followed by measuring the state of the qubit to answer the question “does the cavity state have even/odd parity?”

[20]:
selective_sigma = 500 # ns

# tune selective qubit pulse using Rabi
with system.use_modes([qubit]):
    with qubit.temporarily_set(gaussian_pulse__sigma=selective_sigma):
        _, _, selective_qubit_amp = tune_rabi(
            system, qubit.fock(0), update=False, plot=False, verify=False
        )

# # tune selective cavity pulse using displacement
# with system.use_modes([cavity]):
#     with cavity.temporarily_set(gaussian_pulse__sigma=selective_sigma):
#         _, _, selective_cavity_amp = tune_displacement(
#             system, cavity.fock(0), update=False, plot=False, verify=False
#         )
100%|██████████| 51/51 [00:06<00:00,  7.45it/s]
[21]:
def selective_rotation(qubit, angle, phase=0, detune=0, sigma=selective_sigma):
    with qubit.gaussian_pulse.temporarily_set(sigma=sigma, amp=selective_qubit_amp):
         qubit.rotate(np.pi, phase, detune=detune)

# def selective_displacement(cavity, alpha, detune=0, sigma=selective_sigma):
#     with cavity.gaussian_pulse.temporarily_set(sigma=sigma, amp=selective_cavity_amp):
#         cavity.displace(alpha, detune=detune)

Selective qubit rotation (\(n = 0\))

First, let’s use a selective qubit rotation to answer the question “are there 0 photons in the cavity?”

[22]:
init_states = [
    (f'$|g{n}\\rangle$', system.fock(qubit=0, cavity=n)) for n in range(4)
]
[23]:
# Apply a selective pi pulse that is resonant
# with the qubit when the cavity is in |0>.
results = {}

seq = get_sequence(system)
selective_rotation(qubit, np.pi)

for label, state in tqdm(init_states, desc='Initial states'):
    result = seq.run(state)
    results[label] = result
Initial states: 100%|██████████| 4/4 [00:00<00:00,  5.32it/s]
[24]:
fig, ax = plt.subplots(1, 1)
for label, result in results.items():
    # trace over the cavity
    qubit_states = [state.ptrace(qubit.index) for state in result.states]
    e_pops = qutip.expect(qubit.fock_dm(1, full_space=False), qubit_states)
    ax.plot(result.times, e_pops, label=label)
    ax.grid(True)
    ax.legend(loc=0)
ax.set_xlabel('Time [ns]')
ax.set_ylabel(r'$P(|e\rangle)$')
_ = ax.set_title('Transmon trajectory vs. initial cavity state')
../_images/notebooks_06-transmon-cavity-control_35_0.svg

Measuring the cavity using the qubit

As we can see, our sequence allows us to excite the transmon if and only if the cavity is in \(|0\rangle\). Now let’s use this selective pulse to measure the state of the cavity after a displacement.

[25]:
init_state = system.ground_state()
# qubit in |e> after selective pi pulse means cavity in |0>
e_op = qubit.fock_dm(1, full_space=False)
disp_amps = np.linspace(0.01, 3, 21)
e_pops = []

for amp in tqdm(disp_amps, desc='Disp. amp.'):
    seq = get_sequence(system)

    cavity.displace(amp)
    sync()
    selective_rotation(qubit, np.pi)

    result = seq.run(init_state)
    # trace over the cavity
    transmon_state = result.states[-1].ptrace(qubit.index)
    e_pops.append(qutip.expect(e_op, transmon_state))
Disp. amp.: 100%|██████████| 21/21 [00:05<00:00,  3.85it/s]
[26]:
fig, ax = plt.subplots()
ax.plot(disp_amps, e_pops, 'o')
ax.grid(True)
ax.set_xlabel('Displacement amplitude')
ax.set_ylabel(r'$P(|e\rangle)$')
_ = ax.set_title(r'Displacement sequence with selective $\pi$ pulse')
../_images/notebooks_06-transmon-cavity-control_38_0.svg

Look familiar? This is the exact same sequence as our displacement tuneup, with one crucial difference: in the default displacement tuneup sequence, the y axis the expectation value of the operator \(|g0\rangle\langle g0|\), which we can only only measure in sequencing because we have access to the full joint state of the transmon-cavity system. On the other hand, using our selective \(\pi\) pulse to infer the state of the cavity, we only need to measure the expectation value of an operator on the qubit: \(|e\rangle\langle e|\). This means we can trace over the cavity while still learning information about its state.

Selective \(\pi\) pulse (any \(n\))

Let’s extend this sequence to answer the question “are there \(n\) photons in the cavity?” for any \(n\) we wish.

[27]:
def rotate_qubit_on_n(system, n, angle, qubit_name='qubit', cavity_name='cavity'):
    """Rotate the qubit state iff the cavity is in state |n> by detuning
    the selective qubit pulse by n * chi.
    """
    qubit = system.get_mode(qubit_name)
    cavity = system.get_mode(cavity_name)
    chi = system.cross_kerrs[frozenset([qubit.name, cavity.name])]
    selective_rotation(qubit, angle, detune=n*chi)

Here we are applying a “detuning” to the qubit pulse by adding a time-dependent complex phase using the keyword argument detune. Let’s examine the Hamiltonian coefficients for these detuned pulses.

[28]:
for n in range(4):
    seq = get_sequence(system)
    rotate_qubit_on_n(system, n, np.pi)
    fig, ax = seq.plot_coefficients(subplots=False)
    _ = ax.set_title(f'detune = ${n}\\cdot\\chi$')
    fig.tight_layout()
../_images/notebooks_06-transmon-cavity-control_43_0.svg
../_images/notebooks_06-transmon-cavity-control_43_1.svg
../_images/notebooks_06-transmon-cavity-control_43_2.svg
../_images/notebooks_06-transmon-cavity-control_43_3.svg
[29]:
max_n = 4

init_states = [
    (f'$|g{n}\\rangle$', system.fock(qubit=0, cavity=n)) for n in range(max_n)
]

results = {}

for label, state in tqdm(init_states, desc='Initial states'):
    results[label] = []
    for n in range(max_n):
        seq = get_sequence(system)
        rotate_qubit_on_n(system, n, np.pi)
        result = seq.run(state)
        results[label].append(result)
Initial states: 100%|██████████| 4/4 [00:03<00:00,  1.10it/s]
[30]:
fig, axes = plt.subplots(max_n, 1, sharex=True, sharey=True, figsize=(8,8))
e_op = qubit.fock_dm(1, full_space=False)
for label, results_list in tqdm(results.items(), desc='Plotting'):
    for ax, result_obj in zip(axes, results_list):
        xs = result_obj.times
        ys = [
            qutip.expect(e_op, state.ptrace(qubit.index))
            for state in result_obj.states
        ]
        ax.plot(xs, ys, lw=2.5, label=label)

for n, ax in enumerate(axes):
    ax.grid(True)
    ax.legend(loc=2, fontsize='small')
    ax.set_xlabel('Time [ns]')
    ax.set_ylabel(r'$P(|e\rangle)$')
    ax.set_title(f'Rotate on $|{n}\\rangle$')
fig.tight_layout()
Plotting: 100%|██████████| 4/4 [00:12<00:00,  3.09s/it]
../_images/notebooks_06-transmon-cavity-control_45_1.svg

Of course, if we wish to have easy access to a selective pulse, we can add it to our Transmon object using Transmon.add_pulse():

[31]:
print('sequencing.modes.PulseMode.add_pulse():')
print('\n\t'.join([''] + inspect.getdoc(Transmon.add_pulse).splitlines()))
sequencing.modes.PulseMode.add_pulse():

        Creates a new pulse of type ``cls`` and adds it to ``self.pulses``.

        Keyword arguments are passed to ``cls.__init__()``.

        Args:
            cls (optional, type): Pulse class to instantiate.
                Default: ``GaussianPulse``.
            name (optional, str): Name of the new pulse. If None,
                will use the "snake case" version of the class name,
                e.g. "GaussianPulse" -> "gaussian_pulse". Default: None.
            error_if_exists (optional, bool): Whether to raise an exception
                if a pulse with the same name already exists. Default: False.
[32]:
qubit.add_pulse(
    name='selective_gaussian_pulse',
    sigma=500, amp=selective_qubit_amp
)
[33]:
ax = qubit.gaussian_pulse.plot()
_ = qubit.selective_gaussian_pulse.plot(ax=ax)
ax.set_xlabel('Time [ns]')
ax = ax.set_ylabel('Normalized amplitude')
../_images/notebooks_06-transmon-cavity-control_49_0.svg
[34]:
print(list(qubit.pulses))
['smoothed_constant_pulse', 'gaussian_pulse', 'selective_gaussian_pulse']

Now we can repeat our displacement sequence using a selective transmon pulse to measure the cavity, this time measuring on different photon number peaks.

[35]:
max_n = 4

init_state = system.ground_state()
# qubit in |e> after selective pi pulse means cavity in |n>
e_op = qubit.fock_dm(1, full_space=False)
disp_amps = np.linspace(0.01, 3, 21)
e_pops = []

for n in range(max_n):
    e_pops.append([])
    for amp in tqdm(disp_amps, desc=f'Disp. amp. (measure n={n})'):
        seq = get_sequence(system)

        cavity.displace(amp)
        sync()
        rotate_qubit_on_n(system, n, np.pi)
        result = seq.run(init_state)
        # trace over the cavity
        transmon_state = result.states[-1].ptrace(qubit.index)
        e_pops[-1].append(qutip.expect(e_op, transmon_state))
Disp. amp. (measure n=0): 100%|██████████| 21/21 [00:05<00:00,  3.85it/s]
Disp. amp. (measure n=1): 100%|██████████| 21/21 [00:06<00:00,  3.40it/s]
Disp. amp. (measure n=2): 100%|██████████| 21/21 [00:06<00:00,  3.27it/s]
Disp. amp. (measure n=3): 100%|██████████| 21/21 [00:07<00:00,  2.99it/s]
[36]:
fig, ax = plt.subplots()
for n, es in enumerate(e_pops):
    ax.plot(disp_amps, es, '.-', label=f'measure n = {n}')
ax.legend(loc=0)
ax.grid(True)
ax.set_xlabel('Displacement amplitude')
ax.set_ylabel(r'$P(|e\rangle)$')
_ = ax.set_title(r'Displacement sequence with selective $\pi$ pulse')
../_images/notebooks_06-transmon-cavity-control_53_0.svg

Parity mapping

We can measure the photon number parity of the cavity state using the conditional phase operation \(C_\phi=\exp(i\phi\,a^\dagger a\,|e\rangle\langle e|)\). Let’s define the parity operator \(\Pi = e^{i\pi a^\dagger a} = (-1)^{a^\dagger a}\), where \(a\) is the annihilation operator for the cavity. The expectation value of \(\Pi\) is +1 if there are an even number of photons in the cavity and -1 if there are an odd number of photons. If we take \(\phi = \pi\), we can write the conditional phase \(C_\pi\) in terms of the parity operator:

\[C_\pi = I_c|g\rangle\langle g| + \Pi|e\rangle\langle e|\]

We can think of this operator in two ways:

  1. \(C_\pi\) flips the phase of the qubit state iff there are an odd number of photons in the cavity, or

  2. \(C_\pi\) flips the phase of all odd-parity cavity states iff the qubit is in \(|e\rangle\)

If we sandwich \(C_\pi\) between two entangling unselective \(\pi/2\) pulses on the qubit, we can map the photon number parity of the cavity state onto the qubit state: \(R(\pi/2)C_\pi R(-\pi/2)\) maps even parity to \(|g\rangle\) and odd parity to \(|e\rangle\). If we flip the phase of the second \(\pi/2\) pulse, we can instead map even parity to \(|e\rangle\) and odd parity to \(|g\rangle\)

[37]:
def parity_map(
        system, t_adjust=0, axis=np.pi, map_odd_to_e=True,
        qubit_name='qubit', cavity_name='cavity'
        ):
    qubit = system.get_mode(qubit_name)
    cavity = system.get_mode(cavity_name)
    chi = system.cross_kerrs[frozenset([qubit.name, cavity.name])]
    mapping_time = np.pi / (2 * np.pi * abs(chi))
    # correct for the non-zero pulse length
    pulse_length = qubit.gaussian_pulse.sigma * qubit.gaussian_pulse.chop
    delay_time = mapping_time - pulse_length + t_adjust

    # pulse sequence
    sync()
    # R(-\pi/2)
    qubit.rotate(-np.pi/2, axis)
    # C_\pi
    delay(delay_time)
    if map_odd_to_e:
        # R(\pi/2)
        qubit.rotate(np.pi/2, axis)
    else:
        # R(-\pi/2)
        qubit.rotate(-np.pi/2, axis)
    sync()

The intuition for this sequence is as follows:

We first perform an unselective \(\pi/2\)-pulse on the transmon, placing it in the state \(|g\rangle+e^{i\phi}|e\rangle=|0\rangle+e^{i\phi}|1\rangle\). When the transmon is in \(|1\rangle\), its phase evolves at a rate given by the number of photons in the cavity and the dispersive shift between transmon and cavity, \(a^\dagger a \chi = a^\dagger a \times 2\pi\times2\text{ MHz}\). Therefore, if we wait for a time \(t=\phi/\chi\) after the first \(\pi/2\) pulse, the qubit state will accumulate a phase of \(a^\dagger a \phi\). Let’s take \(\phi = \pi\) once again, and list the phase \(\Delta\phi\) accumulated by the qubit when it is in \(|e\rangle\) as a function of cavity photon number \(n_c\):

  • \(n_c = 0\): \(\Delta\phi = 0\times\pi,\:\Delta\phi\mod2\pi = 0\)

  • \(n_c = 1\): \(\Delta\phi = 1\times\pi,\:\Delta\phi\mod2\pi = \pi\)

  • \(n_c = 2\): \(\Delta\phi = 2\times\pi,\:\Delta\phi\mod2\pi = 0\)

  • \(n_c = 3\): \(\Delta\phi = 3\times\pi,\:\Delta\phi\mod2\pi = \pi\)

Now a second \(\pi/2\) pulse with the opposite sign with rotate the qubit back to \(|g\rangle\) iff \(\Delta\phi\mod2\pi=0\) or up to \(|e\rangle\) iff \(\Delta\phi\mod2\pi=\pi\), giving us our parity map.

Note that for our system, the idle time required for the conditional phase operation \(C_\pi\) is \(t=\pi/\chi = \pi / (2\times 2\pi\times 2\text{ MHz}) = 250\text{ ns}\). Adding the length of our two \(\pi/2\) pulses: 2 * qubit.gaussian_pulse.sigma * qubit.gaussian_pulse.chop = 2 * 5 * 4 = 40 ns, we get a total time of 290 ns for the parity mapping operation. This is significantly faster than the cavity measurement using a selective \(\pi\) pulse: qubit.selective_gaussian_pulse.sigma * qubit.selective_gaussian_pulse.chop = 500 * 4 = 2000 ns. This means that for systems with finite coherence times, measuring the cavity state using parity mapping has the potential to have much higher fidelity than measuring the cavity with a selective qubit pulse.

In practice the mapping time \(\pi/\chi\) may not be exactly optimal (for example, due to the finite length of the \(R(\pi/2)\) operations), resulting in residual entanglement between the qubit and cavity after the second \(\pi/2\) pulse. So we will write a sequence to find the optimal adjustment t_adjust to \(t=\pi/\chi\).

[38]:
init_state = system.fock(qubit=0, cavity=1)
e_op = qubit.fock_dm(1, full_space=False)
e_pops = []
times = np.linspace(-10,10,21)
for t in tqdm(times, desc='Times'):
    seq = get_sequence(system)
    parity_map(system, t_adjust=t)
    result = seq.run(init_state)
    e_pops.append(
        qutip.expect(e_op, result.states[-1].ptrace(qubit.index))
    )
i_opt = np.argmax(e_pops)
t_opt = times[i_opt]
e_opt = e_pops[i_opt]
Times: 100%|██████████| 21/21 [00:01<00:00, 11.89it/s]
[39]:
fig, ax = plt.subplots()
ax.plot(times, e_pops, '.')
ax.plot(
    [t_opt], [e_opt], 'rX',
    label=f'Optimal: {t_opt:.0f} ns, $P(|e\\rangle)$ = {e_opt:.5f}'
)
ax.grid(True)
ax.legend(loc=0)
ax.set_xlabel('t_adjust [ns]')
_ = ax.set_ylabel(r'$P(|e\rangle)$')
../_images/notebooks_06-transmon-cavity-control_58_0.svg
[40]:
max_n = 4

init_states = [
    (f'$|g{n}\\rangle$', system.fock(qubit=0, cavity=n)) for n in range(max_n)
]

odd_to_e_results = {}

seq = get_sequence(system)
parity_map(system, t_adjust=t_opt, map_odd_to_e=True)

for label, state in tqdm(init_states, desc='Initial states'):
    result = seq.run(state)
    odd_to_e_results[label] = result

odd_to_g_results = {}

seq = get_sequence(system)
parity_map(system, t_adjust=t_opt, map_odd_to_e=False)

for label, state in tqdm(init_states, desc='Initial states'):
    result = seq.run(state)
    odd_to_g_results[label] = result
Initial states: 100%|██████████| 4/4 [00:00<00:00, 13.24it/s]
Initial states: 100%|██████████| 4/4 [00:00<00:00, 13.31it/s]
[41]:
fig, (ax, bx) = plt.subplots(2, 1, sharex=True, sharey=True, figsize=(8,6))
for label, result in odd_to_e_results.items():
    # trace over the cavity
    qubit_states = [state.ptrace(qubit.index) for state in result.states]
    e_pops = qutip.expect(qubit.fock_dm(1, full_space=False), qubit_states)
    ax.plot(result.times, e_pops, label=label)
pulse_len = qubit.gaussian_pulse.sigma * qubit.gaussian_pulse.chop
ax.axvspan(pulse_len, result.times.max()-pulse_len, alpha=0.2, color='k')
ax.grid(True)
ax.legend(loc=0)
ax.set_xlabel('Time [ns]')
ax.set_ylabel(r'$P(|e\rangle)$')
ax.set_title('Map odd to $|e\\rangle$\nTransmon trajectory vs. initial cavity state')

for label, result in odd_to_g_results.items():
    # trace over the cavity
    qubit_states = [state.ptrace(qubit.index) for state in result.states]
    e_pops = qutip.expect(qubit.fock_dm(1, full_space=False), qubit_states)
    bx.plot(result.times, e_pops, label=label)
pulse_len = qubit.gaussian_pulse.sigma * qubit.gaussian_pulse.chop
bx.axvspan(pulse_len, result.times.max()-pulse_len, alpha=0.2, color='k')
bx.grid(True)
bx.legend(loc=0)
bx.set_xlabel('Time [ns]')
bx.set_ylabel(r'$P(|e\rangle)$')
bx.set_title('Map odd to $|g\\rangle$\nTransmon trajectory vs. initial cavity state')
fig.tight_layout()
../_images/notebooks_06-transmon-cavity-control_60_0.svg

Note that during the idle time (the \(C_\pi\) operation, shaded gray region in the plots above), the qubit and cavity are entangled, so when we trace over the cavity we are throwing away information about the state of the transmon.

[42]:
from qutip.ipynbtools import version_table
version_table()
[42]:
SoftwareVersion
QuTiP4.7.0
Numpy1.23.2
SciPy1.9.1
matplotlib3.5.3
Cython0.29.32
Number of CPUs2
BLAS InfoOPENBLAS
IPython8.4.0
Python3.8.6 (default, Oct 19 2020, 15:10:29) [GCC 7.5.0]
OSposix [linux]
Tue Aug 30 19:17:12 2022 UTC
[ ]: