sodetlib 0.6.1rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,508 @@
1
+ '''
2
+ Given a complex s21 sweep, returns the data fit to the resonator model and the
3
+ resonator parameters and provides tools for plotting the fit results.
4
+
5
+ Based on equation 11 from Kahlil et al. and adapted from Columbia KIDs open
6
+ source analysis code.
7
+ '''
8
+ import numpy as np
9
+ from lmfit import Model
10
+ import matplotlib.pyplot as plt
11
+ from matplotlib.gridspec import GridSpec
12
+
13
+ def linear_resonator(f, f_0, Q, Q_e_real, Q_e_imag):
14
+ '''
15
+ Function for a resonator with asymmetry parameterized by the imaginary
16
+ part of ``Q_e``. The real part of ``Q_e`` is what we typically refer to as
17
+ the coupled Q, ``Q_c``.
18
+ '''
19
+ Q_e = Q_e_real + 1j*Q_e_imag
20
+ return (1 - (Q * Q_e**(-1) /(1 + 2j * Q * (f - f_0) / f_0) ) )
21
+
22
+ def cable_delay(f, delay, phi, f_min):
23
+ '''
24
+ Function implements a time delay (phase variation linear with frequency).
25
+ '''
26
+ return np.exp(1j * (-2 * np.pi * (f - f_min) * delay + phi))
27
+
28
+ def general_cable(f, delay, phi, f_min, A_mag, A_slope):
29
+ '''
30
+ Function implements a time delay (phase variation linear with frequency) and
31
+ attenuation slope characterizing a background RF cable transfer function.
32
+ '''
33
+ phase_term = cable_delay(f,delay,phi,f_min)
34
+ magnitude_term = ((f-f_min)*A_slope + 1)* A_mag
35
+ return magnitude_term*phase_term
36
+
37
+ def resonator_cable(f, f_0, Q, Q_e_real, Q_e_imag, delay, phi, f_min, A_mag,
38
+ A_slope):
39
+ '''
40
+ Function that includes asymmetric resonator (``linear_resonator``) and cable
41
+ transfer functions (``general_cable``). Which most closely matches our full
42
+ measured transfer function.
43
+ '''
44
+ resonator_term = linear_resonator(f, f_0, Q, Q_e_real, Q_e_imag)
45
+ cable_term = general_cable(f, delay, phi, f_min, A_mag, A_slope)
46
+ return cable_term*resonator_term
47
+
48
+ def full_fit(freqs, real, imag):
49
+ '''
50
+ Fitting function that takes in frequency and real and imaginary parts of the
51
+ transmission of a resonator (needs to trimmed down to only data for a
52
+ single resonator) and returns fitted parameters to the ``resonator_cable``
53
+ model.
54
+
55
+ Args
56
+ ----
57
+ freqs : float ndarray
58
+ Frequencies that line up with complex transmission data.
59
+ real : float ndarray
60
+ Real part of resonator complex transmission to be fit
61
+ imag : float ndarray
62
+ Imaginary part of resonator complex transmission to be fit.
63
+
64
+ Returns
65
+ -------
66
+ result : (`lmfit.Model.ModelResult`)
67
+ This is a class of lmfit that contains all of the fitted parameters as
68
+ well as a number of other pieces of data and metadata related to the fit
69
+ and some helper functions for plotting and manipulating the data in the
70
+ object.
71
+ '''
72
+ s21_complex = np.vectorize(complex)(real, imag)
73
+
74
+ #set our initial guesses
75
+ argmin_s21 = np.abs(s21_complex).argmin()
76
+ fmin = freqs.min()
77
+ fmax = freqs.max()
78
+ f_0_guess = freqs[argmin_s21]
79
+ Q_min = 0.1 * (f_0_guess / (fmax - fmin))
80
+ delta_f = np.diff(freqs)
81
+ min_delta_f = delta_f[delta_f > 0].min()
82
+ Q_max = f_0_guess / min_delta_f
83
+ Q_guess = np.sqrt(Q_min * Q_max)
84
+ s21_min = np.abs(s21_complex[argmin_s21])
85
+ s21_max = np.abs(s21_complex).max()
86
+ Q_e_real_guess = Q_guess / (1 - s21_min / s21_max)
87
+ A_slope, A_offset = np.polyfit(freqs - fmin, np.abs(s21_complex), 1)
88
+ A_mag = A_offset
89
+ A_mag_slope = A_slope / A_mag
90
+ phi_slope, phi_offset = np.polyfit(freqs - fmin, np.unwrap(np.angle(s21_complex)), 1)
91
+ delay = -phi_slope / (2 * np.pi)
92
+
93
+ #make our model
94
+ totalmodel = Model(resonator_cable)
95
+ params = totalmodel.make_params(f_0=f_0_guess,
96
+ Q=Q_guess,
97
+ Q_e_real=Q_e_real_guess,
98
+ Q_e_imag=0,
99
+ delay=delay,
100
+ phi=phi_offset,
101
+ f_min=fmin,
102
+ A_mag=A_mag,
103
+ A_slope=A_mag_slope)
104
+ #set some bounds
105
+ params['f_0'].set(min=freqs.min(), max=freqs.max())
106
+ params['Q'].set(min=Q_min, max=Q_max)
107
+ params['Q_e_real'].set(min=1, max=1e7)
108
+ params['Q_e_imag'].set(min=-1e7, max=1e7)
109
+ params['phi'].set(min=phi_offset-np.pi, max=phi_offset+np.pi)
110
+
111
+ #fit it
112
+ result = totalmodel.fit(s21_complex, params, f=freqs)
113
+ return result
114
+
115
+ def get_qi(Q, Q_e_real, Q_e_imag):
116
+ '''
117
+ Function for deriving the internal quality factor from the fitted quality
118
+ factors (Q and Qc).
119
+
120
+ Args
121
+ ----
122
+ Q : float
123
+ Total resonator quality factor output parameter of ``full_fit``
124
+ Q_e_real : float
125
+ Resonator coupled quality factor output parameter of ``full_fit``
126
+ Returns
127
+ -------
128
+ Qi : float
129
+ Resonator internal quality factor.
130
+ '''
131
+ Qi = (Q**-1 - np.real((Q_e_real+1j*Q_e_imag)**-1))**-1
132
+ return Qi
133
+
134
+ def get_br(Q, f_0):
135
+ '''
136
+ Function for deriving the resonator bandwidth from the fit results.
137
+
138
+ Args
139
+ ----
140
+ Q : float
141
+ Total resonator quality factor output parameter of ``full_fit``
142
+ f_0 : float
143
+ Resonance frequency output parameter of ``full_fit`` in Hz.
144
+ Returns
145
+ -------
146
+ br : float
147
+ Resonator bandwidth in Hz.
148
+ '''
149
+ br = f_0/Q
150
+ return br
151
+
152
+ def reduced_chi_squared(ydata, ymod, n_param=9, sd=None):
153
+ '''
154
+ Reduced chi squared in lmfit does not return something reasonable so this
155
+ is a handwritten function to calculate it since we want standard deviation
156
+ to be the complex error.
157
+
158
+ Args
159
+ ----
160
+ ydata : float ndarray
161
+ complex data to calculate reduced chi squared on.
162
+ ymod : float ndarray
163
+ model fit result of ydata at same x-locations as ydata is sampled.
164
+ n_param : int
165
+ Number of parameters in the fit.
166
+ sd : float ndarray
167
+ standard deviation of data
168
+ Returns
169
+ -------
170
+ br : float
171
+ Resonator bandwidth in Hz.
172
+ '''
173
+ if sd is None:
174
+ sdr = np.std(np.real(ydata))
175
+ sdi = np.std(np.imag(ydata))
176
+ sd = sdr + 1j*sdi
177
+ chisq = np.sum((np.real(ydata) - np.real(ymod))**2/((np.real(sd))**2)) +\
178
+ np.sum((np.imag(ydata) - np.imag(ymod))**2/((np.imag(sd))**2))
179
+ nu=2*ydata.size-n_param #multiply the usual by 2 since complex
180
+ red_chisq = chisq/nu
181
+ return chisq, red_chisq
182
+
183
+ def fit_tune(tunefile):
184
+ """
185
+ Automated fitting of resonator parameters from one tuning file.
186
+
187
+ Args
188
+ ----
189
+ tunefile : str, filepath
190
+ Full directories of one tunning file.
191
+ Returns
192
+ -------
193
+ dres : dict
194
+ a dictionary containing all of the fit results for all resonances in the provided tunefile. The keys are organized as::
195
+
196
+ {
197
+ tunefile: str, filepath - full directories of one tunning file
198
+ band: 1d int array with shape (ndets) - smurf band for each detector
199
+ channels : 1d int array with shape (ndets) - Smurf channel for each
200
+ detector. If unassigned, set to -1
201
+ res_index: 1d int array with shape (ndets) -- Index of the resonator within each band
202
+ res_freqs: 1d int array of resonance freqs (as is found in the tunefile)
203
+ model_params : dictionary for 9 parameters of resonator_cable
204
+ f_0 : 1d array with shape (ndets)
205
+ Q : 1d array with shape (ndets)
206
+ Q_e_real : 1d array with shape (ndets)
207
+ Q_e_imag: 1d array with shape (ndets)
208
+ delay: 1d array with shape (ndets)
209
+ phi: 1d array with shape (ndets)
210
+ f_min: 1d array with shape (ndets)
211
+ A_mag: 1d array with shape (ndets)
212
+ A_slope: 1d array with shape (ndets)
213
+ derived_params : dictionary for derived parameters
214
+ Qi: 1d array with shape (ndets)
215
+ br: 1d array with shape (ndets)
216
+ depth: 1d array with shape (ndets)
217
+ find_freq_ctime : 1d string array with shape (ndets) - ctime find_freq was taken
218
+ S21: 2d array (ndets, nsamps) of measured S21
219
+ scan_freqs: 2d array (ndets, ndsamps) of freqs used for S21 scan
220
+ chi2 : 2d float array of shape (ndets x 2) - chi-squared goodness of fit
221
+ sid : str, session ID
222
+ }
223
+ """
224
+ dres = {}
225
+ data = np.load(tunefile,allow_pickle=True).item()
226
+
227
+ dres['tunefile'] = tunefile
228
+ model_params = {'f_0': [], 'Q': [], 'Q_e_real' : [], 'Q_e_imag': [],
229
+ 'delay': [], 'phi': [], 'f_min': [], 'A_mag': [],
230
+ 'A_slope': []}
231
+ derived_params = {'Qi': [], 'br': [], 'depth': []}
232
+ bands = []
233
+ channels = []
234
+ res_index = []
235
+ chi2 = []
236
+ sid = []
237
+ find_freq_ctime = []
238
+ S21s = []
239
+ scan_freqs = []
240
+ res_freqs = []
241
+
242
+ for band in sorted(list(data.keys())):
243
+ if 'resonances' in list(data[band].keys()):
244
+ for idx in list(data[band]['resonances'].keys()):
245
+ scan=data[band]['resonances'][idx]
246
+ f=scan['freq_eta_scan']
247
+ if (band > 3) & (np.mean(f) > 6000):
248
+ f-=2000
249
+ S21=scan['resp_eta_scan']
250
+ result=full_fit(f,S21.real,S21.imag)
251
+
252
+ # Need to check if this plays well with being in a dict/pickling
253
+ S21_mod = result.best_fit.real+1j*result.best_fit.imag
254
+
255
+ bands.append(band)
256
+ channels.append(scan['channel'])
257
+ res_index.append(idx)
258
+ res_freqs.append(scan['freq'])
259
+ chi2.append(reduced_chi_squared(S21, S21_mod))
260
+ find_freq_ctime.append(data[band]['find_freq']['timestamp'][0])
261
+ S21s.append(S21)
262
+ scan_freqs.append(f)
263
+
264
+ model_params['f_0'].append(result.best_values['f_0'])
265
+ model_params['Q'].append(result.best_values['Q'])
266
+ model_params['Q_e_real'].append(result.best_values['Q_e_real'])
267
+ model_params['Q_e_imag'].append(result.best_values['Q_e_imag'])
268
+ model_params['delay'].append(result.best_values['delay'])
269
+ model_params['phi'].append(result.best_values['phi'])
270
+ model_params['f_min'].append(result.best_values['f_min'])
271
+ model_params['A_mag'].append(result.best_values['A_mag'])
272
+ model_params['A_slope'].append(result.best_values['A_slope'])
273
+
274
+ Qi = get_qi(result.best_values['Q'], result.best_values['Q_e_real'],
275
+ result.best_values['Q_e_imag'])
276
+ br = get_br(result.best_values['Q'], result.best_values['f_0'])
277
+ depth = np.abs(S21_mod).max()/np.abs(S21_mod).min()
278
+ derived_params['Qi'].append(Qi)
279
+ derived_params['br'].append(br)
280
+ derived_params['depth'].append(depth)
281
+
282
+ bands = np.array(bands)
283
+ channels = np.array(channels)
284
+ res_index = np.array(res_index)
285
+ S21s = np.array(S21s)
286
+ scan_freqs = np.array(scan_freqs)
287
+ chi2 = np.array(chi2)
288
+ sid = np.array(sid)
289
+ for par in model_params:
290
+ model_params[par] = np.array(model_params[par])
291
+ for par in derived_params:
292
+ derived_params[par] = np.array(derived_params[par])
293
+
294
+ dres['bands'] = bands
295
+ dres['channels'] = channels
296
+ dres['res_index'] = res_index
297
+ dres['res_freqs'] = res_freqs
298
+ dres['sid'] = sid
299
+ dres['S21'] = S21s
300
+ dres['scan_freqs'] = scan_freqs
301
+ dres['chi2'] = chi2
302
+ dres['model_params'] = model_params
303
+ dres['derived_params'] = derived_params
304
+ return dres
305
+
306
+ def get_resfit_plot_txt(resfit_dict, band, rix):
307
+ '''
308
+ Function to assemble some key fit information out of the fit dictionary
309
+ into a text block for adding to plots.
310
+
311
+ Args
312
+ ----
313
+ resfit_dict : dict
314
+ Dictionary with fit results output from ``fit_tune``
315
+ band : int
316
+ Smurf band of channel to get plot text for.
317
+ rix : int
318
+ Resonator index in tunefile (sorted by frequency order of setup_notches
319
+ channels) of channel to get plot text for.
320
+
321
+ Returns
322
+ -------
323
+ text : str
324
+ text block to add to resonator fit channel plot.
325
+ '''
326
+ # Get index of band, rix pair provided by user
327
+ idx = np.where(
328
+ (resfit_dict['res_index'] == rix)
329
+ & (resfit_dict['bands'] == band)
330
+ )[0][0]
331
+ # idx = int(np.where((resfit_dict['res_index'][:,0] == band) \
332
+ # & (resfit_dict['res_index'][:,1] == rix))[0])
333
+
334
+ mparams = resfit_dict['model_params']
335
+ dparams = resfit_dict['derived_params']
336
+ channel = resfit_dict['channels'][idx]
337
+ chi2 = resfit_dict['chi2'][idx]
338
+ if channel == -1:
339
+ text = f'Channel unassigned'
340
+ else:
341
+ text = f'Band {band} Channel {channel}'
342
+ text += '\n$f_r$: '+ f"{np.round(mparams['f_0'][idx],1)} MHz"
343
+ text += '\n$Q_i$: '+ f"{int(dparams['Qi'][idx])}"
344
+ text += '\nBW: '+ f"{int(dparams['br'][idx]*1e3)} kHz"
345
+ # text += '\nfit $\chi^2$: ' + f"{np.round(chi2,3)}"
346
+ return text
347
+
348
+ def plot_channel_fit(fit_dict, idx):
349
+ '''
350
+ Function for plotting single channel eta_scan data from a tunefile with
351
+ a fit to an asymmetric resonator model ``resonator_cable``.
352
+
353
+ Args
354
+ ----
355
+ fit_dict : dict
356
+ fit results dictionary from ``fit_tune``
357
+ band : int
358
+ smurf band of resonator to plot
359
+ channel : int
360
+ smurf channel of resonator to plot
361
+ '''
362
+
363
+ rix = fit_dict['res_index'][idx]
364
+ band = fit_dict['bands'][idx]
365
+
366
+ freqs = fit_dict['scan_freqs'][idx]
367
+ res_freq = fit_dict['res_freqs'][idx]
368
+ freqs_plot = 1e3*(freqs - res_freq)
369
+ resp_data = fit_dict['S21'][idx]
370
+
371
+ params = {}
372
+ for p_opt in fit_dict['model_params']:
373
+ params[p_opt] = fit_dict['model_params'][p_opt][idx]
374
+
375
+ resp_model = resonator_cable(freqs,**params)
376
+ fr_idx = np.argmin(np.abs(freqs - fit_dict['model_params']['f_0'][idx]))
377
+
378
+ fig = plt.figure(figsize = (12,6),constrained_layout=True)
379
+ gs = GridSpec(2, 6, figure=fig)
380
+ ax1 = fig.add_subplot(gs[:1,0:2])
381
+ ax1.plot(freqs_plot,20*np.log10(np.abs(resp_data)),
382
+ 'C0o',label = 'Data')
383
+ ax1.plot(freqs_plot,20*np.log10(np.abs(resp_model)),
384
+ 'C1-',label = 'Fit')
385
+ ax1.plot(freqs_plot[fr_idx],20*np.log10(np.abs(resp_data[fr_idx])),
386
+ 'rx',ms = 12,label = '$f_r$ - fit')
387
+ ax1.set_xlabel('Offset $(f-f_{min})$ [kHz]')
388
+ ax1.set_ylabel('$|S_{21}|$ [dB]')
389
+ ax1.legend(loc = 'lower left',fontsize = 12)
390
+ ax2 = fig.add_subplot(gs[1:,0:2])
391
+ ax2.plot(freqs_plot,np.rad2deg(np.unwrap(np.angle(resp_data))),'C0o')
392
+ ax2.plot(freqs_plot,np.rad2deg(np.unwrap(np.angle(resp_model))),'C1-')
393
+ ax2.plot(freqs_plot[fr_idx],np.rad2deg(np.unwrap(np.angle(resp_data))[fr_idx]),
394
+ 'rx',ms = 12)
395
+ ax2.set_xlabel('Offset $(f-f_r)$ [kHz]')
396
+ ax2.set_ylabel('Phase $S_{21}$ [$^{\\circ}$]')
397
+ ax3 = fig.add_subplot(gs[:,2:])
398
+ ax3.plot(np.real(resp_data),np.imag(resp_data),'C0o')
399
+ ax3.plot(np.real(resp_model),np.imag(resp_model),'C1-')
400
+ ax3.plot(np.real(resp_model[fr_idx]),np.imag(resp_model[fr_idx]),
401
+ 'rx',ms = 12)
402
+ ax3.set_xlabel('Re($S_{21}$) [I]')
403
+ ax3.set_ylabel('Im($S_{21}$) [Q]')
404
+ mrange = 1.1*np.max(np.concatenate((np.abs(np.real(resp_data)),
405
+ np.abs(np.imag(resp_data)),
406
+ np.abs(np.real(resp_model)),
407
+ np.abs(np.imag(resp_model)))))
408
+ ax3.set_xlim(-mrange,mrange)
409
+ ax3.set_ylim(-mrange,mrange)
410
+ ax3.axhline(0,color = 'k')
411
+ ax3.axvline(0,color = 'k')
412
+
413
+ restext = get_resfit_plot_txt(fit_dict, band, rix)
414
+ ax3.text(0.025,0.05,restext,
415
+ bbox=dict(facecolor='wheat',
416
+ alpha=0.5,
417
+ boxstyle="round",),
418
+ transform=ax3.transAxes)
419
+ return fig, np.array([ax1, ax2, ax3])
420
+
421
+ def plot_fit_summary(fit_dict, plot_style=None, quantile=0.98):
422
+ '''
423
+ Function for plotting full wafer eta_scan data from a tunefile.
424
+ Plots distributions of Qi, dip depth, bandwidth, and frequency spacing
425
+ for all resonators within specified quantile.
426
+
427
+ Args
428
+ ----
429
+ fit_dict : dict
430
+ fit results dictionary from ``fit_tune``
431
+ plot_style : dict
432
+ keyword arguments to pass to the histogram plotting for formatting.
433
+ quantile: float
434
+ inner X percent to graph
435
+ '''
436
+
437
+ Qis = fit_dict['derived_params']['Qi']
438
+ depths = fit_dict['derived_params']['depth']
439
+ bws = fit_dict['derived_params']['br']
440
+ frs = fit_dict['model_params']['f_0']
441
+ seps = np.diff(frs)
442
+
443
+ Qi_quant = Qis[(Qis>=np.nanquantile(Qis, 1-quantile)) \
444
+ & (Qis<=np.nanquantile(Qis, quantile))]
445
+ depth_quant = depths[(depths>=np.nanquantile(depths, 1-quantile)) \
446
+ & (depths<=np.nanquantile(depths, quantile))]
447
+ bw_quant = bws[(bws>=np.nanquantile(bws, 1-quantile)) \
448
+ & (bws<=np.nanquantile(bws, quantile))]
449
+ seps_quant = seps[(seps>=np.nanquantile(seps, 1-quantile)) \
450
+ & (seps<=np.nanquantile(seps, quantile))]
451
+
452
+ fig, axes = plt.subplots(2, 2, figsize=(16, 8))
453
+
454
+ if plot_style is None:
455
+ plot_style = {'bins': 30,
456
+ 'color': 'gold',
457
+ 'alpha': 0.5,
458
+ 'edgecolor': 'orange',
459
+ 'lw': 2}
460
+ #Qi plot
461
+ ax = axes[0, 0]
462
+ ax.hist(Qi_quant/1e5, **plot_style)
463
+ Qi_med = np.median(Qi_quant/1e5)
464
+ ax.axvline(np.median(Qi_quant/1e5),color = 'purple',
465
+ label = f'Median: {np.round(Qi_med,2)} $\\times10^5$')
466
+ ax.legend(loc = 'upper right')
467
+ ax.set_xlabel('$Q_i\\times10^5$')
468
+ ax.set_ylabel('Counts')
469
+
470
+ #Dip depth plot
471
+ ax = axes[0, 1]
472
+ ax.hist(20*np.log10(depth_quant),**plot_style)
473
+ dep_med = np.median(20*np.log10(depth_quant))
474
+ ax.axvline(dep_med,color = 'purple',
475
+ label = f'Median: {np.round(dep_med,2)} dB')
476
+ ax.legend(loc = 'upper right')
477
+ ax.set_xlabel('Dip Depth [dB]')
478
+ ax.set_ylabel('Counts')
479
+
480
+ #Bandwidth plot
481
+ ax = axes[1, 0]
482
+ ax.hist(bw_quant*1e3, **plot_style)
483
+ bw_med = np.median(bw_quant*1e3)
484
+ ax.axvline(bw_med,color = 'purple',
485
+ label = f'Median: {np.round(bw_med,2)} kHz')
486
+ ax.legend(loc = 'upper right')
487
+ ax.set_xlabel('Bandwidth [kHZ]')
488
+ ax.set_ylabel('Counts')
489
+
490
+ #Frequency Separation plot
491
+ ax = axes[1, 1]
492
+ mean, std = np.nanmean(seps_quant), np.std(seps_quant)
493
+ nstd = 2
494
+ rng = (
495
+ max(np.min(seps_quant), mean - nstd * std),
496
+ min(np.max(seps_quant), mean + nstd * std),
497
+ )
498
+
499
+ ax.hist(seps, range=rng, **plot_style)
500
+ sep_med = np.median(seps_quant)
501
+ ax.axvline(sep_med,color = 'purple',
502
+ label = f'Median: {np.round(sep_med,2)} MHz')
503
+ ax.legend(loc = 'upper right')
504
+ ax.set_xlabel('Resonator Separation [Mhz]')
505
+ ax.set_ylabel('Counts')
506
+
507
+ plt.tight_layout()
508
+ return fig, axes