1"""
2A collection of :class:`qugrad.QuantumSystem` s for electron spin resonance (ESR)
3devices.
4"""
5
6from functools import reduce
7
8import numpy as np
9import tensorflow as tf
10
11from scipy.linalg import block_diag
12
13from ._controls import Controls
14from ._device import Device
15from ....hilbert_spaces import QubitSpace
16from ....hilbert_spaces.fermionic import FermionQuditSpace
17from ....pulses.composition import concatenate_functions
18from ....pulses.invertible_functions.scaling import linear_rescaling
19from ...skeletons.qubits import QubitSystem
20from ...skeletons.fermionic import FermionicSystem
21
22PAULI_X = np.array([[0, 1],
23 [1, 0]])
24"""Pauli-x operator"""
25
26PAULI_Z = np.array([[1, 0],
27 [0, -1]])
28"""Pauli-z operator"""
29
[docs]
30class SpinChain(Controls, QubitSystem):
31 r"""
32 A :class:`qugrad.QuantumSystem` for a spin chain with electron spin
33 resonance (ESR) controls. The Hamiltonian is given by
34 $$
35 H(t) = \sum_{i=0}^{\texttt{spins}-1} \frac{1}{2} B_i Z_i
36 + g(t) \sum_{i=0}^{\texttt{spins}-1} \frac{1}{2} X_i
37 + \frac{1}{4} \sum_{i=0}^{\texttt{spins}-2} J_i(t)
38 \vec\sigma_i \cdot \vec\sigma_{i+1},
39 $$
40 where
41 $\vec\sigma_i\equiv\begin{pmatrix}X_i & Y_i & Z_i\end{pmatrix}^\intercal$
42 is the vector of the Pauli-x, -y, and -z operators acting on the $i$th spin,
43 $B_i$ are the Zeeman splittings, $J_i(t)$ is the exchange coupling, and
44 $$
45 g(t) = \sum_{j=0}^{\texttt{spins}-1}\real\left(a_j(t)e^{i\omega_j t}\right),
46 $$
47 is the Rabi drive with frequency components $\omega_j$ and amplitudes
48 $a_j(t)$.
49
50 See Also
51 --------
52 * :class:`SpinChainAngledDrive`
53 * :class:`ValleyChain`
54
55
56 -
57 """
58
59 _ferromagnetic: bool
60 """Whether the exchange coupling is ferromagnetic or antiferromagnetic"""
61
[docs]
62 def __init__(self,
63 spins: int,
64 zeeman_splittings: np.ndarray[float],
65 max_drive_strength: float,
66 J_max: float,
67 J_min: float = 0,
68 feromagnetic: bool = True,
69 use_graph: bool = True):
70 r"""
71 Initialises a spin chain with ESR controls. The Hamiltonian is
72 given by:
73 $$
74 H(t) = \sum_{i=0}^{\texttt{spins}-1} \frac{1}{2} B_i Z_i
75 + g(t) \sum_{i=0}^{\texttt{spins}-1} \frac{1}{2} X_i
76 + \frac{1}{4} \sum_{i=0}^{\texttt{spins}-2} J_i(t)
77 \vec\sigma_i \cdot \vec\sigma_{i+1},
78 $$
79 where
80 $\vec\sigma_i\equiv\begin{pmatrix}X_i&Y_i&Z_i\end{pmatrix}^\intercal$
81 is the vector of the Pauli-x, -y, and -z operators acting on the $i$th
82 spin, $B_i$ corresponds to `zeeman_splittings`, $J_i(t)$ is the exchange
83 coupling, and
84 $$
85 g(t) =
86 \sum_{j=0}^{\texttt{spins}-1}\real\left(a_j(t)e^{i\omega_j t}\right),
87 $$
88 is the Rabi drive with frequency components $\omega_j$ and amplitudes
89 $a_j(t)$.
90
91 Parameters
92 ----------
93 spins : int
94 The number of spins in the chain
95 zeeman_splittings : NDArray[Shape[spins], float]
96 The Zeeman splitting of each of the spins
97 max_drive_strength : float
98 The maximum drive strength that can be applied at a specific
99 frequency and quadrature. That is if their are ``n_drive_ctrl``
100 frequencies and both quadratures are used then the maximum amplitude
101 of the drive that can be applied to the device is::
102
103 np.sqrt(2) * n_drive_ctrl * max_drive_strength
104 J_max : float
105 The minimum value of the exchange coupling $J$
106 J_min : float
107 The maximum value of the exchange coupling $J$, by default 0
108 feromagnetic : bool
109 If ``True``, the exchange coupling is ferromagnetic. If ``False``,
110 the exchange coupling is antiferromagnetic. By default, ``True``.
111 use_graph : bool
112 Whether to use `TensorFlow <https://www.tensorflow.org>`__ graphs
113 during computation, by default ``True``
114 """
115 Controls.__init__(self,
116 zeeman_splittings,
117 max_drive_strength,
118 J_min,
119 J_max)
120 single_qubit_drift_coefficients = \
121 np.multiply.outer(zeeman_splittings, np.array([0, 0, 0.5]))
122
123 single_qubit_ctrl_coefficients = np.array([[[0.5, 0, 0]]*spins])
124
125 forward_connectivity = np.einsum("ij,jk->ijk",
126 np.eye(spins, spins, 0),
127 np.eye(spins, spins, 1)
128 )[:-1]
129 backward_connectivity = np.einsum("ij,jk->ijk",
130 np.eye(spins, spins, 1),
131 np.eye(spins, spins, -1)
132 )[:-1]
133 connectivity = forward_connectivity + backward_connectivity
134 J = 0.5**3*np.multiply.outer(connectivity, np.identity(3))
135 # one factor of 0.5 is for double counting, the other two are the
136 # factors of two differences between spin operators and Pauli
137 # operators
138 if feromagnetic: J *= -1
139 self._ferromagnetic = feromagnetic
140 QubitSystem.__init__(self,
141 QubitSpace(spins),
142 single_qubit_drift_coefficients,
143 np.zeros((spins, spins, 3, 3)),
144 single_qubit_ctrl_coefficients,
145 J,
146 use_graph)
147 @property
148 def ferromagnetic(self) -> bool:
149 """
150 Whether the exchange coupling is ferromagnetic or antiferromagnetic
151 """
152 return self._ferromagnetic
153
[docs]
154class SpinChainAngledDrive(Controls, QubitSystem):
155 r"""
156 A :class:`qugrad.QuantumSystem` for a spin chain with electron spin
157 resonance (ESR) controls. The Hamiltonian is given by
158 $$
159 H(t) = \sum_{i=0}^{\texttt{spins}-1} \frac{1}{2} B_i Z_i
160 + g(t) \sum_{i=0}^{\texttt{spins}-1} \frac{1}{2}
161 \vec v_i \cdot \vec\sigma_i
162 + \frac{1}{4} \sum_{i=0}^{\texttt{spins}-2} J_i(t)
163 \vec\sigma_i \cdot \vec\sigma_{i+1},
164 $$
165 where
166 $\vec\sigma_i\equiv\begin{pmatrix}X_i & Y_i & Z_i\end{pmatrix}^\intercal$
167 is the vector of the Pauli-x, -y, and -z operators acting on the $i$th spin,
168 $B_i$ are the Zeeman splittings, $J_i(t)$ is the exchange coupling,
169 $\vec v_i$ are the drive vectors, and
170 $$
171 g(t) = \sum_{j=0}^{\texttt{spins}-1}\real\left(a_j(t)e^{i\omega_j t}\right),
172 $$
173 is the Rabi drive with frequency components $\omega_j$ and amplitudes
174 $a_j(t)$.
175
176 See Also
177 --------
178 * :class:`SpinChain`
179 * :class:`ValleyChain`
180
181
182 -
183 """
184
185 _ferromagnetic: bool
186 """Whether the exchange coupling is ferromagnetic or antiferromagnetic"""
187
188 _drive_vectors: np.ndarray[float]
189 """The drive vectors for each of the spins"""
190
[docs]
191 def __init__(self,
192 spins: int,
193 zeeman_splittings: np.ndarray[float],
194 drive_vectors: np.ndarray[float],
195 max_drive_strength: float,
196 J_max: float,
197 J_min: float = 0,
198 feromagnetic: bool = True,
199 use_graph: bool = True):
200 r"""
201 Initialises a spin chain with ESR controls. The Hamiltonian is
202 given by:
203 $$
204 H(t) = \sum_{i=0}^{\texttt{spins}-1} \frac{1}{2} B_i Z_i
205 + g(t) \sum_{i=0}^{\texttt{spins}-1} \frac{1}{2}
206 \vec v_i \cdot \vec\sigma_i
207 + \frac{1}{4} \sum_{i=0}^{\texttt{spins}-2} J_i(t)
208 \vec\sigma_i \cdot \vec\sigma_{i+1},
209 $$
210 where
211 $\vec\sigma_i\equiv\begin{pmatrix}X_i&Y_i&Z_i\end{pmatrix}^\intercal$
212 is the vector of the Pauli-x, -y, and -z operators acting on the $i$th
213 spin, $B_i$ corresponds to `zeeman_splittings`, $J_i(t)$ is the exchange
214 coupling, $\vec v_i$ corresponds to `drive_vectors`, and
215 $$
216 g(t) =
217 \sum_{j=0}^{\texttt{spins}-1}\real\left(a_j(t)e^{i\omega_j t}\right),
218 $$
219 is the Rabi drive with frequency components $\omega_j$ and amplitudes
220 $a_j(t)$.
221
222 Parameters
223 ----------
224 spins : int
225 The number of spins in the chain
226 zeeman_splittings : NDArray[Shape[spins], float]
227 The Zeeman splitting of each of the spins
228 drive_vectors : NDArray[Shape[spins, 3], float]
229 The drive vectors for each of the spins
230 max_drive_strength : float
231 The maximum drive strength that can be applied at a specific
232 frequency and quadrature. That is if their are ``n_drive_ctrl``
233 frequencies and both quadratures are used then the maximum amplitude
234 of the drive that can be applied to the device is::
235
236 np.sqrt(2) * n_drive_ctrl * max_drive_strength
237 J_max : float
238 The minimum value of the exchange coupling $J$
239 J_min : float
240 The maximum value of the exchange coupling $J$, by default 0
241 feromagnetic : bool
242 If ``True``, the exchange coupling is ferromagnetic. If ``False``,
243 the exchange coupling is antiferromagnetic. By default, ``True``.
244 use_graph : bool
245 Whether to use `TensorFlow <https://www.tensorflow.org>`__ graphs
246 during computation, by default ``True``
247 """
248 Controls.__init__(self,
249 zeeman_splittings,
250 max_drive_strength,
251 J_min,
252 J_max)
253 self._drive_vectors = np.array(drive_vectors)
254 self._drive_vectors.flags.writeable = False
255 single_qubit_drift_coefficients = \
256 np.multiply.outer(zeeman_splittings, np.array([0, 0, 0.5]))
257
258 forward_connectivity = np.einsum("ij,jk->ijk",
259 np.eye(spins, spins, 0),
260 np.eye(spins, spins, 1)
261 )[:-1]
262 backward_connectivity = np.einsum("ij,jk->ijk",
263 np.eye(spins, spins, 1),
264 np.eye(spins, spins, -1)
265 )[:-1]
266 connectivity = forward_connectivity + backward_connectivity
267 J = 0.5**3*np.multiply.outer(connectivity, np.identity(3))
268 # one factor of 0.5 is for double counting, the other two are the
269 # factors of two differences between spin operators and Pauli
270 # operators
271 if feromagnetic: J *= -1
272 self._ferromagnetic = feromagnetic
273 QubitSystem.__init__(self,
274 QubitSpace(spins),
275 single_qubit_drift_coefficients,
276 np.zeros((spins, spins, 3, 3)),
277 self._drive_vectors,
278 J,
279 use_graph)
280 @property
281 def ferromagnetic(self) -> bool:
282 """
283 Whether the exchange coupling is ferromagnetic or antiferromagnetic
284 """
285 return self._ferromagnetic
286 @property
287 def drive_vectors(self) -> np.ndarray[float]:
288 """The drive vectors for each of the spins"""
289 return self._drive_vectors
290
[docs]
291class ValleyChain(Controls, FermionicSystem):
292 r"""
293 A :class:`qugrad.QuantumSystem` for a linear array of silicon electron
294 quantum dots including the valley degree of freedom and electron spin
295 resonance (ESR) controls. The Hamiltonian is given by
296 $$
297 H(t) = \sum_{\substack{{i,j}\\{\alpha,\beta}}}i_{\alpha}^\dagger j_{\beta}
298 \tilde t^{ij}_{\alpha\beta}
299 +U_0\sum_{i,\left<\alpha,\beta\right>}
300 i_{\alpha}^\dagger i_{\beta}^\dagger i_{\alpha} i_{\beta}
301 +U_1\sum_{i,\alpha,\beta}
302 i_{\alpha}^\dagger i_{\beta}^\dagger i_{\alpha} i_{(\bar\beta_v,\beta_s)},
303 $$
304 where $i_{\alpha}^\dagger$ and $i_{\alpha}$ are the creation and annihilation
305 operators for the $i$th dot and $\alpha$ indexes the spin and valley index
306 degrees of freedom, $\left<\alpha,\beta\right>$ represents unique unordered
307 pairs of $\alpha$ and $\beta$, $(\bar\beta_v,\beta_s)$ has the same spin
308 index as $\beta$ but the oposite valley index, U_0$ is the on-site Coulomb
309 no-valley-flip interaction, $U_1$ is the on-site Coulomb valley-flip
310 interaction, the interdot hoppings have the form
311 $\tilde t^{ij}_{\alpha\beta}=h^{ij}(t)\delta_{\alpha\beta}$, and the
312 intradot hoppings take the form
313 $$
314 \tilde t^{ii}_{\alpha\beta}=\begin{bmatrix}
315 \frac{1}{2}\left(V+Z_i\right)&g(t)^*&&\nu_{\textrm{SO}}^*\\
316 g(t)&\frac{1}{2}\left(V-Z_i\right)&\nu_{\textrm{SO}}&\\
317 &\nu_{\textrm{SO}}^*&\frac{1}{2}\left(-V+Z_i\right)&g(t)^*\\
318 \nu_{\textrm{SO}}&&g(t)&\frac{1}{2}\left(-V-Z_i\right)
319 \end{bmatrix}\begin{matrix}\left(1,\uparrow\right)\\\left(1,\downarrow\right)\\\left(0,\uparrow\right)\\\left(0,\downarrow\right)\end{matrix}
320 $$
321 where $V$ is the valley splitting, $Z_i$ is the Zeeman splitting on the
322 $i$th dot, $\nu_{\textrm{SO}}$ is the valley-spin-orbit coupling, and
323 $$
324 g(t) = \sum_{j=0}^{\texttt{dots}-1}\real\left(a_j(t)e^{i\omega_j t}\right),
325 $$
326 is the Rabi drive with frequency components $\omega_j$ and amplitudes
327 $a_j(t)$.
328
329 See Also
330 --------
331 * :class:`SpinChain`
332 * :class:`SpinChainAngledDrive`
333
334
335 -
336 """
337
338 _u: float
339 """The on-site Coulomb no-valley-flip interaction"""
340
341 _u_valley_flip: float
342 """The on-site Coulomb valley-flip interaction"""
343
344 _valley_spin_orbit_coupling: float
345 """The valley-spin-orbit coupling"""
346
[docs]
347 def __init__(self,
348 dots: int,
349 electrons: int,
350 zeeman_splittings: np.ndarray[complex],
351 valley_splitting: float,
352 u: float,
353 u_valley_flip: float,
354 valley_spin_orbit_coupling: float,
355 max_drive_strength: float,
356 J_max: float,
357 J_min: float = 0,
358 use_graph: bool = True):
359 r"""
360 Initialises a linear array of silicon electron quantum dots including
361 the valley degree of freedom and electron spin resonance (ESR) controls.
362 The Hamiltonian is given by
363 $$
364 H(t) = \sum_{\substack{{i,j}\\{\alpha,\beta}}}
365 i_{\alpha}^\dagger j_{\beta}
366 \tilde t^{ij}_{\alpha\beta}
367 +U_0\sum_{i,\left<\alpha,\beta\right>}
368 i_{\alpha}^\dagger i_{\beta}^\dagger i_{\alpha} i_{\beta}
369 +U_1\sum_{i,\alpha,\beta}
370 i_{\alpha}^\dagger i_{\beta}^\dagger
371 i_{\alpha} i_{(\bar\beta_v,\beta_s)},
372 $$
373 where $i_{\alpha}^\dagger$ and $i_{\alpha}$ are the creation and
374 annihilation operators for the $i$th dot and $\alpha$ indexes the spin
375 and valley index degrees of freedom, $\left<\alpha,\beta\right>$
376 represents unique unordered pairs of $\alpha$ and $\beta$,
377 $(\bar\beta_v,\beta_s)$ has the same spin index as $\beta$ but the
378 oposite valley index, U_0$ is the on-site Coulomb no-valley-flip
379 interaction and corresponds to `u`, $U_1$ is the on-site Coulomb
380 valley-flip interaction and corresponds to `u_valley_flip`, the interdot
381 hoppings have the form
382 $\tilde t^{ij}_{\alpha\beta}=h^{ij}(t)\delta_{\alpha\beta}$, and the
383 intradot hoppings take the form
384 $$
385 \tilde t^{ii}_{\alpha\beta}=\begin{bmatrix}
386 \frac{1}{2}\left(V+Z_i\right)&g(t)^*&&\nu_{\textrm{SO}}^*\\
387 g(t)&\frac{1}{2}\left(V-Z_i\right)&\nu_{\textrm{SO}}&\\
388 &\nu_{\textrm{SO}}^*&\frac{1}{2}\left(-V+Z_i\right)&g(t)^*\\
389 \nu_{\textrm{SO}}&&g(t)&\frac{1}{2}\left(-V-Z_i\right)
390 \end{bmatrix}\begin{matrix}
391 \left(1,\uparrow\right)\\
392 \left(1,\downarrow\right)\\
393 \left(0,\uparrow\right)\\
394 \left(0,\downarrow\right)
395 \end{matrix}
396 $$
397 where $V$ corresponds to `valley_splitting`, $Z_i$ is the Zeeman
398 splitting on the $i$th dot and corresponds `zeeman_splittings`,
399 $\nu_{\textrm{SO}}$ corresponds to `valley_spin_orbit_coupling`, and
400 $$
401 g(t) = \sum_{j=0}^{\texttt{dots}-1}\real\left(a_j(t)e^{i\omega_j t}\right),
402 $$
403 is the Rabi drive with frequency components $\omega_j$ and amplitudes
404 $a_j(t)$.
405
406 Parameters
407 ----------
408 dots : int
409 The number of dots in the array
410 electrons : int
411 The number of electrons in the
412 zeeman_splittings : NDArray[Shape[spins], float]
413 The Zeeman splitting of each of the spins
414 valley_splitting : float
415 The valley splitting
416 u : float
417 The on-site Coulomb no-valley-flip interaction
418 u_valley_flip : float
419 The on-site Coulomb valley-flip interaction
420 valley_spin_orbit_coupling : float
421 The valley-spin-orbit coupling
422 max_drive_strength : float
423 The maximum drive strength that can be applied at a specific
424 frequency and quadrature. That is if their are ``n_drive_ctrl``
425 frequencies and both quadratures are used then the maximum amplitude
426 of the drive that can be applied to the device is::
427
428 np.sqrt(2) * n_drive_ctrl * max_drive_strength
429 J_max : float
430 The minimum value of the exchange coupling $J$
431 J_min : float
432 The maximum value of the exchange coupling $J$, by default 0
433 use_graph : bool
434 Whether to use `TensorFlow <https://www.tensorflow.org>`__ graphs
435 during computation, by default ``True``
436 """
437 # Defining the control amplitude functions
438 rescale_rabi_drive = \
439 linear_rescaling.specify_parameters(min=-max_drive_strength,
440 max=max_drive_strength)
441 rescale_J = linear_rescaling.specify_parameters(min=J_min, max=J_max)
442 hopping_amplitude = lambda x: tf.sqrt(rescale_J(x)*self.u)/2
443 self._rescale_and_concatenate = \
444 concatenate_functions([rescale_rabi_drive, hopping_amplitude])
445 # Initialising the device parameters
446 Device.__init__(self,
447 zeeman_splittings,
448 max_drive_strength,
449 J_min,
450 J_max)
451 self._u = u
452 self._u_valley_flip = u_valley_flip
453 self._valley_spin_orbit_coupling = valley_spin_orbit_coupling
454
455 # Generating the drift hopping Hamiltonian
456 drift_hopping_blocks = []
457 for zeeman_splitting in zeeman_splittings:
458 drift_hopping_blocks.append(
459 valley_spin_orbit_coupling * np.kron(PAULI_X, PAULI_X)
460 + valley_splitting/2 * np.kron(PAULI_Z, np.eye(2))
461 + zeeman_splitting/2 * np.kron(np.eye(2), PAULI_Z))
462 drift_hoppings = block_diag(*drift_hopping_blocks)
463
464 # Generating the Rabi drive Hamiltonian
465 esr_drives = [0.5*np.kron(np.eye(dots*2), PAULI_X)]
466
467 # Generating the interdot hopping Hamiltonian
468 forward_connectivity = np.einsum("ij,jk->ijk",
469 np.eye(dots, dots, 0),
470 np.eye(dots, dots, 1)
471 )[:-1]
472 backward_connectivity = np.einsum("ij,jk->ijk",
473 np.eye(dots, dots, 1),
474 np.eye(dots, dots, -1)
475 )[:-1]
476 copuling_matrix = np.identity(4)
477 # When hopping in the oposite direction the oposite rotation is picked
478 # up to ensure hermiticity
479 reverse_coupling_matrix = copuling_matrix.T.conj()
480 interdot_hoppings = \
481 np.kron(forward_connectivity,
482 np.expand_dims(copuling_matrix, axis=0)) \
483 + np.kron(backward_connectivity,
484 np.expand_dims(reverse_coupling_matrix, axis=0))
485
486 ctrl_hoppings = np.concatenate([esr_drives, interdot_hoppings], axis=0)
487
488 # Generating the Coulomb interaction Hamiltonian
489 # No spin flip
490 spin_coupling = np.einsum("ij,kl->ikjl", np.identity(2), np.identity(2))
491 # Valley couplings
492 hartree_coupling = \
493 np.einsum("ij,kl->ikjl", np.identity(2), np.identity(2))
494 single_flip_valley_coupling = \
495 np.einsum("ij,kl,jk->ijkl", np.identity(2), PAULI_X, np.identity(2))\
496 +np.einsum("ij,kl,jk->ijlk", np.identity(2), PAULI_X, np.identity(2))\
497 +np.einsum("ij,kl,jk->iljk", np.identity(2), PAULI_X, np.identity(2))\
498 +np.einsum("ij,kl,jk->lijk", np.identity(2), PAULI_X, np.identity(2))
499 valley_couplings = -u * hartree_coupling \
500 -u_valley_flip * single_flip_valley_coupling
501 # On-site Coulomb only
502 dot_couplings = np.einsum("ij,kl,jk->ijkl",
503 np.identity(dots),
504 np.identity(dots),
505 np.identity(dots))
506 coulomb_integrals = 0.5*reduce(np.kron, [dot_couplings,
507 valley_couplings,
508 spin_coupling])
509 # The 0.5 accounts for double counting
510 FermionicSystem.__init__(self,
511 FermionQuditSpace(dots, 4, electrons),
512 drift_hoppings,
513 coulomb_integrals,
514 ctrl_hoppings,
515 use_graph)
516
517 @property
518 def u(self) -> float:
519 """The on-site Coulomb no-valley-flip interaction"""
520 return self._u
521 @property
522 def u_valley_flip(self) -> float:
523 """The on-site Coulomb valley-flip interaction"""
524 return self._u_valley_flip
525 @property
526 def valley_spin_orbit_coupling(self) -> float:
527 """The valley-spin-orbit coupling"""
528 return self._valley_spin_orbit_coupling