Skip to content

Commit 318a7eb

Browse files
committed
Allow resonator tail in EOM, add plotting kwargs
1 parent 1cef5ac commit 318a7eb

4 files changed

Lines changed: 2133 additions & 1910 deletions

File tree

examples/00_harmonic_well.ipynb

Lines changed: 591 additions & 597 deletions
Large diffs are not rendered by default.

examples/06_single_electron_trap.ipynb

Lines changed: 1424 additions & 1266 deletions
Large diffs are not rendered by default.

quantum_electron/electron_counter.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ def get_electron_positions(self, n_electrons: int, electron_initial_positions: O
444444

445445
return best_res
446446

447-
def plot_electron_positions(self, res: dict, ax=None, color: str = 'mediumseagreen', marker_size: float = 10.0) -> None:
447+
def plot_electron_positions(self, res: dict, ax=None, color: str = 'mediumseagreen', marker_size: float = 10.0, shadow: bool=True, **kwargs) -> None:
448448
"""Plot electron positions obtained from get_electron_positions
449449
450450
Args:
@@ -455,11 +455,17 @@ def plot_electron_positions(self, res: dict, ax=None, color: str = 'mediumseagre
455455
x, y = r2xy(res['x'])
456456

457457
if ax is None:
458-
plt.plot(x*1e6, y*1e6, 'ok', mfc=color, mew=0.5, ms=marker_size,
459-
path_effects=[pe.SimplePatchShadow(), pe.Normal()])
458+
if shadow:
459+
plt.plot(x*1e6, y*1e6, 'ok', mfc=color, mew=0.5, ms=marker_size,
460+
path_effects=[pe.SimplePatchShadow(), pe.Normal()], **kwargs)
461+
else:
462+
plt.plot(x*1e6, y*1e6, 'ok', mfc=color, mew=0.5, ms=marker_size, **kwargs)
460463
else:
461-
ax.plot(x*1e6, y*1e6, 'ok', mfc=color, mew=0.5, ms=marker_size,
462-
path_effects=[pe.SimplePatchShadow(), pe.Normal()])
464+
if shadow:
465+
ax.plot(x*1e6, y*1e6, 'ok', mfc=color, mew=0.5, ms=marker_size,
466+
path_effects=[pe.SimplePatchShadow(), pe.Normal()], **kwargs)
467+
else:
468+
ax.plot(x*1e6, y*1e6, 'ok', mfc=color, mew=0.5, ms=marker_size, **kwargs)
463469

464470
def animate_voltage_sweep(self, fig, ax, list_of_voltages: list, list_of_electron_positions: list, coor: tuple = (0, 0), dxdy: tuple = (2, 2),
465471
frame_interval_ms: int = 10, print_voltages: bool = False) -> matplotlib.animation.FuncAnimation:

quantum_electron/eom_solver.py

Lines changed: 107 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -36,51 +36,98 @@ def __init__(self, Ex: callable, Ey: callable, Ex_up: callable, Ex_down: callabl
3636
self.curv_xy = curv_xy
3737
self.curv_yy = curv_yy
3838

39+
def _get_common_mode_idx(self, eigenvectors):
40+
"""Helper function to determine the common mode index from eigenvectors in the charge basis
41+
Assumes a 2 node circuit, there is only 1 common mode and 1 diff mode
42+
43+
Args:
44+
eigenvectors (ArrayLike): NDArray such that eigenvectors[n] is an eigenvector
45+
46+
Returns:
47+
int: index of the common mode
48+
"""
49+
for k, ev in enumerate(eigenvectors):
50+
if np.sign(ev[0]) == np.sign(ev[1]):
51+
return k
52+
53+
def _get_diff_mode_idx(self, eigenvectors):
54+
"""Helper function to determine the differential mode index from eigenvectors in the charge basis
55+
Assumes a 2 node circuit, there is only 1 common mode and 1 diff mode
56+
57+
Args:
58+
eigenvectors (ArrayLike): NDArray such that eigenvectors[n] is an eigenvector
59+
60+
Returns:
61+
int: index of the common mode
62+
"""
63+
for k, ev in enumerate(eigenvectors):
64+
if np.sign(ev[0]) != np.sign(ev[1]):
65+
return k
66+
3967
def setup_eom_coupled_lc(self, ri: ArrayLike,
4068
resonator_dict: Dict) -> tuple[ArrayLike]:
4169
"""
4270
Set up the Matrix used for determining the electron motional frequencies and cavity frequency.
4371
This function is used for the coupled LC resonator model. The electrons are located in between the plates of the
4472
capacitor Cdot.
73+
74+
For the equations used in this function, please see
75+
https://journals.aps.org/prapplied/abstract/10.1103/PhysRevApplied.23.024001
4576
4677
Args:
4778
ri (ArrayLike): Electron positions, in the form [x0, y0, x1, y1, ...]
48-
resonator_dict (Dict): Dictionary containing the parameters of the resonator. Must have L1, L2, C1, C2, Cdot, mode.
49-
Here L1, C1 are the inductance and capacitance of the first resonator, L2, C2 are the inductance and capacitance of the second resonator.
79+
resonator_dict (Dict): Dictionary containing the parameters of the resonator. Must have La, Lb, Ca, Cb, Cdot, mode. Ltail is optional.
80+
Here La, Ca are the inductance and capacitance of the first resonator, Lb, Cb are the inductance and capacitance of the second resonator.
5081
Cdot is the coupling capacitance between the two resonators. The mode key sets the f0 parameter and is used in get_cavity_frequency_shift.
5182
5283
Returns:
53-
tuple[ArrayLike]: kinetic matrix K, and mass matrix M
84+
tuple[ArrayLike]: (kinetic matrix K aka [L], and mass matrix M aka [C]^-1) OR if Ltail is nonzero ([L]^-1 [C]^-1)
5485
"""
55-
C1 = resonator_dict['C1']
56-
C2 = resonator_dict['C2']
86+
Ca = resonator_dict['Ca']
87+
Cb = resonator_dict['Cb']
5788
Cdot = resonator_dict['Cdot']
58-
L1 = resonator_dict['L1']
59-
L2 = resonator_dict['L2']
60-
89+
La = resonator_dict['La']
90+
Lb = resonator_dict['Lb']
91+
92+
# Figure out if the Ltail argument is present and nonzero
93+
resonator_has_tail = True if ('Ltail' in resonator_dict.keys()) and abs(resonator_dict['Ltail']) > 0 else False
94+
6195
self.num_cavity_modes = 2
6296

6397
# We first solve the cavity equations without electrons to identify the
6498
# common and differential modes
65-
D = C1 * C2 + C1 * Cdot + C2 * Cdot
66-
67-
# Mass matrix of the cavity only
68-
M = np.array([[L1, 0],
69-
[0, L2]])
99+
D = Ca * Cb + Ca * Cdot + Cb * Cdot
70100

71101
# Kinetic matrix of the cavity only
72-
K = np.array([[(C2 + Cdot) / D, Cdot / D],
73-
[Cdot / D, (C1 + Cdot) / D]])
74-
75-
eigenvalues, _ = scipy.linalg.eigh(K, b=M)
76-
f0, f1 = np.sqrt(eigenvalues) / (2 * np.pi)
77-
78-
# The differential mode is the smaller, because the coupling
79-
# capacitance adds to the resonance
80-
self.f0_diff = np.min([f0, f1])
81-
# The common mode is higher, because the coupling capacitance doesn't
82-
# participate in the resonance.
83-
self.f0_comm = np.max([f0, f1])
102+
K = np.array([[(Cb + Cdot) / D, Cdot / D],
103+
[Cdot / D, (Ca + Cdot) / D]])
104+
105+
# For the inductance matrix we have to be careful depending on whether there is a tail.
106+
# For example, if Ltail is 0, we better use the regular equations of motion.
107+
if resonator_has_tail:
108+
Ltail = resonator_dict['Ltail']
109+
110+
# Calculate the inductances of the effective circuit from the y-delta transform
111+
L1 = (La * Lb + Lb * Ltail + La * Ltail) / La
112+
L2 = (La * Lb + Lb * Ltail + La * Ltail) / Lb
113+
L3 = (La * Lb + Lb * Ltail + La * Ltail) / Ltail
114+
115+
# Mass matrix of the cavity only
116+
Minv = np.array([[1/L1 + 1/L3, -1/L3],
117+
[-1/L3, 1/L2 + 1/L3]])
118+
119+
eigenvalues, eigenvecs = np.linalg.eig(Minv @ K)
120+
121+
else:
122+
# Mass matrix of the cavity only
123+
M = np.array([[La, 0],
124+
[0, Lb]])
125+
126+
eigenvalues, eigenvecs = scipy.linalg.eigh(K, b=M)
127+
128+
# Come up with a different way to identify common vs. diff mode based on eigenvectors
129+
self.f0_diff = np.sqrt(eigenvalues)[self._get_diff_mode_idx(eigenvecs.T)] / (2 * np.pi)
130+
self.f0_comm = np.sqrt(eigenvalues)[self._get_common_mode_idx(eigenvecs.T)] / (2 * np.pi)
84131

85132
if resonator_dict['mode'] == 'comm':
86133
self.f0 = self.f0_comm
@@ -92,29 +139,34 @@ def setup_eom_coupled_lc(self, ri: ArrayLike,
92139

93140
num_electrons = int(len(ri) / 2)
94141
xe, ye = r2xy(ri)
95-
96-
# Set up the inverse of the mass matrix first
97-
M = np.diag(np.array([L1] + [L2] + [m_e] * (2 * num_electrons)))
142+
143+
# Depending on whether Ltail is supplied we either construct the mass matrix containing L on the diagonals
144+
# OR we construct the inverse of this matrix containing inverse inductances and inverse masses.
145+
if resonator_has_tail:
146+
Minv = np.diag(np.array([1/L1 + 1/L3] + [1/L2 + 1/L3] + [1/m_e] * (2 * num_electrons)))
147+
Minv[0, 1] = Minv[1, 0] = -1/L3
148+
else:
149+
M = np.diag(np.array([La] + [Lb] + [m_e] * (2 * num_electrons)))
98150

99151
# Set up the kinetic matrix next
100-
Kij_plus, Kij_minus, Lij = np.zeros(np.shape(M)), np.zeros(
101-
np.shape(M)), np.zeros(np.shape(M))
152+
Kij_plus, Kij_minus, Lij = np.zeros(2 * num_electrons + 2), np.zeros(
153+
2 * num_electrons + 2), np.zeros(2 * num_electrons + 2)
102154
K = np.zeros((2 * num_electrons + 2, 2 * num_electrons + 2))
103155

104156
# Row 1 and column 1 only have bare cavity information, and
105157
# cavity-electron terms
106-
K[:2, :2] = np.array([[(C2 + Cdot) / D, Cdot / D],
107-
[Cdot / D, (C1 + Cdot) / D]])
158+
K[:2, :2] = np.array([[(Cb + Cdot) / D, Cdot / D],
159+
[Cdot / D, (Ca + Cdot) / D]])
108160

109161
K[2:num_electrons + 2, 0] = K[0, 2:num_electrons + 2] = q_e / D * \
110-
((C2 + Cdot) * self.Ex_up(xe, ye) + Cdot * self.Ex_down(xe, ye))
162+
((Cb + Cdot) * self.Ex_up(xe, ye) + Cdot * self.Ex_down(xe, ye))
111163
K[2:num_electrons + 2, 1] = K[1, 2:num_electrons + 2] = q_e / D * \
112-
((C1 + Cdot) * self.Ex_down(xe, ye) + Cdot * self.Ex_up(xe, ye))
164+
((Ca + Cdot) * self.Ex_down(xe, ye) + Cdot * self.Ex_up(xe, ye))
113165

114166
K[num_electrons + 2:2 * num_electrons + 2, 0] = K[0, num_electrons + 2:2 * num_electrons +
115-
2] = q_e / D * ((C2 + Cdot) * self.Ey_up(xe, ye) + Cdot * self.Ey_down(xe, ye))
167+
2] = q_e / D * ((Cb + Cdot) * self.Ey_up(xe, ye) + Cdot * self.Ey_down(xe, ye))
116168
K[num_electrons + 2:2 * num_electrons + 2, 1] = K[1, num_electrons + 2:2 * num_electrons +
117-
2] = q_e / D * ((C1 + Cdot) * self.Ey_down(xe, ye) + Cdot * self.Ey_up(xe, ye))
169+
2] = q_e / D * ((Ca + Cdot) * self.Ey_down(xe, ye) + Cdot * self.Ey_up(xe, ye))
118170

119171
kij_plus = np.zeros((num_electrons, num_electrons))
120172
kij_minus = np.zeros((num_electrons, num_electrons))
@@ -170,7 +222,12 @@ def setup_eom_coupled_lc(self, ri: ArrayLike,
170222
K[2:num_electrons + 2, num_electrons + 2:2 * num_electrons + 2] = Lij
171223
K[num_electrons + 2:2 * num_electrons + 2, 2:num_electrons + 2] = Lij
172224

173-
return K, M
225+
if resonator_has_tail:
226+
# Don't want to just return the inverse of Minv, because it can contain very large numbers if Ltail is small.
227+
# The different output is handled in solve_eom
228+
return Minv @ K, None
229+
else:
230+
return K, M
174231

175232
def setup_eom(self, ri: ArrayLike,
176233
resonator_dict: Dict) -> tuple[ArrayLike]:
@@ -291,15 +348,20 @@ def solve_eom(self, LHS: ArrayLike, RHS: ArrayLike, filter_nan: bool = False,
291348
292349
Args:
293350
LHS (ArrayLike): K, analog of the spring constant matrix.
294-
RHS (ArrayLike): M, analog of the mass matrix.
351+
RHS (ArrayLike, optional): M, analog of the mass matrix. In the case of a resonator tail, one can set this to 0 as long as LHS = L^-1 C^-1
295352
sort_by_cavity_participation (bool, optional): Sorts the eigenvalues/vectors by the participation in the first element of the eigenvector. Defaults to True.
296353
297354
Returns:
298355
tuple[ArrayLike]: Eigenfrequencies, Eigenvectors
299356
"""
300357

301358
# EVals, EVecs = np.linalg.eig(np.dot(np.linalg.inv(RHS), LHS))
302-
EVals, EVecs = scipy.linalg.eigh(LHS, b=RHS)
359+
if RHS is not None:
360+
EVals, EVecs = scipy.linalg.eigh(LHS, b=RHS)
361+
else:
362+
# This case applies if there is a tail, in this case the EOM are different and we don't have
363+
# a separate RHS matrix from setup_eom_coupled_lc
364+
EVals, EVecs = np.linalg.eig(LHS)
303365

304366
if sort_by_cavity_participation:
305367
# The cavity participation is the first element of each
@@ -337,7 +399,7 @@ def get_cavity_frequency_shift(
337399
return eigenfrequencies[0] - self.f0
338400

339401
def plot_eigenvector(self, electron_positions: ArrayLike,
340-
eigenvector: ArrayLike, length: float = 0.5, color: str = 'k') -> None:
402+
eigenvector: ArrayLike, ax=None, length: float = 0.5, color: str = 'k', **kwargs) -> None:
341403
"""Plots the eigenvector at the electron positions.
342404
343405
Args:
@@ -349,6 +411,9 @@ def plot_eigenvector(self, electron_positions: ArrayLike,
349411
e_x, e_y = r2xy(electron_positions)
350412
N_e = len(e_x)
351413

414+
if ax is None:
415+
ax = plt.gca()
416+
352417
# The first index of the eigenvector contains the charge displacement, thus we look at the second index and beyond.
353418
# Normalize the vector to 'length'
354419
evec_norm = eigenvector[self.num_cavity_modes:] / \
@@ -361,8 +426,8 @@ def plot_eigenvector(self, electron_positions: ArrayLike,
361426

362427
for e_idx in range(len(e_x)):
363428
width = 0.025
364-
plt.arrow(e_x[e_idx] * 1e6, e_y[e_idx] * 1e6, dx=dxs[e_idx], dy=dys[e_idx], width=width, head_length=1.5 * 3 * width, head_width=3.5 * width,
365-
edgecolor='k', lw=0.4, facecolor=color)
429+
ax.arrow(e_x[e_idx] * 1e6, e_y[e_idx] * 1e6, dx=dxs[e_idx], dy=dys[e_idx], width=width, head_length=1.5 * 3 * width, head_width=3.5 * width,
430+
edgecolor='k', lw=0.4, facecolor=color, **kwargs)
366431

367432
def animate_eigenvectors(self, fig, axs_list: list, eigenvector_list: List[ArrayLike], electron_positions: ArrayLike, marker_size: float = 10,
368433
amplitude: float = 0.5e-6, time_points: int = 31, frame_interval_ms: int = 10):

0 commit comments

Comments
 (0)