@@ -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