frompyepr.classesimportParameterfrompyepr.utilsimportbuild_table,sop,autoEPRDecoderfrommemoizationimportcachedimportnumpyasnpfromscipy.integrateimportcumulative_trapezoidimportnumbersimportuuidimportjsonimportbase64importmatplotlib.pyplotaspltimportscipy.fftasfftfrompyeprimport__version__importcopyfromfunctoolsimportreducefromitertoolsimportaccumulateclassPulse:""" Represents a general experimental pulse. """def__init__(self,*,tp,t=None,scale=None,flipangle=None,pcyc=[0],name=None,**kwargs)->None:"""The class for a general pulse. Parameters ---------- tp : float The pulse length in ns. scale : float The arbitary experimental pulse amplitude, 0-1. t : float, optional The pulse start time in ns. """iftisnotNone:ifisinstance(t,numbers.Number):self.t=Parameter("t",t,"ns","Start time of pulse")elifisinstance(t,Parameter):self.t=telse:self.t=Noneifisinstance(tp,numbers.Number):self.tp=Parameter("tp",tp,"ns","Length of the pulse")elifisinstance(tp,Parameter):self.tp=tpifisinstance(scale,numbers.Number):self.scale=Parameter("scale",scale,None,"Amplitude of pulse")elifisinstance(scale,Parameter):self.scale=scaleelifscaleisNone:self.scale=Parameter("scale",None,None,"Amplitude of pulse")self.name=nameself.Progression=FalseifflipangleisnotNone:self.flipangle=Parameter("flipangle",flipangle,None,"The target flip angle of the spins")ifpcycisNone:self.pcyc=Noneeliftype(pcyc)isdict:self._addPhaseCycle(pcyc["phases"],detections=pcyc["dets"])else:self._addPhaseCycle(pcyc,detections=None)pass@propertydefbandwidth(self):BW=Parameter("Bandwidth",0,"GHz","Bandwidth of pulse")ifhasattr(self,"freq"):BW.value=0elifhasattr(self,"init_freq")&hasattr(self,"final_freq"):BW.value=np.abs(self.final_freq.value-self.init_freq.value)else:raiseValueError()returnBWdef_addPhaseCycle(self,phases,detections=None):""" Adds a phase cycle to the pulse sequence. Args: phases (list): List of phases to add to the phase cycle. detections (list, optional): List of detection signs. Defaults to None. If None then all cycles are summed. Returns: None """ifdetectionsisNone:detections=np.ones(len(phases))self.pcyc={"Phases":list(phases),"DetSigns":list(detections)}passdef_buildFMAM(self,func,ax=None):""" Builds the amplitude modulation (AM) and frequency modulation (FM) of a given function. Args: func: A function that takes in an array of values and returns two arrays, representing the AM and FM of the function. Returns: Two arrays representing the AM and FM of the function. """self.ax=np.linspace(-self.tp.value/2,self.tp.value/2,1001)dt=self.ax[1]-self.ax[0]self.AM,self.FM=func(self.ax)FM_arg=2*np.pi*cumulative_trapezoid(self.FM,initial=0)*dtself.complex=self.AM*(np.cos(FM_arg)+1j*np.sin(FM_arg))self.FMself.AMreturnself.AM,self.FMdefbuild_shape(self,ax=None):ifaxisNone:ax=self.axdt=ax[1]-ax[0]pulse_points=int(np.around(self.tp.value/dt))total_points=ax.shape[0]remaining_points=total_points-pulse_pointspulse_axis=np.zeros(pulse_points)AM,FM=self.func(pulse_axis)AM=np.pad(AM,(int(np.floor(remaining_points/2)),int(np.ceil(remaining_points/2))),'constant',constant_values=0)FM=np.pad(FM,(int(np.floor(remaining_points/2)),int(np.ceil(remaining_points/2))),'constant',constant_values=0)FM_arg=2*np.pi*cumulative_trapezoid(FM,initial=0)*dtshape=AM*(np.cos(FM_arg)+1j*np.sin(FM_arg))returnshapedefbuild_table(self):""" Builds a table of variables, axes, and UUIDs for all non-static Parameters in the object. Returns: dict: A dictionary containing the following keys: "Variable", "axis", and "uuid". The values for each key are lists of the corresponding values for each non-static Parameter. """progTable={"Variable":[],"axis":[],"uuid":[]}forarginvars(self):attr=getattr(self,arg)iftype(attr)isParameterandnotattr.is_static():fori,axesinenumerate(attr.axis):progTable["Variable"].append(arg)progTable["axis"].append(axes["axis"])progTable["uuid"].append(axes["uuid"])returnprogTabledefis_static(self):""" Check if all parameters in the pulse object are static. Returns: bool: True if all parameters are static, False otherwise. """forarginvars(self):attr=getattr(self,arg)iftype(attr)isParameterandnotattr.is_static():returnFalsereturnTruedefisDelayFocused(self):""" Does the pulse contain a specified time, `t`? If so then it is not delay focused. """ifself.tisNone:returnTrueelse:returnFalsedefisPulseFocused(self):""" Does the pulse contain a specified time, `t`? If so then it is delay focused. """ifself.tisnotNone:returnTrueelse:returnFalsedefplot(self,pad=1000):"""Plots the time domain representation of this pulse. Parameters ---------- pad : int, optional The number of zeros to pad the data with, by default 1000 """dt=self.ax[1]-self.ax[0]ifself.isPulseFocused():tax=self.t.value+self.axelse:tax=self.axfwd_ex=np.linspace(tax[0]-dt*pad,tax[0],pad)rev_ex=np.linspace(tax[-1]+dt,tax[-1]+dt*pad,pad,endpoint=True)tax=np.concatenate((fwd_ex,tax,rev_ex))data=np.pad(self.complex,pad)plt.plot(tax,np.real(data),label="Re")plt.plot(tax,np.imag(data),label="Im")plt.legend()plt.xlabel("Time (ns)")plt.ylabel("Amplitude (A.U.)")passdef_calc_fft(self,pad=10000):self._buildFMAM(self.func)data=np.pad(self.complex,int(pad))pulse_fft=fft.fftshift(fft.fft(data))n=data.shape[0]dt=self.ax[1]-self.ax[0]axis_fft=fft.fftshift(fft.fftfreq(n,dt))returnaxis_fft,pulse_fft@propertydefamp_factor(self):""" The B1 amplitude factor (nutation frequency) for the pulse in GHz"""amp_factor_value=self.flipangle.value/(2*np.pi*np.trapz(self.AM,self.ax))returnParameter("amp_factor",amp_factor_value,"GHz","Amplitude factor for the pulse")# @cached(thread_safe=False)defexciteprofile_old(self,freqs=None,resonator=None):"""Excitation profile Generates the exciatation profiles for this pulse. This function is ported from EasySpin (https://easyspin.org/easyspin/documentation/sop.html) [1-2], and based upon the method from Gunnar Jeschke, Stefan Pribitzer and Andrin Doll[3]. References: +++++++++++ [1] Stefan Stoll, Arthur Schweiger EasySpin, a comprehensive software package for spectral simulation and analysis in EPR J. Magn. Reson. 178(1), 42-55 (2006) [2] Stefan Stoll, R. David Britt General and efficient simulation of pulse EPR spectra Phys. Chem. Chem. Phys. 11, 6614-6625 (2009) [3] Jeschke, G., Pribitzer, S. & DollA. Coherence Transfer by Passage Pulses in Electron Paramagnetic Resonance Spectroscopy. J. Phys. Chem. B 119, 13570-13582 (2015) Parameters ---------- freqs: np.ndarray, optional The frequency axis. Caution: A larger number of points will linearly increase computation time. resonator: ad.ResonatorProfile, optional Returns ------- Mx: np.ndarray The magentisation in the X direction. My: np.ndarray The magentisation in the Y direction. Mz: np.ndarray The magentisation in the Z direction. """eye=np.identity(2)# offsets = np.arange(-2*BW,2*BW,0.002)self._buildFMAM(self.func)iffreqsisNone:fxs,fs=self._calc_fft()fs=np.abs(fs)points=np.argwhere(fs>(fs.max()*0.5))[:,0]BW=fxs[points[-1]]-fxs[points[0]]c_freq=np.mean([fxs[points[-1]],fxs[points[0]]])offsets=np.linspace(-2*BW,2*BW,100)+c_freqelse:offsets=freqsoffsetst=self.axnOffsets=offsets.shape[0]ISignal=np.real(self.complex)*self.amp_factor.valueQSignal=np.imag(self.complex)*self.amp_factor.valueifresonatorisnotNone:FM=self.FMamp_factor=np.interp(FM,resonator.freqs-resonator.LO_c,resonator.profile)amp_factor=np.min([amp_factor,np.ones_like(amp_factor)*self.amp_factor.value],axis=0)ISignal=np.real(self.complex)*amp_factorQSignal=np.imag(self.complex)*amp_factornpoints=ISignal.shape[0]Sx=sop([1/2],"x")Sy=sop([1/2],"y")Sz=sop([1/2],"z")Density0=-SzMag=np.zeros((3,nOffsets),dtype=np.complex128)foriOffsetinrange(0,nOffsets):UPulse=eyeHam0=offsets[iOffset]*Szforitinrange(0,npoints):Ham=ISignal[it]*Sx+QSignal[it]*Sy+Ham0M=-2j*np.pi*(t[1]-t[0])*Hamq=np.sqrt(M[0,0]**2-np.abs(M[0,1])**2)ifnp.abs(q)<1e-10:dU=eye+Melse:dU=np.cosh(q)*eye+(np.sinh(q)/q)*MUPulse=dU@UPulseDensity=UPulse@Density0@UPulse.conjugate().TMag[0,iOffset]=-2*(Sx@Density.T).sum().realMag[1,iOffset]=-2*(Sy@Density.T).sum().realMag[2,iOffset]=-2*(Sz*Density.T).sum().realreturnMag[0,:],Mag[1,:],Mag[2,:]@cached(thread_safe=False)defexciteprofile(self,freqs=None,resonator=None,trajectory=False):"""Excitation profile Generates the exciatation profiles for this pulse, using the two-level system model developed by Jeschke et al. [1]. And then optimised in the PulseShape software package by Maxx Tessmer [2], reproduced under the GNU GPL v3 license. References: +++++++++++ [1] Jeschke, G., Pribitzer, S. & Doll, A. Coherence Transfer by Passage Pulses in Electron Paramagnetic Resonance Spectroscopy. J. Phys. Chem. B 119, 13570-13582 (2015) [2] https://github.com/mtessmer/PulseShape Parameters ---------- freqs: np.ndarray, optional The frequency axis. Caution: A larger number of points will linearly increase computation time. resonator: ad.ResonatorProfile, optional The resonator profile to apply resonator compensation. trajectory: bool, optional If True, the function will return the magnetisation at each time point. Default is False. Returns ------- Mag: np.ndarray The magnetisation in the X, Y, and Z directions. """self._buildFMAM(self.func)iffreqsisNone:fxs,fs=self._calc_fft()fs=np.abs(fs)points=np.argwhere(fs>(fs.max()*0.5))[:,0]BW=fxs[points[-1]]-fxs[points[0]]c_freq=np.mean([fxs[points[-1]],fxs[points[0]]])offsets=np.linspace(-2*BW,2*BW,100)+c_freqelse:offsets=freqsISignal=np.real(self.complex)*self.amp_factor.valueQSignal=np.imag(self.complex)*self.amp_factor.valueifresonatorisnotNone:FM=self.FMamp_factor=np.interp(FM,resonator.freqs-resonator.LO_c,resonator.profile)amp_factor=np.min([amp_factor,np.ones_like(amp_factor)*self.amp_factor.value],axis=0)ISignal=np.real(self.complex)*amp_factorQSignal=np.imag(self.complex)*amp_factort=self.axM0=[0,0,1]M0=np.asarray(M0,dtype=float)iflen(M0.shape)==1:Mmag=np.linalg.norm(M0)M0/=MmagM0=np.tile(M0,(len(offsets),1))Mmag=np.array([Mmagforiinrange(len(offsets))])else:Mmag=np.linalg.norm(M0,axis=1)M0/=Mmag[:,None]Sx=sop([1/2],"x")Sy=sop([1/2],"y")Sz=sop([1/2],"z")density0=0.5*np.array(([[1+M0[:,2],M0[:,0]-1j*M0[:,1]],[M0[:,0]+1j*M0[:,1],1-M0[:,2]]]))density0=np.moveaxis(density0,2,0)dt=t[1]-t[0]H=offsets[:,None,None]*SzH=H[:,None,:,:]+ISignal[:,None,None]*Sx+QSignal[:,None,None]*SyM=-2j*np.pi*dt*Hq=np.sqrt(M[:,:,0,0]**2-np.abs(M[:,:,0,1])**2)dUs=np.cosh(q)[:,:,None,None]*np.eye(2,dtype=complex)+(np.sinh(q)/q)[:,:,None,None]*Mmask=np.abs(q)<1e-10dUs[mask]=np.eye(2,dtype=complex)+M[mask]ifnottrajectory:Upulses=np.empty((len(dUs),2,2),dtype=complex)foriinrange(len(dUs)):Upulses[i]=reduce(lambdax,y:y@x,dUs[i,:-1])density=np.einsum('ijk,ikl,ilm->ijm',Upulses,density0,Upulses.conj().transpose((0,2,1)))density=density.transpose((0,2,1))Mag=np.zeros((len(offsets),3))Mag[...,0]=2*density[...,0,1].real# 2 * (Sx[None, :, :] * density).sum(axis=(1, 2)).realMag[...,1]=-2*density[...,1,0].imag# 2 * (Sy[None, :, :] * density).sum(axis=(1, 2)).realMag[...,2]=density[...,0,0]-density[...,1,1]# 2 * (Sz[None, :, :] * density).sum(axis=(1, 2)).realreturnnp.squeeze(Mag*Mmag[:,None])else:Upulses=np.empty((len(dUs),len(t),2,2),dtype=complex)foriinrange(len(dUs)):Upulses[i]=[np.eye(2)]+list((accumulate(dUs[i,:-1],lambdax,y:y@x)))density=np.einsum('hijk,hkl,hilm->hijm',Upulses,density0,Upulses.conj().transpose((0,1,3,2)))density=density.transpose((0,1,3,2))Mag=np.zeros((len(offsets),len(t),3))Mag[...,0]=2*density[...,0,1].real# 2 * (Sx[None, None, :, :] * density).sum(axis=(2, 3)).realMag[...,1]=-2*density[...,1,0].imag# 2 * (Sy[None, None, :, :] * density).sum(axis=(2, 3)).realMag[...,2]=density[...,0,0]-density[...,1,1]# 2 * (Sz[None, None, :, :] * density).sum(axis=(2, 3)).realreturnMagdefplot_fft(self):ax,ft=self._calc_fft(1e4)plt.plot(ax,np.real(ft),label="Re")plt.plot(ax,np.imag(ft),label="Im")plt.plot(ax,np.abs(ft),label="Im")plt.legend()plt.xlabel("Frequency (GHz)")plt.ylabel("Amplitude (A.U.)")passdef_pcyc_str(self):ifself.pcycisnotNone:pcyc_table="["dets=self.pcyc["DetSigns"]phases=self.pcyc["Phases"]foriinrange(0,len(phases)):ifdets[i]==1:pcyc_table+="+"elifdets[i]==-1:pcyc_table+="-"elifdets[i]==1j:pcyc_table+="+i"elifdets[i]==-1j:pcyc_table+="-i"ifphases[i]==0:pcyc_table+="(+x)"elifphases[i]==np.pi:pcyc_table+="(-x)"elifphases[i]==np.pi/2:pcyc_table+="(+y)"elifphases[i]==-np.pi/2:pcyc_table+="(-y)"elifphases[i]==3*np.pi/2:pcyc_table+="(-y)"pcyc_table+=" "pcyc_table=pcyc_table[:-1]pcyc_table+="]"else:pcyc_table=""returnpcyc_tabledef__str__(self):# Build header lineheader="#"*79+"\n"+"autoDEER Pulse Definition"+ \
"\n"+"#"*79+"\n"# Build Overviewsifself.isPulseFocused():overview_params="Time Pos (ns) \tLength (ns) \tScale (0-1)"+\
"\t Static \n"iftype(self)isDetection:overview_params+=f"{self.t.value}"+"\t\t"+ \
f"{self.tp.value}"+"\t\t"+"\t\t"+\
f"{self.is_static()}"+"\n"+"#"*79+"\n"else:overview_params+=f"{self.t.value}"+"\t\t"+ \
f"{self.tp.value}"+"\t\t"+f"{self.scale.value}"+ \
"\t\t"+f"{self.is_static()}"+"\n"+"#"*79+"\n"elifself.isDelayFocused():overview_params="Length (ns) \tScale (0-1)"+\
"\t Static \n"iftype(self)isDetection:overview_params+=f"{self.tp.value}"+"\t\t"+"\t\t"+\
f"{self.is_static()}"+"\n"+"#"*79+"\n"else:overview_params+=f"{self.tp.value}"+"\t\t"+\
f"{self.scale.value}"+"\t\t"+f"{self.is_static()}"+\
"\n"+"#"*79+"\n"# Build Pulse Parameter Tableparam_table=f"{'Name':^12}\t{'Value':^12}\t{'Unit':^7}\t Description\n"param_table+=f"{'----':^12}\t{'-----':^12}\t{'----':^7}\t -----------\n"forattr_nameindir(self):attr=getattr(self,attr_name)iftype(attr)isParameter:ifattr.name=="flipangle":ifattr.value==np.pi:value=f"{'π':>12}"elifattr.value==np.pi/2:value=f"{'π/2':>12}"elifattr.value=='Hard':value=f"{'Hard':>12}"else:value=f"{attr.value:>5.5g}"elifattr.valueisNone:value=f"{'N/A':>12}"else:value=f"{attr.value:>5.5g}"ifattr.unitisNone:attr.unit="None"ifattr.descriptionisNone:attr.description=""param_table+=f"{attr.name:>12}\t{value:>12}\t"+\
f"{attr.unit:>7}\t{attr.description}\n"# Build Pulse Progression Tableifself.ProgressionisTrue:prog_table="#"*79+"\n"+"Progressive Variables:"+"\n"prog_table+="Variable \t Axis \t Length \n"prog_table+="-------- \t ---- \t ------ \n"prog_table+=f"{self.Prog_var}\t\t{self.Prog_axis}\t"+\
f"{self.Prog_n}\n"else:prog_table=""# Build Pulse Phase Cycle Tableifself.pcycisnotNone:pcyc_table="#"*79+"\n"+"Phase Cycle:"+"\t"pcyc_table+=self._pcyc_str()pcyc_table+="\n"else:pcyc_table=""# Build Footerfooter="#"*79+"\n"+\
f"Built by autoDEER Version: {__version__}"+"\n"+"#"*79# Combine Allstring=header+overview_params+param_table+prog_table+\
pcyc_table+footerreturnstringdefcopy(self,clear=False,**kwargs):"""Creates a deep-copy of the pulse. I.e. Every parameter object is re-created at another memory space. Parameter can be chaged at this stage by adding them as keyword- arguments (kwargs). Returns ------- Pulse A deep copy of the pulse """new_pulse=copy.deepcopy(self)ifclear:forarginvars(new_pulse):attr=getattr(new_pulse,arg)iftype(attr)isParameterandnotgetattr(new_pulse,arg).is_static():getattr(new_pulse,arg).remove_dynamic()forarginkwargs:if(hasattr(new_pulse,arg))and(getattr(new_pulse,arg)isnotNone):attr=getattr(new_pulse,arg)iftype(attr)isParameter:ifisinstance(kwargs[arg],numbers.Number):attr.value=kwargs[arg]elifisinstance(kwargs[arg],Parameter):attr.value=kwargs[arg].valueattr.axis=kwargs[arg].axiselifarg=="pcyc":new_pcyc=kwargs[arg]ifattrisNone:new_pulse.pcyc=Noneeliftype(new_pcyc)isdict:new_pulse._addPhaseCycle(new_pcyc["phases"],detections=new_pcyc["dets"])else:new_pulse._addPhaseCycle(new_pcyc,detections=None)else:attr=kwargs[arg]elifarg=="t":iftype(kwargs[arg])isParameter:kwargs[arg].name="t"setattr(new_pulse,arg,kwargs[arg])elifisinstance(kwargs[arg],numbers.Number):setattr(new_pulse,arg,Parameter("t",kwargs[arg],"ns","Start time of pulse"))else:raiseValueError("new parameters must be either numeric or Parameters")elifarg=="scale":setattr(new_pulse,arg,Parameter("scale",kwargs[arg],None,"Amplitude of pulse"))elifarg=="flipangle":setattr(new_pulse,arg,Parameter("flipangle",kwargs[arg],None,"The target flip angle of the spins"))else:setattr(new_pulse,arg,kwargs[arg])returnnew_pulsedef_to_dict(self):to_return={"version":__version__,"type":"Pulse","subclass":str(type(self))}forkey,varinvars(self).items():ifisinstance(var,Parameter):to_return[key]=var._to_dict()else:to_return[key]=varreturnto_returndef_to_json(self):classautoEPREncoder(json.JSONEncoder):defdefault(self,obj):ifisinstance(obj,np.ndarray):if(len(obj)>0)andisinstance(obj[0],str):returnlist(obj)data=np.ascontiguousarray(obj.data)data_b64=base64.b64encode(data)returndict(__ndarray__=str(data_b64),dtype=str(obj.dtype),shape=obj.shape)ifisinstance(obj,complex):returnstr(obj)ifisinstance(obj,numbers.Number):returnstr(obj)ifisinstance(obj,uuid.UUID):return_dict={"__uuid__":str(obj)}returnreturn_dictifisinstance(obj,Parameter):returnobj._to_dict()ifisinstance(obj,Pulse):returnobj._to_dict()else:returnjson.JSONEncoder.default(self,obj)returnjson.dumps(self._to_dict(),cls=autoEPREncoder,indent=4)defsave(self,filename):"""Save the Pulse to a JSON file. Parameters ---------- filename : str Path to the JSON file. Returns ------- None Raises ------ TypeError If the object cannot be serialized to JSON. Example ------- >>> obj = Pulse() >>> obj.save("my_pulse.json") """withopen(filename,"w")asf:f.write(self._to_json())@classmethoddef_from_dict(cls,dct):new_pulse=cls(tp=Parameter._from_dict(dct["tp"]),name=dct["name"])forkey,varindct.items():ifisinstance(var,dict)and("type"invar):setattr(new_pulse,key,Parameter._from_dict(var))elifkey=="type":continueelifkey=="version":continueelse:setattr(new_pulse,key,var)returnnew_pulse@classmethoddef_from_json(cls,JSONstring):dct=json.loads(JSONstring,object_hook=autoEPRDecoder)returncls._from_dict(dct)@classmethoddefload(cls,filename):"""Load a Pulse object from a JSON file. Parameters ---------- filename : str Path to the JSON file. Returns ------- obj : Pulse The Pulse loaded from the JSON file. Raises ------ FileNotFoundError If the file does not exist. Example ------- >>> obj = Pulse.load("my_pulse.json") """withopen(filename,"r")asf:file_buffer=f.read()returncls._from_json(file_buffer)# =============================================================================# Subclasses# =============================================================================classDetection(Pulse):""" Represents a detection pulse. """def__init__(self,*,tp,t=None,freq=0,**kwargs)->None:"""A general detection pulse. Parameters ---------- tp : float The **total** time of the detection event. The detection event will be symetrical about the centre time. t : float, optional The **centre** time of the detection event freq: float, optional The detection frequency, not all spectrometer support this functionality, by default 0 MHz """Pulse.__init__(self,t=t,tp=tp,**kwargs)self.scale=Noneself.freq=Parameter("freq",freq,"GHz","The detection frequency, if supported")self.pcyc=Nonepass# =============================================================================classDelay(Pulse):""" DEPRECATION WARNING: THIS WILL BE REMOVED SOON. Represents a inter-pulse delay pulse. """def__init__(self,*,tp,t=None)->None:iftisnotNone:self.t=Parameter("Time",t,"ns","Start time of pulse")else:self.t=Noneself.tp=Parameter("Length",tp,"ns","Length of the pulse")self.pcyc=Noneself.scale=None# =============================================================================classRectPulse(Pulse):""" Represents a rectangular monochromatic pulse. """def__init__(self,tp=16,freq=0,t=None,flipangle=None,**kwargs)->None:""" Parameters ---------- tp : flaot, optional Pulse Length in ns, by default 16 freq : float,optional Frequency in MHz, by default 0 t : float, optional Time position in ns, by default None flipangle : _type_, optional The flip angle in radians, by default None """Pulse.__init__(self,tp=tp,t=t,flipangle=flipangle,name='RectPulse',**kwargs)self.freq=Parameter("freq",freq,"GHz","Frequency of the Pulse")self._buildFMAM(self.func)passdeffunc(self,ax):nx=ax.shape[0]AM=np.ones(nx)FM=np.zeros(nx)+self.freq.valuereturnAM,FMclassGaussianPulse(Pulse):""" Represents a Gaussian monochromatic pulse. """def__init__(self,*,tp=32,FWHM=16,freq=0,**kwargs)->None:""" Represents a Gaussian monochromatic pulse. Parameters ---------- tp : float Pulse length in ns, by default 128 FWHM : float, The full width at half maximum of the pulse freq : float, optional The frequency of the pulse, by default 0 """Pulse.__init__(self,tp=tp,name='GaussianPulse',**kwargs)self.freq=Parameter("Freq",freq,"GHz","Frequency of the Pulse")self.FWHM=Parameter("FWHM",FWHM,"ns","Full Width at Half Maximum of the Pulse")self._buildFMAM(self.func)passdeffunc(self,ax):nx=ax.shape[0]sigma=self.FWHM.value/(2*np.sqrt(2*np.log(2)))AM=np.exp(-ax**2/(2*sigma**2))FM=np.zeros(nx)+self.freq.valuereturnAM,FM# =============================================================================classFrequencySweptPulse(Pulse):""" A general parent class for Frequency Swept Pulses. """def__init__(self,*,tp,t=None,scale=None,flipangle=None,pcyc=[0],name=None,**kwargs)->None:super().__init__(tp=tp,t=t,scale=scale,flipangle=flipangle,pcyc=pcyc,name=name,**kwargs)# Extract frequency infomation, can be either specified with an initial and final frequency or a bandwidth combined with one of the two or the central frequencyif"BW"inkwargs:BW=kwargs["BW"]if"init_freq"inkwargs:self.init_freq=Parameter("init_freq",kwargs["init_freq"],"GHz","Initial frequency of pulse")self.final_freq=Parameter("final_freq",self.init_freq.value+BW,"GHz","Final frequency of pulse")elif"final_freq"inkwargs:self.final_freq=Parameter("final_freq",kwargs["final_freq"],"GHz","Final frequency of pulse")self.init_freq=Parameter("init_freq",self.final_freq.value-BW,"GHz","Initial frequency of pulse")else:raiseValueError("Bandwidth must be combined with either an initial or final frequency")elif("init_freq"inkwargs)&("final_freq"inkwargs):self.init_freq=Parameter("init_freq",kwargs["init_freq"],"GHz","Initial frequency of pulse")self.final_freq=Parameter("final_freq",kwargs["final_freq"],"GHz","Final frequency of pulse")else:raiseValueError("Frequency information must be provided")@propertydefQcrit(self):""" The critical Q factor for the pulse."""Qcrit=(2/np.pi)*np.log(2/(1+np.cos(self.flipangle.value)))Qcrit=np.min([Qcrit,5])returnParameter("Qcrit",Qcrit,None,"Critical Q factor for the pulse")@propertydefsweeprate(self):""" The sweep rate of the pulse in GHz/ns"""raiseNotImplementedError("This property must be implemented in the subclass")@propertydefamp_factor(self):""" The B1 amplitude factor (nutation frequency) for the pulse in GHz"""amp_factor_value=np.sqrt(2*np.pi*self.Qcrit.value*self.sweeprate.value)/(2*np.pi)returnParameter("amp_factor",amp_factor_value,"GHz","Amplitude factor for the pulse")classHSPulse(FrequencySweptPulse):""" Represents a hyperboilc secant frequency-swept pulse. """def__init__(self,*,tp=128,order1=1,order2=6,beta=20,**kwargs)->None:FrequencySweptPulse.__init__(self,tp=tp,name='HSPulse',**kwargs)self.order1=Parameter("order1",order1,None,"Order 1 of the HS Pulse")self.order2=Parameter("order2",order2,None,"Order 2 of the HS Pulse")self.beta=Parameter("beta",beta,None,"Beta of the HS Pulse")self._buildFMAM(self.func)passdeffunc(self,ax):beta=self.beta.valueorder1=self.order1.valueorder2=self.order2.valuetp=ax.max()-ax.min()tcent=tp/2ti=axnx=ax.shape[0]# beta_exp1 = np.log(beta*0.5**(1-order1)) / np.log(beta)# beta_exp2 = np.log(beta*0.5**(1-order2)) / np.log(beta)# cut = round(nx/2)# AM = np.zeros(nx)# AM[0:cut] = 1/np.cosh(# beta**beta_exp1 * (ax[0:cut]/tp)**order1)# AM[cut:-1] = 1/np.cosh(# beta**beta_exp2 * (ax[cut:-1]/tp)**order2)# FM = BW * cumulative_trapezoid(AM**2,ax,initial=0) /\# np.trapz(AM**2,ax) + self.init_freq.valuesech=lambdax:1/np.cosh(x)cut=round(nx/2)AM=np.zeros_like(ti)AM[:cut]=sech(beta*0.5*(2*ti[:cut]/tp)**order1)AM[cut:]=sech(beta*0.5*(2*ti[cut:]/tp)**order2)BWinf=(self.bandwidth.value)/np.tanh(beta/2)freq=(BWinf/2)*np.tanh((beta/tp*ti))+np.mean([self.init_freq.value,self.final_freq.value])# phase = 2*np.pi*(BWinf/2) * (tp/beta) * np.log(np.cosh((beta/tp)*ti))# total_phase = phase * 2* np.pi * np.mean([self.init_freq.value,self.final_freq.value])returnAM,freq@propertydefsweeprate(self):""" The sweep rate of the pulse in GHz/ns"""sweeprate_value=self.beta.value*self.bandwidth.value/(2*self.tp.value)returnParameter("sweeprate",sweeprate_value,"GHz/ns","Sweep rate of the pulse")# =============================================================================classChirpPulse(FrequencySweptPulse):""" Represents a linear frequency-swept pulse. """def__init__(self,*,tp=128,**kwargs)->None:FrequencySweptPulse.__init__(self,tp=tp,name='ChirpPulse',**kwargs)self._buildFMAM(self.func)passdeffunc(self,ax):nx=ax.shape[0]AM=np.ones(nx)FM=np.linspace(self.init_freq.value,self.final_freq.value,nx)returnAM,FM@propertydefsweeprate(self):""" The sweep rate of the pulse in GHz/ns"""sweeprate_value=self.bandwidth.value/self.tp.valuereturnParameter("sweeprate",sweeprate_value,"GHz/ns","Sweep rate of the pulse")# =============================================================================classSincPulse(Pulse):def__init__(self,*,tp=128,freq=0,order=6,window=None,**kwargs)->None:""" Represents a sinc shaped monochromatic pulse. Parameters ---------- tp : int Pulse length in ns, by default 128 freq : int, optional The frequency of the pulse, by default 0 order : int, optional The order of this sinc function, by default 6 window : _type_, optional The window function, by default None """Pulse.__init__(self,tp=tp,name='SincPulse',**kwargs)self.freq=Parameter("Freq",freq,"GHz","Frequency of the Pulse")self.order=Parameter("Order",order,None,"The sinc pulse order")self.window=Parameter("Window",window,None,"The type of window function")self._buildFMAM(self.func)passdeffunc(self,ax):nx=ax.shape[0]FM=np.zeros(nx)+self.freq.valueAM=np.sinc(self.order.value*ax)returnAM,FM# class GaussianPulse(Pulse):# """# Represents a Gaussian monochromatic pulse.# """# def __init__(self, *, tp=128, freq=0, **kwargs) -> None:# """ Represents a Gaussian monochromatic pulse.# Parameters# ----------# tp : int# Pulse length in ns, by default 128# freq : int, optional# The frequency of the pulse, by default 0# """# Pulse.__init__(self, tp=tp,name='GaussianPulse', **kwargs)# self.freq = Parameter("Freq", freq, "GHz", "Frequency of the Pulse")# self._buildFMAM(self.func)# pass# def func(self, ax):# nx = ax.shape[0]# FM = np.zeros(nx) + self.freq.value# AM = np.exp(-ax**2/(((self.tp.value/2)**2)/(-1*np.log(0.001))))# return AM, FM# =============================================================================