diff --git a/@MyDaq/MyDaq.mlapp b/@MyDaq/MyDaq.mlapp index b25a020..8649af8 100644 Binary files a/@MyDaq/MyDaq.mlapp and b/@MyDaq/MyDaq.mlapp differ diff --git a/@MyFit/MyFit.m b/@MyFit/MyFit.m index 9c3ef72..c60fa5f 100644 --- a/@MyFit/MyFit.m +++ b/@MyFit/MyFit.m @@ -1,792 +1,793 @@ % Class that implements fitting routines with GUI capabilities. classdef MyFit < dynamicprops & MyAnalysisRoutine & ... matlab.mixin.CustomDisplay properties (Access = public) % MyTrace object contains the data to be fitted to Data MyTrace lim_lower % Lower limits for fit parameters lim_upper % Upper limits for fit parameters % If enabled, plots fit curve in the Axes every time the parameter % values are updated enable_plot fit_color = 'black' % Color of the fit line fit_length = 1e3 % Number of points in the fit trace end properties (GetAccess = public, SetAccess = protected) Axes % The handle which the fit is plotted in Fit % MyTrace object containing the fit Gui = struct() % Handles of GUI elements % Array of cursors with length=2 for the selection of fitting range RangeCursors MyCursor % Output structures from fit() function: FitResult cfit Gof struct FitInfo struct param_vals % Numeric values of fit parameters fit_name fit_tex % tex representation of the fit formula fit_function % fit formula as character string fit_params % character names of fit parameters in fit_function fit_param_names % long informative names of fit parameters anon_fit_fun % fit expression represented by anonimous function % Additional parameters that are calculated from the fit parameters % or inputed externally. Properties of user parameters including % long name and write attribute UserParamList = struct() end properties (Dependent = true, GetAccess = public) n_params % Indices of data points selected for fitting data_selection % Enable cursors for the selection of fit range enable_range_cursors end properties (Access = protected) enable_gui = 1 % Vectors for varying the range of the sliders in GUI slider_vecs end methods (Access = public) function this = MyFit(varargin) % Parse the arguments supplied to the constructor p = inputParser(); addParameter(p, 'fit_name', '') addParameter(p, 'fit_function', 'x') addParameter(p, 'fit_tex', '') addParameter(p, 'fit_params', {}) addParameter(p, 'fit_param_names', {}) addParameter(p, 'Data', MyTrace()); addParameter(p, 'x', []); addParameter(p, 'y', []); addParameter(p, 'Axes', [], @isaxes); addParameter(p, 'enable_gui', true); addParameter(p, 'enable_plot', true); addParameter(p, 'enable_range_cursors', false) % The parameters below are only active when GUI is enabled % If true, adds save trace panel to the fit gui addParameter(p,'save_panel',false,@islogical); addParameter(p,'base_dir', ''); addParameter(p,'session_name','placeholder'); addParameter(p,'file_name','placeholder'); parse(p, varargin{:}); for i=1:length(p.Parameters) % Takes the value from the inputParser to the appropriate % property. if isprop(this, p.Parameters{i}) this.(p.Parameters{i}) = p.Results.(p.Parameters{i}); end end this.Fit = MyTrace(); %Generates the anonymous fit function from the input fit %function. This is used for fast plotting of the initial %values. args=['@(', strjoin([{'x'}, this.fit_params], ','),')']; this.anon_fit_fun=... str2func(vectorize([args,this.fit_function])); %Sets dummy values for the GUI this.param_vals=zeros(1,this.n_params); this.lim_lower=-Inf(1,this.n_params); this.lim_upper=Inf(1,this.n_params); %Allows us to load either x/y data or a MyTrace object directly if ismember('Data',p.UsingDefaults) &&... ~ismember('x',p.UsingDefaults) &&... ~ismember('y',p.UsingDefaults) this.Data.x=p.Results.x; this.Data.y=p.Results.y; end %Creates the structure that contains variables for calibration %of fit results createUserParamList(this); %Creates the gui if the flag is enabled. This function is in a %separate file. if this.enable_gui createGui(this, 'save_panel', p.Results.save_panel) %Generates the slider lookup table genSliderVecs(this); if isempty(p.Results.base_dir) try bd = getLocalSettings('measurement_base_dir'); catch ME warning(ME.message) bd = ''; end else bd = ''; end this.Gui.BaseDir.String = bd; this.Gui.SessionName.String = p.Results.session_name; this.Gui.FileName.String = p.Results.file_name; end if ~isempty(this.Axes) rng_color = [0.2510, 0.1961, 0.4118]; % Add two vertical cursors to the axes xlim = this.Axes.XLim; x1 = xlim(1)+0.1*(xlim(2)-xlim(1)); x2 = xlim(2)-0.1*(xlim(2)-xlim(1)); this.RangeCursors = ... [MyCursor(this.Axes, ... 'orientation', 'vertical', ... 'position', x1, ... 'Label','Fit range 1', 'Color', rng_color),... MyCursor(this.Axes, ... 'orientation', 'vertical', ... 'position', x2, ... 'Label','Fit range 2', 'Color', rng_color)]; % Enabling/disabling of the cursors by setting the class % property can be done only after the cursors are % initialized this.enable_range_cursors = p.Results.enable_range_cursors; end %If data was supplied, generates initial fit parameters if ~isDataEmpty(this.Data) genInitParams(this) end end %Deletion function of object function delete(this) if this.enable_gui %Avoids loops set(this.Gui.Window,'CloseRequestFcn',''); %Deletes the figure delete(this.Gui.Window); %Removes the figure handle to prevent memory leaks this.Gui=[]; end if ismethod(this.Fit, 'delete') % Delete the fit trace, in particular, in order to remove % the fit curve from the axes delete(this.Fit); end if ~isempty(this.RangeCursors) delete(this.RangeCursors); end end %This function is used to set the coefficients, to avoid setting it %to a number not equal to the number of parameters function setFitParams(this,param_vals) assert(length(param_vals)==this.n_params,... ['The length of the coefficient vector (currently %i) ',... 'must be equal to the number of parameters (%i)'],... length(this.param_vals),this.n_params) this.param_vals=param_vals; end %Fits the trace using currently set parameters, depending on the %model. function fitTrace(this) %Check the validity of data validateData(this); %Check for valid limits lim_check=this.lim_upper>this.lim_lower; assert(all(lim_check),... sprintf(['All upper limits must exceed lower limits. ',... 'Check limit %i, fit parameter %s'],find(~lim_check,1),... this.fit_params{find(~lim_check,1)})); %Check the consistency of initial parameters assert(isnumeric(this.param_vals) && isvector(this.param_vals) && ... length(this.param_vals)==this.n_params, ['Starting points must be given as ' ... 'a vector of size %d'],this.n_params); assert(isnumeric(this.lim_lower) && isvector(this.lim_lower) && ... length(this.lim_lower)==this.n_params, ['Lower limits must be given as ' ... 'a vector of size %d'], this.n_params); assert(isnumeric(this.lim_upper) && isvector(this.lim_upper) && ... length(this.lim_upper)==this.n_params, ['Upper limits must be given as ' ... 'a vector of size %d'], this.n_params); %Perform the fit with current parameters as a starting point ind = this.data_selection; this.param_vals = doFit(this, ... this.Data.x(ind), this.Data.y(ind), this.param_vals, ... this.lim_lower, this.lim_upper); %Calculate the fit curve calcFit(this); %Calculate user parameters that depend on the fit parameters calcUserParams(this); %Update fit metadata this.Fit.UserMetadata = createMetadata(this); %Updates the gui if it is enabled if this.enable_gui genSliderVecs(this); updateSliderPanel(this); end %Plots the fit if the flag is on if this.enable_plot plotFit(this); end end %Clears the plots function clearFit(this) - cellfun(@(x) delete(x), this.Fit.PlotLines); + clearData(this.Fit); + deleteLine(this.Fit); end %Plots the trace contained in the Fit MyTrace object function plotFit(this, varargin) % Fit trace does not make its own labels in order to keep the % labels made by the data trace plot(this.Fit, this.Axes, 'Color', this.fit_color, ... 'make_labels', false, varargin{:}); end %Generates model-dependent initial parameters, lower and upper %boundaries. function genInitParams(this) validateData(this); calcInitParams(this); calcFit(this); calcUserParams(this); %Plots the fit function with the new initial parameters if this.enable_plot plotFit(this, 'DisplayName', 'fit'); end %Updates the GUI and creates new lookup tables for the init %param sliders if this.enable_gui genSliderVecs(this); updateSliderPanel(this); end end % Bring the cursors within the axes limits function centerCursors(this) if ~isempty(this.Axes) && ~isempty(this.RangeCursors) ... && all(isvalid(this.RangeCursors)) xlim = this.Axes.XLim; x1 = xlim(1)+0.1*(xlim(2)-xlim(1)); x2 = xlim(2)-0.1*(xlim(2)-xlim(1)); this.RangeCursors(1).value = x1; this.RangeCursors(2).value = x2; end end % Create metadata with all the fitting and user-defined parameters function Mdt = createMetadata(this) % Field for the fit parameters InfoMdt = MyMetadata('title', 'FitInfo'); addObjProp(InfoMdt, this, 'fit_name'); addObjProp(InfoMdt, this, 'fit_function'); % Indicate if the parameter values were obtained manually or % from performing a fit if isempty(this.Gof) param_val_mode = 'manual'; else param_val_mode = 'fit'; end addParam(InfoMdt, 'param_val_mode', param_val_mode, ... 'comment', ['If the parameter values were set manually '... 'or obtained from fit']); % Field for the fit parameters ParValMdt = MyMetadata('title', 'FittingParameters'); if ~isempty(this.Gof) % Add fit parameters with confidence intervals ci = confint(this.FitResult, 0.95); for i=1:length(this.fit_params) str = sprintf('%8.4g (%.4g, %.4g)', ... this.param_vals(i), ci(1,i), ci(2,i)); addParam(ParValMdt, this.fit_params{i}, str, ... 'comment', [this.fit_param_names{i} ... ' (95% confidence interval)']); end else % Add only fit parameters for i=1:length(this.fit_params) addParam(ParValMdt, this.fit_params{i}, ... this.param_vals(i), 'comment', ... this.fit_param_names{i}); end end user_params = fieldnames(this.UserParamList); if ~isempty(user_params) % Add a field with the user parameters UserParMdt = MyMetadata('title', 'UserParameters'); for i=1:length(user_params) tag = user_params{i}; addParam(UserParMdt, tag, this.(tag), ... 'comment', this.UserParamList.(tag).title); end else UserParMdt = MyMetadata.empty(); end if ~isempty(this.Gof) % Field for the goodness of fit which copies the fields of % corresponding structure GofMdt = MyMetadata('title', 'GoodnessOfFit'); addParam(GofMdt, 'sse', this.Gof.sse, 'comment', ... 'Sum of squares due to error'); addParam(GofMdt, 'rsquare', this.Gof.rsquare, 'comment',... 'R-squared (coefficient of determination)'); addParam(GofMdt, 'dfe', this.Gof.dfe, 'comment', ... 'Degrees of freedom in the error'); addParam(GofMdt, 'adjrsquare', this.Gof.adjrsquare, ... 'comment', ['Degree-of-freedom adjusted ' ... 'coefficient of determination']); addParam(GofMdt, 'rmse', this.Gof.rmse, 'comment', ... 'Root mean squared error (standard error)'); else GofMdt = MyMetadata.empty(); end Mdt = [InfoMdt, ParValMdt, UserParMdt, GofMdt]; end end methods (Access = protected) %Creates the GUI of MyFit, in separate file. createGui(this, varargin); %Does the fit with the currently set parameters. This method is %often overloaded in subclasses to improve performance. function fitted_vals = doFit(this, x, y, init_vals, lim_lower, ... lim_upper) %Fits with the below properties. Chosen for maximum accuracy. Ft = fittype(this.fit_function,'coefficients',this.fit_params); Opts = fitoptions('Method','NonLinearLeastSquares',... 'Lower', lim_lower,... 'Upper', lim_upper,... 'StartPoint', init_vals,... 'MaxFunEvals', 2000,... 'MaxIter', 2000,... 'TolFun', 1e-10,... 'TolX', 1e-10); [this.FitResult, this.Gof, this.FitInfo] = fit(x, y, Ft, Opts); %Return the coefficients fitted_vals = coeffvalues(this.FitResult); end %Low level function that generates initial parameters. %The default version of this function is not meaningful, it %should be overloaded in subclasses. function calcInitParams(this) this.param_vals=ones(1,this.n_params); this.lim_lower=-Inf(1,this.n_params); this.lim_upper=Inf(1,this.n_params); end % Calculate user parameters from fit parameters. % Dummy method that needs to be overloaded in subclasses. function calcUserParams(this) %#ok end function addUserParam(this, name, varargin) % Process inputs p = inputParser(); addRequired(p, 'name', @ischar); addParameter(p, 'title', ''); addParameter(p, 'editable', @(x)assert(isequal(x, 'on') || ... isequal(x, 'off'), ['''editable'' property must be ' ... '''on'' or ''off'''])); addParameter(p, 'default', []); parse(p, name, varargin{:}); % Store the information about the user parameter this.UserParamList.(name).title = p.Results.title; this.UserParamList.(name).editable = p.Results.editable; % Create a dynamic property for easy access Mp = addprop(this, name); this.UserParamList.(name).Metaprop = Mp; Mp.GetAccess = 'public'; if ~isempty(p.Results.default) this.(name) = p.Results.default; end if this.UserParamList.(name).editable Mp.SetAccess = 'public'; else Mp.SetAccess = 'private'; end end % addUserParam statements must be contained in this function % overloaded in subclasses. function createUserParamList(this) %#ok end function genSliderVecs(this) %Return values of the slider slider_vals=1:101; %Default scaling vector def_vec=10.^((slider_vals-51)/50); %Sets the cell to the default value for i=1:this.n_params this.slider_vecs{i}=def_vec*this.param_vals(i); set(this.Gui.(sprintf('Slider_%s', this.fit_params{i})),... 'Value',50); end end %Checks if the class is ready to perform a fit function validateData(this) assert(~isempty(this.Data), 'Data is empty'); assert(~isempty(this.Data.x) && ~isempty(this.Data.y) && ... length(this.Data.x)==length(this.Data.y) && ... length(this.Data.x)>=this.n_params, ... ['The data must be vectors of equal length greater ' ... 'than the number of fit parameters.', ... ' Currently the number of fit parameters is %i, the', ... ' length of x is %i and the length of y is %i'], ... this.n_params, length(this.Data.x), length(this.Data.y)); end %Calculates the trace object that represents the fitted curve function calcFit(this) xmin = this.Data.x(1); xmax = this.Data.x(end); if this.enable_range_cursors % If range cursors are active, restrict to the selected % range xmin = max(xmin, min(this.RangeCursors.value)); xmax = min(xmax, max(this.RangeCursors.value)); end this.Fit.x=linspace(xmin, xmax, this.fit_length); input_coeffs=num2cell(this.param_vals); this.Fit.y=this.anon_fit_fun(this.Fit.x, input_coeffs{:}); end %Overload a method of matlab.mixin.CustomDisplay in order to %separate the display of user properties from the others. function PrGroups = getPropertyGroups(this) user_params = fieldnames(this.UserParamList); static_props = setdiff(properties(this), user_params); PrGroups = [matlab.mixin.util.PropertyGroup(static_props), ... matlab.mixin.util.PropertyGroup(user_params)]; end end %Callbacks methods (Access = protected) %Callback for saving the fit trace function saveFitCallback(this,~,~) base_dir=this.Gui.BaseDir.String; session_name=this.Gui.SessionName.String; file_name=this.Gui.FileName.String; % Add extension to the file name if missing [~,~,ext]=fileparts(file_name); if isempty(ext) || (length(ext) > 5) || any(isspace(ext)) file_name=[file_name, '.txt']; end assert(~isempty(base_dir),'Save directory is not specified'); save_path=createSessionPath(base_dir, session_name); save(this.Fit, fullfile(save_path, file_name)); end %Creates callback functions for sliders in GUI. Uses ind to find %out which slider the call is coming from. Note that this gets %triggered whenever the value of the slider is changed. function f = createSliderStateChangedCallback(this, ind) edit_field_name = sprintf('Edit_%s',this.fit_params{ind}); function sliderStateChangedCallback(hObject, ~) %Gets the value from the slider val=hObject.Value; %Find out if the current slider value is correct for the %current init param value. If so, do not change anything. %This is required as the callback also gets called when %the slider values are changed programmatically [~, slider_ind]=... min(abs(this.param_vals(ind)-this.slider_vecs{ind})); if slider_ind~=(val+1) %Updates the scale with a new value from the lookup %table this.param_vals(ind)=... this.slider_vecs{ind}(val+1); %Updates the edit box with the new value from the %slider set(this.Gui.(edit_field_name),... 'String', sprintf('%3.3e',this.param_vals(ind))); %Re-calculate the fit curve. calcFit(this); if this.enable_plot plotFit(this); end end end f = @sliderStateChangedCallback; end function f = createParamFieldEditedCallback(this, ind) function paramEditFieldCallback(hObject, ~) val=str2double(hObject.String); manSetParamVal(this, ind, val); end f = @paramEditFieldCallback; end function f = createSliderMouseReleasedCallback(this, ind) function sliderMouseReleasedCallback(hObject, ~) slider_ind=hObject.Value; val = this.slider_vecs{ind}(slider_ind+1); manSetParamVal(this, ind, val); end f = @sliderMouseReleasedCallback; end %Callback function for the manual update of the values of fit %parameters in GUI. Triggered when values in the boxes are editted %and when pulling a slider is over. function manSetParamVal(this, ind, new_val) %Updates the correct initial parameter this.param_vals(ind)=new_val; %Re-calculate the fit curve. calcFit(this); if this.enable_plot plotFit(this) end %Centers the slider set(this.Gui.(sprintf('Slider_%s',this.fit_params{ind})),... 'Value',50); %Generate the new slider vectors genSliderVecs(this); %Reset fit structures to indicate that the current parameters %were set manually this.FitResult=cfit.empty(); this.Gof=struct.empty(); this.FitInfo=struct.empty(); %Calculate user parameters calcUserParams(this); %Update fit metadata this.Fit.UserMetadata=createMetadata(this); end function f = createLowerLimEditCallback(this, ind) function lowerLimEditCallback(hObject, ~) this.lim_lower(ind)=str2double(hObject.String); end f = @lowerLimEditCallback; end function f = createUpperLimEditCallback(this, ind) function upperLimEditCallback(hObject, ~) this.lim_upper(ind)=str2double(hObject.String); end f = @upperLimEditCallback; end %Create a callback that is executed when an editable user parameter %is set in the GUI function f = createUserParamCallback(this, param_name) function userParamCallback(hObject, ~) this.(param_name) = str2double(hObject.String); calcUserParams(this); end f = @userParamCallback; end %Callback function for analyze button in GUI. Checks if the data is %ready for fitting. function analyzeCallback(this, ~, ~) fitTrace(this); end function acceptFitCallback(this, ~, ~) triggerNewAnalysisTrace(this, 'Trace', copy(this.Fit), ... 'analysis_type', 'fit'); end function enableCursorsCallback(this, hObject, ~) this.enable_range_cursors = hObject.Value; if this.enable_gui if hObject.Value this.Gui.CenterCursorsButton.Enable = 'on'; else this.Gui.CenterCursorsButton.Enable = 'off'; end end end %Callback for clearing the fits on the axis. function clearFitCallback(this, ~, ~) clearFit(this); end %Callback function for the button that generates init parameters. function initParamCallback(this, ~, ~) genInitParams(this); end %Close figure callback simply calls delete function for class function closeFigureCallback(this,~,~) delete(this); end end %Private methods methods(Access = private) %Creates a panel for the GUI, in separate file createUserControls(this, varargin); %Updates the GUI if the edit or slider boxes are changed from %elsewhere. function updateSliderPanel(this) for i=1:this.n_params str=this.fit_params{i}; set(this.Gui.(sprintf('Edit_%s',str)),... 'String',sprintf('%3.3e',this.param_vals(i))); set(this.Gui.(sprintf('Lim_%s_upper',str)),... 'String',sprintf('%3.3e',this.lim_upper(i))); set(this.Gui.(sprintf('Lim_%s_lower',str)),... 'String',sprintf('%3.3e',this.lim_lower(i))); end end end methods % Can set enable_plot to true only if Axes are present function set.enable_plot(this, val) val = logical(val); this.enable_plot = val & ~isempty(this.Axes); %#ok end function set.enable_range_cursors(this, val) if ~isempty(this.RangeCursors) for i=1:length(this.RangeCursors) this.RangeCursors(i).Line.Visible = val; end end try if this.enable_gui && ... this.Gui.CursorsCheckbox.Value ~= val this.Gui.CursorsCheckbox.Value = val; end catch end end % Visibility of the range cursors is the reference if they are % enabled or not function val = get.enable_range_cursors(this) if ~isempty(this.RangeCursors) val = strcmpi(this.RangeCursors(1).Line.Visible, 'on'); else val = false; end end function ind = get.data_selection(this) if this.enable_range_cursors xmin = min(this.RangeCursors.value); xmax = max(this.RangeCursors.value); ind = (this.Data.x>xmin & this.Data.x<=xmax); else ind = true(1, length(this.Data.x)); end end %Calculates the number of parameters in the fit function function n_params=get.n_params(this) n_params=length(this.fit_params); end end end \ No newline at end of file diff --git a/@MyTrace/MyTrace.m b/@MyTrace/MyTrace.m index 7cc91e1..05579dc 100644 --- a/@MyTrace/MyTrace.m +++ b/@MyTrace/MyTrace.m @@ -1,574 +1,584 @@ % Class for XY data representation with labelling, plotting and % saving/loading functionality classdef MyTrace < handle & matlab.mixin.Copyable & matlab.mixin.SetGet properties (Access = public) x = [] y = [] name_x = 'x' name_y = 'y' unit_x = '' unit_y = '' file_name char % Array of MyMetadata objects with information about the trace. % The full metadata also contains information about the trace % properties like units etc. UserMetadata MyMetadata % Formatting options for the metadata metadata_opts cell % Data formatting options column_sep = '\t' % Data column separator line_sep = '\r\n' % Data line separator data_sep = 'Data' % Separator between metadata and data save_prec = 15 % Maximum digits of precision in saved data end properties (GetAccess = public, SetAccess = protected, ... NonCopyable = true) % Cell that contains the handles of Line objects the trace % is plotted in plot_lines = {} end properties (Dependent=true) label_x label_y end methods (Access = public) function this = MyTrace(varargin) P = MyClassParser(this); processInputs(P, this, varargin{:}); end function delete(this) % Delete lines from all the axes the trace is plotted in - cellfun(@delete, this.plot_lines); + deleteLine(this); end %Defines the save function for the class. function save(this, filename, varargin) % Parse inputs for saving p = inputParser; addParameter(p, 'overwrite', false); parse(p, varargin{:}); assert(ischar(filename) && isvector(filename), ... '''filename'' must be a character vector.') this.file_name = filename; % Create the file in the given folder stat = createFile(filename, 'overwrite', p.Results.overwrite); % Returns if the file is not created for some reason if ~stat warning('File not created, returned write_flag %i.', stat); return end % Create metadata header Mdt = getMetadata(this); save(Mdt, filename); % Write the data fileID = fopen(filename,'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 data 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 % signigicantly reduces the file size. data_format_str = ... sprintf(['%%.%ig', this.column_sep, '%%.%ig', ... this.line_sep], this.save_prec, this.save_prec); fprintf(fileID, data_format_str, [this.x, this.y]'); fclose(fileID); end function clearData(this) this.x = []; this.y = []; 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 Line = plot(this, varargin) % Do nothing if there is no data in the trace if isDataEmpty(this) return end % Checks that x and y are the same size assert(validateData(this),... 'The length of x and y must be identical to make a plot') % Parses inputs p = inputParser(); p.KeepUnmatched = true; % Axes in which the trace should be plotted addOptional(p, 'Axes', [], @(x)assert(isaxes(x),... 'Argument must be axes or uiaxes.')); addParameter(p, 'make_labels', true, @islogical); validateInterpreter = @(x) assert( ... ismember(x, {'none', 'tex', 'latex'}),... 'Interpreter must be none, tex or latex'); addParameter(p, 'Interpreter', 'latex', validateInterpreter); parse(p, varargin{:}); line_opts = struct2namevalue(p.Unmatched); %If axes are not supplied get current if ~isempty(p.Results.Axes) Axes = p.Results.Axes; else Axes = gca(); end ind = findLineInd(this, Axes); if ~isempty(ind) && any(ind) set(this.plot_lines{ind},'XData',this.x,'YData',this.y); else this.plot_lines{end+1} = plot(Axes, this.x, this.y); ind = length(this.plot_lines); end Line = this.plot_lines{ind}; % Sets the correct color and label options if ~isempty(line_opts) set(Line, line_opts{:}); end if p.Results.make_labels makeLabels(this, Axes, p.Results.Interpreter) end end % Add labels to the axes function makeLabels(this, Axes, interpreter) if exist('interpreter', 'var') == 0 interpreter = 'latex'; end xlabel(Axes, this.label_x, 'Interpreter', interpreter); ylabel(Axes, this.label_y, 'Interpreter', interpreter); set(Axes, 'TickLabelInterpreter', interpreter); end %If there is a line object from the trace in the figure, this sets %it to the appropriate visible setting. function setVisible(this, Axes, bool) if bool vis='on'; else vis='off'; end ind=findLineInd(this, Axes); if ~isempty(ind) && any(ind) set(this.plot_lines{ind},'Visible',vis) end end %Defines addition of two MyTrace objects function Sum=plus(this,b) checkArithmetic(this,b); Sum=MyTrace('x',this.x,'y',this.y+b.y, ... 'unit_x',this.unit_x,'unit_y',this.unit_y, ... 'name_x',this.name_x,'name_y',this.name_y); end %Defines subtraction of two MyTrace objects function Diff=minus(this,b) checkArithmetic(this,b); Diff=MyTrace('x',this.x,'y',this.y-b.y, ... 'unit_x',this.unit_x,'unit_y',this.unit_y, ... 'name_x',this.name_x,'name_y',this.name_y); end function [max_val,max_x]=max(this) assert(validateData(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(validateData(this),['MyTrace object must contain',... ' nonempty data vectors of equal length to find the fwhm']) [~,~,fwhm,~]=findpeaks(this.y,this.x,'NPeaks',1); end function [mean_x,std_x,mean_y,std_y]=calcZScore(this) mean_x=mean(this.x); std_x=std(this.x); mean_y=mean(this.y); std_y=std(this.y); end % Integrates the trace numerically. Two possible ways to call the % function: % % integrate(Trace) - integrate the entire data % integrate(Trace, xmin, xmax) - integrate over [xmin, xmax] % integrate(Trace, ind) - integrate data with indices ind function area = integrate(this, varargin) assert(validateData(this), ['MyTrace object must contain',... ' nonempty data vectors of equal length to integrate']) switch nargin() case 1 % The function is called as integrate(Trace), integrate % the entire trace xvals = this.x; yvals = this.y; case 2 % The function is called as integrate(Trace, ind) ind = varargin{1}; xvals = this.x(ind); yvals = this.y(ind); case 3 % The function is called as integrate(Trace,xmin,xmax) xmin = varargin{1}; xmax = varargin{2}; % Select all data points within the integration range ind = (this.x > xmin) & (this.x < xmax); xvals = this.x(ind); yvals = this.y(ind); % Add the two points corresponding to the interval ends % if the interval is within data range if xmin >= this.x(1) yb = interp1(this.x, this.y, xmin); xvals = [xmin; xvals]; yvals = [yb; yvals]; end if xmax <= this.x(end) yb = interp1(this.x, this.y, xmax); xvals = [xvals; xmax]; yvals = [yvals; yb]; end otherwise error(['Unrecognized function signature. Check ' ... 'the function definition to see acceptable ' ... 'input argument.']) end % Integrates the data using the trapezoidal method area = trapz(xvals, yvals); end % Picks every n-th element from the trace, % performing a running average first if opt=='avg' function NewTrace = downsample(this, n, opt) n0 = ceil(n/2); if exist('opt', 'var') && ... (strcmpi(opt,'average') || strcmpi(opt,'avg')) % Compute moving average with 'shrink' option so that the % total number of samples is preserved. Endpoints will be % discarded by starting the indexing from n0. tmpy = movmean(this.y, n, 'Endpoints', 'shrink'); new_x = this.x(n0:n:end); new_y = tmpy(n0:n:end); else % Downsample without averaging new_x = this.x(n0:n:end); new_y = this.y(n0:n:end); end NewTrace = MyTrace('x', new_x, 'y', new_y, ... 'unit_x',this.unit_x,'unit_y',this.unit_y, ... 'name_x',this.name_x,'name_y',this.name_y); end %Checks if the object is empty function bool = isDataEmpty(this) bool = isempty(this.x) && isempty(this.y); end %Checks if the data can be processed as a list of {x, y} values, %e.g. integrated over x or plotted function bool = validateData(this) bool =~isempty(this.x) && ~isempty(this.y)... && length(this.x)==length(this.y); end function Line = getLine(this, Ax) ind = findLineInd(this, Ax); if ~isempty(ind) Line = this.plot_lines{ind}; else Line = []; end end - % Delete trace line from an axes + % deleteLine(this, Ax) - Delete trace line from an axes + % deleteLine(this) - Delete all trace lines function deleteLine(this, Ax) - ind = findLineInd(this, Ax); - if ~isempty(ind) + if nargin() == 2 - % Delete the line from plot and remove their handles from - % the list - Line = this.plot_lines{ind}; - delete(Line); - this.plot_lines(ind) = []; + % Delete plot lines from particular axes + ind = findLineInd(this, Ax); + if ~isempty(ind) + + % Delete the line from plot and remove their handles + % from the list + Line = [this.plot_lines{ind}]; + delete(Line); + this.plot_lines(ind) = []; + end + elseif nargin() == 1 + + % Delete all plot lines and clear the list + cellfun(@delete, this.plot_lines); + this.plot_lines = {}; end end end methods (Access = public, Static = true) % Load trace from file function Trace = load(filename, varargin) assert(exist(filename, 'file') ~= 0, ['File does not ' ... 'exist, please choose a different load path.']) % Extract data formatting p = inputParser(); p.KeepUnmatched = true; addParameter(p, 'FormatSource', {}, @(x) isa(x,'MyTrace')); addParameter(p, 'metadata_opts', {}, @iscell); parse(p, varargin{:}); if ~ismember('FormatSource', p.UsingDefaults) Fs = p.Results.FormatSource; % Take formatting from the source object mdt_opts = Fs.metadata_opts; trace_opts = { ... 'column_sep', Fs.column_sep, ... 'line_sep', Fs.line_sep, ... 'data_sep', Fs.data_sep, ... 'save_prec', Fs.save_prec, ... 'metadata_opts', Fs.metadata_opts}; else % Formatting is either default or was suppled explicitly mdt_opts = p.Results.metadata_opts; trace_opts = varargin; end % Load metadata and convert from array to structure [Mdt, n_end_line] = MyMetadata.load(filename, mdt_opts{:}); Info = titleref(Mdt, 'Info'); if ~isempty(Info) && isparam(Info, 'Type') class_name = Info.ParamList.Type; else class_name = 'MyTrace'; end % Instantiate an appropriate type of Trace Trace = feval(class_name, trace_opts{:}); %#ok setMetadata(Trace, Mdt); % Reads x and y data data_array = dlmread(filename, Trace.column_sep, n_end_line,0); Trace.x = data_array(:,1); Trace.y = data_array(:,2); Trace.file_name = filename; end end methods (Access = protected) % Generate metadata that includes measurement headers and % information about trace. This function is used in place of 'get' % method so it can be overloaded in a subclass. function Mdt = getMetadata(this) % Make a field with the information about the trace Info = MyMetadata('title', 'Info'); addParam(Info, 'Type', class(this)); addParam(Info, 'Name1', this.name_x); addParam(Info, 'Name2', this.name_y); addParam(Info, 'Unit1', this.unit_x); addParam(Info, 'Unit2', this.unit_y); % Make a separator for the bulk of trace data DataSep = MyMetadata('title', this.data_sep); Mdt = [Info, this.UserMetadata, DataSep]; % Ensure uniform formatting if ~isempty(this.metadata_opts) set(Mdt, this.metadata_opts{:}); end end % Load metadata into the trace function setMetadata(this, Mdt) Info = titleref(Mdt, 'Info'); if ~isempty(Info) if isparam(Info, 'Unit1') this.unit_x = Info.ParamList.Unit1; end if isparam(Info, 'Unit2') this.unit_y = Info.ParamList.Unit2; end if isparam(Info, 'Name1') this.name_x = Info.ParamList.Name1; end if isparam(Info, 'Name2') this.name_y = Info.ParamList.Name2; end % Remove the metadata containing trace properties Mdt = rmtitle(Mdt, 'Info'); else warning(['No trace metadata found. No units or labels ' ... 'assigned when loading trace from %s.'], filename); end % Remove the empty data separator field Mdt = rmtitle(Mdt, this.data_sep); % Store the remainder as user metadata this.UserMetadata = Mdt; end %Checks if arithmetic can be done with MyTrace objects. function checkArithmetic(this, b) assert(isa(this,'MyTrace') && isa(b,'MyTrace'),... ['Both objects must be of type MyTrace ,',... 'here they are type %s and %s'],class(this),class(b)); assert(strcmp(this.unit_x, b.unit_x) && ... strcmp(this.unit_y,b.unit_y),... 'The trace objects do not have the same units') assert(length(this.x)==length(this.y), ['The length of x ' ... 'and y in the first argument are not equal']); assert(length(b.x)==length(b.y), ['The length of x and y ' ... 'in the second argument are not equal']); assert(all(this.x==b.x),... 'The trace objects do not have identical x-axis ') end % Finds the hline handle that is plotted in the specified axes function ind = findLineInd(this, Axes) if ~isempty(this.plot_lines) AxesLines = findall(Axes, 'Type', 'Line'); ind = cellfun(@(x)ismember(x, AxesLines), this.plot_lines); else ind = []; end end % Overload the standard copy() method to create a deep copy, % i.e. when handle properties are copied recursively function Copy = copyElement(this) Copy = copyElement@matlab.mixin.Copyable(this); % Copy metadata Copy.UserMetadata = copy(this.UserMetadata); end end %% Set and get methods methods %Set function for x, checks if it is a vector of doubles and %reshapes into a column vector function set.x(this, x) assert(isnumeric(x),... 'Data must be of class double'); this.x=x(:); end %Set function for y, checks if it is a vector of doubles and %reshapes into a column vector function set.y(this, y) assert(isnumeric(y),... 'Data must be of class double'); this.y=y(:); end %Set function for unit_x, checks if input is a string. function set.unit_x(this, unit_x) assert(ischar(unit_x),'Unit must be a char, not a %s',... class(unit_x)); this.unit_x=unit_x; end %Set function for unit_y, checks if input is a string function set.unit_y(this, unit_y) assert(ischar(unit_y),'Unit must be a char, not a %s',... class(unit_y)); this.unit_y=unit_y; end %Set function for name_x, checks if input is a string function set.name_x(this, name_x) assert(ischar(name_x),'Name must be a char, not a %s',... class(name_x)); this.name_x=name_x; end %Set function for name_y, checks if input is a string function set.name_y(this, name_y) assert(ischar(name_y),'Name must be a char, not a %s',... class(name_y)); this.name_y=name_y; end %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 end end