|
| 1 | +--- |
| 2 | +jupyter: |
| 3 | + jupytext: |
| 4 | + text_representation: |
| 5 | + extension: .md |
| 6 | + format_name: markdown |
| 7 | + format_version: '1.3' |
| 8 | + jupytext_version: 1.16.4 |
| 9 | + kernelspec: |
| 10 | + display_name: Python 3 |
| 11 | + name: python3 |
| 12 | +--- |
| 13 | + |
| 14 | +```python |
| 15 | +import jax |
| 16 | +import numpy as np |
| 17 | +import qutip as qt |
| 18 | +import qutip_qoc as qoc |
| 19 | +from matplotlib import pyplot as plt |
| 20 | +``` |
| 21 | + |
| 22 | +# QuTiP - Quantum Optimal Control - GOAT and JOPT |
| 23 | + |
| 24 | + |
| 25 | +This tutorial notebook will guide you through the implementation of a **Hadamard** gate using a python defined control (pulse) functions. |
| 26 | + |
| 27 | +We will optimize the control parameters using the **GOAT** and **JOPT** methods available in `qutip-qoc`. |
| 28 | + |
| 29 | + |
| 30 | + |
| 31 | +The first step is to set up our quantum system in familiar QuTiP fashion. |
| 32 | + |
| 33 | +Find out how you can define your system using QuTiP in our [example notebooks](https://qutip.org/qutip-tutorials/). |
| 34 | + |
| 35 | +```python |
| 36 | +# Define quantum system |
| 37 | +σx, σy, σz = qt.sigmax(), qt.sigmay(), qt.sigmaz() |
| 38 | + |
| 39 | +# Energy splitting, tunnelling, amplitude damping |
| 40 | +ω, Δ, γ, π = 0.1, 1.0, 0.1, np.pi |
| 41 | + |
| 42 | +# Time independent drift Hamiltonian |
| 43 | +H_d = 1 / 2 * (ω * σz + Δ * σx) |
| 44 | + |
| 45 | +# And bake into Liouvillian |
| 46 | +L = qt.liouvillian(H=H_d, c_ops=[np.sqrt(γ) * qt.sigmam()]) |
| 47 | +``` |
| 48 | + |
| 49 | +In addition to the system at hand, we need to specify the control Hamiltonian and the pulse function to be optimized. |
| 50 | + |
| 51 | +```python |
| 52 | +def pulse(t, α): |
| 53 | + """Parameterized pulse function.""" |
| 54 | + return α[0] * np.sin(α[1] * t + α[2]) |
| 55 | + |
| 56 | + |
| 57 | +def pulse_grad(t, α, i): |
| 58 | + """Derivative with respect to α and t (only required for GOAT).""" |
| 59 | + if i == 0: |
| 60 | + return np.sin(α[1] * t + α[2]) |
| 61 | + elif i == 1: |
| 62 | + return α[0] * t * np.cos(α[1] * t + α[2]) |
| 63 | + elif i == 2: |
| 64 | + return α[0] * np.cos(α[1] * t + α[2]) |
| 65 | + elif i == 3: |
| 66 | + return α[0] * α[1] * np.cos(α[1] * t) |
| 67 | +``` |
| 68 | + |
| 69 | +In the same way, we would construct a `quip.QuobjEvo`, we merge the constant Louivillian with our time dependent control Hamiltonians and their parameterized pulse functions. |
| 70 | + |
| 71 | +```python |
| 72 | +# Define the control Hamiltonian |
| 73 | +H_c = [qt.liouvillian(H) for H in [σx, σy]] |
| 74 | + |
| 75 | +# And merge it together with the associated pulse functions. |
| 76 | +H = [ |
| 77 | + L, # Note the additional dictionary specifying our gradient function. |
| 78 | + [H_c[0], lambda t, p: pulse(t, p), {"grad": pulse_grad}], |
| 79 | + [H_c[1], lambda t, q: pulse(t, q), {"grad": pulse_grad}], |
| 80 | +] |
| 81 | + |
| 82 | +# Define the optimization goal |
| 83 | +initial = qt.qeye(2) # Identity |
| 84 | +target = qt.gates.hadamard_transform() |
| 85 | + |
| 86 | +# Super-operator form for open systems |
| 87 | +initial = qt.sprepost(initial, initial.dag()) |
| 88 | +target = qt.sprepost(target, target.dag()) |
| 89 | +``` |
| 90 | + |
| 91 | +When defining initial and target state make sure the dimensions match. |
| 92 | + |
| 93 | +Finally we specify the time interval in which we want to optimize the pulses and the overall objective. |
| 94 | + |
| 95 | +```python |
| 96 | +objective = qoc.Objective(initial, H, target) |
| 97 | +tlist = np.linspace(0, 2 * π, 100) |
| 98 | +``` |
| 99 | + |
| 100 | +We can now run the local optimization using the **GOAT** algorithm. The global optimizer can be enabled by an additional keyword. |
| 101 | + |
| 102 | +```python |
| 103 | +res_goat = qoc.optimize_pulses( |
| 104 | + objectives=objective, |
| 105 | + control_parameters={ |
| 106 | + "p": { |
| 107 | + "guess": [1.0, 1.0, 1.0], |
| 108 | + "bounds": [(-1, 1), (0, 1), (0, 2 * π)], |
| 109 | + }, |
| 110 | + "q": { |
| 111 | + "guess": [1.0, 1.0, 1.0], |
| 112 | + "bounds": [(-1, 1), (0, 1), (0, 2 * π)], |
| 113 | + }, |
| 114 | + }, |
| 115 | + tlist=tlist, |
| 116 | + algorithm_kwargs={ |
| 117 | + "fid_err_targ": 0.01, |
| 118 | + "alg": "GOAT", |
| 119 | + }, |
| 120 | + # uncomment for global optimization |
| 121 | + # optimizer_kwargs={"max_iter": 10} |
| 122 | +) |
| 123 | +``` |
| 124 | + |
| 125 | +```python |
| 126 | +res_goat |
| 127 | +``` |
| 128 | + |
| 129 | +Unfortunately the desired target fidelity could not yet be achieved within the given constraints. |
| 130 | + |
| 131 | +To compare the result with the target visually we can take a quick look at the hinton plot. |
| 132 | + |
| 133 | +```python |
| 134 | +fig, (ax0, ax1, ax2) = plt.subplots(1, 3, figsize=(15, 5)) |
| 135 | +ax0.set_title("Initial") |
| 136 | +ax1.set_title("Final") |
| 137 | +ax2.set_title("Target") |
| 138 | + |
| 139 | +qt.hinton(initial, ax=ax0) |
| 140 | +qt.hinton(res_goat.final_states[0], ax=ax1) |
| 141 | +qt.hinton(target, ax=ax2) |
| 142 | +``` |
| 143 | + |
| 144 | +If we don't have the derivative of our pulse function available, we can use JAX defined pulse functions instead and optimize them with the **JOPT** algorithm. |
| 145 | + |
| 146 | +```python |
| 147 | +def sin_jax(t, α): |
| 148 | + return α[0] * jax.numpy.sin(α[1] * t + α[2]) |
| 149 | +``` |
| 150 | + |
| 151 | +To use the function with `qutip-jax` we need to jit compile them. |
| 152 | + |
| 153 | +```python |
| 154 | +@jax.jit |
| 155 | +def sin_x_jax(t, p, **kwargs): |
| 156 | + return sin_jax(t, p) |
| 157 | + |
| 158 | + |
| 159 | +@jax.jit |
| 160 | +def sin_y_jax(t, q, **kwargs): |
| 161 | + return sin_jax(t, q) |
| 162 | +``` |
| 163 | + |
| 164 | +Since JAX comes with automatic differentiation, we can drop the `"grad"` dictionary and functions. |
| 165 | + |
| 166 | +```python |
| 167 | +H_jax = [L, [H_c[0], sin_x_jax], [H_c[1], sin_y_jax]] |
| 168 | +``` |
| 169 | + |
| 170 | +We simply have to change the ``algorithm_kwargs`` to run the **JOPT** algorithm. |
| 171 | + |
| 172 | +```python |
| 173 | +res_jopt = qoc.optimize_pulses( |
| 174 | + objectives=qoc.Objective(initial, H_jax, target), |
| 175 | + tlist=tlist, |
| 176 | + control_parameters={ |
| 177 | + "p": { |
| 178 | + "guess": [1.0, 1.0, 1.0], |
| 179 | + "bounds": [(-1, 1), (0, 1), (0, 2 * π)], |
| 180 | + }, |
| 181 | + "q": { |
| 182 | + "guess": [1.0, 1.0, 1.0], |
| 183 | + "bounds": [(-1, 1), (0, 1), (0, 2 * π)], |
| 184 | + }, |
| 185 | + }, |
| 186 | + algorithm_kwargs={ |
| 187 | + "alg": "JOPT", # Use the JAX optimizer |
| 188 | + "fid_err_targ": 0.01, |
| 189 | + }, |
| 190 | +) |
| 191 | +``` |
| 192 | + |
| 193 | +We end up with the same result. |
| 194 | + |
| 195 | +```python |
| 196 | +res_jopt |
| 197 | +``` |
| 198 | + |
| 199 | +# Multi-objective and time parameter |
| 200 | + |
| 201 | + |
| 202 | +Both algorithms provide the option for multiple objectives, e.g. to account for variations in the Hamiltonian. |
| 203 | + |
| 204 | +```python |
| 205 | +H_low = [0.95 * L, [0.95 * H_c[0], sin_x_jax], [0.95 * H_c[1], sin_y_jax]] |
| 206 | + |
| 207 | +H_high = [1.05 * L, [1.05 * H_c[0], sin_x_jax], [1.05 * H_c[1], sin_y_jax]] |
| 208 | +``` |
| 209 | + |
| 210 | +```python |
| 211 | +objectives = [ |
| 212 | + qoc.Objective(initial, H_low, target, weight=0.25), |
| 213 | + qoc.Objective(initial, H_jax, target, weight=0.50), |
| 214 | + qoc.Objective(initial, H_high, target, weight=0.25), |
| 215 | +] |
| 216 | +``` |
| 217 | + |
| 218 | +Additionally we can loosen the fixed time constraint to reach the desired target fidelity. |
| 219 | + |
| 220 | +```python |
| 221 | +# same control parameters as before |
| 222 | +control_parameters = { |
| 223 | + k: { |
| 224 | + "guess": [1.0, 1.0, 1.0], |
| 225 | + "bounds": [(-1, 1), (0, 1), (0, 2 * π)], |
| 226 | + } |
| 227 | + for k in ["ctrl_x", "ctrl_y"] |
| 228 | +} |
| 229 | + |
| 230 | +# add magic time parameter |
| 231 | +control_parameters["__time__"] = { |
| 232 | + "guess": tlist[len(tlist) // 3], |
| 233 | + "bounds": [tlist[0], tlist[-1]], |
| 234 | +} |
| 235 | +``` |
| 236 | + |
| 237 | +Again we run the optimization with the **JOPT** algorithm. |
| 238 | + |
| 239 | +```python |
| 240 | +# run optimization |
| 241 | +result = qoc.optimize_pulses( |
| 242 | + objectives, |
| 243 | + control_parameters, |
| 244 | + tlist, |
| 245 | + algorithm_kwargs={ |
| 246 | + "alg": "JOPT", |
| 247 | + "fid_err_targ": 0.01, |
| 248 | + }, |
| 249 | +) |
| 250 | +``` |
| 251 | + |
| 252 | +Finaly we manage to achieve the desired target fidelity by loosening the time constraint. |
| 253 | + |
| 254 | +```python |
| 255 | +result |
| 256 | +``` |
| 257 | + |
| 258 | +```python |
| 259 | +fig, (ax0, ax1, ax2) = plt.subplots(1, 3, figsize=(15, 5)) |
| 260 | +ax0.set_title("Initial") |
| 261 | +ax1.set_title("Final") |
| 262 | +ax2.set_title("Target") |
| 263 | + |
| 264 | +qt.hinton(initial, ax=ax0) |
| 265 | +qt.hinton(result.final_states[0], ax=ax1) |
| 266 | +qt.hinton(target, ax=ax2) |
| 267 | +``` |
0 commit comments