[docs]deffind_max(self)->float:"""Calculates the maximum field Returns ------- float Max field """if'B'inself.data.coords:self.max_field=self.data['B'].data[np.abs(self.data).argmax()]else:self.max_field=self.data['X'].data[np.abs(self.data).argmax()]returnself.max_field
[docs]defcalc_gyro(self,LO:float=None)->float:"""Calculates the gyromagnetic ratio for a given frequency Parameters ---------- det_frq : float The detection frequency for the field sweep. Returns ------- float The gyromagnetic ratio in G/GHz. """ifnothasattr(self,"max_field"):self.find_max()ifLOisNone:ifhasattr(self,"LO"):# LO = self.LO.valueLO=self.LOelse:raiseValueError("A LO frequency must eithe be in the dataset or specified as an argument")self.LO=LOself.gyro=LO/self.max_fieldhf_x=LO-self.gyro*self.axisself.fs_x=LO+hf_xself.fs_x=LO-self.gyro*self.axisreturnself.gyro
[docs]defsmooth(self,*args,**kwargs):""" Generates a smoothed version of the data using a 1D smoothing spline. Returns ------- np.ndarray The smoothed data. """smooth_spl=UnivariateSpline(self.axis,self.data,ext=1)smooth_spl.set_smoothing_factor(0.01)smooth_spl_freq=UnivariateSpline(np.flip(self.fs_x),np.flip(self.data),ext=1)smooth_spl_freq.set_smoothing_factor(0.01)self.smooth_data=smooth_spl(self.axis)self.func=smooth_splself.func_freq=smooth_spl_freqreturnself.smooth_data
[docs]deffit(self,spintype='N',**kwargs):ifspintype!='N':raiseValueError("Currently the fit function only supports Nitroxide spins")ifisinstance(self.LO,Parameter):mymodel=create_Nmodel(self.LO.value*1e3)else:mymodel=create_Nmodel(self.LO*1e3)B=np.linspace(self.axis.min(),self.axis.max(),self.data.shape[0])*0.1ifnp.iscomplexobj(self.data):Vexp=dl.correctphase(self.data.to_numpy())else:Vexp=self.data.to_numpy()result=dl.fit(mymodel,Vexp,B,verbose=2,reg=False,**kwargs)self.results=resultself.model=mymodelself.func=lambdax:result.evaluate(mymodel,x*0.1)self.func_freq=lambdax:result.evaluate(mymodel,(-x+self.LO)/self.gyro*1e-1)returnresult
[docs]defplot(self,norm:bool=True,axis:str="field",axs=None,fig=None)->Figure:"""Generate a field sweep plot Parameters ---------- norm : bool, optional Nomarlisation of the plot to a maximum of 1, by default True axis : str, optional plot field sweep on either the "field" axis or "freq" axis Returns ------- Matplotlib.Figure matplotlib figure """ifnormisTrue:data=self.datadata/=np.max(np.abs(data))else:data=self.dataifaxsisNoneandfigisNone:fig,axs=plt.subplots(1,1,figsize=(8,6))# Plot the dataifaxis.lower()=='field':ifnp.iscomplexobj(data):axs.plot(self.axis,np.real(data),label='Re',color=primary_colors[1])axs.plot(self.axis,np.imag(data),label='Im',color=primary_colors[2])else:axs.plot(self.axis,data,label='Re',color=primary_colors[1])axs.legend()axs.set_xlabel('Field G')axs.set_ylabel('Normalised Amplitude')elifaxis.lower()=='freq':ifnothasattr(self,"fs_x"):raiseRuntimeError("Please run fieldsweep.calc_gyro() first")ifnp.iscomplexobj(data):axs.plot(self.fs_x,np.real(data),label='Re',color=primary_colors[1])axs.plot(self.fs_x,np.imag(data),label='Im',color=primary_colors[2])else:axs.plot(self.axis,data,label='Re',color=primary_colors[1])axs.set_xlabel('Frequency GHz')axs.set_ylabel('Normalised Amplitude')# Plot the fitifhasattr(self,"results"):data=self.results.evaluate(self.model,self.axis*0.1)ifnormisTrue:data/=self.results.scaleifaxis.lower()=='field':axs.plot(self.axis,data,label='fit',c=primary_colors[0])elifaxis.lower()=='freq':axs.plot(self.fs_x,np.flip(data),label='fit',c=primary_colors[0])axs.legend()elifhasattr(self,"smooth_data"):ifaxis.lower()=='field':data=self.smooth_data/np.max(np.abs(self.smooth_data))axs.plot(self.axis,data,label='smooth',c=primary_colors[0])elifaxis.lower()=='freq':data=self.func_freq(self.fs_x)data/=np.max(np.abs(data))axs.plot(self.fs_x,data,label='smooth',c=primary_colors[0])axs.legend()returnfig
[docs]deferot(*args):"""Passive rotation matrix. """iflen(args)==0:raiseValueError("No input arguments given!")option=""iflen(args)==1orlen(args)==2:angles=np.asarray(args[0])ifangles.size!=3:raiseValueError("Three angles (either separately or in a 3-element array) expected.")gamma=angles[2]beta=angles[1]alpha=angles[0]iflen(args)==2:option=args[1]eliflen(args)==3orlen(args)==4:alpha=args[0]beta=args[1]gamma=args[2]iflen(args)==4:option=args[3]else:raiseValueError("Wrong number of input arguments!")ifnotisinstance(option,str):raiseValueError("Last argument must be a string, either 'rows' or 'cols'.")ifoption=="":return_rows=Falsereturn_cols=Falseelifoption=="rows":return_rows=Truereturn_cols=Falseelifoption=="cols":return_rows=Falsereturn_cols=Trueelse:raiseValueError("Last argument must be a string, either 'rows' or 'cols'.")# Check anglesifnp.isnan(alpha)ornp.isnan(beta)ornp.isnan(gamma):raiseValueError("At least one of the angles is NaN. Angles must be numbers.")# Precalculate trigonometric functions of anglessa=np.sin(alpha)ca=np.cos(alpha)sb=np.sin(beta)cb=np.cos(beta)sg=np.sin(gamma)cg=np.cos(gamma)# Compute passive rotation matrixR=np.array([[cg*cb*ca-sg*sa,cg*cb*sa+sg*ca,-cg*sb],[-sg*cb*ca-cg*sa,-sg*cb*sa+cg*ca,sg*sb],[sb*ca,sb*sa,cb]])ifreturn_rows:returnR[0,:],R[1,:],R[2,:]elifreturn_cols:returnR[:,0],R[:,1],R[:,2]else:returnR
[docs]defeyekron(M:np.ndarray):""" Calculates the Kronecker product of the identity matrix with a matrix M. Parameters: M (np.ndarray): The matrix to be multiplied with the identity matrix. Returns: np.ndarray: The Kronecker product of the identity matrix with M. """size=np.shape(M)[0]returnnp.kron(np.identity(size),M)
[docs]defkroneye(M):""" Computes the Kronecker product of a matrix with the identity matrix of the same size. Args: M (numpy.ndarray): The matrix to compute the Kronecker product with. Returns: numpy.ndarray: The Kronecker product of M with the identity matrix of the same size. """size=np.shape(M)[0]returnnp.kron(M,np.identity(size))
[docs]defham(SpinSystem,elspins=None,nucspins=None):# Only using the hyperfine part at the moment as only section present in NitroxidesSpinVec=SpinSystem.SpinsnStates=int(SpinSystem.nStates)nElectrons=SpinSystem.nElectronsnNuclei=SpinSystem.nNucleiHhf=bsr_array((nStates,nStates),dtype=np.complex128);ifnNuclei==0:# If there are no Nuclei there this no hyperfine componentreturnHhfifelspinsisNone:elspins=np.arange(0,nElectrons)ifnucspinsisNone:nucspins=np.arange(0,nNuclei)AMatrix=np.atleast_2d(SpinSystem.A)fullAMatrix=np.size(AMatrix,axis=0)>nNuclei# Generate Hamiltonian for hyperfine interactionforeSpinelspins:eidx=np.arange((eSp-1)*3,eSp*3)fornspinnucspins:ifSpinSystem.I[nsp]==0:continueiffullAMatrix:A=AMatrix[np.arange((nsp-1)*3,nsp*3),eidx]else:A=np.diag(AMatrix[nsp,eidx])# TODO: Transform matrix into molecular frame representation forc1,s1inenumerate(['x','y','z']):forc2,s2inenumerate(['x','y','z']):comps=['e']*len(SpinVec)comps[eSp]=s1comps[nElectrons+nsp]=s2comps=''.join(comps)Hhf+=A[c1,c2]*sop(SpinVec,comps)return(Hhf+Hhf.conj())/2
[docs]defresfields(system,Orientations,mwFreq,computeIntensities=True,RejectionRatio=1e-8,Range=(0,1e8),Threshold=0,computeFreq2Field=True):# Generate orientationsnOrientations=Orientations.shape[0]averageOverChi=TrueH0=ham(system)[muxe,muye,muze]=ham_ez(system)[muxn,muyn,muzn]=ham_nz(system)[mux,muy,muz]=[muxe+muxn,muye+muyn,muze+muzn]A=eyekron(H0)-kroneye(H0.conj())+mwFreq*np.eye(H0.shape[0]**2);E=np.diag(eig(A,right=False))ifcomputeIntensities:mux_vec=mux.flatten()muy_vec=muy.flatten()muz_vec=muz.flatten()EigenFields=[]Intensities=[]foriOri,Oriinenumerate(Orientations):[xLab,yLab,zLab]=erot(Ori,'rows')muzL=zLab[0]*mux+zLab[1]*muy+zLab[2]*muzB=-kroneye(muzL.conj())+eyekron(muzL)ifcomputeIntensities:[Fields,Vecs]=eig(A,B);idx=np.argsort(Fields);Fields=Fields[idx]Vecs=Vecs[:,idx]mask=np.abs(Fields.imag)<(np.abs(Fields.real)*RejectionRatio)mask&=np.greater(Fields,0)mask&=np.isfinite(Fields)mask&=np.greater(Fields,Range[0])mask&=np.less(Fields,Range[1])ifnp.equal(mask,False).all():EigenFields.append([])Intensities.append([])else:EigenFields.append(Fields[mask].real)Vecs=Vecs[:,mask]# Normalize eigenvectors to unityNorms=np.sqrt(np.sum(np.abs(Vecs)**2,axis=0))Vecs/=Norms[None,:]# Assuming never parallel modemuxL_vec=xLab[0]*mux_vec+xLab[1]*muy_vec+xLab[2]*muz_vecifaverageOverChi:muyL_vec=yLab[0]*mux_vec+yLab[1]*muy_vec+yLab[2]*muz_vecTransitionRate=(np.abs((muxL_vec[:,None]*Vecs).sum(axis=0))**2+np.abs((muyL_vec[:,None]*Vecs).sum(axis=0)**2))/2else:TransitionRate=np.abs((muxL_vec[:,None]*Vecs).sum(axis=0))**2Polarization=1;Polarization=Polarization/np.prod(2*system.I+1);ifcomputeFreq2Field:n=H0.shape[0]Vecs=np.reshape(Vecs,(n,n,int(Vecs.size/n**2)),order='F')dBdE=np.zeros(Vecs.shape[2])foriVecinrange(Vecs.shape[2]):V=Vecs[:,:,iVec]dBdE[iVec]=1/np.abs(np.trace(-muzL@(V@V.conj().T-V.conj().T@V)))else:dBdE=np.ones(TransitionRate.shape)# Combine factorsIntensities.append(Polarization*np.real(TransitionRate*dBdE).T)mask=Intensities[iOri]>=Threshold*Intensities[iOri].max();EigenFields[iOri]=EigenFields[iOri][mask];Intensities[iOri]=Intensities[iOri][mask];returnEigenFields,Intensities
[docs]defbuild_spectrum(system,mwFreq,Range,knots=19,npoints=1000,Guass_broadening=0.25):"""Build a field sweep spectrum Parameters ---------- system : SpinSystem The spin system it must include: I & S spins, g, A, gn mwFreq : float The microwave frequency in MHz Range : float The field range in mT knots : int, optional The number of knots of orientation averaging, by default 19 npoints : int, optional The number of points in the spectrum, by default 1000 Returns ------- xAxis: np.ndarray The xAxis in mT y: np.ndarray The spectrum intensities normalised to 1 """phi,theta,Weights=dl.sophegrid(1,np.pi/2,knots)Orientations=np.vstack([phi,theta,np.zeros(phi.shape)]).TnOrientations=Orientations.shape[0]EigenFields,Intensities=resfields(system,Orientations,mwFreq)nReson=0forkinEigenFields:nReson+=k.sizenSites=1ifisinstance(Range,np.ndarray):xAxis=Rangexmin=Range.min()xmax=Range.max()npoints=Range.shape[0]prefactor=(npoints-1)/(Range.max()-Range.min())else:xAxis=np.linspace(*Range,npoints)xmin=Range[0]xmax=Range[1]prefactor=(npoints-1)/(Range[1]-Range[0])dx=xAxis[1]-xAxis[0]spec=np.zeros(npoints)foriOriinrange(nOrientations):thisP=EigenFields[iOri]Amplitudes=Intensities[iOri]idxPos=np.around(1+prefactor*(thisP-xmin))outofRange=np.less(idxPos,1)|np.greater(idxPos,npoints)spec[idxPos[~outofRange].astype(int)]+=Amplitudes[~outofRange]*Weights[iOri]# Convolution broadeningwin=signal.windows.gaussian(npoints,Guass_broadening/dx)filtered=signal.convolve(spec,win,mode='same')filtered/=filtered.max()returnxAxis,filtered