1"""
2A collection of :class:`qugrad.QuantumSystem` s for electron spin resonance (ESR)
3devices with a linear array of qubits under the rotating wave approximation.
4"""
5
6import numpy as np
7
8from ._controls import Controls
9from .....hilbert_spaces import QubitSpace
10from ....skeletons.qubits import QubitSystem
11
[docs]
12class SpinChain(Controls, QubitSystem):
13 r"""
14 A :class:`qugrad.QuantumSystem` for a spin chain with electron spin
15 resonance (ESR) controls with a linear array of qubits under the rotating
16 wave approximation. The Hamiltonian is given by
17 $$
18 H(t) = \frac{1}{4} \sum_{i=0}^{\texttt{spins}-1} \left[I_i(t) X_i
19 + Q_i(t)Y_i\right]
20 + \frac{1}{4} \sum_{i=0}^{\texttt{spins}-2} J_i(t)\left[Z_iZ_{i+1}
21 +\cos(\omega_i t)\left[X_iX_{i+1}+Y_iY_{i+1}\right]
22 +\sin(\omega_i t)\left[X_iY_{i+1}-Y_iX_{i+1}\right]\right],
23 $$
24 where
25 $X_i$, $Y_i$, and $Z_i$ are the Pauli-x, -y, and -z operators acting on the
26 $i$th spin, $\omega_i$ are the Zeeman detunings, $J_i(t)$ is the exchange
27 coupling.
28 """
29
30 _ferromagnetic: bool
31 """Whether the exchange coupling is ferromagnetic or antiferromagnetic"""
32
[docs]
33 def __init__(self,
34 spins: int,
35 zeeman_detunings: np.ndarray[float],
36 max_drive_strength: float,
37 J_max: float,
38 J_min: float = 0,
39 feromagnetic: bool = True,
40 use_graph: bool = True):
41 r"""
42 Initialises a spin chain with ESR controls. The Hamiltonian is
43 given by:
44 $$
45 H(t) = \frac{1}{4} \sum_{i=0}^{\texttt{spins}-1} \left[I_i(t) X_i
46 + Q_i(t)Y_i\right]
47 + \frac{1}{4} \sum_{i=0}^{\texttt{spins}-2} J_i(t)\left[Z_iZ_{i+1}
48 +\cos(\omega_i t)\left[X_iX_{i+1}+Y_iY_{i+1}\right]
49 +\sin(\omega_i t)\left[X_iY_{i+1}-Y_iX_{i+1}\right]\right],
50 $$
51 where
52 $X_i$, $Y_i$, and $Z_i$ are the Pauli-x, -y, and -z operators acting on the
53 $i$th spin, $\omega_i$ are the Zeeman detunings, $J_i(t)$ is the exchange
54 coupling.
55
56 Parameters
57 ----------
58 spins : int
59 The number of spins in the chain
60 zeeman_detunings : NDArray[Shape[spins], float]
61 The Zeeman detuning of each of the spins
62 max_drive_strength : float
63 The maximum drive strength that can be applied at a specific
64 frequency and quadrature. That is if their are ``n_drive_ctrl``
65 frequencies and both quadratures are used then the maximum amplitude
66 of the drive that can be applied to the device is::
67
68 np.sqrt(2) * n_drive_ctrl * max_drive_strength
69 J_max : float
70 The minimum value of the exchange coupling $J$
71 J_min : float
72 The maximum value of the exchange coupling $J$, by default 0
73 feromagnetic : bool
74 If ``True``, the exchange coupling is ferromagnetic. If ``False``,
75 the exchange coupling is antiferromagnetic. By default, ``True``.
76 use_graph : bool
77 Whether to use `TensorFlow <https://www.tensorflow.org>`__ graphs
78 during computation, by default ``True``
79 """
80 Controls.__init__(self,
81 zeeman_detunings,
82 max_drive_strength,
83 J_min,
84 J_max)
85 single_qubit_drift_coefficients = np.zeros((spins, 3))
86
87 single_qubit_ctrl_coefficients = \
88 np.stack([np.array([[1/4, 0, 0]]*spins),
89 np.array([[0, 1/4, 0]]*spins)])
90
91 forward_connectivity = np.einsum("ij,jk->ijk",
92 np.eye(spins, spins, 0),
93 np.eye(spins, spins, 1)
94 )[:-1]
95 backward_connectivity = np.einsum("ij,jk->ijk",
96 np.eye(spins, spins, 1),
97 np.eye(spins, spins, -1)
98 )[:-1]
99 connectivity = forward_connectivity + backward_connectivity
100 J_Z = 0.5**3*np.multiply.outer(connectivity, np.diag([0, 0, 1]))
101 J_XX_YY = 0.5**3*np.multiply.outer(connectivity, np.diag([1, 1, 0]))
102 # Check the minus sign convention in the below definition. is it XY-YX
103 # or YX-XY
104
105 J_XY_YX = 0.5**3*np.multiply.outer((np.einsum("ij,j...->ij...", np.eye(spins, spins, 0), np.eye(spins, spins, 1))
106 -np.einsum("ij,j...->ij...", np.eye(spins, spins, 1), np.eye(spins, spins, -1)))[:-1],
107 np.array([[0, 1, 0],
108 [-1, 0, 0],
109 [0, 0, 0]]))
110 # one factor of 0.5 is for double counting, the other two are the
111 # factors of two differences between spin operators and Pauli
112 # operators
113 J = np.stack([J_Z, J_XX_YY, J_XY_YX])
114 if feromagnetic: J *= -1
115 self._ferromagnetic = feromagnetic
116 QubitSystem.__init__(self,
117 QubitSpace(spins),
118 single_qubit_drift_coefficients,
119 np.zeros((spins, spins, 3, 3)),
120 single_qubit_ctrl_coefficients,
121 J,
122 use_graph)
123 @property
124 def ferromagnetic(self) -> bool:
125 """
126 Whether the exchange coupling is ferromagnetic or antiferromagnetic
127 """
128 return self._ferromagnetic