[1]:
import quanguru as qg
import numpy as np
import matplotlib.pyplot as plt
import platform

17 - Jaynes-Cummings Dynamics#

The Jaynes-Cummings Hamiltonian is written as

\(H_{JC} = \hbar\omega_{c} a^{\dagger}a + \frac{1}{2}\hbar\omega_{q}\sigma_{z} + \hbar g(a^{\dagger}\sigma_{-} + a\sigma_{+})\)

where \(\sigma_{\pm} = (\sigma_{x} \pm i\sigma_{y})/2\) are raising/lowering operators for a two-level system, \(\sigma_{\mu}\) are the Pauli spin operators with \(\mu\in\{x,y,z\}\), \(a^{\dagger}\) and \(a\) are the creation and annihilation operators for the field mode, and \(\omega_{c}\), \(\omega_{q}\), and \(g\) are the cavity-field, qubit, and coupling (angular-) frequencies, respectively.

We will use the below parameters in our simulation

[2]:
# parameters for the Hamiltonian
qubitFreq = 1
cavityFreq = 1
couplingFreq = 0.25
cavityDim = 5

# parameters for the evolution
totalTime = 3*(np.pi/couplingFreq)
timeStep = 0.1

and we can describe the JC-Hamiltonian/system in QuanGuru as

[3]:
# Qubit for the JC model
QubitJC = qg.Qubit(frequency=qubitFreq)

# Cavity for the JC model
CavityJC = qg.Cavity(dimension=cavityDim, frequency=cavityFreq)

# JC model consists of a qubit and cavity
# and this is the 'free evolution' part of the JC-Hamiltonian
JCSystem = QubitJC + CavityJC

The interaction Hamiltonian consists of two terms:

  • \(a^{\dagger}\sigma_{-}\): cavity creation and qubit lowering

  • \(a\sigma_{+}\): cavity annihilation and qubit raising

When creating multi-system terms ensure the order of the operators is the same as the QuantumSystem objects.

e.g.

couplingTerm = totalSystem.createTerm(
    qSystem=[quantumSystem1, quantumSystem2],
    operator=[operator1, operator2])

These terms are combined into a single coupling term with frequency (coupling strength) couplingFreq. If you wish to set individual frequencies for the coupling terms, you can specify them with frequency when creating the terms.

[4]:
# coupling part of the JC-Hamiltonian
# first term of the coupling
JCCouplingT1 = JCSystem.createTerm(
    qSystem=[CavityJC, QubitJC],
    operator=[qg.destroy, qg.sigmap])

# second term of the coupling
JCCouplingT2 = JCSystem.createTerm(
    qSystem=[QubitJC, CavityJC],
    operator=[qg.sigmam, qg.create])

# combine the terms into a single term
JCCoupling = qg.QTerm(qSystem=JCSystem,
                      subSys=[JCCouplingT1, JCCouplingT2],
                      frequency=couplingFreq)

We set the simulation with:

  • Simulation parameters defined earlier

  • Initial state [1, 1]: The first excited state of both cavity and qubit

[5]:
# set the simulation parameters
JCSystem.simStepSize = timeStep
JCSystem.simTotalTime = totalTime
JCSystem.initialState = [1, 1]

We create a frequency sweep for the cavity to explore the effect of qubit-cavity detuning.

The sweep explores cavity frequencies from \(\omega_q - \omega_c\) to \(\omega_q + \omega_c\) to observe both on-resonance and off-resonance dynamics.

[6]:

# create a frequency sweep for the cavity freqSweep = JCSystem.simulation.Sweep.createSweep( system=CavityJC, sweepKey="frequency", sweepMax=qubitFreq+cavityFreq, sweepMin=qubitFreq-cavityFreq, sweepStep=.025)

We can create a composite operator in the extended Hilbert space so that measurement can be made without needing to project the state in the compute function.

Here, we create the composite qubit Pauli-Z operator \(\langle \sigma_{z} \rangle\) acting on the full qubit \(\otimes\) cavity space. This gives +1 for \(\vert e \rangle\) and -1 for \(\vert g \rangle\).

[7]:
# create sigma Z operator in the extended Hilbert space
# this allows for measuring expectation values without needing to project the state in the compute function.
compositeSZ = qg.compositeOp(qg.sigmaz(),   dimA=cavityDim)

# calculate the desired results and store
def compute(sim, args):
    stateJC = args[0]
    sim.resultsDict['zJC'].append(qg.expectation(compositeSZ, stateJC))

JCSystem.simCompute = compute

IMPORTANT NOTE FOR WINDOWS USERS : MULTI-PROCESSING (p=True) DOES NOT WORK WITH NOTEBOOK

You can use a python script, but you will need to make sure that the critical parts of the code are under if __name__ == "__main__": We are going to add further tutorials for this later.

[8]:
# do not store the states
JCSystem.simDelStates = True

# run the simulation
# p=True uses multi-processing for the sweep
JCSystem.runSimulation(p=(platform.system() != 'Windows'))
Starting sequential sweep with 81 parameter combinations...
Simulation Start:       2025-11-30 14:22:23
[████████████████████████████████████████] 100.0% | Est. Finish: 2025-11-30 14:22:27
Simulation completed in 00h 00m 04s
[8]:
[]
[9]:
# Plot sigma Z expectation value as a function of time and cavity frequency
Y, X = np.meshgrid(JCSystem.simulation.timeList, freqSweep.sweepList)
plt.pcolormesh(X, Y, JCSystem.simulation.resultsDict['zJC'])
plt.colorbar()
plt.xlabel(r"Frequency $\omega_{c}$")
plt.ylabel("Time")
[9]:
Text(0, 0.5, 'Time')
../../../_images/classes_Tutorials_1_Qubit_17_JaynesCummingsModelDynamics_15_1.png

The plot shows \(\langle \sigma_z \rangle\) as a function of time and cavity frequency and we see:

  • On resonance (\(\Delta \approx 0\)): Complete population transfer between qubit and cavity at rate \(\sim g\)

  • Off resonance (\(|\Delta| > g\)): Partial, faster oscillations at rate \(\sim \Delta\)

  • Chevron pattern: The characteristic shape arises from the detuning-dependent oscillation frequency

The depth of oscillation decreases with increasing detuning, following the relation \(\Omega_{Rabi} = \sqrt{g^2 + \Delta^2}\) where \(\Delta = \omega_q - \omega_c\).