[docs]deffind_longest_pulse(sequence):""" Finds the longest pulse duration in a given sequence. Args: sequence (Sequence): The sequence to analyze. Returns: float: The duration of the longest pulse in microseconds. """longest_pulse=0forpulseinsequence.pulses:tp=pulse.tp.valueiftp>longest_pulse:longest_pulse=tpreturnlongest_pulse/1e3
[docs]defMNR_estimate(Vexp,t,mask=None):""" Estimates the Modulation to Noise Ratio (MNR) of a DEER signal without fitting. This is done by applying a low pass filter to remove noise and then finding the peaks in the signal. Parameters ---------- Vexp : np.ndarray The experimental DEER signal, real part only. t : np.ndarray The time axis of the DEER signal, in microseconds. mask : np.ndarray, optional The mask to apply to the data, by default None Returns ------- float The estimated MNR of the dataset. """ifmaskisnotNone:Vexp=Vexp[mask]t=t[mask]Vexp/=Vexp.max()# Vexp /= Vexp.max()SNR=1/dl.noiselevel(Vexp)# Smoothing spline with a low-pass filterb,a=signal.butter(3,0.1)# 3rd order Butterworth low-pass filter with cutoff at 0.1 MHzVexp_smooth=signal.filtfilt(b,a,Vexp)min_point=np.min(Vexp_smooth)N_points=len(Vexp_smooth)peaks=signal.find_peaks(Vexp_smooth,height=min_point,distance=N_points//4)est_lam=np.sum(peaks[1]['peak_heights']-min_point)MNR=est_lam*SNRreturnMNR
[docs]defDEERanalysis(dataset,compactness=True,model=None,ROI=False,exp_type='5pDEER',verbosity=0,remove_crossing=True,**kwargs):Vexp:np.ndarray=dataset.dataifnp.iscomplexobj(Vexp):Vexp,Vexp_im,_=dl.correctphase(Vexp,full_output=True)elifremove_crossing:Warning("Crossing removal only works with complex data. Skipping")remove_crossing=FalseVexp/=Vexp.max()# Extract params from datasetifhasattr(dataset,"sequence"):sequence=dataset.sequenceifsequence.name=="4pDEER":exp_type="4pDEER"tau1=val_in_us(sequence.tau1)tau2=val_in_us(sequence.tau2)elifsequence.name=="5pDEER":exp_type="5pDEER"tau1=val_in_us(sequence.tau1)tau2=val_in_us(sequence.tau2)tau3=val_in_us(sequence.tau3)elifsequence.name=="3pDEER":exp_type="3pDEER"tau1=val_in_us(sequence.tau1)elifsequence.name=="nDEER-CP":exp_type="4pDEER"tau1=val_in_us(sequence.tau1)tau2=val_in_us(sequence.tau2)tp=find_longest_pulse(sequence)t=val_in_us(sequence.t)elif't'indataset.coords:# If the dataset is a xarray and has sequence datat=dataset['t'].dataif'seq_name'indataset.attrs:ifdataset.attrs['seq_name']=="4pDEER":exp_type="4pDEER"tau1=dataset.attrs['tau1']/1e3tau2=dataset.attrs['tau2']/1e3elifdataset.attrs['seq_name']=="5pDEER":exp_type="5pDEER"tau1=dataset.attrs['tau1']/1e3tau2=dataset.attrs['tau2']/1e3tau3=dataset.attrs['tau3']/1e3elifdataset.attrs['seq_name']=="3pDEER":exp_type="3pDEER"tau1=dataset.attrs['tau1']/1e3else:if'tau1'indataset.attrs:tau1=dataset.attrs['tau1']/1e3elif'tau1'inkwargs:tau1=kwargs.pop('tau1')else:raiseValueError("tau1 not found in dataset or kwargs")if'tau2'indataset.attrs:tau2=dataset.attrs['tau2']/1e3elif'tau2'inkwargs:tau2=kwargs.pop('tau2')else:raiseValueError("tau2 not found in dataset or kwargs")if'tau3'indataset.attrs:tau3=dataset.attrs['tau3']/1e3exp_type="5pDEER"elif'tau3'inkwargs:tau3=kwargs.pop('tau3')exp_type="5pDEER"else:exp_type="4pDEER"else:# Extract params from kwargsif"tau1"inkwargs:tau1=kwargs.pop("tau1")if"tau2"inkwargs:tau2=kwargs.pop("tau2")if"tau3"inkwargs:tau3=kwargs.pop("tau3")exp_type=kwargs.pop("exp_type")# t = dataset.axes[0]t=dataset['X']ift.max()>500:t/=1e3# Find mask if neededifremove_crossing:mask=Noneif'pcyc_name'indataset.attrs:pcyc=dataset.attrs['pcyc_name']elif'nPcyc'indataset.attrs:# guess pcycifdataset.attrs['nPcyc']==16:pcyc='Full'elifdataset.attrs['nPcyc']==2:pcyc='DC'else:pcyc='unknown'ifpcyc=='DC'andexp_type=="5pDEER":# Remove crossing echoest_position=[tau1+tau2-tau3]idx_position=[np.argmin(np.abs(cross-t))forcrossint_position]mask=np.ones_like(Vexp,bool)forlocinidx_position:mask&=remove_echo(Vexp,Vexp_im,loc,criteria=4,extent=3)if"mask"inkwargs:mask=kwargs["mask"]noiselvl=dl.noiselevel(Vexp[mask])elifremove_crossingandmaskisnotNone:noiselvl=dl.noiselevel(Vexp[mask])else:noiselvl=dl.noiselevel(Vexp)mask=None# Determine the pathwaysest_MNR=MNR_estimate(Vexp,t,mask=mask)if"pathways"inkwargs:pathways=kwargs.pop("pathways")else:ifexp_type=='4pDEER':ifest_MNR<40:pathways=[1]else:pathways=[1,2,3]elifexp_type=='5pDEER':ifest_MNR<40:pathways=[1,5]else:pathways=[1,2,3,4,5]elifexp_type=='3pDEER':pathways=[1,2]else:pathways=NoneifmodelisnotNone:# Compactness is only needed with model-free fittingcompactness=Falseifverbosity>1:print(f"Experiment type: {exp_type}, pathways: {pathways}")print(f"Experimental Parameters set to: tau1 {tau1:.2f} us \t tau2 {tau2:.2f} us")ifhasattr(dataset,"sequence"):pulselength=tpelifhasattr(dataset,'attrs')and"pulse0_tp"indataset.attrs:# seach over all pulses to find the longest pulse using regexpulselength=np.max([dataset.attrs[i]foriindataset.attrsifre.match(r"pulse\d*_tp",i)])pulselength/=1e3# Convert to uspulselength/=2# Account for the too long range permitted by deerlabelse:if"pulselength"inkwargs:pulselength=kwargs.pop("pulselength")/1e3else:pulselength=16/1e3ifexp_type=="4pDEER":experimentInfo=dl.ex_4pdeer(tau1=tau1,tau2=tau2,pathways=pathways,pulselength=pulselength)elifexp_type=="5pDEER":experimentInfo=dl.ex_fwd5pdeer(tau1=tau1,tau2=tau2,tau3=tau3,pathways=pathways,pulselength=pulselength)elifexp_type=="3pDEER":experimentInfo=dl.ex_3pdeer(tau=tau1,pathways=pathways,pulselength=pulselength)r_max=np.ceil(np.cbrt(t.max()*6**3/2))r=np.linspace(1.5,r_max,100)# identify experimentif'parametrization'inkwargs:parametrization=kwargs.pop('parametrization')else:parametrization='reftimes'if'Vmodel'inkwargs:Vmodel=kwargs['Vmodel']else:Vmodel=dl.dipolarmodel(t,r,experiment=experimentInfo,Pmodel=model,parametrization=parametrization)Vmodel.pathways=pathwaysifcompactness:compactness_penalty=dl.dipolarpenalty(model,r,'compactness','icc')compactness_penalty.weight.set(lb=5e-3,ub=2e-1)# compactness_penalty.weight.freeze(0.04)else:compactness_penalty=None# Cleanup extra argsextra_args=['tau1','tau2','tau3','exp_type','model','compactness','ROI','verbosity','pulselength','Vmodel','parametrization']forarginextra_args:ifarginkwargs:kwargs.pop(arg)# Core ifverbosity>1:print('Starting Fitting')fit=dl.fit(Vmodel,Vexp,penalties=compactness_penalty,noiselvl=noiselvl,verbose=verbosity,**kwargs)ifverbosity>1:print('Fit complete')mod_labels=re.findall(r"(lam\d*)'",str(fit.keys()))ifmod_labels==[]:mod_labels=['mod']lam=0formodinmod_labels:lam+=getattr(fit,mod)MNR=lam/fit.noiselvlfit.MNR=MNRfit.lam=lamfit.Vmodel=Vmodelfit.dataset=datasetfit.r=rfit.Vexp=Vexpfit.t=tfit.mask=maskifnothasattr(fit,"P"):fit.P=fit.evaluate(model,r)fit.PUncert=fit.propagate(model,r,lb=np.zeros_like(r))ifROI:# tau_max = lambda r: (r**3) *(3/4**3)tau_max=lambdar:(r/3)**3*2fit.ROI=IdentifyROI(fit.P,r,criterion=0.90,method="gauss")iffit.ROI[0]<1:fit.ROI[0]=1rec_tau_max=tau_max(fit.ROI[1])fit.rec_tau_max=rec_tau_maxdt_rec=lambdar:(r**3*0.85)/(4*52.04)fit.rec_dt=np.max([dt_rec(fit.ROI[0]),10e-3])returnfit,rec_tau_maxelse:fit.ROI=Nonereturnfit
[docs]defcalc_correction_factor(fit_result,aim_MNR=25,aim_time=2):""" Calculate the correction factor for the number of averages required to achieve a given MNR in a given time. Parameters ---------- fit_result : Deerlab.FitResult The fit result from the DEER analysis. aim_MNR : float, optional The desired MNR, by default 25 aim_time : float, optional The desired time in hours, by default 2 Returns ------- float The correction factor for the number of averages. """dataset=fit_result.datasetruntime_s=dataset.nAvgs*dataset.nPcyc*dataset.shots*dataset.reptime*dataset.t.shape[0]*1e-6aim_time*=3600factor=fit_result.MNR/aim_MNR*np.sqrt(aim_time/runtime_s)returnfactor
[docs]defDEERanalysis_plot(fit,background:bool,ROI=None,axs=None,fig=None,text=True):"""DEERanalysis_plot Generates a figure showing both the time domain and distance domain data along with extra important infomation such as the Modulation to Noise Ratio (MNR), Region of Interest (ROI) and the recommended dipolar evolution time for future experiments based upon the ROI. Parameters ---------- fit : Deerlab.FitResult _description_ background : bool Should the background fit be plotted. ROI : tuple, optional The minimum and maximum of the Region of Interest (ROI), by default None Returns ------- Figure A Matplotlib Figure object of the figure. """mask=fit.maskt=fit.tr=fit.rVexp=fit.VexpVfit=fit.modelVmodel=fit.Vmodelpathways=Vmodel.pathways# Calculate backgrounddefbackground_func(t,fit):conc=fit.concprod=1scale=1foriinpathways:iftype(i)islist:# linked pathwaylam=getattr(fit,f"lam{i[0]}{i[1]}")scale+=-1*lamforjini:reftime=getattr(fit,f"reftime{pathways.index(j)+1}")prod*=dl.bg_hom3d(t-reftime,conc,lam)else:reftime=getattr(fit,f"reftime{i}")lam=getattr(fit,f"lam{i}")prod*=dl.bg_hom3d(t-reftime,conc,lam)scale+=-1*lamreturnfit.P_scale*scale*prodifaxsisNoneandfigisNone:fig,axs=plt.subplot_mosaic([['Primary_time','Primary_time','Primary_dist','Primary_dist']],figsize=(12.5,6.28))fig.tight_layout(pad=4)fig.subplots_adjust(bottom=0.2,hspace=0.4)# Time ifmaskisnotNone:axs['Primary_time'].plot(t[mask],Vexp[mask],'.',color='0.7',label='Data',ms=6)axs['Primary_time'].plot(t[~mask],Vexp[~mask],'.',color='0.8',label='Data',ms=6)else:axs['Primary_time'].plot(t,Vexp,'.',color='0.7',label='Data',ms=6)axs['Primary_time'].plot(t,Vfit,linewidth=3,color=primary_colors[0],label='Fit')# axs['Primary_time'].fill_between(t, Vci[:, 0], Vci[:, 1], color='C1',# alpha=0.3)ifbackground:axs['Primary_time'].plot(t,background_func(t,fit),linewidth=3,color=primary_colors[1],ls=':',label='Unmod. Cont.',alpha=0.5)axs['Primary_time'].set_xlabel(r"Time / $\mu s$")axs['Primary_time'].set_ylabel(r"A.U.")axs['Primary_time'].legend()# Distance PlotsPfit=fit.PPci=fit.PUncert.ci(95)axs['Primary_dist'].plot(r,Pfit,'-',color=primary_colors[0],lw=3,label='Fit')axs['Primary_dist'].fill_between(r,Pci[:,0],Pci[:,1],color=primary_colors[0],alpha=0.3,label='95% CI')ifROIisnotNone:axs['Primary_dist'].fill_betweenx([0,Pci[:,1].max()],ROI[0],ROI[1],alpha=0.2,color=primary_colors[1],label="ROI",hatch="/")axs['Primary_dist'].set_xlabel(r"Distance / $ nm$")axs['Primary_dist'].set_ylabel(r"$P(r) / nm^{-1}$")axs['Primary_dist'].legend()ifnottext:returnfig# Analysisaxs['Primary_time'].text(0.05,0.05,f"MNR: {fit.MNR:.2f}",transform=fig.transFigure,fontsize="16",color="black")iffit.MNR<10:axs['Primary_time'].text(0.15,0.05,"LOW MNR: More averages requried",transform=fig.transFigure,fontsize="16",color="red")ifROIisnotNone:ROI_error=(r.max()-ROI[1])rec_tau_max=(ROI[1]/3)**3*2axs['Primary_time'].text(0.55,0.05,f"ROI: {ROI[0]:.2f}nm to {ROI[1]:.2f}nm",transform=fig.transFigure,fontsize="16",color="black")axs['Primary_time'].text(0.05,0.01,rf"Recommended $\tau_{{max}}$ = {rec_tau_max:.2f}us",transform=fig.transFigure,fontsize="16",color="black")ifROI_error<0.5:axs['Primary_time'].text(0.55,0.01,rf"ROI is too close to $r_{{max}}$. Increase $\tau_{{max}}$",transform=fig.transFigure,size="x-large",color="red")returnfig
[docs]defDEERanalysis_plot_pub(results,ROI=None,fig=None,axs=None):""" Generates a vertical plot of the DEER analysis results, ready for publication. Parameters ---------- results : Deerlab.FitResult The results of the DEER analysis. ROI : tuple, optional The minimum and maximum of the Region of Interest (ROI), by default None fig : matplotlib.figure.Figure, optional The figure to plot the results on. If None, a new figure is created. axs : matplotlib.axes.Axes, optional The axes to plot the results on. If None, a new axes is created. """mask=results.maskt=results.tr=results.rVexp=results.VexpVfit=results.modelVmodel=results.Vmodelpathways=Vmodel.pathwaysiffigisNone:fig,axs=plt.subplots(2,1,figsize=(5,5),subplot_kw={},gridspec_kw={'hspace':0})else:axs=fig.subplots(2,1,subplot_kw={},gridspec_kw={'hspace':0})axs[0].plot(results.t,results.Vexp,'.',alpha=0.5,color='#D95B6F',mec='none')axs[0].plot(results.t,results.model,'-',alpha=1,color='#D95B6F',lw=2)axs[0].plot(results.t,background_func(results.t,results),'--',alpha=1,color='#42A399',lw=2)axs[0].set_xlabel('Time / $\mu s$')axs[0].xaxis.tick_top()axs[0].xaxis.set_label_position('top')axs[0].set_ylabel('Signal A.U.')r=results.raxs[1].plot(r,results.P,'-',alpha=1.0,color='#D95B6F',label='P(r)')Pci=results.PUncert.ci(95)axs[1].fill_between(r,Pci[:,0],Pci[:,1],color='#D95B6F',alpha=0.3,label='P(r) 95% CI')ifROIisnotNone:axs[1].fill_betweenx([0,Pci[:,1].max()],ROI[0],ROI[1],alpha=0.2,color='#42A399',label="ROI",hatch="/")axs[1].set_xlabel('Distance / $nm$')axs[1].set_ylabel(r"$P(r) / nm^{-1}$")axs[1].legend()returnfig
[docs]defIdentifyROI(P:np.ndarray,r:np.ndarray,criterion:float=0.99,method:str="gauss"):"""IdentifyROI Identifies the region of interest. Two methods are sypported Methods +++++++ 1. Gaussian fitting ("gauss"): 2. Intergration ("int"): Parameters ---------- P : np.ndarray The distance distribution. r : np.ndarray The distance axis criterion : float, optional The fraction of the distance distribution that must be in the ROI, by default 0.99 method: str, optional The method used to calculate region of interest. """ifmethod.lower()=="int":# Normlaize the distributiondist=P/np.trapz(P,r)# cumulative_dist = cumulative_trapezoid(dist,r,initial=0)# min_dist = r[np.argmin(np.abs(1 - cumulative_dist - criterion))]# max_dist = r[np.argmin(np.abs(cumulative_dist - criterion))]c_trapz_dist=np.zeros((dist.shape[0],dist.shape[0]))foriinrange(0,dist.shape[0]):c_trapz_dist[i,i:]=cumulative_trapezoid(dist[i:],r[i:],initial=0)c_trapz_dist[(c_trapz_dist<criterion)]=3ind=np.unravel_index(np.argmin(c_trapz_dist),c_trapz_dist.shape)min_dist=r[ind[0]]max_dist=r[ind[1]]# Enlarge ROIwidth=max_dist-min_distmax_dist=max_dist+width*0.25min_dist=min_dist-width*0.25elifmethod.lower()=="gauss":# Single gaussian approachPmodel=dl.dd_gaussPmodel.mean.lb=0Pmodel.mean.ub=r.max()*2Pmodel.mean.par0=r[P.argmax()]Pmodel.std.lb=0Pmodel.std.ub=r.max()Pmodel.std.par0=(r.max()-r.min())*0.5fit2=dl.fit(Pmodel,P,r)min_dist=fit2.mean-2.5*fit2.stdmax_dist=fit2.mean+2.5*fit2.stdreturn[min_dist,max_dist]
[docs]defremove_echo(Vre:np.ndarray,Vim:np.ndarray,loc:int,criteria:float=4,extent:int=3)->np.ndarray:"""This function removes crossing echoes. Parameters ---------- Vre : np.ndarray The real part of the phase corrected signal. Vim : np.ndarray The imaginary part of the phase corrected signal. loc : int The approximate location of the crossing echo, +- 30 data points criteria : float, optional The detection criteria, in multiples of the std deviation, by default 4 extent : int, optional How many data points either side to remove, by default 3. Returns ------- np.ndarray The mask of points to be ignored. """search_mask=np.ones(Vre.shape[0],bool)search_mask[loc-30:loc+31]=Falsemask=np.abs(Vim-np.mean(Vim))>criteria* \
dl.noiselevel(Vre[search_mask])mask=mask&np.logical_not(search_mask)iskip=-1foriinrange(len(mask)):ifi<iskip:continueifmask[i]:mask[i-extent:i]=Trueifi<len(mask)-extent:mask[i:i+extent]=Trueiskip=i+extentmask=np.logical_not(mask)returnmask
[docs]defshift_pulse_freq(pulse,shift):""" Shifts the frequency of a pulse by a given amount. Args: pulse: The pulse whose frequency should be shifted. shift: The amount by which to shift the frequency. Returns: The pulse with the shifted frequency. """ifhasattr(pulse,'freq'):pulse.freq.value+=shiftelifhasattr(pulse,'init_freq')andhasattr(pulse,'final_freq'):pulse.init_freq.value+=shiftpulse.final_freq.value+=shiftreturnpulse
[docs]defnormalise_01(A):""" Normalizes the input vector A to be between 0 and 1. Parameters: A (numpy.ndarray): Input vector to be normalized. Returns: numpy.ndarray: Normalized vector between 0 and 1. """A=A-np.min(A)A=A/np.max(A)returnA
[docs]defresample_and_shift_vector(A,f,shift):""" Resample the vector A along axis f and shift it by shift and return on original axis f. Parameters: A (numpy.ndarray): The input vector to be resampled and shifted. f (numpy.ndarray): The axis along which to resample the vector. shift (float): The amount by which to shift the resampled vector. Returns: numpy.ndarray: The resampled and shifted vector. """A=np.interp(f,f+shift,A)returnA
[docs]defbuild__lowpass_butter_filter(cutoff):"""Build a lowpass butterworth filter with a cutoff frequency of cutoff Args: cutoff (float): cutoff frequency in GHz """sos=sig.butter(1,cutoff,btype='lowpass',analog=True)deffilter_func(f):w=2*np.pi*fw,h=sig.freqs(*sos,w)returnabs(h)returnfilter_func
[docs]deffunctional(f_axis,fieldsweep,A,B,filter=None,A_shift=0,B_shift=0):"""Functional for optimising the pulse positions Parameters ---------- f_axis : np.ndarray The frequency axis of the field sweep in GHz fieldsweep : ad.FieldSweepAnalysis The FieldSweep analysis object A : np.ndarray The pump pulse profile B : np.ndarray The effective excitation pulse profile filter : np.ndarray, optional The filter profile if applicable, by default None A_shift : int, optional The shift in pump pulse in GHz, by default 0 B_shift : int, optional The shift in effective exciatation pulse in GHz, by default 0 Returns ------- _type_ _description_ """D_profile=resample_and_shift_vector(A,f_axis,A_shift)*fieldsweep*resample_and_shift_vector(B,f_axis,B_shift)A_profile=fieldsweep*(resample_and_shift_vector(A,f_axis,A_shift))-D_profile# PumpB_profile=fieldsweep*(resample_and_shift_vector(B,f_axis,B_shift))-D_profile# Excfsweep_norm=np.trapz(fieldsweep,f_axis)Aspins=np.trapz(A_profile,f_axis)/fsweep_normiffilterisNone:Bspins=np.trapz(B_profile,f_axis)/fsweep_normNoise=1else:Bspins=np.trapz(B_profile*resample_and_shift_vector(filter,f_axis,B_shift),f_axis)/fsweep_normNoise=np.trapz(filter,f_axis)return-1*(Aspins*Bspins)/np.sqrt(Noise)
[docs]defoptimise_pulses(Fieldsweep,pump_pulse,exc_pulse,ref_pulse=None,filter=None,verbosity=0,method='brute',nDEER=False,num_ref_pulses=2,full_output=False,resonator=None,**kwargs):"""Optimise the pulse positions to maximise the pump-exc overlap. Parameters ---------- Fieldsweep : ad.FieldSweepAnalysis The FieldSweep analysis object pump_pulse : ad.Pulse The pump pulse object exc_pulse : ad.Pulse The excitation pulse object ref_pulse : ad.Pulse, optional The refocusing pulse object\, by default None filter : str or number or list, optional The filter profile if applicable, by default None. If it is a number a filter is generated with this cutoff frequency. If the string 'Matched' is used a matched filter is used. If a list is used the optimisation is performed for each filter and the best is returned. verbosity : int, optional The verbosity, by default 0 method : str, optional What search optimisation is used, by default 'grid' nDEER : bool, optional Is the sequence an nDEER sequrence, by default False. If True then the refocusing pulse is not optimised. num_ref_pulses : int, optional The total number of refocusing pulses, by default 2 full_output : bool, optional Return the full output, by default False resonator : ad.ResonatorProfile, optional The resonator profile, by default None Returns ------- ad.Pulse The optimised pump pulse ad.Pulse The optimised excitation pulse ad.Pulse The optimised refocusing pulse str or number The best filter, only if a list of filters is provided float The functional value after optimisation, only if full_output is True tuple The grid of the optimisation, only if full_output is True tuple The output of the optimisation, only if full_output is True """# gyro = Fieldsweep.gyro# if hasattr(Fieldsweep,'results'):# fieldsweep_fun = lambda x: Fieldsweep.results.evaluate(Fieldsweep.model,(x+Fieldsweep.LO) /gyro*1e-1)fieldsweep_fun=Fieldsweep.func_freqf=np.linspace(-0.3,0.3,100)fieldsweep_profile=fieldsweep_fun(f)# fieldsweep_profile = np.flip(fieldsweep_fun(f))pump_Mz=normalise_01(-1*pump_pulse.exciteprofile(freqs=f)[2].real)exc_Mz=normalise_01(-1*exc_pulse.exciteprofile(freqs=f)[2].real)ifref_pulseisnotNone:foriinrange(num_ref_pulses):exc_Mz*=normalise_01(-1*ref_pulse.exciteprofile(freqs=f)[2].real)defoptimise(filter):iffilter=='Matched':# Matched filterfilter_profile=exc_Mzelifisinstance(filter,numbers.Number):filter_profile=build__lowpass_butter_filter(filter)(f)eliffilterisNone:filter_profile=Nonefun=lambdax:functional(f,fieldsweep_profile,pump_Mz,exc_Mz,filter_profile,A_shift=x[0],B_shift=x[1])ifmethod=='grid':X,Y=np.meshgrid(f,f)Z=[fun([x,y])forx,yinzip(X.flatten(),Y.flatten())]Z=np.array(Z).reshape(X.shape)f_idA,f_idB=np.unravel_index(Z.argmin(),Z.shape)fA=f[f_idA]fB=f[f_idB]fval=Z.min()grid=(X,Y)Jout=Zelifmethod=='brute':[fA,fB],fval,grid,Jout=brute(fun,(slice(f.min(),f.max(),0.01),slice(f.min(),f.max(),0.01)),full_output=True)ifverbosity>1:print(f"Filter: {filter:<8} Functional:{Z.min():.3f}\t Pump shift: {fA*1e3:.1f}MHz \t Exc/Ref shift: {fB*1e3:.1f}MHz")returnfval,fA,fB,grid,Joutifisinstance(filter,list):results=[optimise(f)forfinfilter]best=np.argmin([r[0]forrinresults])funct,fA,fB=results[best]ifverbosity>0:print(f"Best filter: {filter[best]}")eliffilterisNone:funct,fA,fB,grid,Jout=optimise(None)ifverbosity>0:print(f"Filter: None Functional:{funct:.3f}\t Pump shift: {fA*1e3:.1f}MHz \t Exc/Ref shift: {fB*1e3:.1f}MHz")else:funct,fA,fB,grid,Jout=optimise(filter)ifverbosity>0:print(f"Filter: {filter:<8} Functional:{funct:.3f}\t Pump shift: {fA*1e3:.1f}MHz \t Exc/Ref shift: {fB*1e3:.1f}MHz")new_pump_pulse=pump_pulse.copy()new_pump_pulse=shift_pulse_freq(new_pump_pulse,fA)new_exc_pulse=exc_pulse.copy()new_exc_pulse=shift_pulse_freq(new_exc_pulse,fB)new_ref_pulse=ref_pulse.copy()ifnotnDEER:new_ref_pulse=shift_pulse_freq(new_ref_pulse,fB)ifisinstance(filter,list):iffull_output:returnnew_pump_pulse,new_exc_pulse,new_ref_pulse,filter[best],funct,grid,Joutelse:returnnew_pump_pulse,new_exc_pulse,new_ref_pulse,filter[best]else:iffull_output:returnnew_pump_pulse,new_exc_pulse,new_ref_pulse,funct,grid,Joutelse:returnnew_pump_pulse,new_exc_pulse,new_ref_pulse,
[docs]defplot_overlap(Fieldsweep,pump_pulse,exc_pulse,ref_pulse,filter=None,respro=None,num_ref_pulses=2,axs=None,fig=None):"""Plots the pump and excitation profiles as well as the fieldsweep and filter profile. Parameters ---------- Fieldsweep : ad.FieldSweepAnalysis The FieldSweep analysis object pump_pulse : ad.Pulse The pump pulse object exc_pulse : ad.Pulse The excitation pulse object ref_pulse : ad.Pulse, optional The refocusing pulse object, by default None filter : str or number, optional The filter profile if applicable, by default None. If it is a number a filter is generated with this cutoff frequency. If the string 'Matched' is used a matched filter is used. respro : ad.ResonatorProfileAnalysis, optional The resonator profile for fitting, by default None. The resonator profile must include the fit. num_ref_pulses : int, optional The total number of refocusing pulses, by default 2 axs : matplotlib.axes, optional The axes to plot on, by default None fig : matplotlib.figure, optional The figure to plot on, by default None """gyro=Fieldsweep.gyrofieldsweep_fun=Fieldsweep.func_freqf=np.linspace(-0.4,0.4,100)fieldsweep_profile=fieldsweep_fun(f)pump_Mz=normalise_01(-1*pump_pulse.exciteprofile(freqs=f)[2].real)exc_Mz=normalise_01(-1*exc_pulse.exciteprofile(freqs=f)[2].real)ifref_pulseisnotNone:foriinrange(num_ref_pulses):exc_Mz*=normalise_01(-1*ref_pulse.exciteprofile(freqs=f)[2].real)iffilter=='Matched':# Matched filterfilter_profile=lambdaf_new:np.interp(f_new,f,exc_Mz)elifisinstance(filter,numbers.Number):filter_profile=build__lowpass_butter_filter(filter)ifaxsisNoneandfigisNone:fig,axs=plt.subplots(1,1,figsize=(5,5),layout='constrained')elifaxsisNone:axs=fig.subplots(1,1,subplot_kw={},gridspec_kw={'hspace':0})# Normalise the fieldsweep profilefieldsweep_profile/=np.abs(fieldsweep_profile).max()# Plot the profilesaxs.plot(f*1e3,fieldsweep_profile,label='Fieldsweep',c='k')axs.fill_between(f*1e3,pump_Mz*fieldsweep_profile,label='Pump Profile',alpha=0.5,color='#D95B6F')axs.fill_between(f*1e3,exc_Mz*fieldsweep_profile,label='Observer Profile',alpha=0.5,color='#42A399')iffilterisnotNone:axs.plot(f*1e3,filter_profile(f),'--',label='Filter')ifresproisnotNone:model_norm=respro.model/np.max(respro.model)axs.plot((respro.model_x-Fieldsweep.LO)*1e3,model_norm,'--',label='Resonator Profile')fmin=f[~np.isclose(fieldsweep_profile,0)].min()fmax=f[~np.isclose(fieldsweep_profile,0)].max()axs.set_xlim(fmin*1e3,fmax*1e3)axs.legend()axs.set_xlabel('Frequency (MHz)')returnfig
[docs]defcalc_deer_settings(experiment:str,CPdecay=None,Refocused2D=None,target_time=2,target_MNR=20,waveform_precision=2):""" Calculates the optimal DEER settings based on the avaliable relaxation data Parameters ---------- experiment : str Type of DEER experiment, either 'auto', '4pDEER' or '5pDEER' CPdecay : ad.CarrPurcellAnalysis Carr-Purcell relaxation data Refocused2D : ad.RefocusedEcho2DAnalysis, optional Refocused 2D data required for '4pDEER', by default None target_time : int, optional Target time for the DEER experiment in hours, by default 2 target_MNR : float, optional Target modulation to noise ratio, by default 20 waveform_precision : int, optional Precision of the waveform in ns, by default 2 Returns ------- dict DEER settings, with keys: -'ExpType': '4pDEER' or '5pDEER' -'tau1': in us -'tau2': in us -'tau3': in us, only for 5pDEER -'AimTime': in hours Notes ----- This function will calcate the optimal DEER settings based on the avaliable relaxation data, depending on the experiment type. For 4pDEER, the optimal tau1 and tau2 are calculated based on the refocused 2D data, and for 5pDEER, the optimal tau2 is calculated based on the CPdecay data or refocused 2D if CP decay data is not availiable. If the optimal tau2 for 5pDEER is less than 1.5us, the function will calculate the optimal tau1 and tau2 for 4pDEER instead. This is only possible if the refocused 2D data is availiable, otherwise a non optimal tau1 of 0.4us is used. """# Calculate the DEER settings from the avaliable relaxation data# Move out of the GUI Code asapdeer_settings={}ifexperiment=='4pDEER':ifRefocused2DisNone:raiseValueError("Refocused2D data required for 4pDEER")# Calcualting a 4pDEER Sequencedeer_settings['ExpType']='4pDEER'# Calculate tau1 and tau2tau1,tau2=Refocused2D.find_optimal(type='4pDEER',SNR_target=target_MNR,target_time=target_time,target_step=0.015)iftau2<2.5:# 2hr dipolar evo too short for meaningful results. Using double the time insteadtarget_time*=2tau1,tau2=Refocused2D.find_optimal(type='4pDEER',SNR_target=target_MNR,target_time=target_time,target_step=0.015)iftau2<2.5:# Dipolar evo time still too short. Hardcoding a 2.5us dipolar evo timetau1,tau2=Refocused2D.optimal_tau1(tau2=2.5)deer_settings['tau1']=round_step(tau1,waveform_precision/1e3)deer_settings['tau2']=round_step(tau2,waveform_precision/1e3)deer_settings['AimTime']=target_timeelif(experiment=='5pDEER')or(experiment=='auto'):# Calculate a 5pDEER SequenceifCPdecayisnotNone:tau2hrs=CPdecay.find_optimal(SNR_target=target_MNR,target_time=target_time,target_step=0.015)tau4hrs=CPdecay.find_optimal(SNR_target=target_MNR,target_time=target_time*2,target_step=0.015)elifRefocused2DisnotNone:tau2hrs=Refocused2D.find_optimal(type='5pDEER',SNR_target=20,target_time=target_time,target_step=0.015)tau4hrs=Refocused2D.find_optimal(type='5pDEER',SNR_target=20,target_time=target_time*2,target_step=0.015)else:raiseValueError("CPdecay data required for 5pDEER")if(tau2hrs<1.5)and(tau4hrs>1.5):# 2hr dipolar evo too short for meaningful results. Using 4hr number insteaddeer_settings['tau1']=round_step(tau4hrs,waveform_precision/1e3)deer_settings['tau2']=round_step(tau4hrs,waveform_precision/1e3)deer_settings['tau3']=round_step(0.3,waveform_precision/1e3)deer_settings['ExpType']='5pDEER'deer_settings['AimTime']=target_time*2elif(tau2hrs<1.5)and(tau4hrs<1.5):# 2hr & 4hr dipolar evo too short for meaningful results. Hardcoding a 2.5us dipolar evo time, in 4pDEER and 4hrs# self.raise_warning(f"2hr dipolar evo too short. Hardcoding a 2.5us dipolar evo time")tau2=2.5ifRefocused2DisnotNone:tau1=Refocused2D.optimal_tau1(tau2=tau2)else:tau1=0.4deer_settings['tau1']=round_step(tau1,waveform_precision/1e3)deer_settings['tau2']=round_step(tau2,waveform_precision/1e3)deer_settings['ExpType']='4pDEER'deer_settings['AimTime']=target_time*2# self.raise_warning(f"2hr dipolar evo {tau2hrs:.2f} us, using 4pDEER")else:# Normal case, using 2hr dipolar evo timedeer_settings['tau1']=round_step(tau2hrs,waveform_precision/1e3)deer_settings['tau2']=round_step(tau2hrs,waveform_precision/1e3)deer_settings['tau3']=round_step(0.3,waveform_precision/1e3)deer_settings['ExpType']='5pDEER'deer_settings['AimTime']=target_timereturndeer_settings