|
| 1 | +--- |
| 2 | +jupyter: |
| 3 | + jupytext: |
| 4 | + text_representation: |
| 5 | + extension: .md |
| 6 | + format_name: markdown |
| 7 | + format_version: '1.3' |
| 8 | + jupytext_version: 1.13.8 |
| 9 | + kernelspec: |
| 10 | + display_name: Python 3 (ipykernel) |
| 11 | + language: python |
| 12 | + name: python3 |
| 13 | +--- |
| 14 | + |
| 15 | +# Steady-State: Optomechanical System in the Single-Photon Strong-Coupling Regime |
| 16 | + |
| 17 | + |
| 18 | +P.D. Nation and J.R. Johansson |
| 19 | + |
| 20 | +For more information about QuTiP see [http://qutip.org](http://qutip.org) |
| 21 | + |
| 22 | +```python |
| 23 | +import time |
| 24 | +import numpy as np |
| 25 | +import matplotlib.pyplot as plt |
| 26 | +from IPython.display import Image |
| 27 | +from qutip import (about, destroy, hinton, ptrace, qdiags, qeye, steadystate, |
| 28 | + tensor, wigner, wigner_cmap, basis, fidelity, mesolve) |
| 29 | + |
| 30 | +%matplotlib inline |
| 31 | +``` |
| 32 | + |
| 33 | +## Optomechanical Hamiltonian |
| 34 | + |
| 35 | + |
| 36 | +The optomechanical Hamiltonian arises from the radiation pressure interaction of light in an optical cavity where one of the cavity mirrors is mechanically compliant. |
| 37 | + |
| 38 | +```python |
| 39 | +Image(filename="images/optomechanical_setup.png", width=500, embed=True) |
| 40 | +``` |
| 41 | + |
| 42 | +Assuming that $a^{+}$, $a$ and $b^{+}$,$b$ are the raising and lowering operators for the cavity and mechanical oscillator, respectively, the Hamiltonian for an optomechanical system driven by a classical monochromatic pump term can be written as |
| 43 | + |
| 44 | + |
| 45 | +\begin{equation} |
| 46 | +\frac{\hat{H}}{\hbar}=-\Delta\hat{a}^{+}\hat{a}+\omega_{m}\hat{b}^{+}\hat{b}+g_{0}(\hat{b}+\hat{b}^{+})\hat{a}^{+}\hat{a}+E\left(\hat{a}+\hat{a}^{+}\right), |
| 47 | +\end{equation} |
| 48 | + |
| 49 | + |
| 50 | +where $\Delta=\omega_{p}-\omega_{c}$ is the detuning between the pump ($\omega_{p}$) and cavity ($\omega_{c}$) mode frequencies, $g_{0}$ is the single-photon coupling strength, and $E$ is the amplitude of the pump mode. It is known that in the single-photon strong-coupling regime, where the cavity frequency shift per phonon is larger than the cavity line width, $g_{0}/\kappa \gtrsim 1$ where $\kappa$ is the decay rate of the cavity, and a single single photon displaces the mechanical oscillator by more than its zero-point amplitude $g_{0}/\omega_{m} \gtrsim 1$, or equiviently, $g^{2}_{0}/\kappa\omega_{m} \gtrsim 1$, the mechanical oscillator can be driven into a nonclassical steady state of the system$+$environment dynamics. Here, we will use the steady state solvers in QuTiP to explore such a state and compare the various solvers. |
| 51 | + |
| 52 | + |
| 53 | +## Solving for the Steady State Density Matrix |
| 54 | + |
| 55 | + |
| 56 | +The steady state density matrix of the optomechanical system plus the environment can be found from the Liouvillian superoperator $\mathcal{L}$ via |
| 57 | + |
| 58 | +\begin{equation} |
| 59 | +\frac{d\rho}{dt}=\mathcal{L}\rho=0\rho, |
| 60 | +\end{equation} |
| 61 | + |
| 62 | +where $\mathcal{L}$ is typically given in Lindblad form |
| 63 | +\begin{align} |
| 64 | +\mathcal{L}[\hat{\rho}]=&-i[\hat{H},\hat{\rho}]+\kappa \mathcal{D}\left[\hat{a},\hat{\rho}\right]\\ |
| 65 | +&+\Gamma_{m}(1+n_{\rm{th}})\mathcal{D}[\hat{b},\hat{\rho}]+\Gamma_{m}n_{\rm th}\mathcal{D}[\hat{b}^{+},\hat{\rho}], \nonumber |
| 66 | +\end{align} |
| 67 | + |
| 68 | +where $\Gamma_{m}$ is the coulping strength of the mechanical oscillator to its thermal environment with average occupation number $n_{th}$. As is customary, here we assume that the cavity mode is coupled to the vacuum. |
| 69 | + |
| 70 | +Although, the steady state solution is nothing but an eigenvalue equation, the numerical solution to this equation is anything but trivial due to the non-Hermitian structure of $\mathcal{L}$ and its worsening condition number as the dimensionality of the truncated Hilbert space increases. |
| 71 | + |
| 72 | + |
| 73 | +## Steady State Solvers in QuTiP v5.0+ |
| 74 | + |
| 75 | + |
| 76 | +As of QuTiP version 5.0, the following steady state solving methods are available: |
| 77 | + |
| 78 | +- **direct**: Direct LU factorization |
| 79 | +- **eigen**: Calculates the eigenvector associated with the zero eigenvalue of $\mathcal{L}\rho$. |
| 80 | +- **svd**: Solution via SVD decomposition (dense matrices only). |
| 81 | +- **power**: Finds zero eigenvector using inverse-power method. |
| 82 | + |
| 83 | +Among these, when using `direct` and `power` methods one can use the following ``solvers`` for the factorization: |
| 84 | + |
| 85 | +- **Dense solvers**: from ``numpy.linalg``. |
| 86 | + - solve - exact solution via LAPACK routine ``_gesv``. |
| 87 | + - lstsq - best least-squares solution by minimizing the L2-norm. |
| 88 | +- **Sparse solvers**: sparse solvers from ``scipy.sparse.linalg`` |
| 89 | + - spsolve - Exact solution via UMFPACK. |
| 90 | + - gmres - Iterative solution via the GMRES solver. |
| 91 | + - lgmres - Iterative solution via the LGMRES solver. |
| 92 | + - bicgstab - Iterative solution via the BICGSTAB solver. |
| 93 | +- **MKL solver**: a sparse solver by ``mkl`` |
| 94 | + - mkl_spsolve - solution via Intel's MKL Pardiso solver. |
| 95 | + |
| 96 | + |
| 97 | +## Setup and Solution |
| 98 | + |
| 99 | + |
| 100 | +### System Parameters |
| 101 | + |
| 102 | +```python |
| 103 | +# System Parameters (in units of wm) |
| 104 | +# ----------------------------------- |
| 105 | +Nc = 4 # Number of cavity states |
| 106 | +Nm = 12 # Number of mech states |
| 107 | +kappa = 0.3 # Cavity damping rate |
| 108 | +E = 0.1 # Driving Amplitude |
| 109 | +g0 = 2.4 * kappa # Coupling strength |
| 110 | +Qm = 0.3 * 1e4 # Mech quality factor |
| 111 | +gamma = 1 / Qm # Mech damping rate |
| 112 | +n_th = 1 # Mech bath temperature |
| 113 | +delta = -0.43 # Detuning |
| 114 | +``` |
| 115 | + |
| 116 | +### Build Hamiltonian and Collapse Operators |
| 117 | + |
| 118 | +```python |
| 119 | +# Operators |
| 120 | +# ---------- |
| 121 | +a = tensor(destroy(Nc), qeye(Nm)) |
| 122 | +b = tensor(qeye(Nc), destroy(Nm)) |
| 123 | +num_b = b.dag() * b |
| 124 | +num_a = a.dag() * a |
| 125 | + |
| 126 | +# Hamiltonian |
| 127 | +# ------------ |
| 128 | +H = -delta * (num_a) + num_b + g0 * (b.dag() + b) * num_a + E * (a.dag() + a) |
| 129 | + |
| 130 | +# Collapse operators |
| 131 | +# ------------------- |
| 132 | +cc = np.sqrt(kappa) * a |
| 133 | +cm = np.sqrt(gamma * (1.0 + n_th)) * b |
| 134 | +cp = np.sqrt(gamma * n_th) * b.dag() |
| 135 | +c_ops = [cc, cm, cp] |
| 136 | +``` |
| 137 | + |
| 138 | +### Run Steady State Solvers |
| 139 | + |
| 140 | +```python |
| 141 | +# all possible methods |
| 142 | +possible_methods = ["direct", "eigen", "svd", "power"] |
| 143 | + |
| 144 | +# all possible solvers for direct (and power) method(s) |
| 145 | +possible_solvers = [ |
| 146 | + "solve", |
| 147 | + "lstsq", |
| 148 | + "spsolve", |
| 149 | + "gmres", |
| 150 | + "lgmres", |
| 151 | + "bicgstab", |
| 152 | + "mkl_spsolve", |
| 153 | +] |
| 154 | + |
| 155 | +# method and solvers used here |
| 156 | +method = "direct" |
| 157 | +solvers = ["spsolve", "gmres"] |
| 158 | + |
| 159 | +mech_dms = [] |
| 160 | +for solver in solvers: |
| 161 | + if solver in ["gmres", "bicgstab"]: |
| 162 | + precond_options = { |
| 163 | + "permc_spec": "NATURAL", |
| 164 | + "diag_pivot_thresh": 0.1, |
| 165 | + "fill_factor": 100, |
| 166 | + "options": {"ILU_MILU": "smilu_2"}, |
| 167 | + } |
| 168 | + solver_options = { |
| 169 | + "use_precond": True, |
| 170 | + "atol": 1e-15, |
| 171 | + "maxiter": int(1e5), |
| 172 | + **precond_options, |
| 173 | + } |
| 174 | + use_rcm = True |
| 175 | + else: |
| 176 | + solver_options = {} |
| 177 | + use_rcm = False |
| 178 | + |
| 179 | + start = time.time() |
| 180 | + rho_ss = steadystate( |
| 181 | + H, |
| 182 | + c_ops, |
| 183 | + method=method, |
| 184 | + solver=solver, |
| 185 | + use_rcm=use_rcm, |
| 186 | + **solver_options, |
| 187 | + ) |
| 188 | + end = time.time() |
| 189 | + |
| 190 | + print(f"Solver: {solver}, Time: {np.round(end-start, 5)}") |
| 191 | + rho_mech = ptrace(rho_ss, 1) |
| 192 | + mech_dms.append(rho_mech) |
| 193 | + |
| 194 | +rho_mech = mech_dms[0] |
| 195 | +mech_dms = [mech_dm.data.as_ndarray() for mech_dm in mech_dms] |
| 196 | +``` |
| 197 | + |
| 198 | +### Check Consistency of Solutions |
| 199 | + |
| 200 | + |
| 201 | +Can check to see if the solutions are the same by looking at the number of nonzero elements (NNZ) in the difference between mechanical density matrices. |
| 202 | + |
| 203 | +```python |
| 204 | +for kk in range(len(mech_dms)): |
| 205 | + c = np.where( |
| 206 | + np.abs(mech_dms[kk].flatten() - mech_dms[0].flatten()) > 1e-5 |
| 207 | + )[0] |
| 208 | + print(f"#NNZ for k = {kk} : {len(c)}") |
| 209 | +``` |
| 210 | + |
| 211 | +## Plot the Mechanical Oscillator Wigner Function |
| 212 | + |
| 213 | + |
| 214 | +It is known that the density matrix for the mechanical oscillator is diagonal in the Fock basis due to phase diffusion. If we look at the `hinton()` plot of the density matrix, we can see the magnitude of the diagonal elements is higher, such that the non-diagonal have a vanishing importance. |
| 215 | + |
| 216 | +```python |
| 217 | +hinton(rho_mech.data.as_ndarray(), x_basis=[""] * Nm, y_basis=[""] * Nm); |
| 218 | +``` |
| 219 | + |
| 220 | +However some small off-diagonal terms show up during the factorization process, which we can display by the using `plt.spy()`. |
| 221 | + |
| 222 | +```python |
| 223 | +plt.spy(rho_mech.data.as_ndarray(), markersize=1); |
| 224 | +``` |
| 225 | + |
| 226 | +Therefore, to remove this error, let use explicitly take the diagonal elements and form a new operator out of them. |
| 227 | + |
| 228 | +```python |
| 229 | +diag = rho_mech.diag() |
| 230 | +rho_mech2 = qdiags(diag, 0, dims=rho_mech.dims, shape=rho_mech.shape) |
| 231 | +hinton(rho_mech2, x_basis=[""] * Nm, y_basis=[""] * Nm); |
| 232 | +``` |
| 233 | + |
| 234 | +Now lets compute the oscillator Wigner function and plot it to see if there are any regions of negativity. |
| 235 | + |
| 236 | +```python |
| 237 | +xvec = np.linspace(-20, 20, 256) |
| 238 | +W = wigner(rho_mech2, xvec, xvec) |
| 239 | +wmap = wigner_cmap(W, shift=-1e-5) |
| 240 | +``` |
| 241 | + |
| 242 | +```python |
| 243 | +fig, ax = plt.subplots(figsize=(8, 6)) |
| 244 | +c = ax.contourf(xvec, xvec, W, 256, cmap=wmap) |
| 245 | +ax.set_xlim([-10, 10]) |
| 246 | +ax.set_ylim([-10, 10]) |
| 247 | +plt.colorbar(c, ax=ax); |
| 248 | +``` |
| 249 | + |
| 250 | +## About |
| 251 | + |
| 252 | +```python |
| 253 | +about() |
| 254 | +``` |
| 255 | + |
| 256 | +## Testing |
| 257 | + |
| 258 | +```python |
| 259 | +# assert obtained steady-state via mesolve evolution |
| 260 | +psi0 = tensor(basis(Nc), basis(Nm)) |
| 261 | +rho0 = psi0 @ psi0.dag() |
| 262 | +tlist = np.linspace(0, 1500, 1500) |
| 263 | +rho_evolve = mesolve(H, rho0, tlist, c_ops) |
| 264 | +rho_final = ptrace(rho_evolve.states[-1], 1) |
| 265 | +assert fidelity(rho_mech, rho_final) > 0.99 |
| 266 | + |
| 267 | +# assert steady-state is diagonally dominant |
| 268 | +rho_mat = np.abs(rho_mech.data.to_array()) |
| 269 | +assert np.all(2 * np.diag(rho_mat) >= np.sum(rho_mat, axis=1)) |
| 270 | + |
| 271 | +# assert for negativity in the Wigner function |
| 272 | +assert np.any(W < 0) |
| 273 | +``` |
0 commit comments