diff --git a/@MyLog/MyLog.m b/@MyLog/MyLog.m index b3a4da0..5914326 100644 --- a/@MyLog/MyLog.m +++ b/@MyLog/MyLog.m @@ -1,304 +1,294 @@ % classdef MyLog < MyInputHandler 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' % Data columns are separated by this symbol data_column_sep = '\t' % File extension that is appended by default when saving the log % if a different one is not specified explicitly file_ext = '.log' file_name = '' % Used to save or load the data data_headers = {} % Cell array of column headers end properties (SetAccess=public, GetAccess=public) timestamps % Times at which data was aqcuired data % Cell array of measurements Headers % MyMetadata object to store labeled time marks end properties (Dependent=true) % Information about the log, including time labels and data headers Metadata % Format specifier for one data line data_line_fmt end methods (Access=public) %% Constructo and destructor methods function this = MyLog(varargin) %Parse input arguments with ConstructionParser and load them %into class properties this@MyInputHandler(varargin{:}); this.Headers=MyMetadata(); % Load the data from file if the file name was provided if ~ismember('file_name',this.ConstructionParser.UsingDefaults) loadLog(this); end end %% Save and load functionality % save the entire data record function saveLog(this, fname) % File name can be either supplied explicitly or given as the % file_name property if nargin()<2 fname = this.file_name; end % Verify that the data can be saved assert(isDataArray(this),... ['Data cannot be reshaped into array, saving in '... 'text format is not possible. You may try saving as ',... '.mat file instead.']); % Add file extension if it is not specified explicitly if ~ismember('.',fname) fname = [fname, this.file_ext]; end try createFile(fname); - fid = fopen(fname,'w'); % Write time labels and column headers printAllHeaders(this.Metadata, fname); - - % Lowbrow code below is to be fixed in the future by - % incorporating this line into printHeader - fprintf(fid,'==Data==\r\n'); - + fid = fopen(fname,'a'); % Write data body fmt=this.data_line_fmt; for i=1:length(this.timestamps) fprintf(fid, fmt,... posixtime(this.timestamps(i)), this.data{i}); end fclose(fid); catch warning('Log was not saved'); % Try closing fid in case it is still open try fclose(fid); catch end end end % Save log header to file function loadLog(this, fname) if nargin()<2 fname=this.file_name; end [this.Headers, end_line_no]=MyMetadata('load_path',fname); % Read data as delimiter-separated values and convert to cell % array mdata = dlmread(filename, this.column_sep, end_line_no, 0); [m,~] = size(mdata); this.data = mat2cell(mdata, ones(1,m)); if ismember('ColumnNames', this.Headers.field_names) cnames=structfun(@(x) x.value, this.Headers.ColumnNames,... 'UniformOutput', false); % The first column name is time, so skip it cnames(1)=[]; % Assign the rest of the names to data headers for i=1:length(cnames) this.data_headers{i}=cnames{i}; end % Delete the field ColumnNames as it is generated % automatically when referring to Metadata and is not % supposed to be stored in the Headers deleteField(this.Headers, 'ColumnNames') end end %% Other functions % Append data point to the log - function appendData(this, time, val, varargin) + function appendPoint(this, time, val, varargin) p=inputParser(); addParameter(p, 'save', false, @islogical); parse(p, varargin{:}); this.timestamps=[this.timestamps; time]; this.data=[this.data; {val}]; if p.Results.save try exstat = exist(this.file_name,'file'); if exstat==0 % if the file does not exist, create it and write % the metadata createFile(this.file_name); printAllHeaders(this.Metadata, this.file_name); - - fid = fopen(this.file_name,'w'); - % Lowbrow code below is to be fixed in the future by - % incorporating this line into printHeader - fprintf(fid,'==Data==\r\n'); - + fid = fopen(this.file_name,'a'); else % otherwise open for appending fid = fopen(this.file_name,'a'); end fprintf(fid, this.data_line_fmt, posixtime(time), val); fclose(fid); catch warning(['Logger cannot save data at time = ',... datestr(time)]); % Try closing fid in case it is still open try fclose(fid); catch end end end end % Add label to the metadata function addTimeLabel(this, time, str) if nargin()<3 % 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}); str=answ{1}; end end time_str=datestr(time); fieldname=genvarname('Lbl1', this.Headers.field_names); addField(this.Headers, fieldname); addParam(this.Headers, fieldname, 'time', time_str); % str can contain multiple lines, record them as separate % parameters [nlines,~]=size(str); for i=1:nlines strname=genvarname('str1',... fieldnames(this.Headers.(fieldname))); addParam(this.Headers, fieldname, strname, str(i,:)); end end % Plot the log data with time labels function plotLog(this, Ax) if nargin()<2 % If axes handle was not supplied, create new axes Ax = axes(); else cla(Ax); end try mdata = cell2mat(this.data); catch warning(['Cannot display logger data, '... 'possibly because of data dimensions being different ',... 'at different times. Can try crlearing data to resolve.']) return end hold(Ax,'on'); [~, n] = size(mdata); % Plot data for i=1:n plot(Ax, this.timestamps, mdata(:,i)); end % Plot time labels hold(Ax,'off'); % Add legend if n>=1 && ~isempty(this.data_headers{:}) legend(Ax, this.data_headers{:},'Location','southwest'); ylabel(Ax, app.y_label); end end function clearLog(this) this.timestamps = {}; this.data = {}; delete(this.Headers); this.Headers = MyMetadata(); end % Check if data is suitable for plotting and saving as a list of % numerical vectors of equal length function bool = isDataArray(this) % An empty cell array passes the test if isempty(this.data) bool = true; return end % Then check if all the elements are numeric vectors and have % the same length. Difference between columns and rows is % disregarded here. l=length(this.data{1}); bool = all(cellfun(@(x)(isreal(x)&&(length(x)==l)),this.data)); end end %% set and get methods methods function data_line_fmt=get.data_line_fmt(this) cs=this.data_column_sep; nl=this.Headers.line_sep; if isempty(this.data) l=0; else % Use end of the data array for better robustness when % appending a measurement l=length(this.data{end}); 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 Metadata=get.Metadata(this) Metadata=copy(this.Headers); if ismember('ColumnNames', Metadata.field_names) deleteField(Metadata, 'ColumnNames') end addField(Metadata, 'ColumnNames'); addParam(Metadata, 'ColumnNames', 'name1',... 'POSIX time (s)') for i=1:length(this.data_headers) tmpname = genvarname('name1',... fieldnames(Metadata.ColumnNames)); addParam(Metadata, 'ColumnNames', tmpname,... this.data_headers{i}) end end end end diff --git a/@MyLogger/MyLogger.m b/@MyLogger/MyLogger.m index a111f7f..281ab8f 100644 --- a/@MyLogger/MyLogger.m +++ b/@MyLogger/MyLogger.m @@ -1,177 +1,117 @@ % Generic logger that executes MeasFcn according to MeasTimer, stores the % results and optionally continuously saves them. MeasFcn should be a % function with no arguments. Saving functionality works properly if % MeasFcn returns a number or array of numbers, while intrinsically the % logger can store any kind of outputs. classdef MyLogger < MyInputHandler properties (Access=public) MeasTimer % Timer object MeasFcn = @()0 save_cont = false - save_file - data_headers = {} % Cell array of column headers - % format specifiers for data saving and display - time_fmt = '%14.3f' % Save time as posixtime up to ms precision - data_field_width = '24' - data_fmt = '%24.14e' % Save data as reals with 14 decimal digits + Log + % Format for displaying last reading label: value disp_fmt = '%15s: %.2e' end properties (SetAccess=protected, GetAccess=public) - timestamps % Times at which data was aqcuired - data % Stored cell array of measurements - last_meas_stat = 2 % If last measurement was succesful + % If last measurement was succesful % 0-false, 1-true, 2-never measured + last_meas_stat = 2 end events + % Event that is triggered each time MeasFcn is successfully executed NewData end methods function this = MyLogger(varargin) %Parse input arguments with ConstructionParser and load them %into class properties this@MyInputHandler(varargin{:}); + + this.Log=MyLog(); if ismember('MeasTimer', this.ConstructionParser.UsingDefaults) % Create and confitugure timer unless it was supplied % externally in varargin this.MeasTimer = timer(); this.MeasTimer.BusyMode = 'drop'; % Fixed spacing mode of operation does not follow the % period very well, but is robust with respect to - % communication delays + % function execution delays this.MeasTimer.ExecutionMode = 'fixedSpacing'; this.MeasTimer.TimerFcn = @(~,event)LoggerFcn(this,event); end end function delete(this) %stop and delete the timer stop(this.MeasTimer); delete(this.MeasTimer); end function LoggerFcn(this, event) time = datetime(event.Data.time); try meas_result = this.MeasFcn(); - % append measurement result together with time stamp - this.timestamps=[this.timestamps; time]; - this.data=[this.data; {meas_result}]; this.last_meas_stat=1; % last measurement ok + triggerNewData(this); catch warning(['Logger cannot take measurement at time = ',... datestr(time)]); this.last_meas_stat=0; % last measurement not ok end - triggerNewData(this); - - % save the point to file if continuous saving is enabled and - % last measurement was succesful - if this.save_cont&&(this.last_meas_stat==1) - try - exstat = exist(this.save_file,'file'); - if exstat==0 - % if the file does not exist, create it and write - % header names - createFile(this.save_file); - fid = fopen(this.save_file,'w'); - writeColumnHeaders(this, fid); - else - % otherwise open for appending - fid = fopen(this.save_file,'a'); - end - fprintf(fid, this.time_fmt, posixtime(time)); - fprintf(fid, this.data_fmt, meas_result); - fprintf(fid,'\r\n'); - fclose(fid); - catch - warning(['Logger cannot save data at time = ',... - datestr(time)]); - % Try closing fid in case it is still open - try - fclose(fid); - catch - end - end + if this.last_meas_stat==1 + % append measurement result together with time stamp + appendPoint(this.Log, time, meas_result,... + 'save', this.save_cont); end end % save the entire data record function saveLog(this) - try - createFile(this.save_file); - fid = fopen(this.save_file,'w'); - writeColumnHeaders(this, fid); - for i=1:length(this.timestamps) - fprintf(fid, this.time_fmt,... - posixtime(this.timestamps(i))); - fprintf(fid, this.data_fmt,... - this.data{i}); - fprintf(fid,'\r\n'); - end - fclose(fid); - catch - warning('Data was not saved'); - % Try closing fid in case it is still open - try - fclose(fid); - catch - end - end + saveLog(this.Log) end function clearLog(this) - this.timestamps = []; - this.data = []; - end - - function writeColumnHeaders(this, fid) - % write data headers to file if specified - fprintf(fid, 'POSIX time [s]'); - for i=1:length(this.data_headers) - fprintf(fid, ['%',this.data_field_width,'s'],... - this.data_headers{i}); - end - fprintf(fid,'\r\n'); + clearLog(this.Log) end function start(this) start(this.MeasTimer); end function stop(this) stop(this.MeasTimer); end function str = dispLastReading(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]; end end end %Triggers event for acquired data function triggerNewData(this) notify(this,'NewData') end end end diff --git a/@MyMetadata/MyMetadata.m b/@MyMetadata/MyMetadata.m index 359cc15..ffe5f5f 100644 --- a/@MyMetadata/MyMetadata.m +++ b/@MyMetadata/MyMetadata.m @@ -1,321 +1,331 @@ classdef MyMetadata < dynamicprops & matlab.mixin.Copyable 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 column_sep % Columns are separated by this symbol comment_sep % Comments start from this symbol line_sep % Limit for column padding. Variables which take more space than % this limit are ignored when calculating the padding length. pad_lim 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=inputParser; addParameter(p,'hdr_spec','==',@ischar); addParameter(p,'load_path','',@ischar); addParameter(p,'end_header','Data',@ischar); addParameter(p,'column_sep',' \t',@ischar); addParameter(p,'comment_sep','%',@ischar); addParameter(p,'line_sep','\r\n',@ischar); addParameter(p,'pad_lim',12,@isreal); parse(p,varargin{:}); this.hdr_spec=p.Results.hdr_spec; this.column_sep=p.Results.column_sep; this.comment_sep=p.Results.comment_sep; this.end_header=p.Results.end_header; this.pad_lim=p.Results.pad_lim; this.line_sep=p.Results.line_sep; this.PropHandles=struct(); if ~isempty(p.Results.load_path) varargout{1}=scanHeaders(this,p.Results.load_path,... 'end_header',p.Results.end_header); 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'; this.PropHandles.(field_name).NonCopyable=false; 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); parse(p,varargin{:}); %Adds the field, making sure that neither value nor comment %contain new line or carriage return characters, which would %mess up formating when saving the header newline_smb={sprintf('\n'),sprintf('\r')}; %#ok if (ischar(value)||isstring(value)) && ... contains(value, newline_smb) fprintf(['Value of ''%s'' must not contain ',... '''\\n'' and ''\\r'' symbols, replacing them ',... 'with '' ''\n'], param_name); this.(field_name).(param_name).value=... replace(value, newline_smb,' '); else this.(field_name).(param_name).value=value; end if contains(p.Results.comment, newline_smb) fprintf(['Comment string for ''%s'' must not contain ',... '''\\n'' and ''\\r'' symbols, replacing them ',... 'with '' ''\n'], param_name); this.(field_name).(param_name).comment= ... replace(p.Results.comment, newline_smb,' '); else this.(field_name).(param_name).comment=p.Results.comment; end this.(field_name).(param_name).fmt_spec=p.Results.fmt_spec; end - function printAllHeaders(this,fullfilename) + function printAllHeaders(this, fullfilename) addTimeHeader(this); for i=1:length(this.field_names) printHeader(this, fullfilename, this.field_names{i}); end + printEndHeader(this, fullfilename); end function printHeader(this, fullfilename, field_name, varargin) %Takes optional inputs p=inputParser; addParameter(p,'title',field_name); parse(p,varargin{:}); title_str=p.Results.title; ParamStruct=this.(field_name); param_names=fieldnames(ParamStruct); %width of the name column name_pad_length=max(cellfun(@(x) length(x), param_names)); % Make list of parameter values converted to strings par_strs=cell(1,length(param_names)); par_lengths=zeros(1,length(param_names)); for i=1:length(param_names) TmpParam=ParamStruct.(param_names{i}); if isempty(TmpParam.fmt_spec) % Convert to string with format specifier % extracted from the varaible calss par_strs{i}=var2str(TmpParam.value); else par_strs{i}=sprintf(TmpParam.fmt_spec, TmpParam.value); end % For beauty, do not account for variables with excessively % long value strings when calculating the padding if length(par_strs{i})<=this.pad_lim par_lengths(i)=length(par_strs{i}); end end %width of the values column val_pad_length=max(par_lengths); fileID=fopen(fullfilename,'a'); %Prints the header - fprintf(fileID,'%s%s%s\r\n', this.hdr_spec, title_str,... - this.hdr_spec); + fprintf(fileID,[this.hdr_spec, title_str,... + this.hdr_spec, this.line_sep]); for i=1:length(param_names) %Capitalize first letter of comment if ~isempty(ParamStruct.(param_names{i}).comment) fmt_comment=[this.comment_sep,' '... upper(ParamStruct.(param_names{i}).comment(1)),... ParamStruct.(param_names{i}).comment(2:end)]; else fmt_comment=''; end print_spec=[sprintf('%%-%is',name_pad_length),... this.column_sep,... sprintf('%%-%is',val_pad_length),... - this.column_sep,'%s\r\n']; + this.column_sep,'%s', this.line_sep]; fprintf(fileID, print_spec, param_names{i}, par_strs{i},... fmt_comment); end %Prints an extra line at the end - fprintf(fileID,'\r\n'); + fprintf(fileID, this.line_sep); + fclose(fileID); + end + + %Print terminator that separates header from data + function printEndHeader(this, fullfilename) + fileID=fopen(fullfilename,'a'); + fprintf(fileID,... + [this.hdr_spec, this.end_header, ... + this.hdr_spec, this.line_sep]); fclose(fileID); end %Adds time header function addTimeHeader(this) if isprop(this,'Time') deleteField(this,'Time') 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',... round(1000*(dv(6)-floor(dv(6)))),'fmt_spec','%i'); end function n_end_header=scanHeaders(this, fullfilename, varargin) %Before we load, we clear all existing fields clearFields(this); fileID=fopen(fullfilename); 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 res_str=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(res_str, this.end_header) break elseif ~isempty(res_str) % Apply genvarname for sefety in case the title string % is not a proper variable name curr_title=genvarname(res_str{1}); addField(this,curr_title); %This runs if there was no match for the regular %expression, i.e. the current line is not a header, and the %current line is not empty. We then add this line to the %current field (curr_title). elseif ~isempty(curr_title) % First separate the comment if present tmp=strsplit(curr_line, this.comment_sep); if length(tmp)>1 % the line has comment comment_str=[tmp{2:end}]; else comment_str=''; end % Then process name-value pair tmp=strsplit(tmp{1}, this.column_sep,... 'CollapseDelimiters',true); if length(tmp)>=2 % If present line does not contain name-value pair, % ignore it name=strtrim(tmp{1}); % Assume everything after the 1-st column separator % to be the value and attempt convertion to number val=strtrim([tmp{2:end}]); val=str2doubleHedged(val); %Store retrieved value addParam(this, curr_title, name, val,... 'comment',comment_str); end end end if isempty(this.field_names) warning('No header found, continuing without header') n_end_header=1; else n_end_header=line_no; end end end 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 3dc6078..b8059e4 100644 --- a/@MyTrace/MyTrace.m +++ b/@MyTrace/MyTrace.m @@ -1,465 +1,460 @@ % Class for XY data representation with labelling, plotting and % saving/loading functionality classdef MyTrace < handle & matlab.mixin.Copyable 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=''; %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); if ~ismember('load_path',this.Parser.UsingDefaults) loadTrace(this,this.load_path); end end %Defines the save function for the class. Note that this is only %used when we want to write only the data with its associated %trace, rather than just the trace. To write just the trace with %fewer input checks, use the writeData function. function save(this,varargin) %Parse inputs for saving p=inputParser; addParameter(p,'filename','placeholder',@ischar); addParameter(p,'save_dir',pwd,@ischar); addParameter(p,'save_prec',15); addParameter(p,'overwrite_flag',false); parse(p,varargin{:}); %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']); %Creates the file in the given folder stat=createFile(fullfilename,'overwrite',overwrite_flag); %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); 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; - - fileID=fopen(fullfilename,'a'); %Writes the metadata header printAllHeaders(this.Metadata,fullfilename); - %Puts in header title for the data - fprintf(fileID,... - [this.Metadata.hdr_spec,'Data',this.Metadata.hdr_spec,'\r\n']); - + 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]'); fclose(fileID); end function clearData(this) this.x=[]; this.y=[]; end function loadTrace(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; 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); %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); this.x=data_array(:,1); this.y=data_array(:,2); this.load_path=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.') %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(); 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{:}); 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; 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