# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file."""Signal object and related classes---------------------------------"""# pylint: disable=invalid-name # Allows short reference names like x, y, ...# pylint: disable=duplicate-codefrom__future__importannotationsfromcontextlibimportcontextmanagerfromtypingimportTYPE_CHECKING,Generator,Typefromuuidimportuuid4importguidata.datasetasgdsimportnumpyasnpimportscipy.signalasspsfromguidata.configtoolsimportget_iconfromguidata.datasetimportrestore_dataset,update_datasetfromguidata.qthelpersimportexec_dialogfromplotpy.builderimportmakefromplotpy.itemsimportCurveItem,XRangeSelectionfromplotpy.toolsimportEditPointToolfromqtpyimportQtWidgetsasQWfromcdl.algorithms.signalimportGaussianModel,LorentzianModel,VoigtModelfromcdl.configimportConf,_fromcdl.core.modelimportbaseifTYPE_CHECKING:fromplotpy.plotimportPlotDialogfromplotpy.stylesimportCurveParamclassCurveStyles:"""Object to manage curve styles"""#: Curve colorsCOLORS=("#1f77b4",# muted blue"#ff7f0e",# safety orange"#2ca02c",# cooked asparagus green"#d62728",# brick red"#9467bd",# muted purple"#8c564b",# chestnut brown"#e377c2",# raspberry yogurt pink"#7f7f7f",# gray"#bcbd22",# curry yellow-green"#17becf",# blue-teal)#: Curve line stylesLINESTYLES=("SolidLine","DashLine","DashDotLine","DashDotDotLine")def__init__(self)->None:self.__suspend=Falseself.curve_style=self.style_generator()@staticmethoddefstyle_generator()->Generator[tuple[str,str],None,None]:"""Cycling through curve styles"""whileTrue:forlinestyleinCurveStyles.LINESTYLES:forcolorinCurveStyles.COLORS:yield(color,linestyle)defapply_style(self,param:CurveParam)->None:"""Apply style to curve"""ifself.__suspend:# Suspend mode: always apply the first stylecolor,linestyle=CurveStyles.COLORS[0],CurveStyles.LINESTYLES[0]else:color,linestyle=next(self.curve_style)param.line.color=colorparam.line.style=linestyleparam.symbol.marker="NoSymbol"defreset_styles(self)->None:"""Reset styles"""self.curve_style=self.style_generator()@contextmanagerdefalternative(self,other_style_generator:Generator[tuple[str,str],None,None])->Generator[None,None,None]:"""Use an alternative style generator"""old_style_generator=self.curve_styleself.curve_style=other_style_generatoryieldself.curve_style=old_style_generator@contextmanagerdefsuspend(self)->Generator[None,None,None]:"""Suspend style generator"""self.__suspend=Trueyieldself.__suspend=FalseCURVESTYLES=CurveStyles()# This is the unique instance of the CurveStyles class
[docs]classROI1DParam(base.BaseROIParam["SignalObj","SegmentROI"]):"""Signal ROI parameters"""# Note: in this class, the ROI parameters are stored as X coordinatesxmin=gds.FloatItem(_("First point coordinate"))xmax=gds.FloatItem(_("Last point coordinate"))
[docs]defto_single_roi(self,obj:SignalObj,title:str="")->SegmentROI:"""Convert parameters to single ROI Args: obj: signal object title: ROI title Returns: Single ROI """returnSegmentROI([self.xmin,self.xmax],False,title=title)
[docs]defget_data(self,obj:SignalObj)->np.ndarray:"""Get signal data in ROI Args: obj: signal object Returns: Data in ROI """imin,imax=np.searchsorted(obj.x,[self.xmin,self.xmax])returnnp.array([obj.x[imin:imax],obj.y[imin:imax]])
classSegmentROI(base.BaseSingleROI["SignalObj",ROI1DParam,XRangeSelection]):"""Segment ROI Args: coords: ROI coordinates (xmin, xmax) title: ROI title """# Note: in this class, the ROI parameters are stored as X indicesdefcheck_coords(self)->None:"""Check if coords are valid Raises: ValueError: invalid coords """iflen(self.coords)!=2:raiseValueError("Invalid ROI segment coords (2 values expected)")ifself.coords[0]>=self.coords[1]:raiseValueError("Invalid ROI segment coords (xmin >= xmax)")defget_data(self,obj:SignalObj)->np.ndarray:"""Get signal data in ROI Args: obj: signal object Returns: Data in ROI """imin,imax=self.get_indices_coords(obj)returnnp.array([obj.x[imin:imax],obj.y[imin:imax]])defto_mask(self,obj:SignalObj)->np.ndarray:"""Create mask from ROI Args: obj: signal object Returns: Mask (boolean array where True values are inside the ROI) """mask=np.ones_like(obj.xydata,dtype=bool)imin,imax=self.get_indices_coords(obj)mask[:,imin:imax]=Falsereturnmask# pylint: disable=unused-argumentdefto_param(self,obj:SignalObj,title:str|None=None)->ROI1DParam:"""Convert ROI to parameters Args: obj: object (signal), for physical-indices coordinates conversion title: ROI title """title=titleorself.titleparam=ROI1DParam(title)param.xmin,param.xmax=self.get_physical_coords(obj)returnparam# pylint: disable=unused-argumentdefto_plot_item(self,obj:SignalObj,title:str|None=None)->XRangeSelection:"""Make and return the annnotated segment associated with the ROI Args: obj: object (signal), for physical-indices coordinates conversion title: title """xmin,xmax=self.get_physical_coords(obj)item=make.range(xmin,xmax)returnitem@classmethoddeffrom_plot_item(cls:SegmentROI,item:XRangeSelection)->SegmentROI:"""Create ROI from plot item Args: item: plot item Returns: ROI """ifnotisinstance(item,XRangeSelection):raiseTypeError("Invalid plot item type")returncls(item.get_range(),False)
[docs]classSignalROI(base.BaseROI["SignalObj",SegmentROI,ROI1DParam,XRangeSelection]):"""Signal Regions of Interest Args: singleobj: if True, when extracting data defined by ROIs, only one object is created (default to True). If False, one object is created per single ROI. If None, the value is get from the user configuration inverse: if True, ROI is outside the region """PREFIX="s"
[docs]@staticmethoddefget_compatible_single_roi_classes()->list[Type[SegmentROI]]:"""Return compatible single ROI classes"""return[SegmentROI]
[docs]defto_mask(self,obj:SignalObj)->np.ndarray[bool]:"""Create mask from ROI Args: obj: signal object Returns: Mask (boolean array where True values are inside the ROI) """mask=np.ones_like(obj.xydata,dtype=bool)forroiinself.single_rois:mask&=roi.to_mask(obj)returnmask
[docs]defcreate_signal_roi(coords:np.ndarray|list[float,float]|list[list[float,float]],indices:bool=False,singleobj:bool|None=None,inverse:bool=False,title:str="",)->SignalROI:"""Create Signal Regions of Interest (ROI) object. More ROIs can be added to the object after creation, using the `add_roi` method. Args: coords: single ROI coordinates `[xmin, xmax]`, or multiple ROIs coordinates `[[xmin1, xmax1], [xmin2, xmax2], ...]` (lists or NumPy arrays) indices: if True, coordinates are indices, if False, they are physical values (default to False for signals) singleobj: if True, when extracting data defined by ROIs, only one object is created (default to True). If False, one object is created per single ROI. If None, the value is get from the user configuration inverse: if True, ROI is outside the region title: title Returns: Regions of Interest (ROI) object Raises: ValueError: if the number of coordinates is not even """coords=np.array(coords,float)ifcoords.ndim==1:coords=coords.reshape(1,-1)roi=SignalROI(singleobj,inverse)forrowincoords:roi.add_roi(SegmentROI(row,indices=indices,title=title))returnroi
defapply_downsampling(item:CurveItem,do_not_update:bool=False)->None:"""Apply downsampling to curve item Args: item: curve item do_not_update: if True, do not update the item even if the downsampling parameters have changed """old_use_dsamp=item.param.use_dsampitem.param.use_dsamp=FalseifConf.view.sig_autodownsampling.get():nbpoints=item.get_data()[0].sizemaxpoints=Conf.view.sig_autodownsampling_maxpoints.get()ifnbpoints>5*maxpoints:item.param.use_dsamp=Trueitem.param.dsamp_factor=nbpoints//maxpointsifnotdo_not_updateandold_use_dsamp!=item.param.use_dsamp:item.update_data()
[docs]classSignalObj(gds.DataSet,base.BaseObj[SignalROI,CurveItem]):"""Signal object"""PREFIX="s"CONF_FMT=Conf.view.sig_formatDEFAULT_FMT="g"VALID_DTYPES=(np.float32,np.float64,np.complex128)uuid=gds.StringItem("UUID").set_prop("display",hide=True)_tabs=gds.BeginTabGroup("all")_datag=gds.BeginGroup(_("Data and metadata"))title=gds.StringItem(_("Signal title"),default=_("Untitled"))xydata=gds.FloatArrayItem(_("Data"),transpose=True,minmax="rows")metadata=gds.DictItem(_("Metadata"),default={})_e_datag=gds.EndGroup(_("Data and metadata"))_unitsg=gds.BeginGroup(_("Titles and units"))title=gds.StringItem(_("Signal title"),default=_("Untitled"))_tabs_u=gds.BeginTabGroup("units")_unitsx=gds.BeginGroup(_("X-axis"))xlabel=gds.StringItem(_("Title"),default="")xunit=gds.StringItem(_("Unit"),default="")_e_unitsx=gds.EndGroup(_("X-axis"))_unitsy=gds.BeginGroup(_("Y-axis"))ylabel=gds.StringItem(_("Title"),default="")yunit=gds.StringItem(_("Unit"),default="")_e_unitsy=gds.EndGroup(_("Y-axis"))_e_tabs_u=gds.EndTabGroup("units")_e_unitsg=gds.EndGroup(_("Titles and units"))_scalesg=gds.BeginGroup(_("Scales"))_prop_autoscale=gds.GetAttrProp("autoscale")autoscale=gds.BoolItem(_("Auto scale"),default=True).set_prop("display",store=_prop_autoscale)_tabs_b=gds.BeginTabGroup("bounds")_boundsx=gds.BeginGroup(_("X-axis"))xscalelog=gds.BoolItem(_("Logarithmic scale"),default=False)xscalemin=gds.FloatItem(_("Lower bound"),check=False).set_prop("display",active=gds.NotProp(_prop_autoscale))xscalemax=gds.FloatItem(_("Upper bound"),check=False).set_prop("display",active=gds.NotProp(_prop_autoscale))_e_boundsx=gds.EndGroup(_("X-axis"))_boundsy=gds.BeginGroup(_("Y-axis"))yscalelog=gds.BoolItem(_("Logarithmic scale"),default=False)yscalemin=gds.FloatItem(_("Lower bound"),check=False).set_prop("display",active=gds.NotProp(_prop_autoscale))yscalemax=gds.FloatItem(_("Upper bound"),check=False).set_prop("display",active=gds.NotProp(_prop_autoscale))_e_boundsy=gds.EndGroup(_("Y-axis"))_e_tabs_b=gds.EndTabGroup("bounds")_e_scalesg=gds.EndGroup(_("Scales"))_e_tabs=gds.EndTabGroup("all")def__init__(self,title=None,comment=None,icon=""):"""Constructor Args: title: title comment: comment icon: icon """gds.DataSet.__init__(self,title,comment,icon)base.BaseObj.__init__(self)self.regenerate_uuid()
[docs]@staticmethoddefget_roi_class()->Type[SignalROI]:"""Return ROI class"""returnSignalROI
[docs]defregenerate_uuid(self):"""Regenerate UUID This method is used to regenerate UUID after loading the object from a file. This is required to avoid UUID conflicts when loading objects from file without clearing the workspace first. """self.uuid=str(uuid4())
[docs]defcopy(self,title:str|None=None,dtype:np.dtype|None=None)->SignalObj:"""Copy object. Args: title: title dtype: data type Returns: Copied object """title=self.titleiftitleisNoneelsetitleobj=SignalObj(title=title)obj.title=titleobj.xlabel=self.xlabelobj.xunit=self.xunitobj.yunit=self.yunitifdtypenotin(None,float,complex,np.complex128):raiseRuntimeError("Signal data only supports float64/complex128 dtype")obj.metadata=base.deepcopy_metadata(self.metadata)obj.xydata=np.array(self.xydata,copy=True,dtype=dtype)returnobj
[docs]defset_data_type(self,dtype:np.dtype)->None:# pylint: disable=unused-argument"""Change data type. Args: Data type """raiseRuntimeError("Setting data type is not support for signals")
[docs]defset_xydata(self,x:np.ndarray|list,y:np.ndarray|list,dx:np.ndarray|list|None=None,dy:np.ndarray|list|None=None,)->None:"""Set xy data Args: x: x data y: y data dx: dx data (optional: error bars) dy: dy data (optional: error bars) """ifxisnotNone:x=np.array(x)ifyisnotNone:y=np.array(y)ifdxisnotNone:dx=np.array(dx)ifdyisnotNone:dy=np.array(dy)ifdxisNoneanddyisNone:self.xydata=np.vstack([x,y])else:ifdxisNone:dx=np.zeros_like(dy)ifdyisNone:dy=np.zeros_like(dx)self.xydata=np.vstack((x,y,dx,dy))
def__get_x(self)->np.ndarray|None:"""Get x data"""ifself.xydataisnotNone:returnself.xydata[0]returnNonedef__set_x(self,data)->None:"""Set x data"""self.xydata[0]=np.array(data)def__get_y(self)->np.ndarray|None:"""Get y data"""ifself.xydataisnotNone:returnself.xydata[1]returnNonedef__set_y(self,data)->None:"""Set y data"""self.xydata[1]=np.array(data)def__get_dx(self)->np.ndarray|None:"""Get dx data"""ifself.xydataisnotNoneandlen(self.xydata)>2:returnself.xydata[2]returnNonedef__set_dx(self,data)->None:"""Set dx data"""ifself.xydataisnotNoneandlen(self.xydata)>2:self.xydata[2]=np.array(data)else:raiseValueError("dx data not available")def__get_dy(self)->np.ndarray|None:"""Get dy data"""ifself.xydataisnotNoneandlen(self.xydata)>3:returnself.xydata[3]returnNonedef__set_dy(self,data)->None:"""Set dy data"""ifself.xydataisnotNoneandlen(self.xydata)>3:self.xydata[3]=np.array(data)else:raiseValueError("dy data not available")x=property(__get_x,__set_x)y=data=property(__get_y,__set_y)dx=property(__get_dx,__set_dx)dy=property(__get_dy,__set_dy)
[docs]defget_data(self,roi_index:int|None=None)->tuple[np.ndarray,np.ndarray]:""" Return original data (if ROI is not defined or `roi_index` is None), or ROI data (if both ROI and `roi_index` are defined). Args: roi_index: ROI index Returns: Data """ifself.roiisNoneorroi_indexisNone:returnself.x,self.ysingle_roi=self.roi.get_single_roi(roi_index)returnsingle_roi.get_data(self)
[docs]defupdate_plot_item_parameters(self,item:CurveItem)->None:"""Update plot item parameters from object data/metadata Takes into account a subset of plot item parameters. Those parameters may have been overriden by object metadata entries or other object data. The goal is to update the plot item accordingly. This is *almost* the inverse operation of `update_metadata_from_plot_item`. Args: item: plot item """update_dataset(item.param.line,self.metadata)update_dataset(item.param.symbol,self.metadata)super().update_plot_item_parameters(item)
[docs]defupdate_metadata_from_plot_item(self,item:CurveItem)->None:"""Update metadata from plot item. Takes into account a subset of plot item parameters. Those parameters may have been modified by the user through the plot item GUI. The goal is to update the metadata accordingly. This is *almost* the inverse operation of `update_plot_item_parameters`. Args: item: plot item """super().update_metadata_from_plot_item(item)restore_dataset(item.param.line,self.metadata)restore_dataset(item.param.symbol,self.metadata)
[docs]defmake_item(self,update_from:CurveItem|None=None)->CurveItem:"""Make plot item from data. Args: update_from: plot item to update from Returns: Plot item """iflen(self.xydata)in(2,3,4):iflen(self.xydata)==2:# x, y signalx,y=self.xydataitem=make.mcurve(x.real,y.real,label=self.title)eliflen(self.xydata)==3:# x, y, dy error bar signalx,y,dy=self.xydataitem=make.merror(x.real,y.real,dy.real,label=self.title)eliflen(self.xydata)==4:# x, y, dx, dy error bar signalx,y,dx,dy=self.xydataitem=make.merror(x.real,y.real,dx.real,dy.real,label=self.title)CURVESTYLES.apply_style(item.param)apply_downsampling(item,do_not_update=True)else:raiseRuntimeError("data not supported")ifupdate_fromisNone:self.update_plot_item_parameters(item)else:update_dataset(item.param,update_from.param)item.update_params()returnitem
[docs]defupdate_item(self,item:CurveItem,data_changed:bool=True)->None:"""Update plot item from data. Args: item: plot item data_changed: if True, data has changed """ifdata_changed:iflen(self.xydata)==2:# x, y signalx,y=self.xydataitem.set_data(x.real,y.real)eliflen(self.xydata)==3:# x, y, dy error bar signalx,y,dy=self.xydataitem.set_data(x.real,y.real,dy=dy.real)eliflen(self.xydata)==4:# x, y, dx, dy error bar signalx,y,dx,dy=self.xydataitem.set_data(x.real,y.real,dx.real,dy.real)item.param.label=self.titleapply_downsampling(item)self.update_plot_item_parameters(item)
[docs]defphysical_to_indices(self,coords:list[float]|np.ndarray)->np.ndarray:"""Convert coordinates from physical (real world) to (array) indices (pixel) Args: coords: coordinates Returns: Indices """self.x:np.ndarrayreturnnp.array([np.abs(self.x-x).argmin()forxincoords])
[docs]defindices_to_physical(self,indices:list[int]|np.ndarray)->np.ndarray:"""Convert coordinates from (array) indices to physical (real world) Args: indices: indices Returns: Coordinates """# We take the real part of the x data to avoid `ComplexWarning` warnings# when creating and manipulating the `XRangeSelection` shape (`plotpy`)returnself.x.real[indices]
[docs]defadd_label_with_title(self,title:str|None=None)->None:"""Add label with title annotation Args: title: title (if None, use signal title) """title=self.titleiftitleisNoneelsetitleiftitle:label=make.label(title,"TL",(0,0),"TL")self.add_annotations_from_items([label])
[docs]defcreate_signal(title:str,x:np.ndarray|None=None,y:np.ndarray|None=None,dx:np.ndarray|None=None,dy:np.ndarray|None=None,metadata:dict|None=None,units:tuple[str,str]|None=None,labels:tuple[str,str]|None=None,)->SignalObj:"""Create a new Signal object. Args: title: signal title x: X data y: Y data dx: dX data (optional: error bars) dy: dY data (optional: error bars) metadata: signal metadata units: X, Y units (tuple of strings) labels: X, Y labels (tuple of strings) Returns: Signal object """assertisinstance(title,str)signal=SignalObj(title=title)signal.title=titlesignal.set_xydata(x,y,dx=dx,dy=dy)ifunitsisnotNone:signal.xunit,signal.yunit=unitsiflabelsisnotNone:signal.xlabel,signal.ylabel=labelsifmetadataisnotNone:signal.metadata.update(metadata)returnsignal
[docs]classSignalTypes(base.Choices):"""Signal types"""#: Signal filled with zerosZEROS=_("zeros")#: Gaussian functionGAUSS=_("gaussian")#: Lorentzian functionLORENTZ=_("lorentzian")#: Voigt functionVOIGT="Voigt"#: Random signal (uniform law)UNIFORMRANDOM=_("random (uniform law)")#: Random signal (normal law)NORMALRANDOM=_("random (normal law)")#: SinusoidSINUS=_("sinus")#: CosinusoidCOSINUS=_("cosinus")#: Sawtooth functionSAWTOOTH=_("sawtooth")#: Triangle functionTRIANGLE=_("triangle")#: Square functionSQUARE=_("square")#: Cardinal sineSINC=_("cardinal sine")#: Step functionSTEP=_("step")#: Exponential functionEXPONENTIAL=_("exponential")#: Pulse functionPULSE=_("pulse")#: Polynomial functionPOLYNOMIAL=_("polynomial")#: Experimental functionEXPERIMENTAL=_("experimental")
[docs]classGaussLorentzVoigtParam(gds.DataSet):"""Parameters for Gaussian and Lorentzian functions"""a=gds.FloatItem("A",default=1.0)ymin=gds.FloatItem("Ymin",default=0.0).set_pos(col=1)sigma=gds.FloatItem("σ",default=1.0)mu=gds.FloatItem("μ",default=0.0).set_pos(col=1)
classFreqUnits(base.Choices):"""Frequency units"""HZ="Hz"KHZ="kHz"MHZ="MHz"GHZ="GHz"@classmethoddefconvert_in_hz(cls,value,unit):"""Convert value in Hz"""factor={cls.HZ:1,cls.KHZ:1e3,cls.MHZ:1e6,cls.GHZ:1e9}.get(unit)iffactorisNone:raiseValueError(f"Unknown unit: {unit}")returnvalue*factor
[docs]classPeriodicParam(gds.DataSet):"""Parameters for periodic functions"""
[docs]defget_frequency_in_hz(self):"""Return frequency in Hz"""returnFreqUnits.convert_in_hz(self.freq,self.freq_unit)
[docs]classStepParam(gds.DataSet):"""Parameters for step function"""a1=gds.FloatItem("A1",default=0.0)a2=gds.FloatItem("A2",default=1.0).set_pos(col=1)x0=gds.FloatItem("X0",default=0.0)
classExponentialParam(gds.DataSet):"""Parameters for exponential function"""a=gds.FloatItem("A",default=1.0)offset=gds.FloatItem(_("Offset"),default=0.0)exponent=gds.FloatItem(_("Exponent"),default=1.0)classPulseParam(gds.DataSet):"""Parameters for pulse function"""amp=gds.FloatItem("Amplitude",default=1.0)start=gds.FloatItem(_("Start"),default=0.0).set_pos(col=1)offset=gds.FloatItem(_("Offset"),default=0.0)stop=gds.FloatItem(_("End"),default=0.0).set_pos(col=1)classPolyParam(gds.DataSet):"""Parameters for polynomial function"""a0=gds.FloatItem("a0",default=1.0)a3=gds.FloatItem("a3",default=0.0).set_pos(col=1)a1=gds.FloatItem("a1",default=1.0)a4=gds.FloatItem("a4",default=0.0).set_pos(col=1)a2=gds.FloatItem("a2",default=0.0)a5=gds.FloatItem("a5",default=0.0).set_pos(col=1)classExperSignalParam(gds.DataSet):"""Parameters for experimental signal"""size=gds.IntItem("Size",default=5).set_prop("display",hide=True)xyarray=gds.FloatArrayItem("XY Values",format="%g",)xmin=gds.FloatItem("Min",default=0).set_prop("display",hide=True)xmax=gds.FloatItem("Max",default=1).set_prop("display",hide=True)defedit_curve(self,*args)->None:# pylint: disable=unused-argument"""Edit experimental curve"""win:PlotDialog=make.dialog(wintitle=_("Select one point then press OK to accept"),edit=True,type="curve",)edit_tool=win.manager.add_tool(EditPointTool,title=_("Edit experimental curve"))edit_tool.activate()plot=win.manager.get_plot()x,y=self.xyarray[:,0],self.xyarray[:,1]curve=make.mcurve(x,y,"-+")plot.add_item(curve)plot.set_active_item(curve)insert_btn=QW.QPushButton(_("Insert point"),win)insert_btn.clicked.connect(edit_tool.trigger_insert_point_at_selection)win.button_layout.insertWidget(0,insert_btn)exec_dialog(win)new_x,new_y=curve.get_data()self.xmax=new_x.max()self.xmin=new_x.min()self.size=new_x.sizeself.xyarray=np.vstack((new_x,new_y)).Tbtn_curve_edit=gds.ButtonItem("Edit curve",callback=edit_curve,icon="signal.svg")defsetup_array(self,size:int|None=None,xmin:float|None=None,xmax:float|None=None,)->None:"""Setup the xyarray from size, xmin and xmax (use the current values is not provided) Args: size: xyarray size (default: None) xmin: X min (default: None) xmax: X max (default: None) """self.size=sizeorself.sizeself.xmin=xminorself.xminself.xmax=xmaxorself.xmaxx_arr=np.linspace(self.xmin,self.xmax,self.size)# type: ignoreself.xyarray=np.vstack((x_arr,x_arr)).T
[docs]classNewSignalParam(gds.DataSet):"""New signal dataset"""hide_signal_type=Falsetitle=gds.StringItem(_("Title"))xmin=gds.FloatItem("Xmin",default=-10.0)xmax=gds.FloatItem("Xmax",default=10.0)size=gds.IntItem(_("Size"),help=_("Signal size (total number of points)"),min=1,default=500)stype=gds.ChoiceItem(_("Type"),SignalTypes.get_choices()).set_prop("display",hide=gds.GetAttrProp("hide_signal_type"))
DEFAULT_TITLE=_("Untitled signal")
[docs]defnew_signal_param(title:str|None=None,stype:str|None=None,xmin:float|None=None,xmax:float|None=None,size:int|None=None,)->NewSignalParam:"""Create a new Signal dataset instance. Args: title: dataset title (default: None, uses default title) stype: signal type (default: None, uses default type) xmin: X min (default: None, uses default value) xmax: X max (default: None, uses default value) size: signal size (default: None, uses default value) Returns: NewSignalParam: new signal dataset instance """title=DEFAULT_TITLEiftitleisNoneelsetitleparam=NewSignalParam(title=title,icon=get_icon("new_signal.svg"))param.title=titleifxminisnotNone:param.xmin=xminifxmaxisnotNone:param.xmax=xmaxifsizeisnotNone:param.size=sizeifstypeisnotNone:param.stype=stypereturnparam
SIG_NB=0deftriangle_func(xarr:np.ndarray)->np.ndarray:"""Triangle function Args: xarr: x data """returnsps.sawtooth(xarr,width=0.5)
[docs]defcreate_signal_from_param(newparam:NewSignalParam,addparam:gds.DataSet|None=None,edit:bool=False,parent:QW.QWidget|None=None,)->SignalObj|None:"""Create a new Signal object from a dialog box. Args: newparam: new signal parameters addparam: additional parameters edit: Open a dialog box to edit parameters (default: False) parent: parent widget Returns: Signal object or None if canceled """globalSIG_NB# pylint: disable=global-statementifnewparamisNone:newparam=new_signal_param()incr_sig_nb=notnewparam.titleifincr_sig_nb:newparam.title=f"{newparam.title}{SIG_NB+1:d}"ifnoteditoraddparamisnotNoneornewparam.edit(parent=parent):prefix=newparam.stype.name.lower()ifincr_sig_nb:SIG_NB+=1signal=create_signal(newparam.title)xarr=np.linspace(newparam.xmin,newparam.xmax,newparam.size)p=addparamifnewparam.stype==SignalTypes.ZEROS:signal.set_xydata(xarr,np.zeros(newparam.size))elifnewparam.stypein(SignalTypes.UNIFORMRANDOM,SignalTypes.NORMALRANDOM):pclass={SignalTypes.UNIFORMRANDOM:base.UniformRandomParam,SignalTypes.NORMALRANDOM:base.NormalRandomParam,}[newparam.stype]ifpisNone:p=pclass(_("Signal")+" - "+prefix)ifeditandnotp.edit(parent=parent):returnNonerng=np.random.default_rng(p.seed)ifnewparam.stype==SignalTypes.UNIFORMRANDOM:yarr=rng.random((newparam.size,))*(p.vmax-p.vmin)+p.vminifsignal.title==DEFAULT_TITLE:signal.title=f"{prefix}(vmin={p.vmin:.3g},vmax={p.vmax:.3g})"elifnewparam.stype==SignalTypes.NORMALRANDOM:yarr=rng.normal(p.mu,p.sigma,size=(newparam.size,))ifsignal.title==DEFAULT_TITLE:signal.title=f"{prefix}(mu={p.mu:.3g},sigma={p.sigma:.3g})"else:raiseNotImplementedError(f"New param type: {prefix}")signal.set_xydata(xarr,yarr)elifnewparam.stypein(SignalTypes.GAUSS,SignalTypes.LORENTZ,SignalTypes.VOIGT,):func,title={SignalTypes.GAUSS:(GaussianModel.func,_("Gaussian")),SignalTypes.LORENTZ:(LorentzianModel.func,_("Lorentzian")),SignalTypes.VOIGT:(VoigtModel.func,"Voigt"),}[newparam.stype]ifpisNone:p=GaussLorentzVoigtParam(title)ifeditandnotp.edit(parent=parent):returnNoneyarr=func(xarr,p.a,p.sigma,p.mu,p.ymin)signal.set_xydata(xarr,yarr)ifsignal.title==DEFAULT_TITLE:signal.title=(f"{prefix}(a={p.a:.3g},sigma={p.sigma:.3g},"f"mu={p.mu:.3g},ymin={p.ymin:.3g})")elifnewparam.stypein(SignalTypes.SINUS,SignalTypes.COSINUS,SignalTypes.SAWTOOTH,SignalTypes.TRIANGLE,SignalTypes.SQUARE,SignalTypes.SINC,):func,title={SignalTypes.SINUS:(np.sin,_("Sinusoid")),SignalTypes.COSINUS:(np.cos,_("Sinusoid")),SignalTypes.SAWTOOTH:(sps.sawtooth,_("Sawtooth function")),SignalTypes.TRIANGLE:(triangle_func,_("Triangle function")),SignalTypes.SQUARE:(sps.square,_("Square function")),SignalTypes.SINC:(np.sinc,_("Cardinal sine")),}[newparam.stype]ifpisNone:p=PeriodicParam(title)ifeditandnotp.edit(parent=parent):returnNonefreq=p.get_frequency_in_hz()yarr=p.a*func(2*np.pi*freq*xarr+np.deg2rad(p.phase))+p.yminsignal.set_xydata(xarr,yarr)ifsignal.title==DEFAULT_TITLE:signal.title=(f"{prefix}(f={p.freq:.3g}{p.freq_unit.value}),"f"a={p.a:.3g},ymin={p.ymin:.3g},phase={p.phase:.3g}°)")elifnewparam.stype==SignalTypes.STEP:ifpisNone:p=StepParam(_("Step function"),comment="y(x) = a1 if x <= x0 else a2")ifeditandnotp.edit(parent=parent):returnNoneyarr=np.ones_like(xarr)*p.a1yarr[xarr>p.x0]=p.a2signal.set_xydata(xarr,yarr)ifsignal.title==DEFAULT_TITLE:signal.title=f"{prefix}(x0={p.x0:.3g},a1={p.a1:.3g},a2={p.a2:.3g})"elifnewparam.stypeisSignalTypes.EXPONENTIAL:ifpisNone:p=ExponentialParam(_("Exponential function"),comment="y(x) = a.e<sup>exponent.x</sup> + offset",)ifeditandnotp.edit(parent=parent):returnNoneyarr=p.a*np.exp(p.exponent*xarr)+p.offsetsignal.set_xydata(xarr,yarr)ifsignal.title==DEFAULT_TITLE:signal.title=(f"{prefix}(a={p.a:.3g},exponent={p.exponent:.3g},"f"offset={p.offset:.3g})")elifnewparam.stypeisSignalTypes.PULSE:ifpisNone:p=PulseParam(_("Pulse function"),comment="y(x) = offset + amp if start <= x <= stop else offset",)ifeditandnotp.edit(parent=parent):returnNoneyarr=np.full_like(xarr,p.offset)yarr[(xarr>=p.start)&(xarr<=p.stop)]+=p.ampsignal.set_xydata(xarr,yarr)ifsignal.title==DEFAULT_TITLE:signal.title=(f"{prefix}(start={p.start:.3g},stop={p.stop:.3g}"f",offset={p.offset:.3g})")elifnewparam.stypeisSignalTypes.POLYNOMIAL:ifpisNone:p=PolyParam(_("Polynomial function"),comment=("y(x) = a<sub>0</sub> + a<sub>1</sub>.x + ""a<sub>2</sub>.x<sup>2</sup> + a<sub>3</sub>.x<sup>3</sup>"" + a<sub>4</sub>.x<sup>4</sup> + a<sub>5</sub>.x<sup>5</sup>"),)ifeditandnotp.edit(parent=parent):returnNoneyarr=np.polyval([p.a5,p.a4,p.a3,p.a2,p.a1,p.a0],xarr)signal.set_xydata(xarr,yarr)ifsignal.title==DEFAULT_TITLE:signal.title=(f"{prefix}(a0={p.a0:2g},a1={p.a1:2g},a2={p.a2:2g},"f"a3={p.a3:2g},a4={p.a4:2g},a5={p.a5:2g})")elifnewparam.stypeisSignalTypes.EXPERIMENTAL:p2=ExperSignalParam(_("Experimental points"))p2.setup_array(size=newparam.size,xmin=newparam.xmin,xmax=newparam.xmax)ifeditandnotp2.edit(parent=parent):returnNonesignal.xydata=p2.xyarray.Tifsignal.title==DEFAULT_TITLE:signal.title=f"{prefix}(npts={p2.size})"returnsignalreturnNone