diff --git a/@MyCollector/MyCollector.m b/@MyCollector/MyCollector.m index e7e79d3..692b861 100644 --- a/@MyCollector/MyCollector.m +++ b/@MyCollector/MyCollector.m @@ -1,192 +1,192 @@ classdef MyCollector < MySingleton & matlab.mixin.Copyable properties (Access=public, SetObservable=true) InstrList % Structure accomodating instruments InstrProps % Properties of instruments MeasHeaders collect_flag end properties (Access=private) Listeners end properties (Dependent=true) running_instruments end events NewDataWithHeaders end methods (Access=private) - % Constructor of a singletone class must be private + % Constructor of a singleton class must be private function this=MyCollector(varargin) p=inputParser; addParameter(p,'InstrHandles',{}); parse(p,varargin{:}); this.collect_flag=true; if ~isempty(p.Results.InstrHandles) cellfun(@(x) addInstrument(this,x),p.Results.InstrHandles); end this.MeasHeaders=MyMetadata(); this.InstrList=struct(); this.InstrProps=struct(); this.Listeners=struct(); end end methods (Access=public) function delete(this) cellfun(@(x) deleteListeners(this,x), this.running_instruments); end function addInstrument(this,instr_handle,varargin) p=inputParser; addParameter(p,'name','UnknownDevice',@ischar) parse(p,varargin{:}); %Find a name for the instrument if ~ismember('name',p.UsingDefaults) name=p.Results.name; elseif isprop(instr_handle,'name') && ~isempty(instr_handle.name) name=genvarname(instr_handle.name, this.running_instruments); else name=genvarname(p.Results.name, this.running_instruments); end if ismethod(instr_handle, 'readHeader') %Defaults to read header this.InstrProps.(name).header_flag=true; else % If class does not have readHeader function, it can still % be added to the collector to transfer trace to Daq this.InstrProps.(name).header_flag=false; warning(['%s does not have a readHeader function, ',... 'measurement headers will not be collected from ',... 'this instrument.'],name) end this.InstrList.(name)=instr_handle; %If the added instrument has a newdata event, we add a listener for it. if contains('NewData',events(this.InstrList.(name))) this.Listeners.(name).NewData=... addlistener(this.InstrList.(name),'NewData',... @(~,InstrEventData) acquireData(this, InstrEventData)); end %Cleans up if the instrument is closed this.Listeners.(name).Deletion=... addlistener(this.InstrList.(name),'ObjectBeingDestroyed',... @(~,~) deleteInstrument(this,name)); end function acquireData(this,InstrEventData) src=InstrEventData.Source; % Check that event data object is MyNewDataEvent, % and fix otherwise if ~isa(InstrEventData,'MyNewDataEvent') InstrEventData=MyNewDataEvent(); InstrEventData.no_new_header=false; InstrEventData.Instr=src; end % Collect the headers if the flag is on and if the triggering % instrument does not request suppression of header collection if this.collect_flag && ~InstrEventData.no_new_header this.MeasHeaders=MyMetadata(); %Add field indicating the time when the trace was acquired addTimeField(this.MeasHeaders, 'AcquisitionTime') addField(this.MeasHeaders,'AcquiringInstrument') if isprop(src,'name') name=src.name; else name='Not Accessible'; end addParam(this.MeasHeaders,'AcquiringInstrument',... 'Name',name); acquireHeaders(this); %We copy the MeasHeaders to the trace. src.Trace.MeasHeaders=copy(this.MeasHeaders); end triggerNewDataWithHeaders(this,InstrEventData); end %Collects headers for open instruments with the header flag on function acquireHeaders(this) for i=1:length(this.running_instruments) name=this.running_instruments{i}; if this.InstrProps.(name).header_flag try TmpMetadata=readHeader(this.InstrList.(name)); addMetadata(this.MeasHeaders, TmpMetadata); catch warning(['Error while reading metadata from %s.',... 'Measurement header collection is switched ',... 'off for this instrument.'],name) this.InstrProps.(name).header_flag=false; end end end end function clearHeaders(this) this.MeasHeaders=MyMetadata(); end function bool=isrunning(this,name) assert(~isempty(name),'Instrument name must be specified') assert(ischar(name)&&isvector(name),... 'Instrument name must be a character vector, not %s',... class(name)); bool=ismember(name,this.running_instruments); end function deleteInstrument(this,name) if isrunning(this,name) %We remove the instrument this.InstrList=rmfield(this.InstrList,name); this.InstrProps=rmfield(this.InstrProps,name); deleteListeners(this,name); end end end methods(Static) % Concrete implementation of the singletone constructor. function this = instance() persistent UniqueInstance if isempty(UniqueInstance)||(~isvalid(UniqueInstance)) disp('Creating new instance of MyCollector') this = MyCollector(); UniqueInstance = this; else this = UniqueInstance; end end end methods (Access=private) function triggerNewDataWithHeaders(this,InstrEventData) notify(this,'NewDataWithHeaders',InstrEventData); end %deleteListeners is in a separate file deleteListeners(this, obj_name); end methods function running_instruments=get.running_instruments(this) running_instruments=fieldnames(this.InstrList); end end end diff --git a/@MyDaq/MyDaq.m b/@MyDaq/MyDaq.m index a5c7c00..449b681 100644 --- a/@MyDaq/MyDaq.m +++ b/@MyDaq/MyDaq.m @@ -1,980 +1,1028 @@ % Acquisition and analysis program that receives data from Collector. Can % also be used for analysis of previously acquired traces. classdef MyDaq < handle properties %Global variable with Daq name is cleared on exit. global_name %Contains GUI handles Gui %Contains Reference trace (MyTrace object) Ref %Contains Data trace (MyTrace object) Data %Contains Background trace (MyTrace object) Background %List of all the programs with run files ProgramList %Struct containing Cursor objects Cursors %Struct containing Cursor labels CrsLabels %Struct containing MyFit objects Fits %Input parser for class constructor ConstructionParser %Struct for listeners Listeners %Sets the colors of fits, data and reference fit_color='k'; data_color='b'; ref_color='r'; bg_color='c'; end properties (Dependent=true) save_dir main_plot open_fits open_crs end properties (Dependent=true, SetAccess=private, GetAccess=public) %Properties for saving files base_dir session_name - filename + filename % File name is always returned with extension end methods (Access=public) %% Class functions %Constructor function function this=MyDaq(varargin) % Initialize variables % Traces this.Ref=MyTrace(); this.Data=MyTrace(); this.Background=MyTrace(); % Lists this.ProgramList=struct(); this.Cursors=struct(); this.CrsLabels=struct(); this.Fits=struct(); this.ConstructionParser; this.Listeners=struct(); % Parse inputs p=inputParser; addParameter(p,'global_name','',@ischar); addParameter(p,'collector_handle',[]); this.ConstructionParser=p; parse(p, varargin{:}); this.global_name = p.Results.global_name; %Sets a listener to the collector if ~isempty(p.Results.collector_handle) this.Listeners.Collector.NewDataWithHeaders=... addlistener(p.Results.collector_handle,... 'NewDataWithHeaders',... @(~,eventdata) acquireNewData(this,eventdata)); else errordlg(['No collector handle was supplied. ',... 'DAQ will be unable to acquire data'],... 'Error: No collector'); end %The list of instruments is automatically populated from the %run files this.ProgramList = readRunFiles(); %We grab the guihandles from a GUI made in Guide. this.Gui=guihandles(eval('GuiDaq')); %This function sets all the callbacks for the GUI. If a new %button is made, the associated callback must be put in the %initGui function initGui(this); % Initialize the menu based on the available run files content = menuFromRunFiles(this.ProgramList,... 'show_in_daq',true); set(this.Gui.InstrMenu,'String',[{'Select the application'};... content.titles]); % Add a property to the menu for storing the program file % names if ~isprop(this.Gui.InstrMenu, 'ItemsData') addprop(this.Gui.InstrMenu, 'ItemsData'); end set(this.Gui.InstrMenu,'ItemsData',[{''};... content.tags]); hold(this.main_plot,'on'); %Initializes saving locations this.base_dir=getLocalSettings('measurement_base_dir'); this.session_name='placeholder'; this.filename='placeholder'; end function delete(this) %Deletes the MyFit objects and their listeners cellfun(@(x) deleteListeners(this,x), this.open_fits); structfun(@(x) delete(x), this.Fits); %Deletes other listeners if ~isempty(fieldnames(this.Listeners)) cellfun(@(x) deleteListeners(this, x),... fieldnames(this.Listeners)); end % clear global variable, to which Daq handle is assigned evalin('base', sprintf('clear(''%s'')', this.global_name)); %A class destructor should never through errors, so enclose the %attempt to close figure into try-catch structure try this.Gui.figure1.CloseRequestFcn=''; %Deletes the figure delete(this.Gui.figure1); %Removes the figure handle to prevent memory leaks this.Gui=[]; catch end end end methods (Access=private) %Sets callback functions for the GUI initGui(this) %Executes when the GUI is closed function closeFigure(this,~,~) delete(this); end %Updates fits function updateFits(this) %Pushes data into fits in the form of MyTrace objects, so that %units etc follow. Also updates user supplied parameters. for i=1:length(this.open_fits) switch this.open_fits{i} case {'Linear','Quadratic','Gaussian',... 'Exponential','Beta'} this.Fits.(this.open_fits{i}).Data=... getFitData(this,'VertData'); case {'Lorentzian','DoubleLorentzian'} this.Fits.(this.open_fits{i}).Data=... getFitData(this,'VertData'); %Here we push the information about line spacing %into the fit object if the reference cursors are %open. Only for Lorentzian fits. if isfield(this.Cursors,'VertRef') ind=findCursorData(this,'Data','VertRef'); this.Fits.(this.open_fits{i}).CalVals.line_spacing=... range(this.Data.x(ind)); end case {'G0'} this.Fits.G0.MechTrace=getFitData(this,'VertData'); this.Fits.G0.CalTrace=getFitData(this,'VertRef'); end end end % If vertical cursors are on, takes only data within cursors. If %the cursor is not open, it takes all the data from the selected %trace in the analysis trace selection dropdown function Trace=getFitData(this,varargin) %Parses varargin input p=inputParser; addOptional(p,'name','',@ischar); parse(p,varargin{:}) name=p.Results.name; %Finds out which trace the user wants to fit. trc_opts=this.Gui.SelTrace.String; trc_str=trc_opts{this.Gui.SelTrace.Value}; % Note the use of copy here! This is a handle %class, so if normal assignment is used, this.Fits.Data and %this.(trace_str) will refer to the same object, causing roblems. %Name input is the name of the cursor to be used to extract data. Trace=copy(this.(trc_str)); %If the cursor is open for the trace we are analyzing, we take %only the data enclosed by the cursor. if isfield(this.Cursors,name) ind=findCursorData(this, trc_str, name); Trace.x=this.(trc_str).x(ind); Trace.y=this.(trc_str).y(ind); end end %Finds data between named cursors in the given trace function ind=findCursorData(this, trc_str, name) crs_pos=sort([this.Cursors.(name){1}.Location,... this.Cursors.(name){2}.Location]); ind=(this.(trc_str).x>crs_pos(1) & this.(trc_str).x %Prints the figure to the clipboard print(newFig,'-clipboard','-dbitmap'); %Deletes the figure delete(newFig); end %Resets the axis to be tight around the plots. function updateAxis(this) axis(this.main_plot,'tight'); end end methods (Access=public) %% Callbacks %Callback for copying the plot to clipboard function copyPlotCallback(this,~,~) copyPlot(this); end %Callback for centering cursors function centerCursorsCallback(this, ~, ~) if ~this.Gui.LogX.Value x_pos=mean(this.main_plot.XLim); else x_pos=10^(mean(log10(this.main_plot.XLim))); end if ~this.Gui.LogY.Value y_pos=mean(this.main_plot.YLim); else y_pos=10^(mean(log10(this.main_plot.YLim))); end for i=1:length(this.open_crs) switch this.Cursors.(this.open_crs{i}){1}.Orientation case 'horizontal' pos=y_pos; case 'vertical' pos=x_pos; end %Centers the position cellfun(@(x) set(x,'Location',pos), ... this.Cursors.(this.open_crs{i})); %Triggers the UpdateCursorBar event, which triggers %listener callback to reposition text cellfun(@(x) notify(x,'UpdateCursorBar'),... this.Cursors.(this.open_crs{i})); %Triggers the EndDrag event, updating the data in the fit %objects. cellfun(@(x) notify(x,'EndDrag'),... this.Cursors.(this.open_crs{i})); end end %Callback for creating vertical data cursors function cursorButtonCallback(this, hObject, ~) name=erase(hObject.Tag,'Button'); %Gets the first four characters of the tag (Vert or Horz) type=name(1:4); %Changes the color of the button and appropriately creates or %deletes the cursors. if hObject.Value hObject.BackgroundColor=[0,1,0.2]; createCursors(this,name,type); else hObject.BackgroundColor=[0.941,0.941,0.941]; deleteCursors(this,name); end end %Callback for the instrument menu function instrMenuCallback(this,hObject,~) val=hObject.Value; if val==1 %Returns if we are on the dummy option ('Select instrument') return else tag = hObject.ItemsData{val}; end try eval(this.ProgramList.(tag).run_expr); catch errordlg(sprintf('An error occured while running %s',... this.ProgramList.(tag).name)) end end %Select trace callback. If we change the trace being analyzed, the %fit objects are updated. function selTraceCallback(this, ~, ~) updateFits(this) end %Saves the data if the save data button is pressed. function saveCallback(this, src, ~) switch src.Tag case 'SaveData' saveTrace(this,'Data'); case 'SaveRef' saveTrace(this,'Ref'); end end - function saveTrace(this, trace_tag) + function saveTrace(this, trace_tag, varargin) + p=inputParser(); + % If file already exists, generate a uinique name rather than + % show user the overwrite dialog. + addParameter(p, 'make_unique_name', false, @islogical); + parse(p,varargin{:}); + %Check if the trace is valid (i.e. x and y are equal length) %before saving if ~this.(trace_tag).validatePlot errordlg(sprintf('%s trace was empty, could not save',... trace_tag)); return end - [~,~,ext]=fileparts(this.filename); - if isempty(ext) - % Add default file extension - fullfilename=fullfile(this.save_dir,[this.filename,'.txt']); - else - % Use file extension supplied in the field - fullfilename=fullfile(this.save_dir,this.filename); + fullfilename=fullfile(this.save_dir,this.filename); + + if p.Results.make_unique_name && exist(fullfilename, 'file')~=0 + fullfilename=makeUniqueFileName(this); end - %Save in a readable format using the method of MyTrace + %Save in readable format using the method of MyTrace save(this.(trace_tag), fullfilename) end + % Make the filename unique within the measurement folder + % by appending _n. This function does not make sure that the + % filename is valid - i.e. does not contain symbols forbidden by + % the file system. + function fullfilename=makeUniqueFileName(this) + [~, fn, ext]=fileparts(this.filename); + % List all the existing files in the measurement directory + % that have the same extension as our filename + DirCont=dir(fullfile(this.save_dir,['*', ext])); + file_ind=~[DirCont.isdir]; + existing_fns={DirCont(file_ind).name}; + + % Remove extensions + [~,existing_fns,~]=cellfun(@fileparts, existing_fns, ... + 'UniformOutput',false); + + % Generate a new file name + if ~isempty(fn) + fn=matlab.lang.makeUniqueStrings(fn, existing_fns); + else + fn=matlab.lang.makeUniqueStrings('placeholder', ... + existing_fns); + end + fullfilename=fullfile(this.save_dir,[fn, ext]); + end + %Toggle button callback for showing the data trace. function showDataCallback(this, hObject, ~) if hObject.Value hObject.BackgroundColor=[0,1,0.2]; setVisible(this.Data,this.main_plot,1); updateAxis(this); else hObject.BackgroundColor=[0.941,0.941,0.941]; setVisible(this.Data,this.main_plot,0); updateAxis(this); end end %Toggle button callback for showing the ref trace function showRefCallback(this, hObject, ~) if hObject.Value hObject.BackgroundColor=[0,1,0.2]; setVisible(this.Ref,this.main_plot,1); updateAxis(this); else hObject.BackgroundColor=[0.941,0.941,0.941]; setVisible(this.Ref,this.main_plot,0); updateAxis(this); end end %Callback for moving the data to reference. function dataToRefCallback(this, ~, ~) if this.Data.validatePlot set(this.Ref,... 'x',this.Data.x,... 'y',this.Data.y,... 'name_x',this.Data.name_x,... 'name_y',this.Data.name_y,... 'unit_x',this.Data.unit_x,... 'unit_y',this.Data.unit_y); %Since UID is automatically reset when y is changed, we now %change it back to be the same as the Data. this.Ref.uid=this.Data.uid; this.Ref.MeasHeaders=copy(this.Data.MeasHeaders); %Plot the reference trace and make it visible plot(this.Ref, this.main_plot, 'Color',this.ref_color,... 'make_labels',true); setVisible(this.Ref, this.main_plot,1); %Update the fit objects updateFits(this); %Change button color this.Gui.ShowRef.Value=1; this.Gui.ShowRef.BackgroundColor=[0,1,0.2]; else warning('Data trace was empty, could not move to reference') end end %Callback for ref to bg button. Sends the reference to background function refToBgCallback(this, ~, ~) if this.Ref.validatePlot this.Background.x=this.Ref.x; this.Background.y=this.Ref.y; this.Background.plot(this.main_plot,... 'Color',this.bg_color,'make_labels',true); this.Background.setVisible(this.main_plot,1); else warning('Reference trace was empty, could not move to background') end end %Callback for data to bg button. Sends the data to background function dataToBgCallback(this, ~, ~) if this.Data.validatePlot this.Background.x=this.Data.x; this.Background.y=this.Data.y; this.Background.plot(this.main_plot,... 'Color',this.bg_color,'make_labels',true); this.Background.setVisible(this.main_plot,1); else warning('Data trace was empty, could not move to background') end end %Callback for clear background button. Clears the background function clearBgCallback(this, ~, ~) this.Background.x=[]; this.Background.y=[]; this.Background.setVisible(this.main_plot,0); end %Callback for LogY button. Sets the YScale to log/lin function logYCallback(this, hObject, ~) if hObject.Value this.main_plot.YScale='Log'; hObject.BackgroundColor=[0,1,0.2]; else this.main_plot.YScale='Linear'; hObject.BackgroundColor=[0.941,0.941,0.941]; end updateAxis(this); updateCursors(this); end %Callback for LogX button. Sets the XScale to log/lin. Updates the %axis and cursors afterwards. function logXCallback(this, hObject, ~) if get(hObject,'Value') set(this.main_plot,'XScale','Log'); set(hObject, 'BackgroundColor',[0,1,0.2]); else set(this.main_plot,'XScale','Linear'); set(hObject, 'BackgroundColor',[0.941,0.941,0.941]); end updateAxis(this); updateCursors(this); end %Base directory callback. Sets the base directory. Also %updates fit objects with the new save directory. function baseDirCallback(this, ~, ~) for i=1:length(this.open_fits) this.Fits.(this.open_fits{i}).base_dir=this.base_dir; end end %Callback for session name edit box. Sets the session name. Also %updates fit objects with the new save directory. function sessionNameCallback(this, ~, ~) for i=1:length(this.open_fits) this.Fits.(this.open_fits{i}).session_name=this.session_name; end end %Callback for filename edit box. Sets the file name. Also %updates fit objects with the new file name. function fileNameCallback(this, ~,~) for i=1:length(this.open_fits) this.Fits.(this.open_fits{i}).filename=this.filename; end end %Callback for the analyze menu (popup menu for selecting fits). %Opens the correct MyFit object. function analyzeMenuCallback(this, hObject, ~) analyze_ind=hObject.Value; %Finds the correct fit name by erasing spaces and other %superfluous strings analyze_name=hObject.String{analyze_ind}; analyze_name=erase(analyze_name,'Fit'); analyze_name=erase(analyze_name,'Calibration'); analyze_name=erase(analyze_name,' '); %Sets the correct tooltip hObject.TooltipString=sprintf(this.Gui.AnalyzeTip{analyze_ind}) ; %Opens the correct analysis tool switch analyze_name case {'Linear','Quadratic','Exponential',... 'Lorentzian','Gaussian',... 'DoubleLorentzian'} openMyFit(this,analyze_name); case 'g0' openMyG(this); case 'Beta' openMyBeta(this); end end function openMyFit(this,fit_name) %Sees if the MyFit object is already open, if it is, changes the %focus to it, if not, opens it. if ismember(fit_name,fieldnames(this.Fits)) %Changes focus to the relevant fit window figure(this.Fits.(fit_name).Gui.Window); else %Gets the data for the fit using the getFitData function %with the vertical cursors DataTrace=getFitData(this,'VertData'); %Makes an instance of MyFit with correct parameters. this.Fits.(fit_name)=MyFit(... 'fit_name',fit_name,... 'enable_plot',1,... 'plot_handle',this.main_plot,... 'Data',DataTrace,... 'base_dir',this.base_dir,... 'session_name',this.session_name,... 'filename',this.filename); updateFits(this); %Sets up a listener for the Deletion event, which %removes the MyFit object from the Fits structure if it is %deleted. this.Listeners.(fit_name).Deletion=... addlistener(this.Fits.(fit_name),'ObjectBeingDestroyed',... @(src, eventdata) deleteFit(this, src, eventdata)); %Sets up a listener for the NewFit. Callback plots the fit %on the main plot. this.Listeners.(fit_name).NewFit=... addlistener(this.Fits.(fit_name),'NewFit',... @(src, eventdata) plotNewFit(this, src, eventdata)); %Sets up a listener for NewInitVal this.Listeners.(fit_name).NewInitVal=... addlistener(this.Fits.(fit_name),'NewInitVal',... @(~,~) updateCursors(this)); end end %Opens MyG class if it is not open. function openMyG(this) if ismember('G0',this.open_fits) figure(this.Fits.G0.Gui.figure1); else %Populate the MyG class with the right data. We assume the %mechanics is in the Data trace. MechTrace=getFitData(this,'VertData'); CalTrace=getFitData(this,'VertRef'); this.Fits.G0=MyG('MechTrace',MechTrace,'CalTrace',CalTrace,... 'name','G0'); %Adds listener for object being destroyed this.Listeners.G0.Deletion=addlistener(this.Fits.G0,... 'ObjectBeingDestroyed',... @(~,~) deleteObj(this,'G0')); end end %Opens MyBeta class if it is not open. function openMyBeta(this) if ismember('Beta', this.open_fits) figure(this.Fits.Beta.Gui.figure1); else DataTrace=getFitData(this); this.Fits.Beta=MyBeta('Data',DataTrace); %Adds listener for object being destroyed, to perform cleanup this.Listeners.Beta.Deletion=addlistener(this.Fits.Beta,... 'ObjectBeingDestroyed',... @(~,~) deleteObj(this,'Beta')); end end %Callback for load data button function loadDataCallback(this, ~, ~) if isempty(this.base_dir) warning('Please input a valid folder name for loading a trace'); this.base_dir=pwd; end [load_name,path_name]=uigetfile('.txt','Select the trace',... this.base_dir); if load_name==0 warning('No file was selected'); return end load_path=[path_name,load_name]; %Finds the destination trace from the GUI dest_trc=this.Gui.DestTrc.String{this.Gui.DestTrc.Value}; %Call the load trace function on the right trace load(this.(dest_trc), load_path); %Color and plot the right trace. plot(this.(dest_trc), this.main_plot,... 'Color',this.(sprintf('%s_color',lower(dest_trc))),... 'make_labels',true); %Update axis and cursors updateAxis(this); updateCursors(this); end % Callback for open folder button function openFolderCallback(this, hObject, eventdata) dir=uigetdir(this.Gui.BaseDir.String); if ~isempty(dir) this.Gui.BaseDir.String=dir; end % Execute the same callback as if the base directory edit % field was manually updated baseDirCallback(this, hObject, eventdata); end end methods (Access=public) %% Listener functions %Callback function for NewFit listener. Plots the fit in the %window using the plotFit function of the MyFit object function plotNewFit(this, src, ~) src.plotFit('Color',this.fit_color); updateAxis(this); updateCursors(this); end %Callback function for the NewData listener function acquireNewData(this, EventData) %Get the currently selected instrument val=this.Gui.InstrMenu.Value; curr_instr_name=this.Gui.InstrMenu.ItemsData{val}; %Get the name of instrument that generated new data SourceInstr = EventData.Instr; source_name = SourceInstr.name; %Check if the data originates from the currently selected %instrument if strcmp(source_name, curr_instr_name) hline=getLineHandle(this.Data,this.main_plot); %Copy the data from the source instrument this.Data=copy(SourceInstr.Trace); %We give the new trace object the right line handle to plot in if ~isempty(hline) this.Data.hlines{1}=hline; end plot(this.Data, this.main_plot,... 'Color',this.data_color,... 'make_labels',true) updateAxis(this); updateCursors(this); updateFits(this); + + % If the save flag is on in EventData, save the new trace + if isprop(EventData, 'save') && EventData.save + if isprop(EventData, 'filename') && ... + ~isempty(EventData.filename) + % If present, use the file name supplied externally + this.filename=EventData.filename; + saveTrace(this, 'Data'); + else + % Generate a new unique filename + saveTrace(this, 'Data', 'make_unique_name', true); + end + end end end %Callback function for MyFit ObjectBeingDestroyed listener. %Removes the relevant field from the Fits struct and deletes the %listeners from the object. function deleteFit(this, src, ~) %Deletes the object from the Fits struct and deletes listeners deleteObj(this,src.fit_name); %Clears the fits src.clearFit; %Updates cursors since the fits are removed from the plot updateCursors(this); end %Callback function for other analysis method deletion listeners. %Does the same as above. function deleteObj(this,name) if ismember(name,this.open_fits) this.Fits=rmfield(this.Fits,name); end deleteListeners(this, name); end %Listener update function for vertical cursor function vertCursorUpdate(this, src) %Finds the index of the cursor. All cursors are tagged %(name)1, (name)2, e.g. VertData2, ind is the number. ind=str2double(src.Tag(end)); tag=src.Tag(1:(end-1)); %Moves the cursor labels set(this.CrsLabels.(tag){ind},'Position',[src.Location,... this.CrsLabels.(tag){ind}.Position(2),0]); if strcmp(tag,'VertData') %Sets the edit box displaying the location of the cursor this.Gui.(sprintf('EditV%d',ind)).String=... num2str(src.Location); %Sets the edit box displaying the difference in locations this.Gui.EditV2V1.String=... num2str(this.Cursors.VertData{2}.Location-... this.Cursors.VertData{1}.Location); end end %Listener update function for horizontal cursor function horzCursorUpdate(this, src) %Finds the index of the cursor. All cursors are tagged %(name)1, (name)2, e.g. VertData2, ind is the number. ind=str2double(src.Tag(end)); tag=src.Tag(1:(end-1)); %Moves the cursor labels set(this.CrsLabels.(tag){ind},'Position',... [this.CrsLabels.(tag){ind}.Position(1),... src.Location,0]); if strcmp(tag,'HorzData') %Sets the edit box displaying the location of the cursor this.Gui.(sprintf('EditH%d',ind)).String=... num2str(src.Location); %Sets the edit box displaying the difference in locations this.Gui.EditH2H1.String=... num2str(this.Cursors.HorzData{2}.Location-... this.Cursors.HorzData{1}.Location); end end %Function that deletes listeners from the listeners struct, %corresponding to an object of name obj_name deleteListeners(this, obj_name); end %Get functions for dependent variables without set functions methods %Get function from save directory function save_dir=get.save_dir(this) save_dir=createSessionPath(this.base_dir,this.session_name); end %Get function for the plot handles function main_plot=get.main_plot(this) main_plot=this.Gui.figure1.CurrentAxes; end %Get function for open fits function open_fits=get.open_fits(this) open_fits=fieldnames(this.Fits); end %Get function that displays names of open cursors function open_crs=get.open_crs(this) open_crs=fieldnames(this.Cursors); end end %Get and set functions for dependent properties with SetAccess methods function base_dir=get.base_dir(this) try base_dir=this.Gui.BaseDir.String; catch base_dir=pwd; end end function set.base_dir(this,base_dir) this.Gui.BaseDir.String=base_dir; end function session_name=get.session_name(this) try session_name=this.Gui.SessionName.String; catch session_name=''; end end function set.session_name(this,session_name) this.Gui.SessionName.String=session_name; end + % Always return filename with extension function filename=get.filename(this) try filename=this.Gui.FileName.String; + [~,~,ext]=fileparts(filename); + if isempty(ext) + % Add default file extension + filename=[filename,'.txt']; + end catch - filename='placeholder'; + filename='placeholder.txt'; end end - function set.filename(this,filename) - this.Gui.FileName.String=filename; + function set.filename(this, str) + this.Gui.FileName.String=str; end end end \ No newline at end of file diff --git a/@MyInstrument/MyInstrument.m b/@MyInstrument/MyInstrument.m index b06f2da..1dbffff 100644 --- a/@MyInstrument/MyInstrument.m +++ b/@MyInstrument/MyInstrument.m @@ -1,259 +1,253 @@ % Generic class to implement communication with instruments classdef MyInstrument < dynamicprops % Access for these variables is 'protected' and in addition % granted to MyClassParser in order to use construction parser properties (GetAccess=public, SetAccess={?MyClassParser,?MyInstrument}) % name is sometimes used as identifier in listeners callbacks, so % it better not to be changed after the instrument object is % created name=''; interface=''; address=''; end properties (Access=public) Device %Device communication object Trace %MyTrace object for storing data end properties (GetAccess=public, SetAccess=protected) idn_str=''; % identification string end properties (Constant=true) % Default parameters for device connection DEFAULT_INP_BUFF_SIZE = 1e7; % buffer size bytes DEFAULT_OUT_BUFF_SIZE = 1e7; % buffer size bytes DEFAULT_TIMEOUT = 10; % Timeout in s end events NewData PropertyRead end methods (Access=public) function this=MyInstrument(interface, address, varargin) P=MyClassParser(); addRequired(P,'interface',@ischar); addRequired(P,'address',@ischar); addParameter(P,'name','',@ischar); processInputs(P, this, interface, address, varargin{:}); % Create an empty trace this.Trace=MyTrace(); % Create dummy device object that supports properties this.Device=struct(); this.Device.Status='not connected'; % Interface and address can correspond to an entry in the list % of local instruments. Read this entry in such case. if strcmpi(interface, 'instr_list') % load the InstrumentList structure InstrumentList = getLocalSettings('InstrumentList'); % In this case 'address' is the instrument name in % the list instr_name = address; if ~isfield(InstrumentList, instr_name) error('%s is not a field of InstrumentList',... instr_name); end if ~isfield(InstrumentList.(instr_name), 'interface') error(['InstrumentList entry ', instr_name,... ' has no ''interface'' field']); else this.interface = InstrumentList.(instr_name).interface; end if ~isfield(InstrumentList.(instr_name), 'address') error(['InstrumentList entry ', instr_name,... ' has no ''address'' field']); else this.address = InstrumentList.(instr_name).address; end % Assign name automatically, but not overwrite if % already specified if isempty(this.name) this.name = instr_name; end end % Connecting device creates a communication object, % but does not attempt communication connectDevice(this); end function delete(this) %Closes the connection to the device closeDevice(this); %Deletes the device object try delete(this.Device); catch end end - %Triggers event for acquired data - function triggerNewData(this,varargin) - EventData = MyNewDataEvent(); + %Trigger event signaling the acquisition of a new trace. + %Any properties of MyNewDataEvent can be set by indicating the + %corresponding name-value pars in varargin. For the list of options + %see the definition of MyNewDataEvent. + function triggerNewData(this, varargin) + EventData = MyNewDataEvent(varargin{:}); EventData.Instr=this; % An option to suppress collection of new header so that % NewData can be used to transfer previously acquired trace % to Daq - EventData.no_new_header=false; - if length(varargin)>=1 - if strcmpi(varargin{1},'no_new_header') - EventData.no_new_header=true; - else - warning(['Keyword %s is unrecognized. Use ',... - '''no_new_header'' to suppress header ',... - 'collection.'],varargin{1}); - end - end + notify(this,'NewData',EventData); end %Triggers event for property read from device function triggerPropertyRead(this) notify(this,'PropertyRead') end % Read all the relevant instrument properties and return as a % MyMetadata object. % Dummy method that needs to be re-defined by a parent class function Hdr=readHeader(this) Hdr=MyMetadata(); % Generate valid field name from instrument name if present and % class name otherwise if ~isempty(this.name) field_name=genvarname(this.name); else field_name=class(this); end addField(Hdr, field_name); % Add identification string as parameter addParam(Hdr, field_name, 'idn', this.idn_str); end %% Connect, open, configure, identificate and close the device % Connects to the device, explicit indication of interface and % address is for ability to handle instr_list as interface function connectDevice(this) int_list={'constructor','visa','tcpip','serial'}; if ~ismember(lower(this.interface), int_list) warning(['Device is not connected, unknown interface ',... this.interface,'. Valid interfaces are ',... '''constructor'', ''visa'', ''tcpip'' and ''serial''']) return end try switch lower(this.interface) % Use 'constructor' interface to connect device with % more that one parameter, specifying its address case 'constructor' % in this case the 'address' is a command % (ObjectConstructorName), e.g. as returned by the % instrhwinfo, that creates communication object % when executed this.Device=eval(this.address); case 'visa' % visa brand is 'ni' by default this.Device=visa('ni', this.address); case 'tcpip' % Works only with default socket. Use 'constructor' % if socket or other options need to be specified this.Device=tcpip(this.address); case 'serial' this.Device=serial(this.address); otherwise error('Unknown interface'); end configureDeviceDefault(this); catch warning(['Device is not connected, ',... 'error while creating communication object.']); end end % Opens the device if it is not open. Does not throw error if % device is already open for communication with another object, but % tries to close existing connections instead. function openDevice(this) if ~isopen(this) try fopen(this.Device); catch % try to find and close all the devices with the same % VISA resource name try instr_list=instrfind('RsrcName',this.Device.RsrcName); fclose(instr_list); fopen(this.Device); warning('Multiple instrument objects of address %s exist',... this.address); catch error('Could not open device') end end end end % Closes the connection to the device function closeDevice(this) if isopen(this) fclose(this.Device); end end function configureDeviceDefault(this) dev_prop_list = properties(this.Device); if ismember('OutputBufferSize',dev_prop_list) this.Device.OutputBufferSize = this.DEFAULT_OUT_BUFF_SIZE; end if ismember('InputBufferSize',dev_prop_list) this.Device.InputBufferSize = this.DEFAULT_INP_BUFF_SIZE; end if ismember('Timeout',dev_prop_list) this.Device.Timeout = this.DEFAULT_TIMEOUT; end end % Checks if the connection to the device is open function bool=isopen(this) try bool=strcmp(this.Device.Status, 'open'); catch warning('Cannot access device Status property'); bool=false; end end %% Identification % Attempt communication and identification of the device function [str, msg]=idn(this) was_open=isopen(this); try openDevice(this); [str,~,msg]=query(this.Device,'*IDN?'); catch ErrorMessage str=''; msg=ErrorMessage.message; end % Remove carriage return and new line symbols from the string newline_smb={sprintf('\n'),sprintf('\r')}; %#ok str=replace(str, newline_smb,' '); this.idn_str=str; % Leave device in the state it was in the beginning if ~was_open try closeDevice(this); catch end end end end end \ No newline at end of file diff --git a/@MyNa/MyNa.m b/@MyNa/MyNa.m index 75f7d88..e69ed0f 100644 --- a/@MyNa/MyNa.m +++ b/@MyNa/MyNa.m @@ -1,187 +1,187 @@ % The class for communication with Agilent E5061B Network Analyzer classdef MyNa < MyScpiInstrument properties(Access=public) Trace1 Trace2 transf_n=1; % trace that triggers NewData event end properties (SetAccess=protected, GetAccess=public) active_trace = -1; % manipulating with active traces seems unavoidable % for selecting the data format. -1 stands for unknown % data formats for the traces 1-2, options: % 'PHAS', 'SLIN', 'SLOG', 'SCOM', 'SMIT', 'SADM', 'MLOG', 'MLIN', %'PLIN', 'PLOG', 'POL' form1 = 'MLOG'; form2 = 'PHAS'; end methods function this=MyNa(interface, address, varargin) this@MyScpiInstrument(interface, address, varargin{:}); this.Trace1 = MyTrace(); this.Trace2 = MyTrace(); this.Trace1.unit_x = 'Hz'; this.Trace1.name_x = 'Frequency'; this.Trace2.unit_x = 'Hz'; this.Trace2.name_x = 'Frequency'; end % Generate a new data event with header collection suppressed function transferTrace(this, n_trace) trace_tag = sprintf('Trace%i', n_trace); - % Assign either Trace1 or 2 to Trace with keeping the metadata + % Assign either Trace1 or 2 to Trace while keeping the metadata this.(trace_tag).MeasHeaders=copy(this.Trace.MeasHeaders); this.Trace=copy(this.(trace_tag)); - triggerNewData(this,'no_new_header'); + triggerNewData(this,'no_new_header',true); end function data = readTrace(this, n_trace) writeActiveTrace(this, n_trace); freq_str = strsplit(query(this.Device,':SENS1:FREQ:DATA?'),','); data_str = strsplit(query(this.Device,':CALC1:DATA:FDAT?'),','); data = struct(); data.x = str2double(freq_str); % In the returned string there is in general 2 values for each % frequency point. In the Smith data format this can be used to % transfer magnitude and phase of the signal in one trace. With % MLOG, MLIN and PHAS format settings every 2-nd element should % be 0 data.y1 = str2double(data_str(1:2:end)); data.y2 = str2double(data_str(2:2:end)); % set the Trace properties trace_tag = sprintf('Trace%i', n_trace); this.(trace_tag).x = data.x; this.(trace_tag).y = data.y1; if this.transf_n==n_trace this.Trace=copy(this.(trace_tag)); triggerNewData(this); end end function writeActiveTrace(this, n_trace) fprintf(this.Device, sprintf(':CALC1:PAR%i:SEL',n_trace)); this.active_trace = n_trace; end function writeTraceFormat(this, n_trace, fmt) this.writeActiveTrace(n_trace); n_str = num2str(n_trace); this.(['form',n_str]) = fmt; fprintf(this.Device, sprintf(':CALC1:FORM %s', fmt)); end function singleSweep(this) openDevice(this); writeProperty(this,'cont_trig', true); % Set the triger source to remote control writeProperty(this,'trig_source', 'BUS'); % Start a sweep cycle fprintf(this.Device,':TRIG:SING'); % Wait for the sweep to finish (for the query to return 1) query(this.Device,'*OPC?'); closeDevice(this); end function startContSweep(this) openDevice(this); writeProperty(this,'cont_trig', true); % Set the triger source to be internal writeProperty(this,'trig_source', 'INT'); closeDevice(this); end function abortSweep(this) openDevice(this); writeProperty(this, 'trig_source', 'BUS'); fprintf(this.Device,':ABOR'); closeDevice(this); end end %% Protected functions methods (Access=protected) % Command attributes are {class, attributtes} accepted by % validateattributes() function createCommandList(this) addCommand(this,... 'cent_freq',':SENS1:FREQ:CENT', 'default',1.5e6,... 'fmt_spec','%e',... 'info','(Hz)'); addCommand(this,... 'start_freq',':SENS1:FREQ:START', 'default',1e6,... 'fmt_spec','%e',... 'info','(Hz)'); addCommand(this,... 'stop_freq',':SENS1:FREQ:STOP', 'default',2e6,... 'fmt_spec','%e',... 'info','(Hz)'); addCommand(this,... 'span',':SENS1:FREQ:SPAN', 'default',1e6,... 'fmt_spec','%e',... 'info','(Hz)'); % IF bandwidth addCommand(this,... 'ifbw',':SENS1:BAND', 'default',100,... 'fmt_spec','%e',... 'info','IF bandwidth (Hz)'); % number of points in the sweep addCommand(this,... 'point_no',':SENS1:SWE:POIN', 'default',1000,... 'fmt_spec','%i'); % number of averages addCommand(this,... 'average_no',':SENS1:AVER:COUN', 'default',1,... 'fmt_spec','%i'); % number of traces addCommand(this,... 'trace_no',':CALC1:PAR:COUN', 'default',1,... 'fmt_spec','%i',... 'info','Number of traces'); % linear or log sweep addCommand(this,... 'sweep_type',':SENS1:SWE:TYPE', 'default','LIN',... 'fmt_spec','%s',... 'info','Linear or log sweep'); % switch the output signal on/off addCommand(this,... 'enable_out',':OUTP', 'default',0,... 'fmt_spec','%b',... 'info','output signal on/off'); % probe power [dB] addCommand(this,... 'power',':SOUR:POW:LEV:IMM:AMPL', 'default',-10,... 'fmt_spec','%e',... 'info','Probe power (dB)'); % windows arrangement on the display, e.g 'D1' addCommand(this,... 'disp_type',':DISP:WIND1:SPL', 'default','D1',... 'fmt_spec','%s',... 'info','Window arrangement'); % Continuous sweep triggering addCommand(this,... 'cont_trig',':INIT1:CONT', 'default', 0,... 'fmt_spec','%b'); addCommand(this,... 'trig_source', ':TRIG:SOUR', 'default', 'BUS',... 'fmt_spec','%s') % Parametric commands for traces, i can be extended to 4 for i = 1:2 % measurement parameters for the traces 1-2, e.g. 'S21' i_str = num2str(i); addCommand(this,... ['meas_par',i_str],[':CALC1:PAR',i_str,':DEF'],... 'default','S21',... 'fmt_spec','%s',... 'info','Measurement parameter'); end end end end diff --git a/@MyNewDataEvent/MyNewDataEvent.m b/@MyNewDataEvent/MyNewDataEvent.m index 2bf70b2..a2af18a 100644 --- a/@MyNewDataEvent/MyNewDataEvent.m +++ b/@MyNewDataEvent/MyNewDataEvent.m @@ -1,7 +1,33 @@ %Class for NewData events that are generated by MyInstrument classdef MyNewDataEvent < event.EventData properties + % Handle of the instrument that triggered the event. Usefult for + % passing the event data forward, e.g. by triggering + % NewDataWithHeaders Instr - no_new_header + + % If true then MyCollector does not acquire new measurement headers + % for this trace. Setting no_new_header = false allows transferring + % an existing trace to Daq by triggering NewData. + no_new_header = false + + % If the new data should be automatically saved by Daq. + save = false + + % If 'save' is true and 'filename' is not empty, Daq uses the + % supplied file name to save the trace. This file name is relative + % to the measurement session folder. + filename = '' + end + + methods + + % Use parser to process properties supplied as name-value pairs via + % varargin + function this=MyNewDataEvent(varargin) + P=MyClassParser(this); + processInputs(P, this, varargin{:}); + end + end end \ No newline at end of file diff --git a/@MyTrace/MyTrace.m b/@MyTrace/MyTrace.m index eeb1fa1..241c241 100644 --- a/@MyTrace/MyTrace.m +++ b/@MyTrace/MyTrace.m @@ -1,470 +1,471 @@ % Class for XY data representation with labelling, plotting and % saving/loading functionality % If instantiated as MyTrace(load_path) then % the content is loaded from file classdef MyTrace < handle & matlab.mixin.Copyable & matlab.mixin.SetGet properties (Access=public) x=[]; y=[]; name_x='x'; name_y='y'; unit_x=''; unit_y=''; % MyMetadata storing information about how the trace was taken MeasHeaders file_name=''; % Data column and line separators column_sep = '\t' line_sep='\r\n' %Cell that contains handles the trace is plotted in hlines={}; end properties (Dependent=true) %MyMetadata containing the MeasHeaders and %information about the trace Metadata label_x label_y end methods (Access=public) function this=MyTrace(varargin) P=MyClassParser(this); % options for MeasHeaders addParameter(P, 'metadata_opts',{},@iscell); if mod(length(varargin),2)==1 % odd number of elements in varargin - interpret the first % element as file name and the rest as name-value pairs load_path=varargin{1}; assert(ischar(load_path)&&isvector(load_path),... '''file_name'' must be a vector of characters'); processInputs(P, this, varargin{2:end}); this.file_name=load_path; else % Parse varargin as a list of name-value pairs processInputs(P, this, varargin{:}); load_path=[]; end this.MeasHeaders=MyMetadata(P.Results.metadata_opts{:}); if ~isempty(load_path) load(this, load_path); end end %Defines the save function for the class. Note that this is only %used when we want to write only the data with its associated %trace, rather than just the trace. To write just the trace with %fewer input checks, use the writeData function. function save(this, varargin) %Parse inputs for saving p=inputParser; addParameter(p,'save_prec',15); addParameter(p,'overwrite',false); if mod(length(varargin),2)==1 % odd number of elements in varargin - interpret the first % element as file name and the rest as name-value pairs fname=varargin{1}; assert(ischar(fname)&&isvector(fname),... '''filename'' must be a vector of characters'); this.file_name=fname; parse(p,varargin{2:end}); else % Parse varargin as a list of name-value pairs and take % file name from the class property fname=this.file_name; parse(p,varargin{:}); end %Creates the file in the given folder stat=createFile(fname, 'overwrite', p.Results.overwrite); %Returns if the file is not created for some reason - if ~stat - error('File not created, returned write_flag %i',stat); + if stat + %We now write the data to the file + writeData(this, fname, 'save_prec', p.Results.save_prec); + else + warning('File not created, returned write_flag %i',stat); end - %We now write the data to the file - writeData(this, fname, 'save_prec', p.Results.save_prec); end %Writes the data to a file. This is separated so that other %programs can write to the file from the outside. We circumvent the %checks for the existence of the file here, assuming it is done %outside. function writeData(this, fullfilename, varargin) p=inputParser; addRequired(p,'fullfilename',@ischar); addParameter(p,'save_prec',15); parse(p,fullfilename,varargin{:}); fullfilename=p.Results.fullfilename; save_prec=p.Results.save_prec; %Writes the metadata header save(this.Metadata,fullfilename); fileID=fopen(fullfilename,'a'); %Pads the vectors if they are not equal length diff=length(this.x)-length(this.y); if diff<0 this.x=[this.x;zeros(-diff,1)]; warning(['Zero padded x vector as the saved vectors are',... ' not of the same length']); elseif diff>0 this.y=[this.y;zeros(diff,1)]; warning(['Zero padded y vector as the saved vectors are',... ' not of the same length']); end %Save in the more compact of fixed point and scientific %notation with trailing zeros removed %If save_prec=15, we get %.15g\t%.15g\r\n %Formatting without column padding may looks ugly but makes %files significantly smaller data_format_str=sprintf(['%%.%ig',this.column_sep,'%%.%ig',... this.line_sep],save_prec,save_prec); fprintf(fileID, data_format_str,[this.x, this.y]'); fclose(fileID); end function clearData(this) this.x=[]; this.y=[]; end function load(this, file_path, varargin) p=inputParser; addParameter(p,'hdr_spec',... this.MeasHeaders.hdr_spec,@ischar); addParameter(p,'end_header',... this.MeasHeaders.end_header,@ischar); parse(p,varargin{:}); this.MeasHeaders.hdr_spec=p.Results.hdr_spec; this.MeasHeaders.end_header=p.Results.end_header; if ~exist(file_path,'file') error('File does not exist, please choose a different load path') end %Read metadata. We get the line number we want to read %the main data from as an output. end_line_no=load(this.MeasHeaders, file_path); %Tries to assign units and names and then delete the Info field %from MeasHeaders try this.unit_x=this.MeasHeaders.Info.Unit1.value; this.unit_y=this.MeasHeaders.Info.Unit2.value; this.name_x=this.MeasHeaders.Info.Name1.value; this.name_y=this.MeasHeaders.Info.Name2.value; deleteField(this.MeasHeaders,'Info'); catch warning(['No metadata found. No units or labels assigned',... ' when loading trace from %s'],file_path) this.name_x='x'; this.name_y='y'; this.unit_x='x'; this.unit_y='y'; end %Reads x and y data data_array=dlmread(file_path, this.column_sep, ... end_line_no,0); this.x=data_array(:,1); this.y=data_array(:,2); this.file_name=file_path; end %Plots the trace on the given axes, using the class variables to %define colors, markers, lines and labels. Takes all optional %parameters of the class as inputs. function plot(this, varargin) % Do nothing if there is no data in the trace if isempty(this) return end %Checks that x and y are the same size assert(validatePlot(this),... 'The length of x and y must be identical to make a plot') %Parses inputs p=inputParser(); % Axes in which log should be plotted addOptional(p, 'plot_axes', [], @(x)assert( ... isa(x,'matlab.graphics.axis.Axes')||... isa(x,'matlab.ui.control.UIAxes'),... 'Argument must be axes or uiaxes.')); validateColor=@(x) assert(iscolor(x),... 'Input must be a valid color. See iscolor function'); addParameter(p,'Color','b',validateColor); validateMarker=@(x) assert(ismarker(x),... 'Input must be a valid marker. See ismarker function'); addParameter(p,'Marker','none',validateMarker); validateLine=@(x) assert(isline(x),... 'Input must be a valid linestyle. See isline function'); addParameter(p,'LineStyle','-',validateLine); addParameter(p,'MarkerSize',6,... @(x) validateattributes(x,{'numeric'},{'positive'})); addParameter(p,'make_labels',false,@islogical); interpreters={'none','tex','latex'}; validateInterpreter=@(x) assert(contains(x,interpreters),... 'Interpreter must be none, tex or latex'); addParameter(p,'Interpreter','latex',validateInterpreter); parse(p,varargin{:}); %If axes are not supplied get current if ~isempty(p.Results.plot_axes) plot_axes=p.Results.plot_axes; else plot_axes=gca(); end ind=findLineInd(this, plot_axes); if ~isempty(ind) && any(ind) set(this.hlines{ind},'XData',this.x,'YData',this.y); else this.hlines{end+1}=plot(plot_axes,this.x,this.y); ind=length(this.hlines); end %Sets the correct color and label options set(this.hlines{ind},'Color',p.Results.Color,'LineStyle',... p.Results.LineStyle,'Marker',p.Results.Marker,... 'MarkerSize',p.Results.MarkerSize); if p.Results.make_labels interpreter=p.Results.Interpreter; xlabel(plot_axes,this.label_x,'Interpreter',interpreter); ylabel(plot_axes,this.label_y,'Interpreter',interpreter); set(plot_axes,'TickLabelInterpreter',interpreter); end end %If there is a line object from the trace in the figure, this sets %it to the appropriate visible setting. function setVisible(this, plot_axes, bool) if bool vis='on'; else vis='off'; end ind=findLineInd(this, plot_axes); if ~isempty(ind) && any(ind) set(this.hlines{ind},'Visible',vis) end end %Defines addition of two MyTrace objects function sum=plus(a,b) checkArithmetic(a,b); sum=MyTrace('x',a.x,'y',a.y+b.y,'unit_x',a.unit_x,... 'unit_y',a.unit_y,'name_x',a.name_x,'name_y',a.name_y); end %Defines subtraction of two MyTrace objects function sum=minus(a,b) checkArithmetic(a,b); sum=MyTrace('x',a.x,'y',a.y-b.y,'unit_x',a.unit_x,... 'unit_y',a.unit_y,'name_x',a.name_x,'name_y',a.name_y); end function [max_val,max_x]=max(this) assert(validatePlot(this),['MyTrace object must contain',... ' nonempty data vectors of equal length to find the max']) [max_val,max_ind]=max(this.y); max_x=this.x(max_ind); end function fwhm=calcFwhm(this) assert(validatePlot(this),['MyTrace object must contain',... ' nonempty data vectors of equal length to find the fwhm']) [~,~,fwhm,~]=findPeaks(this.y,this.x,'NPeaks',1); end %Integrates the trace numerically function area=integrate(this,varargin) assert(validatePlot(this),['MyTrace object must contain',... ' nonempty data vectors of equal length to integrate']) %Input parser for optional inputs p=inputParser; %Default is to use all the data in the trace addOptional(p,'ind',true(1,length(this.x))); parse(p,varargin{:}); ind=p.Results.ind; %Integrates the data contained in the indexed part. area=trapz(this.x(ind),this.y(ind)); end % Picks every n-th element from the trace, % performing a running average first if opt=='avg' function downsample(this, n, opt) n0=ceil(n/2); if nargin()==3 && (strcmpi(opt,'average') || strcmpi(opt,'vg')) % Compute moving average with 'shrink' option so that the % total number of samples is preserved. Endpoints will be % discarded by starting the indexing from n0. tmpy=movmean(this.y, 'Endpoints', 'shrink'); this.x=this.x(n0:n:end); this.y=tmpy(n0:n:end); else % Downsample without averaging this.x=this.x(n0:n:end); this.y=this.y(n0:n:end); end end %Checks if the object is empty function bool=isempty(this) bool=isempty(this.x) && isempty(this.y); end %Checks if the data can be plotted function bool=validatePlot(this) bool=~isempty(this.x) && ~isempty(this.y)... && length(this.x)==length(this.y); end function hline=getLineHandle(this,ax) ind=findLineInd(this,ax); if ~isempty(ind) hline=this.hlines{ind}; else hline=[]; end end end methods (Access=private) %Checks if arithmetic can be done with MyTrace objects. function checkArithmetic(a,b) assert(isa(a,'MyTrace') && isa(b,'MyTrace'),... ['Both objects must be of type MyTrace to add,',... 'here they are type %s and %s'],class(a),class(b)); assert(strcmp(a.unit_x, b.unit_x) && strcmp(a.unit_y,b.unit_y),... 'The MyTrace classes must have the same units for arithmetic'); assert(length(a.x)==length(a.y) && length(a.x)==length(a.y),... 'The length of x and y must be equal for arithmetic'); assert(all(a.x==b.x),... 'The MyTrace objects must have identical x-axis for arithmetic') end %Finds the hline handle that is plotted in the specified axes function ind=findLineInd(this, plot_axes) if ~isempty(this.hlines) ind=cellfun(@(x) ismember(x,findall(plot_axes,... 'Type','Line')),this.hlines); else ind=[]; end end end %Set and get methods methods %Set function for MeasHeaders function set.MeasHeaders(this, MeasHeaders) assert(isa(MeasHeaders,'MyMetadata'),... ['MeasHeaders must be an instance of MyMetadata, ',... 'it is %s'],class(MeasHeaders)); this.MeasHeaders=MeasHeaders; end %Set function for x, checks if it is a vector of doubles and %reshapes into a column vector function set.x(this, x) assert(isnumeric(x),... 'Data must be of class double'); this.x=x(:); end %Set function for y, checks if it is a vector of doubles and %reshapes into a column vector function set.y(this, y) assert(isnumeric(y),... 'Data must be of class double'); this.y=y(:); end %Set function for unit_x, checks if input is a string. function set.unit_x(this, unit_x) assert(ischar(unit_x),'Unit must be a char, not a %s',... class(unit_x)); this.unit_x=unit_x; end %Set function for unit_y, checks if input is a string function set.unit_y(this, unit_y) assert(ischar(unit_y),'Unit must be a char, not a %s',... class(unit_y)); this.unit_y=unit_y; end %Set function for name_x, checks if input is a string function set.name_x(this, name_x) assert(ischar(name_x),'Name must be a char, not a %s',... class(name_x)); this.name_x=name_x; end %Set function for name_y, checks if input is a string function set.name_y(this, name_y) assert(ischar(name_y),'Name must be a char, not a %s',... class(name_y)); this.name_y=name_y; end function set.file_name(this, file_name) assert(ischar(file_name),'File path must be a char, not a %s',... class(file_name)); this.file_name=file_name; end %Get function for label_x, creates label from name_x and unit_x. function label_x=get.label_x(this) label_x=sprintf('%s (%s)', this.name_x, this.unit_x); end %Get function for label_y, creates label from name_y and unit_y. function label_y=get.label_y(this) label_y=sprintf('%s (%s)', this.name_y, this.unit_y); end %Generates the full metadata of the trace function Metadata=get.Metadata(this) %First we update the trace information Metadata=MyMetadata(); addField(Metadata,'Info'); addParam(Metadata,'Info','Name1',this.name_x); addParam(Metadata,'Info','Name2',this.name_y); addParam(Metadata,'Info','Unit1',this.unit_x); addParam(Metadata,'Info','Unit2',this.unit_y); addMetadata(Metadata,this.MeasHeaders); end end end