diff --git a/.gitignore b/.gitignore index a7071fb..a9143d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.asv *.asv *.asv +*.DS_Store diff --git a/@MyClassParser/MyClassParser.m b/@MyClassParser/MyClassParser.m index afc965e..bcc1867 100644 --- a/@MyClassParser/MyClassParser.m +++ b/@MyClassParser/MyClassParser.m @@ -1,115 +1,114 @@ % Input parser, which functionality was extended to automatically add % class properties to the scheme and assign the results after parsing is % done. % Different to inputParser, KeepUnmatched is 'true' by default. classdef MyClassParser < inputParser properties (Dependent=true) unmatched_nv % List of unmatched name-value pairs end methods (Access=public) function this = MyClassParser(varargin) this@inputParser(); % KeepUnmatched is true so that the unmatched options could be % passed to another class this.KeepUnmatched=true; if nargin()==1 % If an object is supplied via varargin, add its properties % to the parser scheme addClassProperties(this, varargin{1}); end end % Add all the properties the class which are not already present % in the scheme of the parser and which have set access % permitted for MyClassParser function addClassProperties(this, obj) objMetaclass = metaclass(obj); for i=1:length(objMetaclass.PropertyList) Tmp = objMetaclass.PropertyList(i); % If parameter is already present in the parser scheme, % skip if ismember(Tmp.Name, this.Parameters) continue end % Constant, Dependent and Abstract propeties cannot be set, % so skip in this case also. if Tmp.Constant||Tmp.Abstract||Tmp.Dependent continue end % Check if the parser has access to the property. This % can be true in two cases: 1) SetAccess is public % 2) MyClassParser class was explicitly given access sa=Tmp.SetAccess; if ischar(sa) has_access=strcmpi(sa,'public'); elseif iscell(sa) % Case when SetAcces is specified as cell array of % metaclasses has_access = any(... cellfun(@(x) strcmpi(x.Name, class(this)),sa)); else has_access=false; end % If the property has set access and default value, % add it as parameter to the parser scheme if has_access if Tmp.HasDefault def = Tmp.DefaultValue; % Create validation function based on the class of % default value val_fcn = @(x)assert(isa(x, class(def)),... ['The value must be of the class ',class(def),... ' while the present one is of the class ',... class(x),'.']); addParameter(this, Tmp.Name, def, val_fcn); end end end end % parse varargin and assign results to class properties % with the same names as parameters function processInputs(this, obj, varargin) - parse(this, varargin{:}); + parse(this, varargin{:}); % assign results that have associated class properties with % public set access for i=1:length(this.Parameters) par = this.Parameters{i}; - if ~ismember(par, this.UsingDefaults)&&... - isprop(obj, par) + if ~ismember(par, this.UsingDefaults) && isprop(obj, par) try obj.(par) = this.Results.(par); catch warning(['Value of the input parameter ''',... par,''' could not be assigned to property']) end end end end end methods function unmatched_nv=get.unmatched_nv(this) fns=fieldnames(this.Unmatched); vals=struct2cell(this.Unmatched); unmatched_nv=cell(1,2*length(fns)); for i=1:length(fns) unmatched_nv{2*i-1}=fns{i}; unmatched_nv{2*i}=vals{i}; end end end end diff --git a/@MyCollector/MyCollector.m b/@MyCollector/MyCollector.m index 24b1524..4d1da87 100644 --- a/@MyCollector/MyCollector.m +++ b/@MyCollector/MyCollector.m @@ -1,168 +1,170 @@ classdef MyCollector < handle & 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=public) 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 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(); addField(this.MeasHeaders,'AcquiringInstrument') if isprop(src,'name') name=src.name; else name='Not Accessible'; end addParam(this.MeasHeaders,'AcquiringInstrument',... 'Name',name); + %Add field indicating the time when the trace was acquired + addTimeField(this.MeasHeaders, 'AcquisitionTime') 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),... 'Instrument name must be a character, not %s',... class(name)); bool=ismember(this.running_instruments,name); end end methods (Access=private) function triggerNewDataWithHeaders(this,InstrEventData) notify(this,'NewDataWithHeaders',InstrEventData); end %deleteListeners is in a separate file deleteListeners(this, obj_name); function deleteInstrument(this,name) %We remove the instrument this.InstrList=rmfield(this.InstrList,name); this.InstrProps=rmfield(this.InstrProps,name); deleteListeners(this,name); end 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 cf3e44a..1c87850 100644 --- a/@MyDaq/MyDaq.m +++ b/@MyDaq/MyDaq.m @@ -1,951 +1,975 @@ % 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 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)); this.Gui.figure1.CloseRequestFcn=''; %Deletes the figure delete(this.Gui.figure1); %Removes the figure handle to prevent memory leaks this.Gui=[]; 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) %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)); + errordlg(sprintf('%s trace was empty, could not save',... + trace_tag)); return end - %Uses the protected save function of MyTrace - save(this.(trace_tag),... - 'save_dir',this.save_dir,... - 'filename',this.filename) + [~,~,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); + end + + %Save in a readable format using the method of MyTrace + save(this.(trace_tag), fullfilename) 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 - setTrace(this.Ref,... + 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) + '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 - this.Ref.plotTrace(this.main_plot,'Color',this.ref_color,... + plot(this.Ref, this.main_plot, 'Color',this.ref_color,... 'make_labels',true); - this.Ref.setVisible(this.main_plot,1); + 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.plotTrace(this.main_plot,... + 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.plotTrace(this.main_plot,... + 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 - loadTrace(this.(dest_trc),load_path); + load(this.(dest_trc), load_path); %Color and plot the right trace. - this.(dest_trc).plotTrace(this.main_plot,... + 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 - this.Data.plotTrace(this.main_plot,... + 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); 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 function filename=get.filename(this) try filename=this.Gui.FileName.String; catch filename='placeholder'; end end function set.filename(this,filename) this.Gui.FileName.String=filename; end end end \ No newline at end of file diff --git a/@MyDaq/initGui.m b/@MyDaq/initGui.m index ff78f97..08e89f7 100644 --- a/@MyDaq/initGui.m +++ b/@MyDaq/initGui.m @@ -1,116 +1,119 @@ %Initializes MyDaq Gui. Needs no inputs, but should be modified if you wish %to change the callbacks etc. function initGui(this) %Close request function is set to delete function of the class this.Gui.figure1.CloseRequestFcn=@(hObject,eventdata)... closeFigure(this, hObject, eventdata); %Sets callback for the edit box of the base directory this.Gui.BaseDir.Callback=@(hObject, eventdata)... baseDirCallback(this, hObject, eventdata); %Sets callback for the session name edit box this.Gui.SessionName.Callback=@(hObject, eventdata)... sessionNameCallback(this, hObject, eventdata); %Sets callback for the file name edit box this.Gui.FileName.Callback=@(hObject, eventdata) ... fileNameCallback(this, hObject,eventdata); %Sets callback for the save data button this.Gui.SaveData.Callback=@(hObject, eventdata) ... saveCallback(this, hObject,eventdata); %Sets callback for the save ref button this.Gui.SaveRef.Callback=@(hObject, eventdata)... saveCallback(this, hObject, eventdata); %Sets callback for the show data button this.Gui.ShowData.Callback=@(hObject, eventdata)... showDataCallback(this, hObject, eventdata); %Sets callback for the show reference button this.Gui.ShowRef.Callback=@(hObject, eventdata)... showRefCallback(this, hObject, eventdata); %Sets callback for the data to reference button this.Gui.DataToRef.Callback=@(hObject, eventdata)... dataToRefCallback(this, hObject, eventdata); %Sets callback for the LogY button this.Gui.LogY.Callback=@(hObject, eventdata) ... logYCallback(this, hObject, eventdata); %Sets callback for the LogX button this.Gui.LogX.Callback=@(hObject, eventdata)... logXCallback(this, hObject, eventdata); %Sets callback for the data to background button this.Gui.DataToBg.Callback=@(hObject, eventdata) ... dataToBgCallback(this, hObject,eventdata); %Sets callback for the ref to background button this.Gui.RefToBg.Callback=@(hObject, eventdata) ... refToBgCallback(this, hObject,eventdata); %Sets callback for the clear background button this.Gui.ClearBg.Callback=@(hObject, eventdata)... clearBgCallback(this, hObject,eventdata); %Sets callback for the select trace popup menu this.Gui.SelTrace.Callback=@(hObject,eventdata)... selTraceCallback(this, hObject,eventdata); %Sets callback for the vertical cursor button this.Gui.VertDataButton.Callback=@(hObject, eventdata)... cursorButtonCallback(this, hObject,eventdata); %Sets callback for the horizontal cursors button this.Gui.HorzDataButton.Callback=@(hObject, eventdata)... cursorButtonCallback(this, hObject,eventdata); %Sets callback for the reference cursors button this.Gui.VertRefButton.Callback=@(hObject, eventdata)... cursorButtonCallback(this, hObject,eventdata); %Sets callback for the center cursors button this.Gui.CenterCursors.Callback=@(hObject,eventdata)... centerCursorsCallback(this,hObject,eventdata); %Sets callback for the center cursors button this.Gui.CopyPlot.Callback=@(hObject,eventdata)... copyPlotCallback(this,hObject,eventdata); %Sets callback for load trace button this.Gui.LoadButton.Callback=@(hObject,eventdata)... loadDataCallback(this,hObject,eventdata); +%Sets callback for open directory button +this.Gui.OpenFolderButton.Callback=@(hObject,eventdata)... + openFolderCallback(this,hObject,eventdata); %Initializes the AnalyzeMenu this.Gui.AnalyzeMenu.Callback=@(hObject, eventdata)... analyzeMenuCallback(this, hObject,eventdata); %List of available analysis routines this.Gui.AnalyzeMenu.String={'Select a routine...',... 'Linear Fit','Quadratic Fit','Exponential Fit',... 'Gaussian Fit','Lorentzian Fit','Double Lorentzian Fit',... 'g0 Calibration','Beta Calibration'}; %Tooltips for AnalyzeMenu standard_tip=['Generate initial parameters with gen. init. param. \n',... 'Press Analyze to fit. Move the sliders or enter numbers to',... ' change initial parameters.\n']; this.Gui.AnalyzeTip{1}='Select a routine to open it.'; this.Gui.AnalyzeTip{2}=standard_tip; this.Gui.AnalyzeTip{3}=standard_tip; this.Gui.AnalyzeTip{4}=[standard_tip,... 'Calculation of the quality factor (assuming amplitude ringdown) ',... 'is included']; this.Gui.AnalyzeTip{5}=standard_tip; this.Gui.AnalyzeTip{6}=... [standard_tip,... 'To calibrate optical linewidths, move the reference cursors to',... ' have the correct number of resonances between them \n',... 'The number of resonances can be changed by changing the number of ',... 'lines in the GUI. \n The FSR is entered in the line spacing field \n',... 'The tab for mechanics is automatically populated and calculated.']; this.Gui.AnalyzeTip{7}= [standard_tip,... 'To calibrate optical linewidths, move the reference cursors to',... ' have the correct number of resonances between them \n',... 'The number of resonances can be changed by changing the number of ',... 'lines in the GUI. \n The FSR is entered in the line spacing field \n',... 'The modal spacing is automatically calculated']; this.Gui.AnalyzeTip{8}=['Put calibration tone between reference cursors.\n',... 'Put mechanical mode signal between vertical cursors.\n',... 'Enter the correct temperature and beta, then press analyze to find g0.']; this.Gui.AnalyzeTip{9}=['Automatically calculates beta based on first ',... 'and second order phase modulation sidebands']; %Initializes the InstrMenu this.Gui.InstrMenu.Callback=@(hObject,eventdata) ... instrMenuCallback(this,hObject,eventdata); %Initializes the axis this.main_plot.Box='on'; this.main_plot.XLabel.String='x'; this.main_plot.YLabel.String='y'; end \ No newline at end of file diff --git a/@MyLog/MyLog.m b/@MyLog/MyLog.m index 1659e7f..840607c 100644 --- a/@MyLog/MyLog.m +++ b/@MyLog/MyLog.m @@ -1,701 +1,716 @@ % Class to store data versus time. % Data can be continuously appended and saved. It is possible to add % labels (time marks) for particular moments in time. Data can be saved % and plotted with the time marks. -% Metadata for this class is stored independently +% Metadata for this class is stored independently. +% If instantiated as MyLog(fname) or MyLog('file_name', fname) then +% the content is loaded from file classdef MyLog < matlab.mixin.Copyable properties (Access=public) % Save time as posixtime up to ms precision time_fmt = '%14.3f' % Save data as reals with up to 14 decimal digits. Trailing zeros % are removed by %g data_fmt = '%.14g' % Format for displaying the last reading (column name: value) disp_fmt = '%15s: %.2g' - % Data columns are separated by this symbol - data_column_sep = '\t' - + % Data column and line separators + column_sep = '\t' line_sep='\r\n' % File extension that is appended by default when saving the log % if a different one is not specified explicitly data_file_ext = '.log' % File extension for metadata meta_file_ext = '.meta' file_name = '' % Used to save or load the data data_headers = {} % Cell array of column headers length_lim = Inf % Keep the log length below this limit end properties (SetAccess=public, GetAccess=public) timestamps % Times at which data was aqcuired data % Array of measurements TimeLabels % Structure array that stores labeled time marks % Structure array that stores all the axes the log is plotted in PlotList; % Information about the log in saveable format, % including time labels and data headers Metadata end properties (Dependent=true) data_line_fmt % Format specifier for one data row to be printed column_headers % Time column header + data column headers data_file_name % File name with extension for data saving meta_file_name % File name with extension for metadata saving timestamps_num % timestamps converted to numeric format end methods (Access=public) %% Constructor and destructor methods function this = MyLog(varargin) P=MyClassParser(this); - processInputs(P, this, varargin{:}); + % 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 + fname=varargin{1}; + assert(ischar(fname)&&isvector(fname),... + '''file_name'' must be a vector of characters'); + processInputs(P, this, varargin{2:end}); + this.file_name=fname; + else + % Parse varargin as a list of name-value pairs + processInputs(P, this, varargin{:}); + end - this.Metadata=MyMetadata(P.unmatched_nv{:}); + this.Metadata=MyMetadata(P.Results.metadata_opts{:}); % Create an empty structure array of time labels this.TimeLabels=struct(... 'time',{},... % datetime object 'time_str',{},... % time in text format 'text_str',{}); % message string % Create an empty structure array of axes this.PlotList=struct(... 'Axes',{},... % axes handles 'DataLines',{},... % data line handles 'LbLines',{},... % labels line handles 'LbText',{}); % labels text handles % Load the data from file if the file name was provided - if ~ismember('file_name', P.UsingDefaults) - load(this, P.Results.file_name); + if ~isempty(this.file_name) + load(this, this.file_name); end end %% Save and load functionality % save the entire data record function save(this, fname) % Verify that the data can be saved assertDataMatrix(this); % File name can be either supplied explicitly or given as the % file_name property if nargin()<2 fname = this.file_name; else this.file_name=fname; end assert(~isempty(fname), 'File name is not provided.'); datfname=this.data_file_name; metfname=this.meta_file_name; stat=createFile(datfname); if stat % Save time labels in separate file save(this.Metadata, metfname, 'overwrite', true); fid = fopen(datfname,'w'); % Write column headers str=printDataHeaders(this); fprintf(fid,'%s',str); % Write data body fmt=this.data_line_fmt; for i=1:length(this.timestamps) fprintf(fid, fmt, this.timestamps_num(i), ... this.data(i,:)); end fclose(fid); end end % Load log from file function load(this, fname) if nargin()==2 this.file_name=fname; end assert(~isempty(this.file_name), 'File name is not provided.'); assert(exist(this.data_file_name, 'file')==2, ... ['File ''',this.data_file_name,''' is not found.']) % Load metadata if file is found % Fields of Metadata are re-initialized by its get method, so % need to copy in order for the loaded information to be not % overwritten, on one hand, and on the other hand to use the % formatting defined by Metadata. M=copy(this.Metadata); clearFields(M); if exist(this.meta_file_name, 'file')==2 load(M, this.meta_file_name); end % Read column headers from data file fid=fopen(fname,'r'); - dat_col_heads=strsplit(fgetl(fid),this.data_column_sep, ... + dat_col_heads=strsplit(fgetl(fid),this.column_sep, ... 'CollapseDelimiters', true); fclose(fid); % Read data as delimiter-separated values and convert to cell % array, skip the first line containing column headers - fulldata = dlmread(fname, this.data_column_sep, 1, 0); + fulldata = dlmread(fname, this.column_sep, 1, 0); this.data = fulldata(:,2:end); this.timestamps = fulldata(:,1); % Process metadata % Assign column headers first, prioritizing those found in % the metadata file over those found in the main file. This is % done because the column names in the main file are not % updated once they are printed, while the column names in % metadata are always up to date. if ismember('ColumnNames', M.field_names) && ... length(M.ColumnNames.Name.value)>=2 % Assign column headers from metadata if present this.data_headers=M.ColumnNames.Name.value(2:end); elseif length(dat_col_heads)>=2 this.data_headers=dat_col_heads(2:end); end % Assign time labels if ismember('TimeLabels', M.field_names) Lbl=M.TimeLabels.Lbl.value; for i=1:length(Lbl) this.TimeLabels(i).time_str=Lbl(i).time_str; this.TimeLabels(i).time=datetime(Lbl(i).time_str); this.TimeLabels(i).text_str=Lbl(i).text_str; end end % Convert the time stamps to datetime if the time column % format is posixtime if ~isempty(dat_col_heads) && ... contains(dat_col_heads{1},'posix','IgnoreCase',true) this.timestamps=datetime(this.timestamps, ... 'ConvertFrom','posixtime'); end end %% Plotting % Plot the log data with time labels. Reurns plotted line objects. function Pls = plot(this, varargin) % Verify that the data is a numeric matrix, % otherwise it cannot be plotted assertDataMatrix(this); [~, ncols] = size(this.data); p=inputParser(); % Axes in which log should be plotted addOptional(p, 'Ax', [], @(x)assert( ... isa(x,'matlab.graphics.axis.Axes')||... isa(x,'matlab.ui.control.UIAxes'),... 'Argument must be axes or uiaxes.')); % If time labels are to be displayed addParameter(p, 'time_labels', true, @islogical); % If legend is to be displayed addParameter(p, 'legend', true, @islogical); % Logical vector defining the data columns to be displayed addParameter(p, 'isdisp', true(1,ncols), @(x) assert(... islogical(x) && isvector(x) && length(x)==ncols, ... ['''isdisp'' must be a logical vector of the size ',... 'equal to the number of data columns.'])); % If 'reset' is true than all the data lines and time labels are % re-plotted with default style even if they are already present % in the plot addParameter(p, 'reset', false, @islogical); parse(p, varargin{:}); if ~isempty(p.Results.Ax) Ax=p.Results.Ax; else Ax=gca(); end % Find out if the log was already plotted in these axes. If % not, appned Ax to the PlotList. ind=findPlotInd(this, Ax); if isempty(ind) l=length(this.PlotList); this.PlotList(l+1).Axes=Ax; ind=l+1; end % Plot data if isempty(this.PlotList(ind).DataLines) % If the log was never plotted in Ax, % plot using default style and store the line handles Pls=line(Ax, this.timestamps, this.data); this.PlotList(ind).DataLines=Pls; else % Replace existing data Pls=this.PlotList(ind).DataLines; for i=1:length(Pls) try Pls(i).XData=this.timestamps; Pls(i).YData=this.data(:,i); catch warning(['Could not update plot for '... '%i-th data column'],i); end end end % Set the visibility of lines if ~ismember('isdisp',p.UsingDefaults) for i=1:ncols Pls(i).Visible=p.Results.isdisp(i); end end % Plot time labels and legend if (p.Results.time_labels) plotTimeLabels(this, Ax); end if (p.Results.legend)&&(~isempty(this.data_headers))&&... (~isempty(this.data)) % Add legend only for for those lines that are displayed disp_ind = cellfun(@(x)strcmpi(x,'on'),{Pls.Visible}); legend(Ax, Pls(disp_ind), this.data_headers{disp_ind},... 'Location','southwest'); end end %% Manipulations with log data % Append data point to the log function appendData(this, time, val, varargin) p=inputParser(); addParameter(p, 'save', false, @islogical); parse(p, varargin{:}); % Format checks on the input data assert(isa(time,'datetime')||isnumeric(time),... ['''time'' argument must be numeric or ',... 'of the class datetime.']); assert(isrow(val),'''val'' argument must be a row vector.'); if ~isempty(this.data) [~, ncols]=size(this.data); assert(length(val)==ncols,['Length of ''val'' ',... 'does not match the number of data columns']); end % Append new data and time stamps this.timestamps=[this.timestamps; time]; this.data=[this.data; val]; % Ensure the log length is within the length limit trim(this); % Optionally save the new data point to file if p.Results.save try exstat = exist(this.data_file_name,'file'); if exstat==0 % if the file does not exist, create it and write % the column headers createFile(this.data_file_name); fid = fopen(this.data_file_name,'w'); str=printDataHeaders(this); fprintf(fid,'%s',str); else % otherwise open for appending fid = fopen(this.data_file_name,'a'); end % Convert the new timestamps to numeric form for saving if isa(time,'datetime') time_num=posixtime(time); else time_num=time; end % Append new data points to file fprintf(fid, this.data_line_fmt, time_num, val); fclose(fid); catch warning(['Logger cannot save data at time = ',... datestr(datetime('now'))]); % Try closing fid in case it is still open try fclose(fid); catch end end end end %% Time labels function plotTimeLabels(this, Ax) % Find out if the log was already plotted in these axes ind=findPlotInd(this, Ax); if isempty(ind) l=length(this.PlotList); this.PlotList(l+1).Axes=Ax; ind=l+1; end % Remove existing labels eraseTimeLabels(this, Ax); % Define marker lines to span over the entire plot yminmax=ylim(Ax); ymin=yminmax(1); ymax=yminmax(2); markline = linspace(ymin, ymax, 2); % Plot labels for i=1:length(this.TimeLabels) T=this.TimeLabels(i); marktime = [T.time,T.time]; % Add text label to plot, with 5% offset from % the boundary for beauty Txt=text(Ax, T.time, 0.95, T.text_str,... 'Units','data',... 'HorizontalAlignment','right',... 'VerticalAlignment','bottom',... 'FontWeight', 'bold',... 'Rotation',90,... 'BackgroundColor','white',... 'Clipping','on',... 'Margin',1); % Add line to plot Pl=line(Ax, marktime, markline,'color','black'); % Store the handles of text and line this.PlotList(ind).LbLines = ... [this.PlotList(ind).LbLines,Pl]; this.PlotList(ind).LbText = ... [this.PlotList(ind).LbText,Txt]; end end % Remove existing labels from the plot function eraseTimeLabels(this, Ax) % Find out if the log was already plotted in these axes ind=findPlotInd(this, Ax); if ~isempty(ind) % Remove existing labels delete(this.PlotList(ind).LbLines); this.PlotList(ind).LbLines=[]; delete(this.PlotList(ind).LbText); this.PlotList(ind).LbText=[]; else warning('Cannot erase time labels. Axes not found.') end end % Add label % Form with optional arguments: addTimeLabel(this, time, str) function addTimeLabel(this, varargin) p=inputParser(); addOptional(p, 'time', datetime('now'), ... @(x)assert(isa(x,'datetime'), ... '''time'' must be of the type datetime.')); addOptional(p, 'str', '', ... @(x) assert(iscellstr(x)||ischar(x)||isstring(x), ... '''str'' must be a string or cell array of strings.')); addParameter(p, 'save', false, @islogical); parse(p, varargin{:}); if any(ismember({'time','str'}, p.UsingDefaults)) % Invoke a dialog to add the label time and name answ = inputdlg({'Label text', 'Time'},'Add time label',... [2 40; 1 40],{'',datestr(datetime('now'))}); if isempty(answ)||isempty(answ{1}) return else % Conversion of the inputed value to datetime to % ensure proper format time=datetime(answ{2}); % Store multiple lines as cell array str=cellstr(answ{1}); end end % Need to calculate length explicitly as using 'end' fails % for an empty array l=length(this.TimeLabels); this.TimeLabels(l+1).time=time; this.TimeLabels(l+1).time_str=datestr(time); this.TimeLabels(l+1).text_str=str; % Order time labels by ascending time sortTimeLabels(this); if p.Results.save==true % Save metadata with new time labels save(this.Metadata, this.meta_file_name, ... 'overwrite', true); end end % Modify text or time of an exising label. If new time and text are % not provided as arguments, modifyTimeLabel(this, ind, time, str), % invoke a dialog. % ind - index of the label to be modified in TimeLabels array. function modifyTimeLabel(this, ind, varargin) p=inputParser(); addRequired(p, 'ind', @(x)assert((rem(x,1)==0)&&(x>0), ... '''ind'' must be a positive integer.')); addOptional(p, 'time', datetime('now'), ... @(x)assert(isa(x,'datetime'), ... '''time'' must be of the type datetime.')); addOptional(p, 'str', '', ... @(x) assert(iscellstr(x)||ischar(x)||isstring(x), ... '''str'' must be a string or cell array of strings.')); addParameter(p, 'save', false, @islogical); parse(p, ind, varargin{:}); if any(ismember({'time','str'}, p.UsingDefaults)) Tlb=this.TimeLabels(ind); answ = inputdlg({'Label text', 'Time'},'Modify time label',... [2 40; 1 40],{char(Tlb.text_str), Tlb.time_str}); if isempty(answ)||isempty(answ{1}) return else % Conversion of the inputed value to datetime to % ensure proper format time=datetime(answ{2}); % Store multiple lines as cell array str=cellstr(answ{1}); end end this.TimeLabels(ind).time=time; this.TimeLabels(ind).time_str=datestr(time); this.TimeLabels(ind).text_str=str; % Order time labels by ascending time sortTimeLabels(this); if p.Results.save==true % Save metadata with new time labels save(this.Metadata, this.meta_file_name, ... 'overwrite', true); end end % Show the list of labels in readable format function lst=printTimeLabelList(this) lst=cell(length(this.TimeLabels),1); for i=1:length(this.TimeLabels) if ischar(this.TimeLabels(i).text_str) ||... isstring(this.TimeLabels(i).text_str) tmpstr=this.TimeLabels(i).text_str; elseif iscell(this.TimeLabels(i).text_str) % If text is cell array, elements corresponding to % multiple lines, display the first line tmpstr=this.TimeLabels(i).text_str{1}; end lst{i}=[this.TimeLabels(i).time_str,' ', tmpstr]; end end %% Misc public functions % Clear log data and time labels function clear(this) % Clear while preserving the array types this.TimeLabels(:)=[]; % Delete all the data lines and time labels for i=1:length(this.PlotList) delete(this.PlotList(i).DataLines); delete(this.PlotList(i).LbLines); delete(this.PlotList(i).LbText); end this.PlotList(:)=[]; % Clear data and its type this.timestamps = []; this.data = []; end % Verify that the data can be saved or plotted function assertDataMatrix(this) assert(ismatrix(this.data)&&isnumeric(this.data),... ['Data is not a numeric matrix, saving in '... 'text format is not possible.']); end % Display last reading function str = printLastReading(this) if isempty(this.timestamps) str = ''; else str = ['Last reading ',char(this.timestamps(end)),newline]; last_data = this.data(end,:); for i=1:length(last_data) if length(this.data_headers)>=i lbl = this.data_headers{i}; else lbl = sprintf('data%i',i); end str = [str,... sprintf(this.disp_fmt,lbl,last_data(i)),newline]; %#ok end end end end methods (Access=private) %% Auxiliary private functions % Ensure the log length is within length limit function trim(this) l=length(this.timestamps); if l>this.length_lim dn=l-this.length_lim; this.timestamps(1:dn)=[]; this.data(1:dn)=[]; end end % Print column names to a string function str=printDataHeaders(this) - cs=this.data_column_sep; + cs=this.column_sep; str=sprintf(['%s',cs], this.column_headers{:}); str=[str,sprintf(this.line_sep)]; end % Find out if the log was already plotted in the axes Ax and return % the corresponding index of PlotList if it was function ind=findPlotInd(this, Ax) assert(isvalid(Ax),'Ax must be valid axes or uiaxes') if ~isempty(this.PlotList) ind_b=cellfun(@(x) isequal(x, Ax),{this.PlotList.Axes}); % Find index of the first match ind=find(ind_b,1); else ind=[]; end end % Re-order the elements of TimeLabels array so that labels % corresponding to later times have larger index function sortTimeLabels(this) times=[this.TimeLabels.time]; [~,ind]=sort(times); this.TimeLabels=this.TimeLabels(ind); end end %% set and get methods methods function set.length_lim(this, val) assert(isreal(val),'''length_lim'' must be a real number'); % Make length_lim non-negative and integer this.length_lim=max(0, round(val)); % Apply the new length limit to log trim(this); end function set.data_headers(this, val) assert(iscellstr(val) && isrow(val), ['''data_headers'' must '... 'be a row cell array of character strings.']) %#ok this.data_headers=val; end % The get function for file_name adds extension if it is not % already present and also ensures proper file separators % (by splitting and combining the file name) function fname = get.data_file_name(this) fname = this.file_name; [filepath,name,ext] = fileparts(fname); if isempty(ext) ext=this.data_file_ext; end fname = fullfile(filepath,[name,ext]); end function fname = get.meta_file_name(this) fname = this.file_name; [filepath,name,~] = fileparts(fname); ext=this.meta_file_ext; fname = fullfile(filepath,[name,ext]); end function data_line_fmt=get.data_line_fmt(this) - cs=this.data_column_sep; + cs=this.column_sep; nl=this.line_sep; if isempty(this.data) l=0; else [~,l]=size(this.data); end data_line_fmt = this.time_fmt; for i=1:l data_line_fmt = [data_line_fmt, cs, this.data_fmt]; %#ok end data_line_fmt = [data_line_fmt, nl]; end function hdrs=get.column_headers(this) % Add header for the time column if isa(this.timestamps,'datetime') time_title_str = 'POSIX time (s)'; else time_title_str = 'Time'; end hdrs=[time_title_str,this.data_headers]; end function time_num_arr=get.timestamps_num(this) % Convert time stamps to numbers if isa(this.timestamps,'datetime') time_num_arr=posixtime(this.timestamps); else time_num_arr=this.timestamps; end end function Mdt=get.Metadata(this) Mdt=this.Metadata; % Clear Metadata but preserve formatting clearFields(Mdt); % Add column names addField(Mdt, 'ColumnNames'); addParam(Mdt, 'ColumnNames', 'Name', this.column_headers) % Add time labels (textual part of TimeLabels structure) addField(Mdt, 'TimeLabels'); Lbl=struct('time_str', {this.TimeLabels.time_str},... 'text_str', {this.TimeLabels.text_str}); addParam(Mdt, 'TimeLabels', 'Lbl', Lbl) end end end diff --git a/@MyMetadata/MyMetadata.m b/@MyMetadata/MyMetadata.m index 891fdde..9b51a48 100644 --- a/@MyMetadata/MyMetadata.m +++ b/@MyMetadata/MyMetadata.m @@ -1,429 +1,452 @@ % MyMetadata stores parameter-value pairs grouped by fields. MyMetadata can % be saved and read in a readable text format. % Parameters can be strings, numerical values or cells, as well as any % arrays and structures of such with arbitrary nesting. Sub-indices are % automatically expanded when saving. +% If instantiated as MyMetadata(fname) or MyMetadata('file_name', fname) +% then the content is loaded from file classdef MyMetadata < dynamicprops properties (Access=public) % Header sections are separated by [hdr_spec,hdr_spec,hdr_spec] hdr_spec='==' % Data starts from the line next to [hdr_spec,end_header,hdr_spec] end_header='Data' % Columns are separated by this symbol (space-tab by default) column_sep=' \t' % Comments start from this symbol comment_sep='%' line_sep='\r\n' % Limit for column padding. Variables which take more space than % this limit are ignored when calculating the padding length. pad_lim=12 end properties (Access=private) PropHandles %Used to store the handles of the dynamic properties end properties (Dependent=true) field_names end methods function [this,varargout]=MyMetadata(varargin) P=MyClassParser(this); - addParameter(P, 'load_path','',@ischar); - processInputs(P,this, varargin{:}); - load_path=P.Results.load_path; + addParameter(P, 'file_name','',@ischar); + + 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),... + '''file_name'' must be a vector of characters'); + processInputs(P, this, varargin{2:end}); + else + % Parse varargin as a list of name-value pairs + processInputs(P, this, varargin{:}); + fname=P.Results.file_name; + end this.PropHandles=struct(); - if ~isempty(load_path) - varargout{1}=load(this, load_path); + if ~isempty(fname) + varargout{1}=load(this, fname); end end %Fields are added using this command. The field is a property of %the class, populated by the parameters with their values and %string specifications for later printing function addField(this, field_name) assert(isvarname(field_name),... 'Field name must be a valid MATLAB variable name.'); assert(~ismember(field_name, this.field_names),... ['Field with name ',field_name,' already exists.']); this.PropHandles.(field_name)=addprop(this,field_name); this.PropHandles.(field_name).SetAccess='protected'; % Dynamic properties must not be copyable, copying of % MyMetadata objects is handled by dedicated overloaded method this.PropHandles.(field_name).NonCopyable=true; this.(field_name)=struct(); end %Deletes a named field function deleteField(this, field_name) assert(isvarname(field_name),... 'Field name must be a valid MATLAB variable name.'); assert(ismember(field_name,this.field_names),... ['Attemped to delete field ''',field_name ... ,''' that does not exist.']); % Delete dynamic property from the class delete(this.PropHandles.(field_name)); % Erase entry in PropHandles this.PropHandles=rmfield(this.PropHandles,field_name); end %Clears the object of all fields function clearFields(this) cellfun(@(x) deleteField(this, x), this.field_names) end % Copy all the fields of another Metadata object to this object function addMetadata(this, Metadata) assert(isa(Metadata,'MyMetadata'),... 'Input must be of class MyMetadata, current input is %s',... class(Metadata)); assert(~any(ismember(this.field_names,Metadata.field_names)),... ['The metadata being added contain fields with the same ',... 'name. This conflict must be resolved before adding']) for i=1:length(Metadata.field_names) fn=Metadata.field_names{i}; addField(this,fn); param_names=fieldnames(Metadata.(fn)); cellfun(@(x) addParam(this,fn,x,Metadata.(fn).(x).value,... 'fmt_spec', Metadata.(fn).(x).fmt_spec,... 'comment', Metadata.(fn).(x).comment),... param_names); end end %Adds a parameter to a specified field. The field must be created %first. function addParam(this, field_name, param_name, value, varargin) assert(ischar(field_name),'Field name must be a char'); assert(isprop(this,field_name),... '%s is not a field, use addField to add it',param_name); assert(ischar(param_name),'Parameter name must be a char'); p=inputParser(); % Format specifier for printing the value addParameter(p,'fmt_spec','',@ischar); % Comment to be added to the line addParameter(p,'comment','',@ischar); addParameter(p,'SubStruct',struct('type',{},'subs',{}),... @isstruct) parse(p,varargin{:}); S=p.Results.SubStruct; comment=p.Results.comment; %Making sure that the comment does not %contain new line or carriage return characters, which would %mess up formating when saving metadata newline_smb={sprintf('\n'),sprintf('\r')}; %#ok if contains(comment, newline_smb) warning(['Comment string for ''%s'' must not contain ',... '''\\n'' and ''\\r'' symbols, replacing them ',... 'with space.'], param_name); comment=replace(comment, newline_smb,' '); end this.(field_name).(param_name).comment=comment; if isempty(S) % Assign value directly this.(field_name).(param_name).value=value; else % Assign using subref structure tmp=feval([class(value),'.empty']); this.(field_name).(param_name).value=subsasgn(tmp,S,value); end this.(field_name).(param_name).fmt_spec=p.Results.fmt_spec; end function save(this, filename, varargin) createFile(filename, varargin{:}); - addTimeField(this); for i=1:length(this.field_names) printField(this, this.field_names{i}, filename); end printEndMarker(this, filename); end function printField(this, field_name, filename, varargin) %Takes optional inputs p=inputParser(); addParameter(p,'title',field_name); parse(p,varargin{:}); title_str=p.Results.title; ParStruct=this.(field_name); %Compose the list of parameter names expanded over subscripts %except for those which are already character arrays par_names=fieldnames(ParStruct); %Expand parameters over subscripts, except for the character %arrays exp_par_names=cell(1, length(par_names)); maxnmarr=zeros(1, length(par_names)); for i=1:length(par_names) tmpval=ParStruct.(par_names{i}).value; exp_par_names{i}=printSubs(tmpval, ... 'own_name', par_names{i}, ... 'expansion_test',@(y) ~ischar(y)); %Max name length for this parameter including subscripts maxnmarr(i)=max(cellfun(@(x) length(x), exp_par_names{i})); end %Calculate width of the name column name_pad_length=min(max(maxnmarr), this.pad_lim); %Compose list of parameter values converted to char strings par_strs=cell(1, length(par_names)); %Width of the values column will be the maximum parameter %string width val_pad_length=0; for i=1:length(par_names) TmpPar=ParStruct.(par_names{i}); for j=1:length(exp_par_names{i}) tmpnm=exp_par_names{i}{j}; TmpS=str2substruct(tmpnm); if isempty(TmpS) tmpval=TmpPar.value; else tmpval=subsref(TmpPar.value, TmpS); end %Do check to detect unsupported data type if ischar(tmpval)&&~isvector(tmpval)&&~isempty(tmpval) warning(['Argument ''%s'' is a multi-dimensional ',... 'character array. It will be converted to ',... 'single string during saving. Use cell',... 'arrays to save data as a set of separate ',... 'strings.'],tmpnm) % Flatten tmpval=tmpval(:); end %Check for new line symbols in strings if (ischar(tmpval)||isstring(tmpval)) && ... any(ismember({newline,sprintf('\r')},tmpval)) warning(['String value must not contain ',... '''\\n'' and ''\\r'' symbols, replacing them ',... 'with '' ''']); tmpval=replace(tmpval,{newline,sprintf('\r')},' '); end if isempty(TmpPar.fmt_spec) % Convert to string with format specifier % extracted from the varaible calss par_strs{i}{j}=var2str(tmpval); else par_strs{i}{j}=sprintf(TmpPar.fmt_spec, tmpval); end % Find maximum length to determine the colum width, % but, for beauty, do not account for variables with % excessively long value strings tmplen=length(par_strs{i}); if (val_pad_length1 + assert(ischar(t_field_name)&&isvector(t_field_name),... + 'Time field name must be a character vector') + else + t_field_name='Time'; end + + if ismember(t_field_name, this.field_names) + deleteField(this, t_field_name) + end + dv=datevec(datetime('now')); - addField(this,'Time'); - addParam(this,'Time','Year',dv(1),'fmt_spec','%i'); - addParam(this,'Time','Month',dv(2),'fmt_spec','%i'); - addParam(this,'Time','Day',dv(3),'fmt_spec','%i'); - addParam(this,'Time','Hour',dv(4),'fmt_spec','%i'); - addParam(this,'Time','Minute',dv(5),'fmt_spec','%i'); - addParam(this,'Time','Second',floor(dv(6)),'fmt_spec','%i'); - addParam(this,'Time','Millisecond',... + addField(this,t_field_name); + addParam(this,t_field_name,'Year',dv(1),'fmt_spec','%i'); + addParam(this,t_field_name,'Month',dv(2),'fmt_spec','%i'); + addParam(this,t_field_name,'Day',dv(3),'fmt_spec','%i'); + addParam(this,t_field_name,'Hour',dv(4),'fmt_spec','%i'); + addParam(this,t_field_name,'Minute',dv(5),'fmt_spec','%i'); + addParam(this,t_field_name,'Second',... + floor(dv(6)),'fmt_spec','%i'); + addParam(this,t_field_name,'Millisecond',... round(1000*(dv(6)-floor(dv(6)))),'fmt_spec','%i'); end function n_end_header=load(this, filename) %Before we load, we clear all existing fields clearFields(this); fileID=fopen(filename,'r'); title_exp=[this.hdr_spec,'(\w.*)',this.hdr_spec]; %Loop initialization line_no=0; curr_title=''; %Loop continues until we reach the next header or we reach %the end of the file while ~feof(fileID) line_no=line_no+1; %Grabs the current line curr_line=fgetl(fileID); %Gives an error if the file is empty, i.e. fgetl returns -1 if curr_line==-1 error('Tried to read empty file. Aborting.') end %Skips if current line is empty if isempty(curr_line) continue end title_token=regexp(curr_line,title_exp,'once','tokens'); %If we find a title, first check if it is the specified %end header. Then change the title if a title was found, %then if no title was found, put the data under the current %title. if ismember(this.end_header, title_token) break elseif ~isempty(title_token) % Apply genvarname for sefety in case the title string % is not a proper variable name curr_title=genvarname(title_token{1}); addField(this, curr_title); %This runs if there was no match for the header regular %expression, i.e. the current line is not a filed %separator, and the current line is not empty. We then %add this line to the current field (curr_title), possibly %iterating over the parameter subscripts. elseif ~isempty(curr_title) % First separate the comment if present tmp=regexp(curr_line,this.comment_sep,'split','once'); if length(tmp)>1 % the line has comment comment_str=tmp{2}; else comment_str=''; end % Then process name-value pair. Regard everything after % the first column separator as value. tmp=regexp(tmp{1},this.column_sep,'split','once'); if length(tmp)<2 % Ignore the line if a name-value pair is not found continue else % Attempt convertion of value to number val=str2doubleHedged(strtrim(tmp{2})); end % Infer the variable name and subscript reference try [S, name]=str2substruct(strtrim(tmp{1})); catch name=''; end if isempty(name) % Ignore the line if variable name is not missing continue elseif ismember(name, fieldnames(this.(curr_title))) % If the variable name already presents among % parameters, add new subscript value this.(curr_title).(name).value= ... subsasgn(this.(curr_title).(name).value,S,val); else % Add new parameter with comment addParam(this, curr_title, name, val,... 'SubStruct', S, 'comment', comment_str); end end end fclose(fileID); if isempty(this.field_names) warning('No metadata found, continuing without metadata.') n_end_header=1; else n_end_header=line_no; end end % Need a custom copy method as the one provided by % matlab.mixin.Copyable does not re-create the handles of dynamic - % properties + % properties stored in this.PropHandles function NewMet=copy(this) NewMet=MyMetadata(); Mc=metaclass(NewMet); % Copy static public non-dependent properties for i=1:length(Mc.PropertyList) TmpProp=Mc.PropertyList(i); if strcmpi(TmpProp.GetAccess,'public') && ... strcmpi(TmpProp.SetAccess,'public') && ... ~TmpProp.Dependent NewMet.(TmpProp.Name)=this.(TmpProp.Name); end end % Copy dynamic properties addMetadata(NewMet, this); end end %% Set and Get methods methods function field_names=get.field_names(this) field_names=fieldnames(this.PropHandles); end end end \ No newline at end of file diff --git a/@MyTrace/MyTrace.m b/@MyTrace/MyTrace.m index b8059e4..242500c 100644 --- a/@MyTrace/MyTrace.m +++ b/@MyTrace/MyTrace.m @@ -1,460 +1,451 @@ % Class for XY data representation with labelling, plotting and % saving/loading functionality -classdef MyTrace < handle & matlab.mixin.Copyable +% If instantiated as MyTrace(fname) or MyTrace('file_name', fname) 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 - load_path=''; + file_name=''; + uid=''; + + % Data column and line separators + column_sep = '\t' + line_sep='\r\n' + %Cell that contains handles the trace is plotted in hlines={}; - uid=''; - end - - properties (Access=private) - Parser end properties (Dependent=true) %MyMetadata containing the MeasHeaders and %information about the trace Metadata label_x label_y end - methods (Access=private) - %Creates the input parser for the class. Includes default values - %for all optional parameters. - function createParser(this) - p=inputParser; - addParameter(p,'x',[]); - addParameter(p,'y',[]); - addParameter(p,'unit_x','x',@ischar); - addParameter(p,'unit_y','y',@ischar); - addParameter(p,'name_x','x',@ischar); - addParameter(p,'name_y','y',@ischar); - addParameter(p,'load_path','',@ischar); - addParameter(p,'uid','',@ischar); - addParameter(p,'MeasHeaders',MyMetadata(),... - @(x) isa(x,'MyMetadata')); - this.Parser=p; - end - - %Sets the class variables to the inputs from the inputParser. Can - %be used to reset class to default values if default_flag=true. - function parseInputs(this, inputs, default_flag) - parse(this.Parser,inputs{:}); - for i=1:length(this.Parser.Parameters) - %Sets the value if there was an input or if the default - %flag is on. The default flag is used to reset the class to - %its default values. - if default_flag || ~any(ismember(this.Parser.Parameters{i},... - this.Parser.UsingDefaults)) - this.(this.Parser.Parameters{i})=... - this.Parser.Results.(this.Parser.Parameters{i}); - end - end - end - end - methods (Access=public) function this=MyTrace(varargin) - createParser(this); - parseInputs(this,varargin,true); + P=MyClassParser(this); + % options for MeasHeaders + addParameter(P, 'metadata_opts',{},@iscell); - if ~ismember('load_path',this.Parser.UsingDefaults) - loadTrace(this,this.load_path); + 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),... + '''file_name'' must be a vector of characters'); + processInputs(P, this, varargin{2:end}); + this.file_name=fname; + else + % Parse varargin as a list of name-value pairs + processInputs(P, this, varargin{:}); + end + + this.MeasHeaders=MyMetadata(P.Results.metadata_opts{:}); + + if ~isempty(this.file_name) + load(this, this.file_name); 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) + function save(this, varargin) %Parse inputs for saving p=inputParser; - addParameter(p,'filename','placeholder',@ischar); - addParameter(p,'save_dir',pwd,@ischar); addParameter(p,'save_prec',15); - addParameter(p,'overwrite_flag',false); - parse(p,varargin{:}); + addParameter(p,'overwrite',false); - %Assign shorter names - filename=p.Results.filename; - save_dir=p.Results.save_dir; - save_prec=p.Results.save_prec; - overwrite_flag=p.Results.overwrite_flag; - %Puts together the full file name - fullfilename=fullfile([save_dir,filename,'.txt']); + 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(fullfilename,'overwrite',overwrite_flag); + 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); end %We now write the data to the file - writeData(this, fullfilename,'save_prec',save_prec); + 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 - printAllHeaders(this.Metadata,fullfilename); + 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 look ugly, but it makes - %files quite a bit smaller - data_format_str=sprintf('%%.%ig\t%%.%ig\r\n',... - save_prec,save_prec); - fprintf(fileID,data_format_str,[this.x, this.y]'); + %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 loadTrace(this, file_path, varargin) + 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{:}); - hdr_spec=p.Results.hdr_spec; - end_header=p.Results.end_header; + 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 - %Instantiate a header object from the file you are loading. We - %get the line number we want to read from as an output. - [this.MeasHeaders,end_line_no]=MyMetadata(... - 'load_path',file_path,... - 'hdr_spec',hdr_spec,... - 'end_header',end_header); + %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,'\t',end_line_no,0); + data_array=dlmread(file_path, this.column_sep, ... + end_line_no,0); this.x=data_array(:,1); this.y=data_array(:,2); - this.load_path=file_path; + this.file_name=file_path; end - %Allows setting of multiple properties in one command. - function setTrace(this, varargin) - parseInputs(this, varargin, false); - 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 plotTrace(this,plot_axes,varargin) - %Checks that there are axes to plot - assert(exist('plot_axes','var') && ... - (isa(plot_axes,'matlab.graphics.axis.Axes')||... - isa(plot_axes,'matlab.ui.control.UIAxes')),... - 'Please input axes to plot in.') + function plot(this, varargin) %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 %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. 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 %generates a new UID for the trace function set.y(this, y) assert(isnumeric(y),... 'Data must be of class double'); this.y=y(:); this.uid=genUid(); %#ok 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.load_path(this, load_path) - assert(ischar(load_path),'File path must be a char, not a %s',... - class(load_path)); - this.load_path=load_path; + 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 function set.uid(this, uid) assert(ischar(uid),'UID must be a char, not a %s',... class(uid)); this.uid=uid; 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','uid',this.uid); 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 diff --git a/GUIs/GuiDaq.fig b/GUIs/GuiDaq.fig index 469c62d..0a413f0 100644 Binary files a/GUIs/GuiDaq.fig and b/GUIs/GuiDaq.fig differ diff --git a/GUIs/GuiDaq.m b/GUIs/GuiDaq.m index f1cec15..d364ad7 100644 --- a/GUIs/GuiDaq.m +++ b/GUIs/GuiDaq.m @@ -1,348 +1,249 @@ function varargout = GuiDaq(varargin) % GUIDAQ MATLAB code for GuiDaq.fig % GUIDAQ, by itself, creates a new GUIDAQ or raises the existing % singleton*. % % H = GUIDAQ returns the handle to a new GUIDAQ or the handle to % the existing singleton*. % % GUIDAQ('CALLBACK',hObject,eventData,handles,...) calls the local % function named CALLBACK in GUIDAQ.M with the given input arguments. % % GUIDAQ('Property','Value',...) creates a new GUIDAQ or raises the % existing singleton*. Starting from the left, property value pairs are % applied to the GUI before GuiDaq_OpeningFcn gets called. An % unrecognized property name or invalid value makes property application % stop. All inputs are passed to GuiDaq_OpeningFcn via varargin. % % *See GUI Options on GUIDE's Tools menu. Choose "GUI allows only one % instance to run (singleton)". % % See also: GUIDE, GUIDATA, GUIHANDLES % Edit the above text to modify the response to help GuiDaq -% Last Modified by GUIDE v2.5 24-Aug-2018 16:39:21 +% Last Modified by GUIDE v2.5 25-Dec-2018 15:48:30 % Begin initialization code - DO NOT EDIT gui_Singleton = 1; gui_State = struct('gui_Name', mfilename, ... 'gui_Singleton', gui_Singleton, ... 'gui_OpeningFcn', @GuiDaq_OpeningFcn, ... 'gui_OutputFcn', @GuiDaq_OutputFcn, ... 'gui_LayoutFcn', [] , ... 'gui_Callback', []); if nargin && ischar(varargin{1}) gui_State.gui_Callback = str2func(varargin{1}); end if nargout [varargout{1:nargout}] = gui_mainfcn(gui_State, varargin{:}); else gui_mainfcn(gui_State, varargin{:}); end % End initialization code - DO NOT EDIT % --- Executes just before GuiDaq is made visible. function GuiDaq_OpeningFcn(hObject, eventdata, handles, varargin) % This function has no output args, see OutputFcn. % hObject handle to figure % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) % varargin command line arguments to GuiDaq (see VARARGIN) % Choose default command line output for GuiDaq handles.output = hObject; % Update handles structure guidata(hObject, handles); % --- Outputs from this function are returned to the command line. function varargout = GuiDaq_OutputFcn(hObject, eventdata, handles) % varargout cell array for returning output args (see VARARGOUT); % hObject handle to figure % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) % Get default command line output from handles structure varargout{1} = handles.output; % --- Executes during object creation, after setting all properties. function InstrMenu_CreateFcn(hObject, eventdata, handles) % hObject handle to InstrMenu (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles empty - handles not created until after all CreateFcns called % Hint: popupmenu controls usually have a white background on Windows. % See ISPC and COMPUTER. if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) set(hObject,'BackgroundColor','white'); end % --- Executes during object creation, after setting all properties. function EditV1_CreateFcn(hObject, eventdata, handles) % hObject handle to EditV1 (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles empty - handles not created until after all CreateFcns called % Hint: edit controls usually have a white background on Windows. % See ISPC and COMPUTER. if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) set(hObject,'BackgroundColor','white'); end % --- Executes during object creation, after setting all properties. function EditV2_CreateFcn(hObject, eventdata, handles) % hObject handle to EditV2 (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles empty - handles not created until after all CreateFcns called % Hint: edit controls usually have a white background on Windows. % See ISPC and COMPUTER. if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) set(hObject,'BackgroundColor','white'); end % --- Executes during object creation, after setting all properties. function EditV2V1_CreateFcn(hObject, eventdata, handles) % hObject handle to EditV2V1 (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles empty - handles not created until after all CreateFcns called % Hint: edit controls usually have a white background on Windows. % See ISPC and COMPUTER. if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) set(hObject,'BackgroundColor','white'); end % --- Executes during object creation, after setting all properties. function EditH1_CreateFcn(hObject, eventdata, handles) % hObject handle to EditH1 (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles empty - handles not created until after all CreateFcns called % Hint: edit controls usually have a white background on Windows. % See ISPC and COMPUTER. if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) set(hObject,'BackgroundColor','white'); end % --- Executes during object creation, after setting all properties. function EditH2_CreateFcn(hObject, eventdata, handles) % hObject handle to EditH2 (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles empty - handles not created until after all CreateFcns called % Hint: edit controls usually have a white background on Windows. % See ISPC and COMPUTER. if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) set(hObject,'BackgroundColor','white'); end % --- Executes during object creation, after setting all properties. function EditH2H1_CreateFcn(hObject, eventdata, handles) % hObject handle to EditH2H1 (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles empty - handles not created until after all CreateFcns called % Hint: edit controls usually have a white background on Windows. % See ISPC and COMPUTER. if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) set(hObject,'BackgroundColor','white'); end % --- Executes during object creation, after setting all properties. function AnalyzeMenu_CreateFcn(hObject, eventdata, handles) % hObject handle to AnalyzeMenu (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles empty - handles not created until after all CreateFcns called % Hint: popupmenu controls usually have a white background on Windows. % See ISPC and COMPUTER. if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) set(hObject,'BackgroundColor','white'); end % --- Executes during object creation, after setting all properties. function SelTrace_CreateFcn(hObject, eventdata, handles) % hObject handle to SelTrace (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles empty - handles not created until after all CreateFcns called % Hint: popupmenu controls usually have a white background on Windows. % See ISPC and COMPUTER. if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) set(hObject,'BackgroundColor','white'); end % --- Executes during object creation, after setting all properties. function BaseDir_CreateFcn(hObject, eventdata, handles) % hObject handle to BaseDir (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles empty - handles not created until after all CreateFcns called % Hint: edit controls usually have a white background on Windows. % See ISPC and COMPUTER. if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) set(hObject,'BackgroundColor','white'); end % --- Executes during object creation, after setting all properties. function SessionName_CreateFcn(hObject, eventdata, handles) % hObject handle to SessionName (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles empty - handles not created until after all CreateFcns called % Hint: edit controls usually have a white background on Windows. % See ISPC and COMPUTER. if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) set(hObject,'BackgroundColor','white'); end % --- Executes during object creation, after setting all properties. function FileName_CreateFcn(hObject, eventdata, handles) % hObject handle to FileName (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles empty - handles not created until after all CreateFcns called % Hint: edit controls usually have a white background on Windows. % See ISPC and COMPUTER. if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) set(hObject,'BackgroundColor','white'); end -% --- Executes on button press in record. -function record_Callback(hObject, eventdata, handles) -% hObject handle to record (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -%Takes the data from the fit -try - h_main_plot=getappdata(0,'h_main_plot'); - fit_meta_data=getappdata(h_main_plot,'fit_meta_data'); - - f=fit_meta_data(1); - lw=fit_meta_data(2); - Q=fit_meta_data(3); -catch - error('No fit parameters found') -end - -%Standardized save path -save_path=[handles.Drive_Letter,':\Measurement Campaigns\']; - -%Checks if a session name and file name is given -if ~isstr(get(handles.SessionName,'string')) - error('No session name given') -elseif ~isstr(get(handles.FileName,'string')) - error('No file name given') -end - -%Puts the date in front of the session name -session_name=[datestr(now,'yyyy-mm-dd '),... - get(handles.SessionName,'string'),'\']; - -%Makes the path if it does not exist -if ~exist([save_path,session_name],'dir') - mkdir(save_path,session_name); -end - -%Full path -final_path=[save_path,session_name,'Q factor','.txt']; - -%Creates the file if it does not exist, otherwise opens the file -if ~exist(final_path,'file') - fileID=fopen(final_path,'w'); - %Creates headers in the file - fmt=['%s\t%s\t%s\t\t%s\t%s\t\r\n']; - fprintf(fileID,fmt,'Beam#','f(MHz)','Q(10^6)','Q*f(10^14)','lw'); -else - fileID=fopen(final_path,'a'); -end - -%Formatting string -fmt=['%s\t%3.3f\t%3.3f\t\t%3.3f\t\t%3.3f\r\n']; -tag=get(handles.edit_tag,'string'); -fprintf('Data saved in %s',final_path); -%Reshapes the data in appropriate units -fprintf(fileID,fmt,tag{1},f/1e6,Q/1e6,Q*f/1e14,lw); -fclose(fileID); - -function edit_tag_Callback(hObject, eventdata, handles) -% hObject handle to edit_tag (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Hints: get(hObject,'String') returns contents of edit_tag as text -% str2double(get(hObject,'String')) returns contents of edit_tag as a double - % --- Executes during object creation, after setting all properties. function edit_tag_CreateFcn(hObject, eventdata, handles) % hObject handle to edit_tag (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles empty - handles not created until after all CreateFcns called % Hint: edit controls usually have a white background on Windows. % See ISPC and COMPUTER. if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) set(hObject,'BackgroundColor','white'); end -% --- Executes on button press in Subtract_BG. -function Subtract_BG_Callback(hObject, eventdata, handles) -% hObject handle to Subtract_BG (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Hint: get(hObject,'Value') returns toggle state of Subtract_BG -h_main_plot=getappdata(0,'h_main_plot'); -if (get(hObject,'Value')==1) - set(hObject, 'BackGroundColor',[0,1,.2]); - y_data=getappdata(h_main_plot,'y_data')-getappdata(h_main_plot,'y_BG'); -else - set(hObject, 'BackGroundColor',[0.941,0.941,0.941]); - y_data=getappdata(h_main_plot,'y_data')+getappdata(h_main_plot,'y_BG'); -end -setappdata(h_main_plot,'y_data',y_data); -update_axes - - -% --- Executes on button press in togglebutton9. -function togglebutton9_Callback(hObject, eventdata, handles) -% hObject handle to togglebutton9 (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) - -% Hint: get(hObject,'Value') returns toggle state of togglebutton9 % --- Executes during object creation, after setting all properties. function figure1_CreateFcn(hObject, eventdata, handles) % hObject handle to figure1 (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles empty - handles not created until after all CreateFcns called % --- Executes during object creation, after setting all properties. function DestTrc_CreateFcn(hObject, eventdata, handles) % hObject handle to DestTrc (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles empty - handles not created until after all CreateFcns called % Hint: popupmenu controls usually have a white background on Windows. % See ISPC and COMPUTER. if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) set(hObject,'BackgroundColor','white'); end - - -% --- Executes when selected object is changed in uipanel1. -function uipanel1_SelectionChangedFcn(hObject, eventdata, handles) -% hObject handle to the selected object in uipanel1 -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) diff --git a/GUIs/GuiScope.mlapp b/GUIs/GuiScope.mlapp index ff1659b..f548da6 100644 Binary files a/GUIs/GuiScope.mlapp and b/GUIs/GuiScope.mlapp differ