diff --git a/@MyAnalysisRoutine/MyAnalysisRoutine.m b/@MyAnalysisRoutine/MyAnalysisRoutine.m new file mode 100644 index 0000000..c871c5b --- /dev/null +++ b/@MyAnalysisRoutine/MyAnalysisRoutine.m @@ -0,0 +1,49 @@ +% Superclass for DAQ-compatible analysis routines +% Must accept Axes as an optional argument + +classdef MyAnalysisRoutine < handle + + properties (Abstract, Access = public) + Data MyTrace + end + + properties (Abstract, GetAccess = public, SetAccess = protected) + Axes + end + + events + NewAnalysisTrace + end + + methods (Access = public) + + %Triggered for transferring of the fit trace to DAQ + function triggerNewAnalysisTrace(this, varargin) + EventData = MyNewAnalysisTraceEvent(varargin{:}); + notify(this, 'NewAnalysisTrace', EventData); + end + end + + methods (Static, Access = public) + + % Method for validation of the compliance with this class, it is + % useful when subclassig cannot be implemented, as in the case of + % MATLAB apps (as of MATLAB 2019a). + % Obj is either an object instance or the name of its class. + function validate(Obj) + if ischar(Obj) + class_name = Obj; + else + class_name = class(Obj); + end + + assert(ismember('Data', properties(class_name)), ... + 'Analysis routine must define ''Data'' property') + assert(ismember('NewAnalysisTrace', events(class_name)), ... + 'Analysis routine must define ''NewAnalysisTrace'' event') + assert(nargin(class_name) == -1, ... + 'Analysis routine accept varargin input') + end + end +end + diff --git a/@MyAppColors/MyAppColors.m b/@MyAppColors/MyAppColors.m new file mode 100644 index 0000000..b0570f6 --- /dev/null +++ b/@MyAppColors/MyAppColors.m @@ -0,0 +1,154 @@ +% Set of colors indended to introduce some uniformity in GUIs + +classdef (Abstract) MyAppColors + + methods (Static) + %% Predefined colors + % Colors are represented by rgb triplets returned by static methods + + function rgb = ok() + rgb = [0.47, 0.67, 0.19]; % Green + end + + function rgb = warning() + rgb = [0.93, 0.69, 0.13]; % Orange + end + + function rgb = error() + rgb = [1,0,0]; % Red + end + + % Labview-style lamp indicator colors + function rgb = lampOn() + rgb = [0,1,0]; % Bright green + end + + function rgb = lampOff() + rgb = [0,0.4,0]; % Dark green + end + + % Recolor app according to a new color scheme + function applyScheme(Obj, scheme) + persistent init_default default_main_color ... + default_label_text_color default_edit_text_color ... + default_edit_field_color default_axes_label_color + + if ~exist('scheme', 'var') + scheme = 'default'; + end + + switch lower(scheme) + case 'dark' + main_color = [8, 62, 118]/255; % [0.18, 0.19,0.57] + label_text_color = [1,1,1]; + edit_text_color = [0,0,0]; + edit_field_color = [1,1,1]; + axes_label_color = [0.9,0.9,1]; + case 'bright' + main_color = [1,1,1]; + label_text_color = [0.,0,0.]; + edit_text_color = [0,0,0.]; + edit_field_color = [1,1,1]; + axes_label_color = [0,0,0]; + case 'default' + if isempty(init_default) + + % Create invisible components and read their + % colors + Uf = uifigure('Visible', false); + Ef = uieditfield(Uf); + Lbl = uilabel(Uf); + Ax = axes(Uf); + + default_main_color = Uf.Color; + default_label_text_color = Lbl.FontColor; + default_edit_text_color = Ef.FontColor; + default_edit_field_color = Ef.BackgroundColor; + default_axes_label_color = Ax.XColor; + delete(Uf); + + init_default = false; + end + + main_color = default_main_color; + label_text_color = default_label_text_color; + edit_text_color = default_edit_text_color; + edit_field_color = default_edit_field_color; + axes_label_color = default_axes_label_color; + otherwise + error('Unknown scheme %s', scheme) + end + + if isa(Obj, 'matlab.apps.AppBase') + Fig = findFigure(Obj); + MyAppColors.applyScheme(Fig, scheme); + return + end + + if ~isprop(Obj, 'Type') + return + end + + switch Obj.Type + case 'figure' + Obj.Color = main_color; + case 'uitabgroup' + % Nothing to do + case 'uitab' + Obj.ForegroundColor = edit_text_color; + Obj.BackgroundColor = main_color; + case 'uibutton' + Obj.FontColor = label_text_color; + Obj.BackgroundColor = main_color; + case 'uistatebutton' + Obj.FontColor = label_text_color; + Obj.BackgroundColor = main_color; + case 'uidropdown' + Obj.FontColor = label_text_color; + Obj.BackgroundColor = main_color; + case {'uieditfield', 'uispinner', 'uitextarea', 'uilistbox'} + Obj.FontColor = edit_text_color; + Obj.BackgroundColor = edit_field_color; + case {'uilabel', 'uicheckbox', 'uiradiobutton'} + Obj.FontColor = label_text_color; + case {'uipanel', 'uibuttongroup'} + Obj.ForegroundColor = label_text_color; + Obj.BackgroundColor = main_color; + case 'axes' + try + + % This property is only present in uiaxes + Obj.BackgroundColor = main_color; + catch + end + Obj.XColor = axes_label_color; + Obj.YColor = axes_label_color; + Obj.GridColor = [0.15, 0.15, 0.15]; + Obj.MinorGridColor = [0.15, 0.15, 0.15]; + case 'uimenu' + Obj.ForegroundColor = edit_text_color; + case 'uicontrol' + + % The following switch case is for programmatically + % created components + switch Obj.Style + case {'text', 'pushbutton', 'togglebutton'} + Obj.ForegroundColor = label_text_color; + Obj.BackgroundColor = main_color; + case 'popupmenu' + Obj.ForegroundColor = edit_text_color; + Obj.BackgroundColor = edit_field_color; + end + end + + if isprop(Obj, 'Children') + + % Recolor children + for i = 1:length(Obj.Children) + MyAppColors.applyScheme(Obj.Children(i), scheme); + end + end + end + end +end + diff --git a/@MyAvgTrace/MyAvgTrace.m b/@MyAvgTrace/MyAvgTrace.m index 224daed..12592e2 100644 --- a/@MyAvgTrace/MyAvgTrace.m +++ b/@MyAvgTrace/MyAvgTrace.m @@ -1,154 +1,173 @@ % Adds averaging capabilities to MyTrace % % The averaging type is 'lin' (or 'linear')/ 'exp' (or 'exponential'). % Linear averaging is a simple mean % x=\sum_{n=0}^N x_n, % exponential is an unlimited weighted sum % x=(1-exp(-1/n_avg))*\sum_{n=0}^\inf x_n exp(-n/n_avg). classdef MyAvgTrace < MyTrace - properties (Access=public) + properties (Access = public, SetObservable = true) + % Target number of averages, when it is reached or exceeded % AveragingDone event is triggered - n_avg=1 + n_avg = 1 - avg_type='lin' + avg_type = 'lin' end - properties (GetAccess=public, SetAccess=protected) + properties (GetAccess = public, SetAccess = protected, ... + SetObservable = true) + % Counter for the averaging function, can be reset by clearData - avg_count=0 + avg_count = 0 end - methods (Access=public) + methods (Access = public) % Adds data to the average accumulator. When the averaging counter % reaches n_avg (or exceeds it in the exponential case), completed % is set to 'true', otherwise 'false'. % In exponential regime the averaging proceeds indefinitely so that % avg_count can exceed n_avg. % In linear regime when the averaging counter exceeds n_avg, new % data is discarded. function completed = addAverage(this, b) assert(isa(b,'MyTrace'), ['Second argument must be a ' ... 'MyTrace object']); - if isempty(this) || length(this.x)~=length(b.x) || ... + if isDataEmpty(this) || length(this.x)~=length(b.x) || ... any(this.x~=b.x) + % Initialize new data and return this.x=b.x; this.y=b.y; this.name_x=b.name_x; this.unit_x=b.unit_x; this.name_y=b.name_y; this.unit_y=b.unit_y; this.avg_count=1; completed=(this.avg_count>=this.n_avg); return end assert(length(b.y)==length(this.y), ... ['New vector of y values must be of the same', ... 'length as the exisiting y data of MyTrace in ', ... 'order to perform averanging']) switch this.avg_type case 'lin' if this.avg_count=this.n_avg); otherwise error('Averaging type %s is not supported', ... this.avg_type) end end % Provide restricted access to the trace averaging counter function resetCounter(this) - this.avg_count=0; + this.avg_count = 0; end - % Overload clearData so that it reset the averaging counter in + % Overload the method so that it reset the averaging counter in % addition to clearing the x and y values function clearData(this) - this.x=[]; - this.y=[]; + this.x = []; + this.y = []; resetCounter(this); end + end + + methods (Access = protected) % Extend the info stored in trace metadata compare to MyTrace - function Mdt=makeMetadata(this) - Mdt=makeMetadata@MyTrace(this); - addParam(Mdt,'Info','avg_type',this.avg_type, ... - 'comment','Averaging type, linear or exponential'); - addParam(Mdt,'Info','avg_count',this.avg_count, 'comment', ... + function Mdt = getMetadata(this) + Mdt = getMetadata@MyTrace(this); + + Info = titleref(Mdt, 'Info'); + addParam(Info, 'avg_type', this.avg_type, ... + 'comment', 'Averaging type, linear or exponential'); + addParam(Info, 'avg_count', this.avg_count, 'comment', ... 'Number of accomplished averages'); - addParam(Mdt,'Info','n_avg',this.n_avg, 'comment', ... + addParam(Info, 'n_avg', this.n_avg, 'comment', ... ['Target number of averages (lin) or exponential ' ... 'averaging constant (exp)']); end - function setFromMetadata(this, Mdt) - setFromMetadata@MyTrace(this, Mdt) - if isfield(Mdt.Info, 'avg_type') - this.avg_type=Mdt.Info.avg_type.value; - end - if isfield(Mdt.Info, 'n_avg') - this.n_avg=Mdt.Info.n_avg.value; - end - if isfield(Mdt.Info, 'avg_count') - this.avg_count=Mdt.Info.avg_count.value; + + function setMetadata(this, Mdt) + Info = titleref(Mdt, 'Info'); + if ~isempty(Info) + if isparam(Info, 'avg_type') + this.avg_type = Info.ParamList.avg_type; + end + if isparam(Info, 'n_avg') + this.n_avg = Info.ParamList.n_avg; + end + if isparam(Info, 'avg_count') + this.avg_count = Info.ParamList.avg_count; + end end + + setMetadata@MyTrace(this, Mdt); end end %% Set and get methods methods % Ensure the supplied value for averaging mode is assigned in its % standard form - lowercase and abbreviated function set.avg_type(this, val) - old_val=this.avg_type; + old_val = this.avg_type; switch lower(val) case {'lin', 'linear'} this.avg_type='lin'; case {'exp', 'exponential'} this.avg_type='exp'; otherwise error(['Averaging type must be ''lin'' ' ... '(''linear'') or ''exp'' (''exponential'')']) end + % Clear data if the averaging type was changed - if this.avg_type~=old_val + if this.avg_type ~= old_val clearData(this); end end function set.n_avg(this, val) % The number of averages should be integer not smaller than one - this.n_avg=max(1, round(val)); + this.n_avg = max(1, round(val)); end end end diff --git a/@MyBeta/MyBeta.m b/@MyBeta/MyBeta.m deleted file mode 100644 index 14b5a8d..0000000 --- a/@MyBeta/MyBeta.m +++ /dev/null @@ -1,124 +0,0 @@ -classdef MyBeta < handle - properties (Access=public) - name; - %Trace - Data - end - - properties (GetAccess=public, SetAccess=private) - %Struct containing GUI handles - Gui; - - beta; - enable_gui; - end - - properties (Access=private) - %Input parser - Parser; - end - - methods (Access=public) - function this=MyBeta(varargin) - %Parses inputs to the constructor - createParser(this); - parse(this.Parser,varargin{:}); - this.Data=this.Parser.Results.Data; - this.enable_gui=this.Parser.Results.enable_gui; - - %Default enables gui - if this.enable_gui - this.Gui=guihandles(eval('GuiBeta')); - initGui(this); - end - - end - - %Deletion function for cleanup - function delete(this) - if this.enable_gui - set(this.Gui.figure1,'CloseRequestFcn',''); - %Deletes the figure - delete(this.Gui.figure1); - %Removes the figure handle to prevent memory leaks - this.Gui=[]; - end - end - - %Callback for analyze button - function analyzeCallback(this) - calcBeta(this); - end - - %Calculates beta, updates the gui with the value and stores it in - %the class. - function calcBeta(this) - %Number of points next to peak we will use to find the area - %under each peak - n=10; - % Finding the index of the the centeral maximum and the side bands - min_pk_dist=200; - [peaks,pk_inds] = findpeaks(this.Data.y,... - 'minpeakdistance',min_pk_dist); - %First we sort by height of the peaks - amp_sort=sortrows([peaks, pk_inds]); - %Then we sort the five highest peaks by location - sort_mat=sortrows(amp_sort((end-4):end,:),2); - %The sorted indices of the peaks - ind_sort=sort_mat(:,2); - - % Finds indices for the data in the neighborhood of each peak - %Peak 1 is the leftmost (2nd order peak), Peak 2 is the left - %1st order peak, Peak 3 is the central peak etc. - ind_pks=cell(5,1); - for i=1:5 - ind_pks{i}=(ind_sort(i)-n):(ind_sort(i)+n); - end - - %Here we integrate the data surrounding the peaks and take the - %square root of the areas. - v_rms_pks=cellfun(@(x) sqrt(integrate(this.Data,x)),ind_pks); - - % Then we can calculate the FM beta by using: - sdratio_01 = 0.5*(v_rms_pks(4)+v_rms_pks(2))/v_rms_pks(3); - sdratio_02 = 0.5*(v_rms_pks(5)+v_rms_pks(1))/v_rms_pks(3); - - syms b; - %Then find the beta by numerically solution of the bessel - %functions - beta_01 = double(vpasolve(besselj(1,b) == ... - sdratio_01 * besselj(0,b), b, 2*sdratio_01)); - beta_02 = double(vpasolve(besselj(2,b) == ... - sdratio_02 * besselj(0,b), b, 2*sdratio_02)); - - %We use beta_02 as it is less affected by residual intensity - %noise. - this.beta=beta_02; - %Sets the values on the GUI - set(this.Gui.Beta02,'String',num2str(beta_02,5)); - set(this.Gui.Beta01,'String',num2str(beta_01,5)); - end - end - - methods (Access=private) - %Initializes the GUI with correct callbacks - function initGui(this) - this.Gui.AnalyzeButton.Callback=@(~,~) analyzeCallback(this); - this.Gui.figure1.CloseRequestFcn=@(~,~) closeFigure(this); - end - - %The close figure function calls the deletion method. - function closeFigure(this) - delete(this); - end - - %Creates input parser for constructor - function createParser(this) - p=inputParser; - addParameter(p,'Data',MyTrace()) - addParameter(p,'enable_gui',true); - this.Parser=p; - end - end - -end diff --git a/@MyCeCryo/MyCeCryo.m b/@MyCeCryo/MyCeCryo.m deleted file mode 100644 index 5154cb4..0000000 --- a/@MyCeCryo/MyCeCryo.m +++ /dev/null @@ -1,65 +0,0 @@ -% Class for controlling the auto manifold of ColdEdge stinger cryostat. -% The manifold is managed by an Arduino board that communicates with -% computer via serial protocol. - -classdef MyCeCryo < MyScpiInstrument - - methods (Access = public) - function this = MyCeCryo(interface, address, varargin) - this@MyScpiInstrument(interface, address, varargin{:}); - - % Buffer size of 64 kB should be way an overkill. The labview - % program provided by ColdEdge use 256 Bytes. - this.Device.InputBufferSize=2^16; - this.Device.OutputBufferSize=2^16; - end - - function switchAllOff(this) - writePropertyHedged(this, ... - 'valve1', false, ... - 'valve2', false, ... - 'valve3', false, ... - 'valve4', false, ... - 'valve5', false, ... - 'valve7', false, ... - 'recirc', false, ... - 'cryocooler', false); - end - end - - methods (Access = protected) - function createCommandList(this) - - % Valve states - for i=1:7 - if i == 6 - continue % No valve 6 - end - - tag = sprintf('valve%i',i); - cmd = sprintf(':VALVE%i',i); - addCommand(this, tag, cmd,... - 'default', false, ... - 'fmt_spec', '%b', ... - 'info', 'Valve open(true)/clsed(false)'); - end - - addCommand(this, 'recirc', ':REC',... - 'default', false, ... - 'fmt_spec', '%b', ... - 'info', 'Recirculator on/off'); - - addCommand(this, 'cryocooler', ':COOL',... - 'default', false, ... - 'fmt_spec', '%b', ... - 'info', 'Cryocooler on/off'); - - addCommand(this, 'press', ':PRES',... - 'default', false, ... - 'fmt_spec', '%e', ... - 'access', 'r', ... - 'info', 'Supply pressure (PSI)'); - end - end -end - diff --git a/@MyClassParser/MyClassParser.m b/@MyClassParser/MyClassParser.m index 330d988..508f958 100644 --- a/@MyClassParser/MyClassParser.m +++ b/@MyClassParser/MyClassParser.m @@ -1,115 +1,101 @@ % 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) + 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 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); + 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 + if Tmp.Constant || Tmp.Abstract|| ... + (Tmp.Dependent && isempty(Tmp.SetMethod)) 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; + sa = Tmp.SetAccess; if ischar(sa) - has_access=strcmpi(sa,'public'); + 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); + validationFcn = @(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) '.']); + opt_vars = {def, validationFcn}; + else + def = []; + opt_vars = {def}; end + + addParameter(this, Tmp.Name, opt_vars{:}); end end end - - + % parse varargin and assign results to class properties % with the same names as parameters - function processInputs(this, obj, varargin) + function processInputs(this, Obj, 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); + Obj.(par) = this.Results.(par); catch ME warning(['Value of the input parameter ''' par ... ''' could not be assigned to property. ' ... ME.message]) 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 7cf4c61..8a16dd0 100644 --- a/@MyCollector/MyCollector.m +++ b/@MyCollector/MyCollector.m @@ -1,198 +1,452 @@ -classdef MyCollector < MySingleton & matlab.mixin.Copyable - properties (Access=public, SetObservable=true) - InstrList % Structure accomodating instruments - InstrProps % Properties of instruments - MeasHeaders - collect_flag +classdef MyCollector < MySingleton + + properties (GetAccess = public, SetAccess = private, ... + SetObservable = true) + + % Structure accomodating handles of instrument objects + InstrList = struct() + + % Properties of instruments + InstrProps = struct() + + % Structure accomodating handles of apps which contain user + % interface elements (excluding instrument GUIs) + AppList = struct() end - properties (Access=private) - Listeners + properties (Access = private) + Listeners = struct() + + % Metadata indicating the state of Collector + Metadata MyMetadata end - properties (Dependent=true) + properties (Dependent = true) running_instruments + running_apps end events NewDataWithHeaders end - methods (Access=private) - % Constructor of a singleton class must be private - function this=MyCollector(varargin) - p=inputParser; - addParameter(p,'InstrHandles',{}); - parse(p,varargin{:}); - - this.collect_flag=true; - - if ~isempty(p.Results.InstrHandles) - cellfun(@(x) addInstrument(this,x),p.Results.InstrHandles); - end + methods (Access = private) + + % The constructor of a singleton class must be private + function this = MyCollector() + disp(['Creating a new instance of ' class(this)]) - this.MeasHeaders=MyMetadata(); - this.InstrList=struct(); - this.InstrProps=struct(); - this.Listeners=struct(); + disp('Switching opengl to software mode') + opengl('software'); end end - methods (Access=public) - + methods (Access = public) function delete(this) - cellfun(@(x) deleteListeners(this,x), this.running_instruments); + 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); + function addInstrument(this, name, Instrument, varargin) + assert(isvarname(name), ['Instrument name must be a valid ' ... + 'MATLAB variable name.']) + + assert(~ismember(name, this.running_instruments), ... + ['Instrument ' name ' is already present in the ' ... + 'collector. Delete the existing instrument before ' ... + 'adding a new one with the same name.']) + + p = inputParser(); + + % Optional - put the instrument in global workspace + addParameter(p, 'make_global', true, @islogical); + + % Read the settings of this instrument when new data is + % acquired + addParameter(p, 'collect_header', true, @islogical); + + parse(p, varargin{:}); + + this.InstrList.(name) = Instrument; + + % Configure instrument properties + this.InstrProps.(name) = struct( ... + 'collect_header', p.Results.collect_header, ... + 'global_name', ''); + + if p.Results.make_global + global_name = name; + + % Assign instrument handle to a variable in global + % workspace for quick reference + if isValidBaseVar(global_name) + base_ws_vars = evalin('base', 'who'); + + warning(['A valid variable named ''' global_name ... + ''' already exists in global workspace.']) + + % Generate a new name excluding all the variable names + % existing in the base workspace + global_name = matlab.lang.makeUniqueStrings( ... + global_name, base_ws_vars); + end + + % Put the instrument in global workspace + assignin('base', global_name, Instrument); + + this.InstrProps.(name).global_name = global_name; 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, ',... + if this.InstrProps.(name).collect_header && ... + ~ismethod(Instrument, 'readSettings') + + % If the class does not have a header generation function, + % it can still be added to the collector and transfer data + % to Daq + this.InstrProps.(name).collect_header = false; + warning(['%s does not have a readSettings 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=... + % If the added instrument has a newdata event, we add a + % listener for it. + if ismember('NewData', events(this.InstrList.(name))) + this.Listeners.(name).NewData = ... addlistener(this.InstrList.(name),'NewData',... - @(~,InstrEventData) acquireData(this, InstrEventData)); + @(~, EventData) acquireData(this, name, EventData)); end %Cleans up if the instrument is closed - this.Listeners.(name).Deletion=... - addlistener(this.InstrList.(name),'ObjectBeingDestroyed',... - @(~,~) deleteInstrument(this,name)); + this.Listeners.(name).Deletion = ... + addlistener(this.InstrList.(name), ... + 'ObjectBeingDestroyed', ... + @(~,~) instrumentDeletedCallback(this, name)); end - function acquireData(this, InstrEventData) - src=InstrEventData.Source; + % Get existing instrument + function Instr = getInstrument(this, name) + assert(isfield(this.InstrList, name), ... + ['Name must correspond to one of the running ' ... + 'instruments.']) + + Instr = this.InstrList.(name); + end + + % Interface for accessing internally stored instrument properties + function val = getInstrumentProp(this, instr_name, prop_name) + assert(isfield(this.InstrProps, instr_name), ... + ['''instr_name'' must correspond to one of the ' ... + 'running instruments.']) + + assert(isfield(this.InstrProps.(instr_name), prop_name), ... + ['''prop_name'' must correspond to one of the following'... + 'instrument properties: ' ... + var2str(fieldnames(this.InstrProps.(instr_name)))]) + + val = this.InstrProps.(instr_name).(prop_name); + end + + function setInstrumentProp(this, instr_name, prop_name, val) + assert(isfield(this.InstrProps, instr_name), ... + ['''instr_name'' must correspond to one of the ' ... + 'running instruments.']) + + assert(isfield(this.InstrProps.(instr_name), prop_name), ... + ['''prop_name'' must correspond to one of the following'... + 'instrument properties: ' ... + var2str(fieldnames(this.InstrProps.(instr_name)))]) + + this.InstrProps.(instr_name).(prop_name) = val; + end + + function addApp(this, App, app_name) + assert(~isfield(this.AppList, app_name), ['App with name ''' ... + app_name ''' is already present in the collector.']) + + this.AppList.(app_name) = App; + + % Set up a listener that will update the list when the app + % is deleted + addlistener(App, 'ObjectBeingDestroyed', ... + @(~,~)removeApp(this, app_name)); + end + + function App = getApp(this, app_name) + assert(isfield(this.AppList, app_name), [app_name ... + ' does not correspond to any of the running apps.']) + + App = this.AppList.(app_name); + end + + function acquireData(this, name, InstrEventData) + src = InstrEventData.Source; % Check that event data object is MyNewDataEvent, % and fix otherwise - if ~isa(InstrEventData,'MyNewDataEvent') - InstrEventData=MyNewDataEvent(); - InstrEventData.new_header=true; - InstrEventData.Trace=copy(src.Trace); - try - InstrEventData.src_name=src.name; - catch - InstrEventData.src_name='UnknownDevice'; - end + if ~isa(InstrEventData, 'MyNewDataEvent') + InstrEventData = MyNewDataEvent(); + InstrEventData.new_header = true; + InstrEventData.Trace = copy(src.Trace); end + % Indicate the name of acquiring instrument + InstrEventData.src_name = name; + % 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.new_header - this.MeasHeaders=MyMetadata(); - %Add field indicating the time when the trace was acquired - addTimeField(this.MeasHeaders, 'AcquisitionTime') - addField(this.MeasHeaders,'AcquiringInstrument') - %src_name is a valid matlab variable name as ensured by - %its set method - addParam(this.MeasHeaders,'AcquiringInstrument',... - 'Name', InstrEventData.src_name); - acquireHeaders(this); + if InstrEventData.new_header - %We copy the MeasHeaders to both copies of the trace - the - %one that is with the source and the one that is forwarded - %to Daq. - InstrEventData.Trace.MeasHeaders=copy(this.MeasHeaders); - src.Trace.MeasHeaders=copy(this.MeasHeaders); + % Add the name of acquisition instrument + AcqInstrMdt = MyMetadata('title', 'AcquiringInstrument'); + addParam(AcqInstrMdt, 'Name', InstrEventData.src_name); + + % Make the full metadata + Mdt = [AcqInstrMdt, acquireHeaders(this)]; + + %We copy the metadata to both copies of the trace - the + %one that remains within the source and the one that is + %passed to Daq. + InstrEventData.Trace.UserMetadata = copy(Mdt); + src.Trace.UserMetadata = copy(Mdt); end - triggerNewDataWithHeaders(this,InstrEventData); + 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}; + % Collects headers for open instruments with the header flag on + function Mdt = acquireHeaders(this, varargin) + p = inputParser(); + addParameter(p, 'add_collector_metadata', false); + parse(p, varargin{:}); + + add_collector_metadata = p.Results.add_collector_metadata; + + Mdt = MyMetadata.empty(); + + for i = 1:length(this.running_instruments) + name = this.running_instruments{i}; - if this.InstrProps.(name).header_flag + if this.InstrProps.(name).collect_header 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; + TmpMdt = readSettings(this.InstrList.(name)); + TmpMdt.title = name; + Mdt = [Mdt, TmpMdt]; %#ok + catch ME + warning(['Error while reading metadata from ' ... + '%s. Measurement header collection is '... + 'switched off for this instrument.' ... + '\nError: %s'], name, ME.message) + this.InstrProps.(name).collect_header = false; end - end end + + % Add field indicating the time when the trace was acquired + TimeMdt = MyMetadata.time('title', 'AcquisitionTime'); + + Mdt = [TimeMdt, Mdt]; + + if add_collector_metadata + + % Add information about the state of Collector + CollMdt = getMetadata(this); + + Mdt = [Mdt, CollMdt]; + end end - function clearHeaders(this) - this.MeasHeaders=MyMetadata(); - end - - function bool=isrunning(this,name) - assert(~isempty(name),'Instrument name must be specified') + function bool = isrunning(this, name) assert(ischar(name)&&isvector(name),... 'Instrument name must be a character vector, not %s',... class(name)); - bool=ismember(name,this.running_instruments); + bool = ismember(name, this.running_instruments); end - function deleteInstrument(this,name) - if isrunning(this,name) - %We remove the instrument - this.InstrList=rmfield(this.InstrList,name); - this.InstrProps=rmfield(this.InstrProps,name); - deleteListeners(this,name); + % Remove instrument from collector without deleting the instrument + % object + function removeInstrument(this, name) + if isrunning(this, name) + + % Remove the instrument entries + this.InstrList = rmfield(this.InstrList, name); + this.InstrProps = rmfield(this.InstrProps, name); + + deleteListeners(this, name); end end + function removeApp(this, name) + if isfield(this.AppList, name) + this.AppList = rmfield(this.AppList, name); + end + end + + % Delete all presesently running instruments and apps. + % We rely on the deletion callbacks to do cleanup. + function flush(this) + instr_names = this.running_instruments; + for i = 1:length(instr_names) + delete(this.InstrList.(instr_names{i})); + end + + app_names = this.running_apps; + for i = 1:length(app_names) + + % Delete by closing the app window + closeApp(this.AppList.(app_names{i})); + end + end end - methods(Static) - % Concrete implementation of the singletone constructor. + methods (Access = private) + function instrumentDeletedCallback(this, name) + + % Clear the base workspace wariable + gn = this.InstrProps.(name).global_name; + if ~isempty(gn) + try + evalin('base', sprintf('clear(''%s'');', gn)); + catch ME + warning(['Could not clear global variable ''' ... + gn '''. Error: ' ME.message]); + end + end + + % Remove the instrument entry from Collector + removeInstrument(this, name); + end + + % Create metadata that stores information about the Collector + % state + function Mdt = getMetadata(this) + + % Create new metadata if it has not yet been initialized + if isempty(this.Metadata) + this.Metadata = MyMetadata('title', 'SessionInfo'); + + addParam(this.Metadata, 'instruments', {}, 'comment', ... + 'Instruments active during the session'); + + addParam(this.Metadata, 'apps', {}, 'comment', ... + 'Applications active during the session'); + + addParam(this.Metadata, 'InstrProps', struct(), ... + 'comment', ['Instrument properties. gui_position ' ... + 'has format [x, y] and is measured in pixels.']); + + addParam(this.Metadata, 'AppProps', struct()); + end + + % Introduce a shorthand notation + M = this.Metadata; + + % Update metadata parameters + M.ParamList.instruments = this.running_instruments; + M.ParamList.apps = this.running_apps; + + for i = 1:length(this.running_instruments) + nm = this.running_instruments{i}; + + M.ParamList.InstrProps.(nm).collect_header =... + this.InstrProps.(nm).collect_header; + + M.ParamList.InstrProps.(nm).is_global = ... + ~isempty(this.InstrProps.(nm).global_name); + + % Indicate if the instrument has gui + has_gui = isprop(this.InstrList.(nm), 'Gui') && ... + ~isempty(this.InstrList.(nm).Gui); + + M.ParamList.InstrProps.(nm).has_gui = has_gui; + + if has_gui + + % Add the position of GUI on the screen in pixels + Fig = findFigure(this.InstrList.(nm).Gui); + original_units = Fig.Units; + Fig.Units = 'pixels'; + + % We record only x and y position but not the width and + % hight of the window, as the latter is a possible + % subject to change + pos = Fig.Position(1:2); + + % Restore the figure settings + Fig.Units = original_units; + + M.ParamList.InstrProps.(nm).gui_position = pos; + else + M.ParamList.InstrProps.(nm).gui_position = ''; + end + end + + % Add information about running apps + for i = 1:length(this.running_apps) + nm = this.running_apps{i}; + + M.ParamList.AppProps.(nm).class = class(this.AppList.(nm)); + + % Add the position of GUI on the screen in pixels + Fig = findFigure(this.AppList.(nm)); + + if isempty(Fig) + + % An app should in principle have a figure, we skip + % it if it does not + M.ParamList.AppProps.(nm).position = ''; + continue + end + + original_units = Fig.Units; + Fig.Units = 'pixels'; + + % We record only x and y position but not the width and + % hight of the window, as the latter is a possible + % subject to change + pos = Fig.Position(1:2); + + % Restore the figure settings + Fig.Units = original_units; + + M.ParamList.AppProps.(nm).position = pos; + end + + Mdt = copy(M); + end + end + + methods(Static = true) + + % Singletone constructor. function this = instance() persistent UniqueInstance if isempty(UniqueInstance)||(~isvalid(UniqueInstance)) - disp('Creating new instance of MyCollector') this = MyCollector(); UniqueInstance = this; else this = UniqueInstance; end end end - methods (Access=private) - function triggerNewDataWithHeaders(this,InstrEventData) - notify(this,'NewDataWithHeaders',InstrEventData); + methods (Access = private) + function triggerNewDataWithHeaders(this, InstrEventData) + notify(this, 'NewDataWithHeaders', InstrEventData); end %deleteListeners is in a separate file deleteListeners(this, obj_name); end methods - function running_instruments=get.running_instruments(this) - running_instruments=fieldnames(this.InstrList); + function val = get.running_instruments(this) + val = fieldnames(this.InstrList); + end + + function val = get.running_apps(this) + val = fieldnames(this.AppList); end end end diff --git a/@MyCommCont/MyCommCont.m b/@MyCommCont/MyCommCont.m new file mode 100644 index 0000000..6337a3e --- /dev/null +++ b/@MyCommCont/MyCommCont.m @@ -0,0 +1,176 @@ +% Communicator container. +% This class provides extended functionality for communication using VISA, +% tcpip and serial objects or any other objects that have a similar usage. + +classdef MyCommCont < handle + + properties (Access = public) + interface = 'serial' + address = 'placeholder' + + % Communication object + Comm + end + + methods (Access = public) + function this = MyCommCont(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + end + + function delete(this) + + % Close the connection to the device + try + closeComm(this); + catch ME + warning(['Connection could not be closed. Error: ' ... + ME.message]); + end + + % Delete the device object + try + delete(this.Comm); + catch ME + warning(['Communication object could not be deleted. ' ... + 'Error: ' ME.message]); + end + end + + %% Set up communication + + % Create an interface object + function connect(this) + if ~isempty(this.Comm) + + % Delete the existing object before creating a new one. + delete(this.Comm); + end + + try + switch lower(this.interface) + + % Use 'constructor' interface to create an object with + % more that one parameter passed to the constructor + case 'constructor' + + % In this case 'address' is a MATLAB command that + % creates communication object when executed. + % Such commands, for example, are returned by + % instrhwinfo as ObjectConstructorName. + this.Comm = eval(this.address); + case 'visa' + + % visa brand is 'ni' by default + this.Comm = visa('ni', this.address); + case 'tcpip' + + % Works only with default socket. Use 'constructor' + % if socket or other options need to be specified + this.Comm = tcpip(this.address); + case 'serial' + this.Comm = serial(this.address); + otherwise + error(['Unknown interface ''' this.interface ... + ''', a communication object is not created.'... + ' Valid interfaces are ''constructor'', ' ... + '''visa'', ''tcpip'' and ''serial''']) + end + + configureCommDefault(this); + catch ME + warning(ME.message); + + % Create a dummy + this.Comm = serial('placeholder'); + end + end + + % Set larger buffer sizes and longer timeout than the MATLAB default + function configureCommDefault(this) + comm_props = properties(this.Comm); + if ismember('OutputBufferSize',comm_props) + this.Comm.OutputBufferSize = 1e7; % bytes + end + if ismember('InputBufferSize',comm_props) + this.Comm.InputBufferSize = 1e7; % bytes + end + if ismember('Timeout',comm_props) + this.Comm.Timeout = 10; % s + end + end + + function bool = isopen(this) + try + bool = strcmp(this.Comm.Status, 'open'); + catch + warning('Cannot access the communicator Status property'); + bool = false; + end + end + + % Opens the device if it is not open. Does not throw error if + % device is already open for communication with another object, but + % tries to close existing connections instead. + function openComm(this) + try + fopen(this.Comm); + catch + if isa(this.Comm, 'serial') + + % Try to find and close all the serial objects + % connected to the same port + instr_list = instrfind('Port', this.Comm.Port); + else + + % Try to find and close all the devices with the same + % VISA resource name + instr_list = instrfind('RsrcName', this.Comm.RsrcName); + end + + fclose(instr_list); + fopen(this.Comm); + + warning(['Multiple instrument objects of ' ... + 'address %s exist'], this.address); + end + end + + function closeComm(this) + fclose(this.Comm); + end + + %% Communication + + % Write textual command + function writeString(this, str) + try + fprintf(this.Comm, str); + catch ME + try + % Attempt re-opening communication + openComm(this); + fprintf(this.Comm, str); + catch + rethrow(ME); + end + end + end + + % Query textual command + function result = queryString(this, str) + try + result = query(this.Comm, str); + catch ME + try + % Attempt re-opening communication + openComm(this); + result = query(this.Comm, str); + catch + rethrow(ME); + end + end + end + end +end + diff --git a/@MyCursor/MyCursor.m b/@MyCursor/MyCursor.m new file mode 100644 index 0000000..3f80a27 --- /dev/null +++ b/@MyCursor/MyCursor.m @@ -0,0 +1,218 @@ +classdef MyCursor < handle + + properties (GetAccess = public, SetAccess = protected) + Axes % Axes in which the cursor is plotted + Line % Line object that represents the cursor + end + + properties (Access = protected) + Figure % Figure that contains Axes + + % Variables for the temporary storage of information during + % processing the interaction callbacks + originalWbmFcn + originalWbuFcn + originalWbdFcn + originalXLimMode + originalYLimMode + + was_dragged = false + + % Minimum interval between the subsequent updatings of the cursor + % position when it is dragged (s) + DragDelay + + % Time of the previous drag + TlastDrag + end + + properties (Dependent = true) + + % User-friendly ways to refer to the properties of Line + orientation % vertical/horizontal + value % cursor position + end + + methods (Access = public) + function this = MyCursor(Axes, varargin) + p = inputParser(); + p.KeepUnmatched = true; + addRequired(p, 'Axes', @isaxes); + addParameter(p, 'position', []); + addParameter(p, 'orientation', 'vertical', @ischar); + parse(p, Axes, varargin{:}); + + % All the unmatched parameters are assumed to be line + % parameters and will be passed to the line constructor + line_nv = struct2namevalue(p.Unmatched); + + this.Axes = Axes; + this.Figure = Axes.Parent; + + this.DragDelay = milliseconds(100); + + % Draw the cursor line + if strcmpi(p.Results.orientation, 'vertical') + if ~isempty(p.Results.position) + pos = p.Results.position; + else + pos = (this.Axes.XLim(1)+this.Axes.XLim(2))/2; + end + + this.Line = xline(Axes, pos, line_nv{:}); + else + if ~isempty(p.Results.position) + pos = p.Results.position; + else + pos = (this.Axes.YLim(1)+this.Axes.YLim(2))/2; + end + + this.Line = yline(Axes, pos, line_nv{:}); + end + + % Configure the line + this.Line.ButtonDownFcn = @this.cursorButtonDownFcn; + + % Do not display cursors in legends + this.Line.Annotation.LegendInformation.IconDisplayStyle='off'; + end + + function delete(this) + delete(this.Line); + end + end + + methods (Access = protected) + + % Callback invoked when the cursor is clicked by mouse + function cursorButtonDownFcn(this, ~, ~) + + % Freeze the limits of axes + this.originalXLimMode = this.Axes.XLimMode; + this.originalYLimMode = this.Axes.YLimMode; + + this.Axes.XLimMode = 'manual'; + this.Axes.YLimMode = 'manual'; + + % Replace figure callbacks + this.originalWbmFcn = this.Figure.WindowButtonMotionFcn; + this.originalWbuFcn = this.Figure.WindowButtonUpFcn; + this.originalWbdFcn = this.Figure.WindowButtonDownFcn; + + this.Figure.WindowButtonMotionFcn = @this.localWbmFcn; + this.Figure.WindowButtonUpFcn = @this.localWbuFcn; + this.Figure.WindowButtonDownFcn = @this.localWbdFcn; + + this.Line.Selected = 'on'; + + this.TlastDrag = datetime('now'); + end + + % Replacement callback that is active when the cursor is being + % dragged + function localWbmFcn(this, ~, ~) + this.was_dragged = true; + + Tnow = datetime('now'); + + if (Tnow-this.TlastDrag) > this.DragDelay + moveLineToMouseTip(this); + this.TlastDrag = Tnow; + end + end + + % Replacement callback that is active when the cursor is being + % dragged + function localWbuFcn(this, ~, ~) + if this.was_dragged + + % If the cursor was dragged while the mouse button was + % down, finish the interaction + this.was_dragged = false; + restoreAxes(this); + else + + % If it was not dragged, disable the mouse motion callback + % and wait for a new mouse click to move the cursor + this.Figure.WindowButtonMotionFcn = this.originalWbmFcn; + end + end + + % Replacement callback that is active when the cursor is being + % dragged + function localWbdFcn(this, ~, ~) + moveLineToMouseTip(this); + restoreAxes(this); + end + + function moveLineToMouseTip(this) + + % Move the cursor line to the current position of the mouse + % tip withing the plot area. + switch this.Line.InterceptAxis + case 'x' + new_x = this.Axes.CurrentPoint(1,1); + new_x = min(new_x, this.Axes.XLim(2)); + new_x = max(new_x, this.Axes.XLim(1)); + + if new_x ~= this.Line.Value + this.Line.Value = new_x; + end + case 'y' + new_y = this.Axes.CurrentPoint(1,2); + new_y = min(new_y, this.Axes.YLim(2)); + new_y = max(new_y, this.Axes.YLim(1)); + + if new_y ~= this.Line.Value + this.Line.Value = new_y; + end + end + end + + % Restore the axes in the state before user interaction + function restoreAxes(this) + + % Restore the original figure callbacks when the cursor drag is + % finished + this.Figure.WindowButtonMotionFcn = this.originalWbmFcn; + this.Figure.WindowButtonUpFcn = this.originalWbuFcn; + this.Figure.WindowButtonDownFcn = this.originalWbdFcn; + + % Restore the axes limits mode + this.Axes.XLimMode = this.originalXLimMode; + this.Axes.YLimMode = this.originalYLimMode; + + this.Line.Selected = 'off'; + end + end + + methods + function val = get.orientation(this) + try + switch this.Line.InterceptAxis + case 'x' + val = 'vertical'; + case 'y' + val = 'horizontal'; + otherwise + val = ''; + end + catch + val = ''; + end + end + + function val = get.value(this) + try + val = this.Line.Value; + catch + val = NaN; + end + end + + function set.value(this, val) + this.Line.Value = val; + end + end +end + diff --git a/@MyDaq/MyDaq.m b/@MyDaq/MyDaq.m deleted file mode 100644 index 54807f4..0000000 --- a/@MyDaq/MyDaq.m +++ /dev/null @@ -1,1047 +0,0 @@ -% 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() - %Struct containing Cursor objects - Cursors=struct() - %Struct containing Cursor labels - CrsLabels=struct() - %Struct containing MyFit objects - Fits=struct() - %Input parser for class constructor - ConstructionParser - %Struct for listeners - Listeners=struct() - - %Sets the colors of fits, data and reference - fit_color='k' - data_color='b' - ref_color='r' - bg_color='c' - - default_ext='.txt' % default data file extension - 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 % File name is always returned with extension - end - - methods (Access=public) - %% Class functions - %Constructor function - function this=MyDaq(varargin) - % 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 - - % Initialize traces - this.Ref=MyTrace(); - this.Data=MyTrace(); - this.Background=MyTrace(); - - %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]); - - % Add Data, Ref and Bg traces in the proper order on the plot - % as empty lines. - hold(this.main_plot,'on'); - this.Background.hlines{1}=line(this.main_plot, ... - 'XData',[],'YData',[],'Color',this.bg_color); - this.Ref.hlines{1}=line(this.main_plot, ... - 'XData',[],'YData',[],'Color',this.ref_color); - this.Data.hlines{1}=line(this.main_plot, ... - 'XData',[],'YData',[],'Color',this.data_color); - - %Initializes saving locations - this.base_dir=getLocalSettings('measurement_base_dir'); - this.session_name='placeholder'; - this.filename='placeholder'; - end - - function delete(this) - %Deletes the MyFit objects and their listeners - cellfun(@(x) deleteListeners(this,x), this.open_fits); - structfun(@(x) delete(x), this.Fits); - - %Deletes other listeners - if ~isempty(fieldnames(this.Listeners)) - cellfun(@(x) deleteListeners(this, x),... - fieldnames(this.Listeners)); - end - - % clear global variable, to which Daq handle is assigned - evalin('base', sprintf('clear(''%s'')', this.global_name)); - - %A class destructor should never through errors, so enclose the - %attempt to close figure into try-catch structure - try - this.Gui.figure1.CloseRequestFcn=''; - %Deletes the figure - delete(this.Gui.figure1); - %Removes the figure handle to prevent memory leaks - this.Gui=[]; - catch - end - end - end - - methods (Access=private) - - %Sets callback functions for the GUI - initGui(this) - - %Executes when the GUI is closed - function closeFigure(this,~,~) - delete(this); - end - - %Updates fits - function updateFits(this) - %Pushes data into fits in the form of MyTrace objects, so that - %units etc follow. Also updates user supplied parameters. - for i=1:length(this.open_fits) - switch this.open_fits{i} - case {'Linear','Quadratic','Gaussian',... - 'Exponential','Beta'} - this.Fits.(this.open_fits{i}).Data=... - getFitData(this,'VertData'); - case {'Lorentzian','DoubleLorentzian'} - this.Fits.(this.open_fits{i}).Data=... - getFitData(this,'VertData'); - %Here we push the information about line spacing - %into the fit object if the reference cursors are - %open. Only for Lorentzian fits. - if isfield(this.Cursors,'VertRef') - ind=findCursorData(this,'Data','VertRef'); - this.Fits.(this.open_fits{i}).CalVals.line_spacing=... - range(this.Data.x(ind)); - end - case {'G0'} - this.Fits.G0.MechTrace=getFitData(this,'VertData'); - this.Fits.G0.CalTrace=getFitData(this,'VertRef'); - end - end - end - - % If vertical cursors are on, takes only data within cursors. If - %the cursor is not open, it takes all the data from the selected - %trace in the analysis trace selection dropdown - function Trace=getFitData(this,varargin) - %Parses varargin input - p=inputParser; - addOptional(p,'name','',@ischar); - parse(p,varargin{:}) - name=p.Results.name; - - %Finds out which trace the user wants to fit. - trc_opts=this.Gui.SelTrace.String; - trc_str=trc_opts{this.Gui.SelTrace.Value}; - % Note the use of copy here! This is a handle - %class, so if normal assignment is used, this.Fits.Data and - %this.(trace_str) will refer to the same object, causing roblems. - %Name input is the name of the cursor to be used to extract data. - Trace=copy(this.(trc_str)); - %If the cursor is open for the trace we are analyzing, we take - %only the data enclosed by the cursor. - if isfield(this.Cursors,name) - ind=findCursorData(this, trc_str, name); - Trace.x=this.(trc_str).x(ind); - Trace.y=this.(trc_str).y(ind); - end - end - - %Finds data between named cursors in the given trace - function ind=findCursorData(this, trc_str, name) - crs_pos=sort([this.Cursors.(name){1}.Location,... - this.Cursors.(name){2}.Location]); - ind=(this.(trc_str).x>crs_pos(1) & this.(trc_str).x - %Prints the figure to the clipboard - print(newFig,'-clipboard','-dbitmap'); - %Deletes the figure - delete(newFig); - end - - %Resets the axis to be tight around the plots. - function updateAxis(this) - axis(this.main_plot,'tight'); - end - end - - methods (Access=public) - %% Callbacks - - %Callback for copying the plot to clipboard - function copyPlotCallback(this,~,~) - copyPlot(this); - end - - %Callback for centering cursors - function centerCursorsCallback(this, ~, ~) - if ~this.Gui.LogX.Value - x_pos=mean(this.main_plot.XLim); - else - x_pos=10^(mean(log10(this.main_plot.XLim))); - end - - if ~this.Gui.LogY.Value - y_pos=mean(this.main_plot.YLim); - else - y_pos=10^(mean(log10(this.main_plot.YLim))); - end - - for i=1:length(this.open_crs) - switch this.Cursors.(this.open_crs{i}){1}.Orientation - case 'horizontal' - pos=y_pos; - case 'vertical' - pos=x_pos; - end - - %Centers the position - cellfun(@(x) set(x,'Location',pos), ... - this.Cursors.(this.open_crs{i})); - %Triggers the UpdateCursorBar event, which triggers - %listener callback to reposition text - cellfun(@(x) notify(x,'UpdateCursorBar'),... - this.Cursors.(this.open_crs{i})); - %Triggers the EndDrag event, updating the data in the fit - %objects. - cellfun(@(x) notify(x,'EndDrag'),... - this.Cursors.(this.open_crs{i})); - end - end - - %Callback for creating vertical data cursors - function cursorButtonCallback(this, hObject, ~) - name=erase(hObject.Tag,'Button'); - %Gets the first four characters of the tag (Vert or Horz) - type=name(1:4); - - %Changes the color of the button and appropriately creates or - %deletes the cursors. - if hObject.Value - hObject.BackgroundColor=[0,1,0.2]; - createCursors(this,name,type); - else - hObject.BackgroundColor=[0.941,0.941,0.941]; - deleteCursors(this,name); - end - end - - %Callback for the instrument menu - function instrMenuCallback(this,hObject,~) - val=hObject.Value; - if val==1 - %Returns if we are on the dummy option ('Select instrument') - return - else - tag = hObject.ItemsData{val}; - end - - try - eval(this.ProgramList.(tag).run_expr); - catch - errordlg(sprintf('An error occured while running %s',... - this.ProgramList.(tag).name)) - end - - end - - %Select trace callback. If we change the trace being analyzed, the - %fit objects are updated. - function selTraceCallback(this, ~, ~) - updateFits(this) - end - - %Saves the data if the save data button is pressed. - function saveCallback(this, src, ~) - switch src.Tag - case 'SaveData' - saveTrace(this,'Data'); - case 'SaveRef' - saveTrace(this,'Ref'); - end - end - - function saveTrace(this, trace_tag, varargin) - p=inputParser(); - % If file already exists, generate a uinique name rather than - % show user the overwrite dialog. - addParameter(p, 'make_unique_name', false, @islogical); - parse(p,varargin{:}); - - %Check if the trace is valid (i.e. x and y are equal length) - %before saving - if ~this.(trace_tag).validatePlot - errordlg(sprintf('%s trace was empty, could not save',... - trace_tag)); - return - end - - fullfilename=fullfile(this.save_dir,this.filename); - - if p.Results.make_unique_name && exist(fullfilename, 'file')~=0 - fullfilename=makeUniqueFileName(this); - end - - %Save in readable format using the method of MyTrace - save(this.(trace_tag), fullfilename) - end - - % Make the filename unique within the measurement folder - % by appending _n. This function does not make sure that the - % filename is valid - i.e. does not contain symbols forbidden by - % the file system. - function fullfilename=makeUniqueFileName(this) - [~, fn, ext]=fileparts(this.filename); - % List all the existing files in the measurement directory - % that have the same extension as our filename - DirCont=dir(fullfile(this.save_dir,['*', ext])); - file_ind=~[DirCont.isdir]; - existing_fns={DirCont(file_ind).name}; - - % Remove extensions - [~,existing_fns,~]=cellfun(@fileparts, existing_fns, ... - 'UniformOutput',false); - - % Generate a new file name - if ~isempty(fn) - fn=matlab.lang.makeUniqueStrings(fn, existing_fns); - else - fn=matlab.lang.makeUniqueStrings('placeholder', ... - existing_fns); - end - fullfilename=fullfile(this.save_dir,[fn, ext]); - end - - %Toggle button callback for showing the data trace. - function showDataCallback(this, hObject, ~) - if hObject.Value - hObject.BackgroundColor=[0,1,0.2]; - setVisible(this.Data,this.main_plot,1); - updateAxis(this); - else - hObject.BackgroundColor=[0.941,0.941,0.941]; - setVisible(this.Data,this.main_plot,0); - updateAxis(this); - end - end - - %Toggle button callback for showing the ref trace - function showRefCallback(this, hObject, ~) - if hObject.Value - hObject.BackgroundColor=[0,1,0.2]; - setVisible(this.Ref,this.main_plot,1); - updateAxis(this); - else - hObject.BackgroundColor=[0.941,0.941,0.941]; - setVisible(this.Ref,this.main_plot,0); - updateAxis(this); - end - end - - %Callback for moving the data to reference. - function dataToRefCallback(this, ~, ~) - if this.Data.validatePlot - % Copy Data to Reference and pass Reference the handle to - % the line in the main plot - hline=getLineHandle(this.Ref, this.main_plot); - this.Ref=copy(this.Data); - if ~isempty(hline) - this.Ref.hlines{1}=hline; - end - %Plot the reference trace and make it visible - plot(this.Ref, this.main_plot, 'Color',this.ref_color,... - 'make_labels',true); - setVisible(this.Ref, this.main_plot,1); - %Update the fit objects - updateFits(this); - %Change button color - this.Gui.ShowRef.Value=1; - this.Gui.ShowRef.BackgroundColor=[0,1,0.2]; - - else - warning('Data trace was empty, could not move to reference') - end - end - - %Callback for ref to bg button. Sends the reference to background - function refToBgCallback(this, ~, ~) - if this.Ref.validatePlot - hline=getLineHandle(this.Background, this.main_plot); - this.Background=copy(this.Ref); - if ~isempty(hline) - this.Background.hlines{1}=hline; - end - 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 - hline=getLineHandle(this.Background, this.main_plot); - this.Background=copy(this.Data); - if ~isempty(hline) - this.Background.hlines{1}=hline; - end - 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 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(this.default_ext, ... - '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}; - - %Get the line handle from the trace to not create a new line - hline=getLineHandle(this.(dest_trc), this.main_plot); - - %Reset and load the destination trace - this.(dest_trc)=MyTrace(); - load(this.(dest_trc), load_path); - - % Assign existing line handle to the trace - if ~isempty(hline) - this.(dest_trc).hlines{1}=hline; - end - - %Color and plot the right trace. - plot(this.(dest_trc), this.main_plot,... - 'Color',this.(sprintf('%s_color',lower(dest_trc))),... - 'make_labels',true); - %Update axis and cursors - updateAxis(this); - updateCursors(this); - end - - % Callback for open folder button - function openFolderCallback(this, hObject, eventdata) - dir=uigetdir(this.Gui.BaseDir.String); - if ~isempty(dir) - this.Gui.BaseDir.String=dir; - end - - % Execute the same callback as if the base directory edit - % field was manually updated - baseDirCallback(this, hObject, eventdata); - end - end - - methods (Access=public) - %% Listener functions - %Callback function for NewFit listener. Plots the fit in the - %window using the plotFit function of the MyFit object - function plotNewFit(this, src, ~) - src.plotFit('Color',this.fit_color); - updateAxis(this); - updateCursors(this); - end - - %Callback function for the NewData listener - function acquireNewData(this, EventData) - %Get the currently selected instrument - val=this.Gui.InstrMenu.Value; - curr_instr_name=this.Gui.InstrMenu.ItemsData{val}; - - %Check if the data originates from the currently selected - %instrument - if strcmp(EventData.src_name, curr_instr_name) - hline=getLineHandle(this.Data,this.main_plot); - %Copy the data from the source instrument - this.Data=copy(EventData.Trace); - %We give the new trace object the right line handle to plot in - if ~isempty(hline) - this.Data.hlines{1}=hline; - end - plot(this.Data, this.main_plot,... - 'Color',this.data_color,... - 'make_labels',true) - updateAxis(this); - updateCursors(this); - updateFits(this); - - % If the save flag is on in EventData, save the new trace - % making sure that a unique filename is generated to not - % prompt the owerwrite dialog - if isprop(EventData, 'save') && EventData.save - if isprop(EventData, 'filename_ending') - % If present, use the file name ending supplied - % externally. By default filename_ending is empty. - [~,fn,ext]=fileparts(this.filename); - this.filename=[fn, EventData.filename_ending, ext]; - - saveTrace(this, 'Data', 'make_unique_name', true); - - % Return the file name to its original value - this.filename=[fn, ext]; - else - saveTrace(this, 'Data', 'make_unique_name', true); - end - end - end - end - - %Callback function for MyFit ObjectBeingDestroyed listener. - %Removes the relevant field from the Fits struct and deletes the - %listeners from the object. - function deleteFit(this, src, ~) - %Deletes the object from the Fits struct and deletes listeners - deleteObj(this,src.fit_name); - - %Clears the fits - src.clearFit; - - %Updates cursors since the fits are removed from the plot - updateCursors(this); - end - - %Callback function for other analysis method deletion listeners. - %Does the same as above. - function deleteObj(this,name) - if ismember(name,this.open_fits) - this.Fits=rmfield(this.Fits,name); - end - deleteListeners(this, name); - end - - %Listener update function for vertical cursor - function vertCursorUpdate(this, src) - %Finds the index of the cursor. All cursors are tagged - %(name)1, (name)2, e.g. VertData2, ind is the number. - ind=str2double(src.Tag(end)); - tag=src.Tag(1:(end-1)); - %Moves the cursor labels - set(this.CrsLabels.(tag){ind},'Position',[src.Location,... - this.CrsLabels.(tag){ind}.Position(2),0]); - if strcmp(tag,'VertData') - %Sets the edit box displaying the location of the cursor - this.Gui.(sprintf('EditV%d',ind)).String=... - num2str(src.Location); - %Sets the edit box displaying the difference in locations - this.Gui.EditV2V1.String=... - num2str(this.Cursors.VertData{2}.Location-... - this.Cursors.VertData{1}.Location); - end - end - - %Listener update function for horizontal cursor - function horzCursorUpdate(this, src) - %Finds the index of the cursor. All cursors are tagged - %(name)1, (name)2, e.g. VertData2, ind is the number. - ind=str2double(src.Tag(end)); - tag=src.Tag(1:(end-1)); - %Moves the cursor labels - set(this.CrsLabels.(tag){ind},'Position',... - [this.CrsLabels.(tag){ind}.Position(1),... - src.Location,0]); - if strcmp(tag,'HorzData') - %Sets the edit box displaying the location of the cursor - this.Gui.(sprintf('EditH%d',ind)).String=... - num2str(src.Location); - %Sets the edit box displaying the difference in locations - this.Gui.EditH2H1.String=... - num2str(this.Cursors.HorzData{2}.Location-... - this.Cursors.HorzData{1}.Location); - end - end - - %Function that deletes listeners from the listeners struct, - %corresponding to an object of name obj_name - deleteListeners(this, obj_name); - end - - %Get functions for dependent variables without set functions - methods - %Get function from save directory - function save_dir=get.save_dir(this) - save_dir=createSessionPath(this.base_dir,this.session_name); - end - - %Get function for the plot handles - function main_plot=get.main_plot(this) - main_plot=this.Gui.figure1.CurrentAxes; - end - - %Get function for open fits - function open_fits=get.open_fits(this) - open_fits=fieldnames(this.Fits); - end - - %Get function that displays names of open cursors - function open_crs=get.open_crs(this) - open_crs=fieldnames(this.Cursors); - end - end - - %Get and set functions for dependent properties with SetAccess - methods - function base_dir=get.base_dir(this) - try - base_dir=this.Gui.BaseDir.String; - catch - base_dir=pwd; - end - end - - function set.base_dir(this,base_dir) - this.Gui.BaseDir.String=base_dir; - end - - function session_name=get.session_name(this) - try - session_name=this.Gui.SessionName.String; - catch - session_name=''; - end - end - - function set.session_name(this,session_name) - this.Gui.SessionName.String=session_name; - end - - % Always return filename with extension - function filename=get.filename(this) - try - filename=this.Gui.FileName.String; - [~,~,ext]=fileparts(filename); - if isempty(ext) - % Add default file extension - filename=[filename, this.default_ext]; - end - catch - filename=['placeholder', this.default_ext]; - end - end - - function set.filename(this, str) - [~,fn,ext]=fileparts(str); - if strcmpi(ext, this.default_ext) - % By default display filename without extension - this.Gui.FileName.String=fn; - else - this.Gui.FileName.String=[fn,ext]; - end - end - - function set.default_ext(this, str) - assert(ischar(str)&&(str(1)=='.'), ['Default file ' ... - 'extension must be a character string startign ' ... - 'with ''.''']) - this.default_ext=str; - end - end -end \ No newline at end of file diff --git a/@MyDaq/MyDaq.mlapp b/@MyDaq/MyDaq.mlapp new file mode 100644 index 0000000..8649af8 Binary files /dev/null and b/@MyDaq/MyDaq.mlapp differ diff --git a/@MyDaq/deleteListeners.m b/@MyDaq/deleteListeners.m deleted file mode 100644 index 2582fb7..0000000 --- a/@MyDaq/deleteListeners.m +++ /dev/null @@ -1,19 +0,0 @@ -function deleteListeners(this,obj_name) -%Finds if the object has listeners in the listeners structure -if ismember(obj_name, fieldnames(this.Listeners)) - %Grabs the fieldnames of the object's listeners structure - names=fieldnames(this.Listeners.(obj_name)); - for i=1:length(names) - %Deletes the listeners - if iscell(this.Listeners.(obj_name).(names{i})) - cellfun(@(x) delete(x), this.Listeners.(obj_name).(names{i})); - else - delete(this.Listeners.(obj_name).(names{i})); - end - %Removes the field from the structure - this.Listeners.(obj_name)=... - rmfield(this.Listeners.(obj_name),names{i}); - end - %Removes the object's field from the structure - this.Listeners=rmfield(this.Listeners, obj_name); -end \ No newline at end of file diff --git a/@MyDaq/initGui.m b/@MyDaq/initGui.m deleted file mode 100644 index f9380f3..0000000 --- a/@MyDaq/initGui.m +++ /dev/null @@ -1,116 +0,0 @@ -%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 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/@MyDataSource/MyDataSource.m b/@MyDataSource/MyDataSource.m index 9fc3b0d..be32d65 100644 --- a/@MyDataSource/MyDataSource.m +++ b/@MyDataSource/MyDataSource.m @@ -1,72 +1,51 @@ % Class that contains functionality of transferring trace to Collector and % then to Daq classdef MyDataSource < handle - properties (GetAccess=public, SetAccess={?MyClassParser,?MyDataSource}) - % name is sometimes used as identifier in listeners callbacks, so - % it better not to be changed after the object is created. - % Granting MyClassParser access to this variable allows to - % conveniently assign it in a subclass constructor from name-value - % pairs. - % Explicitly granting access to MyDataSource makes setting this - % property to be accessible to subclasses (i.e. protected and not - % private). - name='MyDataSource' - end - - % There does not seem to be a way to have a read-only protected access - % for a handle variable, so keep it public - properties (Access=public) - Trace % An object derived from MyTrace + properties (Access = public) + + % An object derived from MyTrace + Trace end events NewData end - methods (Access=public) + methods (Access = public) + + function this = MyDataSource() + this.Trace = MyTrace(); + end - %Trigger event signaling the acquisition of a new trace. - %Any properties of MyNewDataEvent can be set by indicating the - %corresponding name-value pars in varargin. For the list of options - %see the definition of MyNewDataEvent. + % Trigger event signaling the acquisition of a new trace. + % Any properties of MyNewDataEvent can be set by indicating the + % corresponding name-value pars in varargin. For the list of + % options see the definition of MyNewDataEvent. function triggerNewData(this, varargin) EventData = MyNewDataEvent(varargin{:}); - EventData.src_name=this.name; + % Pass trace by value to make sure that it is not modified % before being transferred if isempty(EventData.Trace) + % EventData.Trace can be set either automaticallt here or % explicitly as a name-value pair supplied to the function. - EventData.Trace=copy(this.Trace); + EventData.Trace = copy(this.Trace); end - notify(this,'NewData',EventData); + + notify(this, 'NewData', EventData); end end - %% Set and get methods - methods - - % Ensures that the instrument name is a valid Matlab variable - function set.name(this, str) - assert(ischar(str), ['The value assigned to ''name'' ' ... - 'property must be char']) - if ~isempty(str) - str=matlab.lang.makeValidName(str); - else - str=class(this); - end - this.name=str; - end - function set.Trace(this, Val) assert(isa(Val, 'MyTrace'), ['Trace must be a derivative ' ... 'of MyTrace class.']) - this.Trace=Val; + this.Trace = Val; end end end diff --git a/@MyDpo/MyDpo.m b/@MyDpo/MyDpo.m deleted file mode 100644 index 744540b..0000000 --- a/@MyDpo/MyDpo.m +++ /dev/null @@ -1,173 +0,0 @@ -% Class for controlling 4-channel Tektronix DPO scopes. -% Tested with DPO4034, DPO3034 -classdef MyDpo < MyScpiInstrument - properties (SetAccess=protected, GetAccess=public) - % List of the physical knobs, which can be rotated programmatically - knob_list = {'GPKNOB1','GPKNOB2','HORZPos','HORZScale',... - 'TRIGLevel','PANKNOB1','VERTPOS','VERTSCALE','ZOOM'}; - end - - properties (Constant=true) - N_CHANNELS = 4; % number of channels - end - - methods (Access=public) - function this=MyDpo(interface, address, varargin) - this@MyScpiInstrument(interface, address, varargin{:}); - % 2e7 is the maximum trace size of DPO4034-3034 - %(10 mln point of 2-byte integers) - this.Device.InputBufferSize = 2.1e7; %byte - this.Trace.name_x='Time'; - this.Trace.name_y='Voltage'; - end - - function readTrace(this) - %set data format to be signed integer, reversed byte order - fprintf(this.Device,':DATA:ENCDG SRIbinary'); - %2 bytes per measurement point - fprintf(this.Device,':DATA:WIDTH 2'); - % read the entire trace - fprintf(this.Device,':DATA:START 1;STOP %i',this.point_no); - fprintf(this.Device,':CURVE?'); - y_data = int16(binblockread(this.Device,'int16')); - - % Reading the relevant parameters from the scope - readProperty(this,'unit_y','unit_x',... - 'step_x','step_y','x_zero','y_zero','y_offset'); - - % Calculating the y data - y = (double(y_data)-this.y_offset)*this.step_y+this.y_zero; - n_points=length(y); - % Calculating the x axis - x = linspace(this.x_zero,... - this.x_zero+this.step_x*(n_points-1),n_points); - - this.Trace.x = x; - this.Trace.y = y; - % Discard "" when assiging the Trace labels - this.Trace.unit_x = this.unit_x(2:end-1); - this.Trace.unit_y = this.unit_y(2:end-1); - triggerNewData(this); - end - - function acquireContinuous(this) - openDevice(this); - fprintf(this.Device,... - ':ACQuire:STOPAfter RUNSTop;:ACQuire:STATE ON'); - closeDevice(this); - end - - function acquireSingle(this) - openDevice(this); - fprintf(this.Device,... - ':ACQuire:STOPAfter SEQuence;:ACQuire:STATE ON'); - closeDevice(this); - end - - function turnKnob(this,knob,nturns) - openDevice(this); - fprintf(this.Device, sprintf(':FPAnel:TURN %s,%i',knob,nturns)); - closeDevice(this); - end - end - - %% Protected functions - methods (Access=protected) - function createCommandList(this) - addCommand(this,'channel',':DATa:SOUrce',... - 'default',1,... - 'fmt_spec','CH%i',... - 'info','Channel from which the data is transferred'); - addCommand(this, 'ctrl_channel', ':SELect:CONTROl',... - 'default',1,... - 'fmt_spec','CH%i',... - 'info','Channel currently selected in the scope display'); - % units and scale for x and y waveform data - addCommand(this,'unit_x',':WFMOutpre:XUNit','access','r',... - 'classes',{'char'}); - addCommand(this,'unit_y',':WFMOutpre:YUNit','access','r',... - 'classes',{'char'}); - addCommand(this,'step_y',':WFMOutpre:YMUlt','access','r',... - 'classes',{'numeric'}); - addCommand(this,'step_x',':WFMOutpre:XINcr','access','r',... - 'classes',{'numeric'}); - addCommand(this,'x_zero',':WFMOutpre:XZEro','access','r',... - 'classes',{'numeric'}); - addCommand(this,'y_zero',':WFMOutpre:YZEro','access','r',... - 'classes',{'numeric'}); - addCommand(this,'y_offset',':WFMOutpre:YOFf','access','r',... - 'classes',{'numeric'}); - addCommand(this, 'point_no',':HORizontal:RECOrdlength',... - 'default', 100000,... - 'val_list', {1000, 10000, 100000, 1000000, 10000000},... - 'fmt_spec','%i',... - 'info','Numbers of points in the scope trace'); - % time scale in s per div - addCommand(this, 'time_scale',':HORizontal:SCAle',... - 'default',10E-3,... - 'fmt_spec','%e',... - 'info','Time scale (s/div)'); - % trigger level - addCommand(this, 'trig_lev', ':TRIGger:A:LEVel',... - 'default',1,... - 'fmt_spec','%e'); - % trigger slope - addCommand(this, 'trig_slope', ':TRIGger:A:EDGE:SLOpe',... - 'default', 'RISe', 'val_list',{'RISe','RIS','FALL'},... - 'fmt_spec','%s'); - % trigger source - addCommand(this, 'trig_source', ':TRIGger:A:EDGE:SOUrce',... - 'default', 'AUX', 'val_list', {'CH1','CH2','CH3','CH4',... - 'AUX','EXT','LINE'},... - 'fmt_spec','%s'); - % trigger mode - addCommand(this, 'trig_mode', ':TRIGger:A:MODe',... - 'default', 'AUTO', 'val_list',{'AUTO','NORMal','NORM'},... - 'fmt_spec','%s'); - % state of the data acquisition by the scope - addCommand(this, 'acq_state', ':ACQuire:STATE',... - 'default',true,... - 'fmt_spec','%b',... - 'info','State of data acquisition by the scope'); - % acquisition mode - addCommand(this, 'acq_mode', ':ACQuire:MODe',... - 'default', 'HIR', 'val_list',{'SAM','PEAK','HIR','AVE',... - 'ENV','SAMple','PEAKdetect','HIRes','AVErage','ENVelope'},... - 'fmt_spec','%s',... - 'info',['Acquisition mode: sample, peak detect, ',... - 'high resolution, average or envelope']); - - % Parametric commands - for i = 1:this.N_CHANNELS - i_str = num2str(i); - % coupling, AC, DC or GND - addCommand(this,... - ['cpl',i_str],[':CH',i_str,':COUP'],... - 'default','DC', 'val_list', {'AC','DC','GND'},... - 'fmt_spec','%s',... - 'info','Channel coupling: AC, DC or GND'); - % impedances, 1MOhm or 50 Ohm - addCommand(this,... - ['imp',i_str],[':CH',i_str,':IMPedance'],... - 'default','MEG', 'val_list', {'FIFty','FIF','MEG'},... - 'fmt_spec','%s',... - 'info','Channel impedance: 1 MOhm or 50 Ohm'); - % offset - addCommand(this,... - ['offset',i_str],[':CH',i_str,':OFFSet'],'default',0,... - 'fmt_spec','%e',... - 'info','(V)'); - % scale, V/Div - addCommand(this,... - ['scale',i_str],[':CH',i_str,':SCAle'],'default',1,... - 'fmt_spec','%e',... - 'info','Channel y scale (V/div)'); - % channel enabled - addCommand(this,... - ['enable',i_str],[':SEL:CH',i_str],'default',true,... - 'fmt_spec','%b',... - 'info','Channel enabled'); - end - end - end -end \ No newline at end of file diff --git a/@MyDso/MyDso.m b/@MyDso/MyDso.m deleted file mode 100644 index fa6cf1d..0000000 --- a/@MyDso/MyDso.m +++ /dev/null @@ -1,175 +0,0 @@ -% Class for controlling 4-channel Agilent DSO scopes. -% Tested with DSO7034A -classdef MyDso 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)})); - switch this.fit_name - case 'Linear' - %Fits polynomial of order 1 - this.coeffs=polyfit(this.Data.x,this.Data.y,1); - case 'Quadratic' - %Fits polynomial of order 2 - this.coeffs=polyfit(this.Data.x,this.Data.y,2); - this.Fit.y=polyval(this.coeffs,this.Fit.x); - case 'Lorentzian' - scale_factor=max(this.Data.y); - this.Data.y=this.Data.y/scale_factor; - this.init_params(1)=this.init_params(1)/scale_factor; - this.init_params(4)=this.init_params(4)/scale_factor; - doFit(this); - this.coeffs(1)=this.coeffs(1)*scale_factor; - this.coeffs(4)=this.coeffs(4)*scale_factor; - this.Data.y=this.Data.y*scale_factor; - case 'Exponential' - x_tr=min(this.Data.x); - this.Data.x=this.Data.x-x_tr; - this.init_params(1)=... - this.init_params(1)*exp(this.init_params(2)*x_tr); - doFit(this); - this.Data.x=this.Data.x+x_tr; - this.coeffs(1)=... - this.coeffs(1)*exp(-this.coeffs(2)*x_tr); - case {'LorentzianGrad','Gaussian',... - 'DoubleLorentzian','DoubleLorentzianGrad',... - 'Exponential','Gorodetsky2000',... - 'Gorodetsky2000plus'} - doFit(this) - otherwise - error('Selected fit is invalid'); - end - - %This function calculates the fit trace, using this.x_vec as - %the x axis + %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); - %This function calculates user-defined parameters + + %Calculate user parameters that depend on the fit parameters calcUserParams(this); - %Sets the new initial parameters to be the fitted parameters - this.init_params=this.coeffs; + + %Update fit metadata + this.Fit.UserMetadata = createMetadata(this); + %Updates the gui if it is enabled if this.enable_gui genSliderVecs(this); - updateGui(this); + updateSliderPanel(this); end + %Plots the fit if the flag is on - if this.enable_plot; plotFit(this); end - %Triggers new fit event - triggerNewFit(this); - end - - %This function calculates all the user-defined parameters shown in - %the GUI. To add more parameters, add them in createUserGuiStruct, - %then add them here when you wish to calculate them. - function calcUserParams(this) - switch this.fit_name - case 'Lorentzian' - this.mech_lw=this.coeffs(2); %#ok - this.mech_freq=this.coeffs(3); %#ok - this.Q=this.mech_freq/this.mech_lw; %#ok - this.opt_lw=convOptFreq(this,this.coeffs(2)); %#ok - this.Qf=this.mech_freq*this.Q; %#ok - case 'DoubleLorentzian' - this.opt_lw1=convOptFreq(this,this.coeffs(2)); %#ok - this.opt_lw2=convOptFreq(this,this.coeffs(5)); %#ok - splitting=abs(this.coeffs(6)-this.coeffs(3)); - this.mode_split=convOptFreq(this,splitting); %#ok - case 'Exponential' - if ~isempty(this.coeffs) - this.tau=abs(1/this.coeffs(2)); %#ok - this.lw=abs(this.coeffs(2)/pi); %#ok - end - this.Q=pi*this.freq*this.tau; %#ok - this.Qf=this.Q*this.freq; %#ok - otherwise - %If fit_name is not listed, do nothing + if this.enable_plot + plotFit(this); end - - end - - %This function is used to convert the x-axis to frequency. - function real_freq=convOptFreq(this,freq) - real_freq=freq*this.spacing*this.line_no/this.CalVals.line_spacing; end - %This struct is used to generate the UserGUI. Fields are seen under - %tabs in the GUI. To create a new tab, you have to enter it under - %this.UserGui.Tabs. A tab must have a tab_title and a field to add - %Children. To add a field, use the addUserField function. - function createUserGuiStruct(this) - this.UserGui=struct('Fields',struct(),'Tabs',struct()); - switch this.fit_name - case 'Lorentzian' - %Parameters for the tab relating to mechanics - this.UserGui.Tabs.Mech.tab_title='Mech.'; - this.UserGui.Tabs.Mech.Children={}; - addUserField(this,'Mech','mech_lw','Linewidth (Hz)',1,... - 'enable_flag','off') - addUserField(this,'Mech','Q',... - 'Qualify Factor (x10^6)',1e6,... - 'enable_flag','off','conv_factor',1e6) - addUserField(this,'Mech','mech_freq','Frequency (MHz)',1e6,... - 'conv_factor',1e6, 'enable_flag','off') - addUserField(this,'Mech','Qf','Q\times f (10^{14} Hz)',1e14,... - 'conv_factor',1e14,'enable_flag','off'); - - %Parameters for the tab relating to optics - this.UserGui.Tabs.Opt.tab_title='Optical'; - this.UserGui.Tabs.Opt.Children={}; - addUserField(this,'Opt','spacing',... - 'Line Spacing (MHz)',1e6,'conv_factor',1e6,... - 'Callback', @(~,~) calcUserParams(this)); - addUserField(this,'Opt','line_no','Number of lines',10,... - 'Callback', @(~,~) calcUserParams(this)); - addUserField(this,'Opt','opt_lw','Linewidth (MHz)',1e6,... - 'enable_flag','off','conv_factor',1e6); - - case 'DoubleLorentzian' - this.UserGui.Tabs.Opt.tab_title='Optical'; - this.UserGui.Tabs.Opt.Children={}; - addUserField(this,'Opt','spacing',... - 'Line Spacing (MHz)',1e6,'conv_factor',1e6,... - 'Callback', @(~,~) calcUserParams(this)); - addUserField(this,'Opt','line_no','Number of lines',10,... - 'Callback', @(~,~) calcUserParams(this)); - addUserField(this,'Opt','opt_lw1','Linewidth 1 (MHz)',1e6,... - 'enable_flag','off','conv_factor',1e6); - addUserField(this,'Opt','opt_lw2','Linewidth 2 (MHz)',1e6,... - 'enable_flag','off','conv_factor',1e6); - addUserField(this,'Opt','mode_split',... - 'Modal splitting (MHz)',1e6,... - 'enable_flag','off','conv_factor',1e6); - case 'Exponential' - this.UserGui.Tabs.Q.tab_title='Q'; - this.UserGui.Tabs.Q.Children={}; - addUserField(this,'Q','tau','\tau (s)',1,... - 'enable_flag','off') - addUserField(this,'Q','lw','Linewidth (Hz)',1,... - 'enable_flag','off') - addUserField(this,'Q','Q',... - 'Qualify Factor (x10^6)',1e6,... - 'enable_flag','off','conv_factor',1e6) - addUserField(this,'Q','freq','Frequency (MHz)',1e6,... - 'conv_factor',1e6, 'enable_flag','on',... - 'Callback',@(~,~) calcUserParams(this)); - addUserField(this,'Q','Qf','Q\times f (10^{14} Hz)',1e14,... - 'conv_factor',1e14,'enable_flag','off'); - addUserField(this,'Q','tag','Tag (number)','',... - 'enable_flag','on') - otherwise - %Do nothing if there is no defined user parameters - end + %Clears the plots + function clearFit(this) + clearData(this.Fit); + deleteLine(this.Fit); end - %Parent is the parent tab for the userfield, tag is the tag given - %to the GUI element, title is the text written next to the field, - %initial value is the initial value of the property and change_flag - %determines whether the gui element is enabled for writing or not. - %conv_factor is used to have different units in the field. In the - %program, the value is always saved as the bare value. - function addUserField(this, parent, tag, title, ... - init_val,varargin) - %Parsing inputs - p=inputParser(); - addRequired(p,'Parent'); - addRequired(p,'Tag'); - addRequired(p,'Title'); - addRequired(p,'init_val'); - addParameter(p,'enable_flag','on'); - addParameter(p,'Callback',''); - addParameter(p,'conv_factor',1); - - parse(p,parent,tag,title,init_val,varargin{:}); - tag=p.Results.Tag; - - %Populates the UserGui struct - this.UserGui.Fields.(tag).parent=p.Results.Parent; - this.UserGui.Fields.(tag).title=p.Results.Title; - this.UserGui.Fields.(tag).init_val=p.Results.init_val; - this.UserGui.Fields.(tag).enable_flag=... - p.Results.enable_flag; - this.UserGui.Fields.(tag).conv_factor=p.Results.conv_factor; - this.UserGui.Fields.(tag).Callback=... - p.Results.Callback; - - this.UserGui.Tabs.(p.Results.Parent).Children{end+1}=tag; - %Adds the new property to the class - addUserProp(this, tag); + %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 - %Every user field has an associated property, which is added by - %this function. The get and set functions are set to use the GUI - %through the getUserVal and setUserVal functions if the GUI is - %enabled. - function addUserProp(this,tag) - prop=addprop(this,tag); - if this.enable_gui - prop.GetMethod=@(this) getUserVal(this,tag); - prop.SetMethod=@(this, val) setUserVal(this, val, tag); - prop.Dependent=true; - end - end - - %This function gets the value of the userprop from the GUI. The GUI - %is the single point of truth - function val=getUserVal(this, tag) - conv_factor=this.UserGui.Fields.(tag).conv_factor; - val=str2double(this.Gui.([tag,'Edit']).String)*conv_factor; - end - - %As above, but instead we set the GUI through setting the property - function setUserVal(this, val, tag) - conv_factor=this.UserGui.Fields.(tag).conv_factor; - this.Gui.([tag,'Edit']).String=num2str(val/conv_factor); - end %Generates model-dependent initial parameters, lower and upper %boundaries. function genInitParams(this) - assert(validateData(this), ['The data must be vectors of',... - ' equal length greater than the number of fit parameters.',... - ' Currently the number of fit parameters is %d, the',... - ' length of x is %d and the length of y is %d'],... - this.n_params,length(this.Data.x),length(this.Data.y)); - %Cell for putting parameters in to be interpreted in the - %parser. Element 1 contains the init params, Element 2 contains - %the lower limits and Element 3 contains the upper limits. - params={}; - - switch this.fit_name - case 'Exponential' - [params{1},params{2},params{3}]=... - initParamExponential(this.Data.x,this.Data.y); - case 'Gaussian' - [params{1},params{2},params{3}]=... - initParamGaussian(this.Data.x,this.Data.y); - case 'Lorentzian' - [params{1},params{2},params{3}]=... - initParamLorentzian(this.Data.x,this.Data.y); - case 'LorentzianGrad' - [params{1},params{2},params{3}]=... - initParamLorentzianGrad(this.Data.x,this.Data.y); - case 'DoubleLorentzian' - [params{1},params{2},params{3}]=... - initParamDblLorentzian(this.Data.x,this.Data.y); - case 'DoubleLorentzianGrad' - [params{1},params{2},params{3}]=... - initParamDblLorentzianGrad(this.Data.x,this.Data.y); - case 'Gorodetsky2000' - [params{1},params{2},params{3}]=... - initParamGorodetsky2000(this.Data.x,this.Data.y); - case 'Gorodetsky2000plus' - [params{1},params{2},params{3}]=... - initParamGorodetsky2000Plus(this.Data.x,this.Data.y); - end - - %Validates the initial parameters - p=createFitParser(this.n_params); - parse(p,params{:}); + validateData(this); - %Loads the parsed results into the class variables - this.init_params=p.Results.init_params; - this.lim_lower=p.Results.lower; - this.lim_upper=p.Results.upper; + calcInitParams(this); + calcFit(this); + calcUserParams(this); %Plots the fit function with the new initial parameters - if this.enable_plot; plotInitFun(this); end + 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); - updateGui(this); + updateSliderPanel(this); end end - %Calculates the trace object for the fit - function calcFit(this) - this.Fit.x=this.x_vec; - input_coeffs=num2cell(this.coeffs); - this.Fit.y=this.anon_fit_fun(this.Fit.x,input_coeffs{:}); + + % 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 - %Plots the trace contained in the Fit MyTrace object after - %calculating the new values - function plotFit(this,varargin) - calcFit(this); - assert((isa(this.plot_handle,'matlab.graphics.axis.Axes')||... - isa(this.plot_handle,'matlab.ui.control.UIAxes')),... - 'plot_handle property must be defined to valid axis in order to plot') - this.Fit.plot(this.plot_handle,varargin{:}); - clearInitFun(this); + + % 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) - %Clears the plots - function clearFit(this) - cellfun(@(x) delete(x), this.Fit.hlines); - clearInitFun(this); - this.Fit.hlines={}; + %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 - function clearInitFun(this) - delete(this.hline_init); - this.hline_init=[]; + % Calculate user parameters from fit parameters. + % Dummy method that needs to be overloaded in subclasses. + function calcUserParams(this) %#ok end - %Function for plotting fit model with current initial parameters. - function plotInitFun(this) - %Substantially faster than any alternative - generating - %anonymous functions is very cpu intensive. - - input_cell=num2cell(this.init_params); - y_vec=feval(this.FitStruct.(this.fit_name).anon_fit_fun,... - this.x_vec,input_cell{:}); - if isempty(this.hline_init) - this.hline_init=plot(this.plot_handle,this.x_vec,y_vec,... - 'Color',this.init_color); + 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 - set(this.hline_init,'XData',this.x_vec,'YData',y_vec); + Mp.SetAccess = 'private'; end end - end - - %Callbacks - methods - %Save fit function callback - function saveFitCallback(this,~,~) - assert(~isempty(this.base_dir),'Save directory is not specified'); - assert(ischar(this.base_dir),... - ['Save directory is not specified.',... - ' Should be of type char but is %s.'], ... - class(this.base_dir)) - this.Fit.save('name',this.filename,... - 'base_dir',this.save_path) - end - %Callback for saving parameters - function saveParamCallback(this,~,~) - saveParams(this); + % 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.init_params(i); - set(this.Gui.(sprintf('Slider_%s',this.fit_params{i})),... + this.slider_vecs{i}=def_vec*this.param_vals(i); + set(this.Gui.(sprintf('Slider_%s', this.fit_params{i})),... 'Value',50); end - - %Here we can put special cases such that the sliders can behave - %differently for different fits. - if validateData(this) - switch this.fit_name - case 'Lorentzian' - %We choose to have the slider go over the range of - %the x-values of the plot for the center of the - %Lorentzian. - this.slider_vecs{3}=... - linspace(this.x_vec(1),this.x_vec(end),101); - %Find the index closest to the init parameter - [~,ind]=... - min(abs(this.init_params(3)-this.slider_vecs{3})); - %Set to ind-1 as the slider goes from 0 to 100 - set(this.Gui.(sprintf('Slider_%s',... - this.fit_params{3})),'Value',ind-1); - end - end end - %Callback functions for sliders in GUI. Uses param_ind to find out - %which slider the call is coming from, this was implemented to - %speed up the callback. Note that this gets triggered whenever the - %value of the slider is changed. - function sliderCallback(this, param_ind, hObject, ~) - %Gets the value from the slider - val=get(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 - [~,ind]=... - min(abs(this.init_params(param_ind)-this.slider_vecs{param_ind})); - if ind~=(val+1) - %Updates the scale with a new value from the lookup table - this.init_params(param_ind)=... - this.slider_vecs{param_ind}(val+1); - %Updates the edit box with the new value from the slider - set(this.Gui.(sprintf('Edit_%s',this.fit_params{param_ind})),... - 'String',sprintf('%3.3e',this.init_params(param_ind))); - if this.enable_plot; plotInitFun(this); end - end - end - - %Callback function for edit boxes in GUI - function editCallback(this, hObject, ~) - init_param=str2double(hObject.String); - param_ind=str2double(hObject.Tag); - - %Centers the slider - set(this.Gui.(sprintf('Slider_%s',this.fit_params{param_ind})),... - 'Value',50); - %Updates the correct initial parameter - this.init_params(param_ind)=init_param; - if this.enable_plot; plotInitFun(this); end - %Triggers event for new init values - triggerNewInitVal(this); - %Generate the new slider vectors - genSliderVecs(this); + %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 - %Callback function for editing limits in the GUI - function limEditCallback(this, hObject,~) - lim = str2double(hObject.String); - %Regexp finds type (lower or upper bound) and index - expr = '(?Upper|Lower)(?\d+)'; - s=regexp(hObject.Tag,expr,'names'); - ind=str2double(s.ind); - - switch s.type - case 'Lower' - this.lim_lower(ind)=lim; - case 'Upper' - this.lim_upper(ind)=lim; - otherwise - error('%s is not properly named for assignment of limits',... - hObject.Tag); + %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 - %Callback function for analyze button in GUI. Checks if the data is - %ready for fitting. - function analyzeCallback(this, ~, ~) - fitTrace(this); - end - - %Callback for clearing the fits on the axis. - function clearFitCallback(this,~,~) - clearFit(this); - end - - %Callback function for generate init parameters button. - function initParamCallback(this,~,~) - genInitParams(this); + %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 - %Private methods - methods(Access=private) - %Creates the GUI of MyFit, in separate file. - createGui(this); - - %Creates a panel for the GUI, in separate file - createTab(this, tab_tag, bg_color, button_h); + %Callbacks + methods (Access = protected) - %Creats two vboxes (from GUI layouts) to display values of - %quantities - createUnitBox(this, bg_color, h_parent, name); + %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 an edit box inside a UnitDisp for showing label and value of - %a quantity. Used in conjunction with createUnitBox - createUnitDisp(this,varargin); + %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})); - %Sets the class variables to the inputs from the inputParser. - function parseInputs(this) - for i=1:length(this.Parser.Parameters) - %Takes the value from the inputParser to the appropriate - %property. - if isprop(this,this.Parser.Parameters{i}) - this.(this.Parser.Parameters{i})=... - this.Parser.Results.(this.Parser.Parameters{i}); + 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 - end - - %Does the fit with the currently set parameters - function doFit(this) - ft=fittype(this.fit_function,'coefficients',this.fit_params); - opts=fitoptions('Method','NonLinearLeastSquares',... - 'Lower',this.lim_lower,... - 'Upper',this.lim_upper,... - 'StartPoint',this.init_params,... - 'MaxFunEvals',2000,... - 'MaxIter',2000,... - 'TolFun',1e-6,... - 'TolX',1e-6); - %Fits with the below properties. Chosen for maximum accuracy. - [this.Fitdata,this.Gof,this.FitInfo]=... - fit(this.Data.x,this.Data.y,ft,opts); - %Puts the coeffs into the class variable. - this.coeffs=coeffvalues(this.Fitdata); - end - - %Triggers the NewFit event such that other objects can use this to - %e.g. plot new fits - function triggerNewFit(this) - notify(this,'NewFit'); - end - - %Triggers the NewInitVal event - function triggerNewInitVal(this) - notify(this,'NewInitVal'); - end - - %Creates the struct used to get all things relevant to the fit - %model. Ensure that fit parameters are listed alphabetically, as - %otherwise the anon_fit_fun will not work properly. - function createFitStruct(this) - %Adds fits - addFit(this,'Linear','a*x+b','$$ax+b$$',{'a','b'},... - {'Gradient','Offset'}) - addFit(this,'Quadratic','a*x^2+b*x+c','$$ax^2+bx+c$$',... - {'a','b','c'},{'Quadratic coeff.','Linear coeff.','Offset'}); - addFit(this,'Gaussian','a*exp(-((x-c)/b)^2/2)+d',... - '$$ae^{-\frac{(x-c)^2}{2b^2}}+d$$',{'a','b','c','d'},... - {'Amplitude','Width','Center','Offset'}); - addFit(this,'Lorentzian','1/pi*a*b/2/((x-c)^2+(b/2)^2)+d',... - '$$\frac{a}{\pi}\frac{b/2}{(x-c)^2+(b/2)^2}+d$$',... - {'a','b','c','d'},... - {'Amplitude','Width','Center','Offset'}); - addFit(this,'LorentzianGrad','1/pi*a*b/2/((x-c)^2+(b/2)^2)+d*(x-c)+e',... - '$$\frac{a}{\pi}\frac{b/2}{(x-c)^2+(b/2)^2}+d*(x-b)+e$$',... - {'a','b','c','d','e'},... - {'Amplitude','Width','Center','Gradient','Offset'}); - addFit(this,'Exponential','a*exp(b*x)+c',... - '$$ae^{bx}+c$$',{'a','b','c'},... - {'Amplitude','Rate','Offset'}); - addFit(this,'DoubleLorentzian',... - '1/pi*b/2*a/((x-c)^2+(b/2)^2)+1/pi*e/2*d/((x-f)^2+(e/2)^2)+g',... - '$$\frac{a}{\pi}\frac{b/2}{(x-c)^2+(b/2)^2}+\frac{d}{\pi}\frac{e/2}{(x-f)^2+(e/2)^2}+g$$',... - {'a','b','c','d','e','f','g'},... - {'Amplitude 1','Width 1','Center 1','Amplitude 2',... - 'Width 2','Center 2','Offset'}); - addFit(this,'DoubleLorentzianGrad',... - '1/pi*b/2*a/((x-c)^2+(b/2)^2)+1/pi*e/2*d/((x-f)^2+(e/2)^2)+g*(x-c)+h',... - '$$\frac{a}{\pi}\frac{b/2}{(x-c)^2+(b/2)^2}+\frac{d}{\pi}\frac{e/2}{(x-f)^2+(e/2)^2}+g*x+h$$',... - {'a','b','c','d','e','f','g','h'},... - {'Amplitude 1','Width 1','Center 1','Amplitude 2',... - 'Width 2','Center 2','Gradient','Offset'}); - addFit(this,'Gorodetsky2000',... - ['a*abs( (k0^2/4 - kex^2/4 + gamma^2/4 - (x-b).^2 + i*k0.*(x-b))',... - './( (k0 + kex)^2/4 + gamma^2/4 - (x-b).^2 + i.*(x-b)*(k0 + kex) )).^2+c*(x-b)'],... - ['$$a\left|\frac{\kappa_0^2/4-\kappa_{ex}^2/4+\gamma^2/4-(x-b)^2+i\kappa_0(x-b)/2}',... - '{(\kappa_0+\kappa_{ex})^2/4+\gamma^2/4-(x-b)^2+i(x-b)(\kappa_0+\kappa_{ex})}\right|^2$$+c(x-b)'],... - { 'a','b','c','gamma','k0', 'kex'},... - {'Background','Center','BG Slope','Mode splitting',... - 'Intrinsic','Extrinsic'}); - addFit(this,'Gorodetsky2000plus',... - ['(a+c*x+d*x.^2+e*x.^2)*abs( (k0^2/4 - kex^2/4 + gamma^2/4 - (x-b).^2 + i*k0.*(x-b))',... - './( (k0 + kex)^2/4 + gamma^2/4 - (x-b).^2 + i.*(x-b)*(k0 + kex) )).^2+f'],... - ['$$\left[a+cx+dx^2\right]\left|\frac{\kappa_0^2/4-\kappa_{ex}^2/4+\gamma^2/4-(x-b)^2+i\kappa_0(x-b)/2}',... - '{(\kappa_0+\kappa_{ex})^2/4+\gamma^2/4-(x-b)^2+i(x-b)(\kappa_0+\kappa_{ex})}\right|^2$$'],... - { 'a','b','c','d','e','f','gamma','k0', 'kex'},... - {'Baseline','Center','Lin. Coeff','Quad. Coeff','Cubic Coeff.',... - 'Background','Mode splitting','Intrinsic','Extrinsic'}); + f = @sliderStateChangedCallback; end - %Adds a fit to the list of fits. See above for real examples - %fit_function: the function used to fit to in MATLAB form - %fit_tex: the fit function written in tex for display in the GUI - %fit_params: the fit parameters - %fit_param_names: longer names of fit parameters for GUI - function addFit(this,fit_name,fit_function,fit_tex,fit_params,... - fit_param_names) - this.FitStruct.(fit_name).fit_function=fit_function; - this.FitStruct.(fit_name).fit_tex=fit_tex; - this.FitStruct.(fit_name).fit_params=fit_params; - this.FitStruct.(fit_name).fit_param_names=fit_param_names; - %Generates the anonymous fit function from the above - args=['@(x,', strjoin(fit_params,','),')']; - this.FitStruct.(fit_name).anon_fit_fun=... - str2func(vectorize([args,fit_function])); - end - - %Updates the GUI if the edit or slider boxes are changed from - %elsewhere. - function updateGui(this) - for i=1:this.n_params - str=this.fit_params{i}; - set(this.Gui.(sprintf('Edit_%s',str)),... - 'String',sprintf('%3.3e',this.init_params(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))) + function f = createParamFieldEditedCallback(this, ind) + function paramEditFieldCallback(hObject, ~) + val=str2double(hObject.String); + manSetParamVal(this, ind, val); end + + f = @paramEditFieldCallback; end - %Checks if the class is ready to perform a fit - function bool=validateData(this) - bool=~isempty(this.Data.x) && ~isempty(this.Data.y) && ... - length(this.Data.x)==length(this.Data.y) && ... - length(this.Data.x)>=this.n_params; - end - end - - % Set function for nondependent variable - methods - %Set function for fit_name. - function set.fit_name(this,fit_name) - assert(ischar(fit_name),'The fit name must be a string'); - %Capitalizes the first letter - fit_name=[upper(fit_name(1)),lower(fit_name(2:end))]; - %Checks it is a valid fit name - ind=strcmpi(fit_name,this.valid_fit_names);%#ok - assert(any(ind),'%s is not a supported fit name',fit_name); - - this.fit_name=this.valid_fit_names{ind}; %#ok - end - end - - % Get functions for dependent variables - methods - %Generates the valid fit names - function valid_fit_names=get.valid_fit_names(this) - valid_fit_names=fieldnames(this.FitStruct); + 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 - %Grabs the correct fit function from FitStruct - function fit_function=get.fit_function(this) - fit_function=this.FitStruct.(this.fit_name).fit_function; + %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 - %Grabs the correct tex string from FitStruct - function fit_tex=get.fit_tex(this) - fit_tex=this.FitStruct.(this.fit_name).fit_tex; + function f = createLowerLimEditCallback(this, ind) + function lowerLimEditCallback(hObject, ~) + this.lim_lower(ind)=str2double(hObject.String); + end + + f = @lowerLimEditCallback; end - %Grabs the correct anon fit function from FitStruct - function anon_fit_fun=get.anon_fit_fun(this) - anon_fit_fun=this.FitStruct.(this.fit_name).anon_fit_fun; + function f = createUpperLimEditCallback(this, ind) + function upperLimEditCallback(hObject, ~) + this.lim_upper(ind)=str2double(hObject.String); + end + + f = @upperLimEditCallback; end - - %Grabs the correct fit parameters from FitStruct - function fit_params=get.fit_params(this) - fit_params=this.FitStruct.(this.fit_name).fit_params; + %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 - %Grabs the correct fit parameter names from FitStruct - function fit_param_names=get.fit_param_names(this) - fit_param_names=this.FitStruct.(this.fit_name).fit_param_names; - end - - %Calculates the number of parameters in the fit function - function n_params=get.n_params(this) - n_params=length(this.fit_params); + %Callback function for analyze button in GUI. Checks if the data is + %ready for fitting. + function analyzeCallback(this, ~, ~) + fitTrace(this); end - %Generates a vector of x values for plotting - function x_vec=get.x_vec(this) - x_vec=linspace(min(this.Data.x),max(this.Data.x),1e3); + function acceptFitCallback(this, ~, ~) + triggerNewAnalysisTrace(this, 'Trace', copy(this.Fit), ... + 'analysis_type', 'fit'); end - %Used when creating the UserGUI, finds the number of user fields. - function n_user_fields=get.n_user_fields(this) - n_user_fields=length(this.user_field_tags); + 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 - %Finds all the user field tags - function user_field_tags=get.user_field_tags(this) - user_field_tags=fieldnames(this.UserGui.Fields); + %Callback for clearing the fits on the axis. + function clearFitCallback(this, ~, ~) + clearFit(this); end - %Finds all the titles of the user field tags - function user_field_names=get.user_field_names(this) - user_field_names=cellfun(@(x) this.UserGui.Fields.(x).title,... - this.user_field_tags,'UniformOutput',0); + %Callback function for the button that generates init parameters. + function initParamCallback(this, ~, ~) + genInitParams(this); end - %Finds all the values of the user fields - function user_field_vals=get.user_field_vals(this) - user_field_vals=cellfun(@(x) this.(x), this.user_field_tags); + %Close figure callback simply calls delete function for class + function closeFigureCallback(this,~,~) + delete(this); end + end + + %Private methods + methods(Access = private) - %Generates a full path for saving - function fullpath=get.fullpath(this) - fullpath=[this.save_path,this.filename,'.txt']; - end + %Creates a panel for the GUI, in separate file + createUserControls(this, varargin); - %Generates a base path for saving - function save_path=get.save_path(this) - save_path=createSessionPath(this.base_dir,this.session_name); + %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 - %Set and get functions for dependent variables with SetAccess methods - %Gets the base dir from the gui - function base_dir=get.base_dir(this) - if this.enable_gui - base_dir=this.Gui.BaseDir.String; - else - base_dir=this.SaveInfo.base_dir; - end - end - function set.base_dir(this,base_dir) - if this.enable_gui - this.Gui.BaseDir.String=base_dir; - else - this.SaveInfo.base_dir=base_dir; - end + % 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 session_name=get.session_name(this) - if this.enable_gui - session_name=this.Gui.SessionName.String; - else - session_name=this.SaveInfo.session_name; + 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 - function set.session_name(this,session_name) - if this.enable_gui - this.Gui.SessionName.String=session_name; + % 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 - this.SaveInfo.session_name=session_name; + val = false; end end - function filename=get.filename(this) - if this.enable_gui - filename=this.Gui.FileName.String; + 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 - filename=this.SaveInfo.filename; + ind = true(1, length(this.Data.x)); end end - function set.filename(this,filename) - if this.enable_gui - this.Gui.FileName.String=filename; - else - this.SaveInfo.filename=filename; - 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/@MyFit/createGui.m b/@MyFit/createGui.m index dcb616d..7ecb8dd 100644 --- a/@MyFit/createGui.m +++ b/@MyFit/createGui.m @@ -1,265 +1,343 @@ -function createGui(this) +function createGui(this, varargin) -%Makes the fit name have the first letter capitalized -fit_name=[upper(this.fit_name(1)),this.fit_name(2:end)]; +p=inputParser(); + +%Parameter that tells the function if save panel should be created +addParameter(p,'save_panel',true,@islogical); +parse(p,varargin{:}); +enable_save_panel=p.Results.save_panel; + +if ~isempty(this.fit_name) + + %Makes the fit name have the first letter capitalized + fit_name=[upper(this.fit_name(1)),this.fit_name(2:end)]; +else + fit_name=''; +end + +%% Calculate the dimensions of GUI panels %Defines the colors for the Gui rgb_blue=[0.1843,0.4157,1]; rgb_white=[1,1,1]; %Width of the edit boxes in the GUI edit_width=140; %Height of buttons in GUI button_h=25; -%Minimum height of the four vboxes composing the gui. +%Minimum height of the hboxes composing the gui. title_h=40; equation_h=100; -savebox_h=100; +fitbox_h=3*button_h; +savebox_h=100; %Only relevant when save_panel input argument is true slider_h=130; min_fig_width=560; -%Finds the minimum height in button heights of the user field panel. This +%Finds the height in button heights of the user field panel. This %is used to calculate the height of the figure. -tab_fields=fieldnames(this.UserGui.Tabs); -max_fields=max(cellfun(@(x) length(this.UserGui.Tabs.(x).Children),tab_fields)); -if max_fields>3 - min_user_h=max_fields+2; +n_user_pars = length(fieldnames(this.UserParamList)); +if n_user_pars>0 + userpanel_h = button_h*(n_user_pars+1); else - min_user_h=5; + % Skip the user panel completely + userpanel_h = 0; end -userpanel_h=min_user_h*button_h; -fig_h=title_h+equation_h+slider_h++savebox_h+userpanel_h; +if enable_save_panel + fig_h=title_h+equation_h+fitbox_h+slider_h+savebox_h+userpanel_h; +else + fig_h=title_h+equation_h+fitbox_h+slider_h+userpanel_h; +end %Sets a minimum width -if this.n_params<4; edit_width=min_fig_width/this.n_params; end +if this.n_params < 4 + if this.n_params ~= 0 + edit_width=min_fig_width/this.n_params; + else + + % Support the case of dummy fit with no parameters + edit_width=min_fig_width; + end +end fig_width=edit_width*this.n_params; +%% Create GUI elements + %Name sets the title of the window, NumberTitle turns off the FigureN text %that would otherwise be before the title, MenuBar is the menu normally on %the figure, toolbar is the toolbar normally on the figure. %HandleVisibility refers to whether gcf, gca etc will grab this figure. -this.Gui.Window = figure('Name', 'MyFit', 'NumberTitle', 'off', ... +this.Gui.Window = figure('Name', class(this), 'NumberTitle', 'off', ... 'MenuBar', 'none', 'Toolbar', 'none', 'HandleVisibility', 'off',... - 'Units','Pixels','Position',[500,500,fig_width,fig_h]); + 'Units','Pixels','Position',[100,100,fig_width,fig_h]); + +%Place the figure in the center of the screen +centerFigure(this.Gui.Window); + %Sets the close function (runs when x is pressed) to be class function -set(this.Gui.Window, 'CloseRequestFcn',... - @(hObject,eventdata) closeFigure(this, hObject,eventdata)); +set(this.Gui.Window, 'CloseRequestFcn', @this.closeFigureCallback); + %The main vertical box. The four main panes of the GUI are stacked in the %box. We create these four boxes first so that we do not need to redraw %them later -this.Gui.MainVbox=uix.VBox('Parent',this.Gui.Window,'BackgroundColor',rgb_white); +this.Gui.MainVbox=uix.VBox('Parent',this.Gui.Window, ... + 'BackgroundColor',rgb_white); + %The title box this.Gui.Title=annotation(this.Gui.MainVbox,'textbox',[0.5,0.5,0.3,0.3],... 'String',fit_name,'Units','Normalized',... 'HorizontalAlignment','center','VerticalAlignment','middle',... 'FontSize',16,'BackgroundColor',rgb_white); + %Displays the fitted equation this.Gui.Equation=annotation(this.Gui.MainVbox,'textbox',[0.5,0.5,0.3,0.3],... 'String',this.fit_tex,... 'Units','Normalized','Interpreter','LaTeX',... 'HorizontalAlignment','center','VerticalAlignment','middle',... 'FontSize',20,'BackgroundColor',rgb_white); -%Creates an HBox for extracted parameters and user interactions with GUI -this.Gui.UserHbox=uix.HBox('Parent',this.Gui.MainVbox,... - 'BackgroundColor',rgb_white); -%Creates the HBox for saving parameters -this.Gui.SaveHbox=uix.HBox('Parent',this.Gui.MainVbox,... +%Creates an HBox for the buttons providing basic fit functionality +this.Gui.FitHbox=uix.HBox('Parent',this.Gui.MainVbox,... 'BackgroundColor',rgb_white); -%Creates the HBox for the fitting parameters -this.Gui.FitHbox=uix.HBox('Parent',this.Gui.MainVbox,'BackgroundColor',... - rgb_white); +if ~isempty(fieldnames(this.UserParamList)) + %Creates an HBox for extracted parameters and user interactions with GUI + this.Gui.UserHbox=uix.HBox('Parent',this.Gui.MainVbox,... + 'BackgroundColor',rgb_white); +end %Sets the heights and minimum heights of the five vertical boxes. -1 means %it resizes with the window -set(this.Gui.MainVbox,'Heights',[title_h,-1,userpanel_h,savebox_h,slider_h],... - 'MinimumHeights',[title_h,equation_h,userpanel_h,savebox_h,slider_h]); - -%Here we create the fit panel in the GUI. -this.Gui.FitPanel=uix.BoxPanel( 'Parent', this.Gui.UserHbox,... - 'Padding',0,'BackgroundColor', rgb_white,... - 'Title','Fit Panel','TitleColor',rgb_blue); -%Here we create the panel for the useful parameters -this.Gui.UserPanel=uix.BoxPanel( 'Parent', this.Gui.UserHbox,... - 'Padding',0,'BackgroundColor', 'w',... - 'Title','Calculated parameters','TitleColor',rgb_blue); -%Sets the widths of the above -set(this.Gui.UserHbox,'Widths',[-1,-2],'MinimumWidths',[0,375]); - -%This makes the buttons that go inside the FitPanel -this.Gui.FitVbox=uix.VBox('Parent',this.Gui.FitPanel,'BackgroundColor',... - rgb_white); -%Creates the button for analysis inside the VBox -this.Gui.AnalyzeButton=uicontrol('Parent',this.Gui.FitVbox,... - 'style','pushbutton','Background','w','String','Analyze','Callback',... - @(hObject, eventdata) analyzeCallback(this, hObject, eventdata)); -%Creates button for generating new initial parameters -this.Gui.InitButton=uicontrol('Parent',this.Gui.FitVbox,... - 'style','pushbutton','Background','w',... - 'String','Generate Init. Params','Callback',... - @(hObject, eventdata) initParamCallback(this, hObject, eventdata)); -%Creates button for clearing fits -this.Gui.ClearButton=uicontrol('Parent',this.Gui.FitVbox,... - 'style','pushbutton','Background','w','String','Clear fits','Callback',... - @(hObject, eventdata) clearFitCallback(this, hObject, eventdata)); - - -set(this.Gui.FitVbox,'Heights',[button_h,button_h,button_h]); +if enable_save_panel + %Creates the HBox for saving parameters + this.Gui.SaveHbox=uix.HBox('Parent', this.Gui.MainVbox,... + 'BackgroundColor',rgb_white); +end -this.Gui.TabPanel=uix.TabPanel('Parent',this.Gui.UserPanel,... +%Creates the HBox for the fitting parameters +this.Gui.SliderHbox=uix.HBox('Parent',this.Gui.MainVbox, ... 'BackgroundColor',rgb_white); -%Creates the user values panel with associated tabs. The cellfun here -%creates the appropriately named tabs. To add a tab, add a new field to the -%UserGuiStruct. +% Set the hights of the main box elements +% -1 stands for the element that is resized with the window +h_list = [title_h,-1,fitbox_h,slider_h]; +min_h_list = [title_h,equation_h,fitbox_h,slider_h]; -usertabs=fieldnames(this.UserGui.Tabs); - -if ~isempty(usertabs) - cellfun(@(x) createTab(this,x,rgb_white,button_h),usertabs); - this.Gui.TabPanel.TabTitles=... - cellfun(@(x) this.UserGui.Tabs.(x).tab_title, usertabs,... - 'UniformOutput',0); +if ~isempty(fieldnames(this.UserParamList)) + % Add space for the panel with user parameters + h_list = [h_list(1:end-1), userpanel_h, h_list(end)]; + min_h_list = [min_h_list(1:end-1), userpanel_h, min_h_list(end)]; end +if enable_save_panel + % Add space for the save panel + h_list = [h_list(1:end-1), savebox_h, h_list(end)]; + min_h_list = [min_h_list(1:end-1), savebox_h, min_h_list(end)]; +end + +set(this.Gui.MainVbox, 'Heights', h_list, 'MinimumHeights', min_h_list); -this.Gui.SavePanel=uix.BoxPanel( 'Parent', this.Gui.SaveHbox,... +%Here we create the fit panel in the GUI. +this.Gui.FitPanel=uix.BoxPanel('Parent', this.Gui.FitHbox,... 'Padding',0,'BackgroundColor', rgb_white,... - 'Title','Save Panel','TitleColor',rgb_blue); -this.Gui.DirPanel=uix.BoxPanel('Parent',this.Gui.SaveHbox,... - 'Padding',0,'BackgroundColor',rgb_white,... - 'Title','Directory Panel','TitleColor',rgb_blue); + 'Title','Fit Panel','TitleColor',rgb_blue); +this.Gui.FitInnerHbox = uix.HBox('Parent', this.Gui.FitPanel, ... + 'BackgroundColor', rgb_white); +this.Gui.FitVbox1 = uix.VBox('Parent', this.Gui.FitInnerHbox, ... + 'BackgroundColor', rgb_white); +this.Gui.FitVbox2 = uix.VBox('Parent', this.Gui.FitInnerHbox, ... + 'BackgroundColor', rgb_white); +this.Gui.FitVbox3 = uix.VBox('Parent', this.Gui.FitInnerHbox, ... + 'BackgroundColor', rgb_white); -set(this.Gui.SaveHbox,'Widths',[-1,-2],'MinimumWidths',[0,375]); +%Creates the button for analysis inside the VBox +this.Gui.AnalyzeButton=uicontrol('Parent',this.Gui.FitVbox1,... + 'style','pushbutton','Background','w','String','Analyze', ... + 'Callback', @this.analyzeCallback); +%Creates button for generating new initial parameters +this.Gui.InitButton=uicontrol('Parent',this.Gui.FitVbox1,... + 'style','pushbutton','Background','w',... + 'String','Generate initial parameters', ... + 'Callback', @this.initParamCallback); +%Creates button for clearing fits +this.Gui.ClearButton=uicontrol('Parent',this.Gui.FitVbox2,... + 'style','pushbutton','Background','w','String','Clear fit', ... + 'Callback', @this.clearFitCallback); +%Button for triggering NewAcceptedFit event +this.Gui.AcceptFitButton=uicontrol('Parent',this.Gui.FitVbox2,... + 'style','pushbutton','Background','w','String','Accept fit', ... + 'Callback', @this.acceptFitCallback); +%Checkbox for enabling cursors +this.Gui.CursorsCheckbox=uicontrol('Parent',this.Gui.FitVbox3,... + 'style','checkbox','Background','w', ... + 'String', 'Range selection cursors', ... + 'Units', 'normalized', ... + 'Position', [0.5,0.05,0.3,0.3], ... + 'Callback', @this.enableCursorsCallback); +%Button for centering the range selection cursors +this.Gui.CenterCursorsButton=uicontrol('Parent',this.Gui.FitVbox3,... + 'style','pushbutton','Background','w','String','Center cursors', ... + 'Enable', 'off', 'Callback', @(~,~)centerCursors(this)); + +set(this.Gui.FitVbox1,... + 'Heights', button_h*ones(1,length(this.Gui.FitVbox1.Children))); +set(this.Gui.FitVbox2,... + 'Heights', button_h*ones(1,length(this.Gui.FitVbox2.Children))); +set(this.Gui.FitVbox3,... + 'Heights', button_h*ones(1,length(this.Gui.FitVbox3.Children))); + +if ~isempty(fieldnames(this.UserParamList)) + %Here we create the panel for the user parameters + this.Gui.UserPanel=uix.BoxPanel( 'Parent', this.Gui.UserHbox,... + 'Padding',0,'BackgroundColor', 'w',... + 'Title','Calculated parameters','TitleColor',rgb_blue); -%Here we create the buttons and edit boxes inside the save box -this.Gui.SaveButtonBox=uix.VBox('Parent',this.Gui.SavePanel,... - 'BackgroundColor',rgb_white); -this.Gui.DirHbox=uix.HBox('Parent',this.Gui.DirPanel,... - 'BackgroundColor',rgb_white); -this.Gui.FileNameLabelBox=uix.VBox('Parent',this.Gui.DirHbox,... - 'BackgroundColor',rgb_white); -this.Gui.FileNameBox=uix.VBox('Parent',this.Gui.DirHbox,... - 'BackgroundColor',rgb_white); -set(this.Gui.DirHbox,'Widths',[-1,-2]); - -%Buttons for saving the fit and parameters -this.Gui.SaveParamButton=uicontrol('Parent',this.Gui.SaveButtonBox,... - 'style','pushbutton','Background','w','String','Save Parameters',... - 'Callback', @(hObject, eventdata) saveParamCallback(this, hObject, eventdata)); -this.Gui.SaveFitButton=uicontrol('Parent',this.Gui.SaveButtonBox,... - 'style','pushbutton','Background','w','String','Save Fit',... - 'Callback', @(hObject, eventdata) saveFitCallback(this, hObject, eventdata)); -set(this.Gui.SaveButtonBox,'Heights',[button_h,button_h]) - -%Labels for the edit boxes -this.Gui.BaseDirLabel=annotation(this.Gui.FileNameLabelBox,... - 'textbox',[0.5,0.5,0.3,0.3],... - 'String','Save Directory','Units','Normalized',... - 'HorizontalAlignment','Left','VerticalAlignment','middle',... - 'FontSize',10,'BackgroundColor',rgb_white); -this.Gui.SessionNameLabel=annotation(this.Gui.FileNameLabelBox,... - 'textbox',[0.5,0.5,0.3,0.3],... - 'String','Session Name','Units','Normalized',... - 'HorizontalAlignment','Left','VerticalAlignment','middle',... - 'FontSize',10,'BackgroundColor',rgb_white); -this.Gui.FileNameLabel=annotation(this.Gui.FileNameLabelBox,... - 'textbox',[0.5,0.5,0.3,0.3],... - 'String','File Name','Units','Normalized',... - 'HorizontalAlignment','Left','VerticalAlignment','middle',... - 'FontSize',10,'BackgroundColor',rgb_white); -set(this.Gui.FileNameLabelBox,'Heights',button_h*ones(1,3)); - -%Boxes for editing the path and filename -this.Gui.BaseDir=uicontrol('Parent',this.Gui.FileNameBox,... - 'style','edit','String',this.SaveInfo.base_dir,'HorizontalAlignment','Left',... - 'FontSize',10); -this.Gui.SessionName=uicontrol('Parent',this.Gui.FileNameBox,... - 'style','edit','String',this.SaveInfo.session_name,'HorizontalAlignment','Left',... - 'FontSize',10); -this.Gui.FileName=uicontrol('Parent',this.Gui.FileNameBox,... - 'style','edit','String',this.SaveInfo.filename,'HorizontalAlignment','Left',... - 'FontSize',10); -set(this.Gui.FileNameBox,'Heights',button_h*ones(1,3)); + %Fill the user panel with controls + createUserControls(this, 'field_hight', button_h, ... + 'background_color', 'w'); +end +if enable_save_panel + %This creates the boxes for saving files and for specifying file saving + %properties + this.Gui.SavePanel=uix.BoxPanel( 'Parent', this.Gui.SaveHbox,... + 'Padding',0,'BackgroundColor', rgb_white,... + 'Title','Save Panel','TitleColor',rgb_blue); + this.Gui.DirPanel=uix.BoxPanel('Parent',this.Gui.SaveHbox,... + 'Padding',0,'BackgroundColor',rgb_white,... + 'Title','Directory Panel','TitleColor',rgb_blue); + + set(this.Gui.SaveHbox,'Widths',[-1,-2],'MinimumWidths',[0,375]); + + %Here we create the buttons and edit boxes inside the save box + this.Gui.SaveButtonBox=uix.VBox('Parent',this.Gui.SavePanel,... + 'BackgroundColor',rgb_white); + this.Gui.DirHbox=uix.HBox('Parent',this.Gui.DirPanel,... + 'BackgroundColor',rgb_white); + this.Gui.FileNameLabelBox=uix.VBox('Parent',this.Gui.DirHbox,... + 'BackgroundColor',rgb_white); + this.Gui.FileNameBox=uix.VBox('Parent',this.Gui.DirHbox,... + 'BackgroundColor',rgb_white); + set(this.Gui.DirHbox,'Widths',[-1,-2]); + + %Buttons for saving the fit and parameters + this.Gui.SaveFitButton=uicontrol('Parent',this.Gui.SaveButtonBox,... + 'style','pushbutton','Background','w','String','Save Fit',... + 'Callback', @(hObject, eventdata) saveFitCallback(this, hObject, eventdata)); + set(this.Gui.SaveButtonBox,'Heights',button_h* ... + ones(1,length(this.Gui.SaveButtonBox.Children))); + + %Labels for the edit boxes + this.Gui.BaseDirLabel=annotation(this.Gui.FileNameLabelBox,... + 'textbox',[0.5,0.5,0.3,0.3],... + 'String','Save Directory','Units','Normalized',... + 'HorizontalAlignment','Left','VerticalAlignment','middle',... + 'FontSize',10,'BackgroundColor',rgb_white); + this.Gui.SessionNameLabel=annotation(this.Gui.FileNameLabelBox,... + 'textbox',[0.5,0.5,0.3,0.3],... + 'String','Session Name','Units','Normalized',... + 'HorizontalAlignment','Left','VerticalAlignment','middle',... + 'FontSize',10,'BackgroundColor',rgb_white); + this.Gui.FileNameLabel=annotation(this.Gui.FileNameLabelBox,... + 'textbox',[0.5,0.5,0.3,0.3],... + 'String','File Name','Units','Normalized',... + 'HorizontalAlignment','Left','VerticalAlignment','middle',... + 'FontSize',10,'BackgroundColor',rgb_white); + set(this.Gui.FileNameLabelBox,'Heights',button_h*ones(1,3)); + + %Boxes for editing the path and filename + this.Gui.BaseDir=uicontrol('Parent',this.Gui.FileNameBox,... + 'style','edit','HorizontalAlignment','Left',... + 'FontSize',10); + this.Gui.SessionName=uicontrol('Parent',this.Gui.FileNameBox,... + 'style','edit','HorizontalAlignment','Left',... + 'FontSize',10); + this.Gui.FileName=uicontrol('Parent',this.Gui.FileNameBox,... + 'style','edit','HorizontalAlignment','Left',... + 'FontSize',10); + set(this.Gui.FileNameBox,'Heights',button_h*ones(1,3)); +end %We first make the BoxPanels to speed up the process. Otherwise everything %in the BoxPanel must be redrawn every time we make a new one. panel_str=cell(1,this.n_params); for i=1:this.n_params %Generates the string for the panel handle panel_str{i}=sprintf('Panel_%s',this.fit_params{i}); %Creates the panels - this.Gui.(panel_str{i})=uix.BoxPanel( 'Parent', this.Gui.FitHbox ,... + this.Gui.(panel_str{i})=uix.BoxPanel( 'Parent', this.Gui.SliderHbox ,... 'Padding',0,'BackgroundColor', 'w',... 'Title',sprintf('%s (%s)',this.fit_param_names{i},this.fit_params{i}),... 'TitleColor',rgb_blue,... 'Position',[1+edit_width*(i-1),1,edit_width,slider_h],... 'Visible','off'); end %Loops over number of parameters to create a fit panel for each one for i=1:this.n_params %Generates the string for the vbox handle vbox_str=sprintf('Vbox_%s',this.fit_params{i}); %Generates the string for the slider handle slider_str=sprintf('Slider_%s',this.fit_params{i}); %Generates string for edit panels edit_str=sprintf('Edit_%s',this.fit_params{i}); %Generates a string for the limit boxes lim_str=sprintf('Lim_%s',this.fit_params{i}); %Creates the vbox inside the panel that allows stacking this.Gui.(vbox_str) =uix.VBox( 'Parent', ... this.Gui.(panel_str{i}),'Padding',0,'BackgroundColor', 'w'); %Generates edit box for fit parameters this.Gui.(edit_str)=uicontrol('Parent',this.Gui.(vbox_str),... - 'Style','edit','String',sprintf('%3.3e',this.init_params(i)),... - 'FontSize',14,'Tag',num2str(i),'HorizontalAlignment','Right',... - 'Position',[1,48,edit_width-4,30],'Units','Pixels','Callback',... - @(hObject,eventdata) editCallback(this, hObject, eventdata)); + 'Style','edit','String',sprintf('%3.3e',this.param_vals(i)),... + 'FontSize',14,'HorizontalAlignment','Right',... + 'Position',[1,48,edit_width-4,30],'Units','Pixels', ... + 'Callback', createParamFieldEditedCallback(this, i)); + %Sets up HBox for the lower and upper limits this.Gui.(lim_str)=uix.HBox('Parent',this.Gui.(vbox_str),... 'Padding',0,'BackgroundColor','w'); + %Generates edit box for limits this.Gui.([lim_str,'_lower'])=uicontrol('Parent',this.Gui.(lim_str),... 'Style','edit','String',sprintf('%3.3e',this.lim_lower(i)),... - 'FontSize',10,'Tag',sprintf('Lower%i',i),'HorizontalAlignment','Right',... - 'Position',[1,1,edit_width-4,30],'Units','Pixels','Callback',... - @(hObject,eventdata) limEditCallback(this, hObject, eventdata)); + 'FontSize',10,'HorizontalAlignment','Right',... + 'Position',[1,1,edit_width-4,30],'Units','Pixels', ... + 'Tooltip', 'Lower limit', ... + 'Callback', createLowerLimEditCallback(this, i)); + this.Gui.([lim_str,'_upper'])=uicontrol('Parent',this.Gui.(lim_str),... 'Style','edit','String',sprintf('%3.3e',this.lim_upper(i)),... - 'FontSize',10,'Tag',sprintf('Upper%i',i),'HorizontalAlignment','Right',... - 'Position',[1,1,edit_width-4,30],'Units','Pixels','Callback',... - @(hObject,eventdata) limEditCallback(this, hObject, eventdata)); + 'FontSize',10,'HorizontalAlignment','Right',... + 'Position',[1,1,edit_width-4,30],'Units','Pixels', ... + 'Tooltip', 'Upper limit', ... + 'Callback', createUpperLimEditCallback(this, i)); %Generates java-based slider. Looks nicer than MATLAB slider this.Gui.(slider_str)=uicomponent('Parent',this.Gui.(vbox_str),... 'style','jslider','Value',50,'Orientation',0,... 'MajorTickSpacing',20,'MinorTickSpacing',5,'Paintlabels',0,... 'PaintTicks',1,'Background',java.awt.Color.white,... 'pos',[1,-7,edit_width-4,55]); %Sets up callbacks for the slider this.Gui.([slider_str,'_callback'])=handle(this.Gui.(slider_str),... 'CallbackProperties'); %Note that this is triggered whenever the state changes, even if it is %programatically this.Gui.([slider_str,'_callback']).StateChangedCallback = .... - @(hObject, eventdata) sliderCallback(this,i,hObject,eventdata); + createSliderStateChangedCallback(this, i); this.Gui.([slider_str,'_callback']).MouseReleasedCallback = .... - @(~, ~) triggerNewInitVal(this); + createSliderMouseReleasedCallback(this, i); %Sets heights and minimum heights for the elements in the fit vbox set(this.Gui.(vbox_str),'Heights',[30,30,55],'MinimumHeights',[30,30,55]) end %Makes all the panels at the bottom visible at the same time cellfun(@(x) set(this.Gui.(x),'Visible','on'),panel_str); end \ No newline at end of file diff --git a/@MyFit/createTab.m b/@MyFit/createTab.m deleted file mode 100644 index 0a553a6..0000000 --- a/@MyFit/createTab.m +++ /dev/null @@ -1,32 +0,0 @@ -function createTab(this,tab_tag,bg_color, button_h) -tab_field=sprintf('%sTab',tab_tag); -%Creates a tab inside the user panel -this.Gui.(tab_field)=uix.Panel('Parent', this.Gui.TabPanel,... - 'Padding', 0, 'BackgroundColor',bg_color); - -%Creates VBoxes for the quantities to be displayed -createUnitBox(this,bg_color,this.Gui.(tab_field),tab_tag); - -%Creates boxes to show numbers and labels for quality factor, frequency and -%linewidth -ind=structfun(@(x) strcmp(x.parent,tab_tag), this.UserGui.Fields); -names=fieldnames(this.UserGui.Fields); -names=names(ind); - -for i=1:length(names) - field=this.UserGui.Fields.(names{i}); - createUnitDisp(this,... - 'BackgroundColor',bg_color,... - 'Tag',names{i},... - 'Parent',tab_tag,... - 'Title',field.title,... - 'Enable',field.enable_flag,... - 'init_val',field.init_val/field.conv_factor); -end -%Sets the heights of the edit boxes -name_vbox=sprintf('%sNameVBox',tab_tag); -value_vbox=sprintf('%sEditVBox',tab_tag); - -set(this.Gui.(name_vbox),'Heights',button_h*ones(1,length(names))); -set(this.Gui.(value_vbox),'Heights',button_h*ones(1,length(names))); -end \ No newline at end of file diff --git a/@MyFit/createUnitBox.m b/@MyFit/createUnitBox.m deleted file mode 100644 index fb446bb..0000000 --- a/@MyFit/createUnitBox.m +++ /dev/null @@ -1,13 +0,0 @@ -function createUnitBox(this, bg_color, h_parent,name) -hbox_str=sprintf('%sDispHBox',name); -this.Gui.(hbox_str)=uix.HBox('Parent',h_parent,... - 'BackgroundColor',bg_color); -this.Gui.(sprintf('%sNameVBox',name))=... - uix.VBox('Parent',this.Gui.(hbox_str),... - 'BackgroundColor',bg_color); -this.Gui.(sprintf('%sEditVBox',name))=... - uix.VBox('Parent',this.Gui.(hbox_str),... - 'BackgroundColor',bg_color); -set(this.Gui.(hbox_str),'Widths',[-2,-1]); - -end \ No newline at end of file diff --git a/@MyFit/createUnitDisp.m b/@MyFit/createUnitDisp.m deleted file mode 100644 index 00b5cc0..0000000 --- a/@MyFit/createUnitDisp.m +++ /dev/null @@ -1,31 +0,0 @@ -function createUnitDisp(this,varargin) -p=inputParser; -addParameter(p,'BackgroundColor','w'); -addParameter(p,'Tag','Placeholder',@ischar); -addParameter(p,'Parent','Placeholder',@ischar); -addParameter(p,'Title','Placeholder',@ischar); -addParameter(p,'Enable','on',@ischar); -addParameter(p,'init_val',1,@isnumeric); -parse(p,varargin{:}); - -tag=p.Results.Tag; -vbox_name=sprintf('%sNameVBox',p.Results.Parent); -vbox_edit=sprintf('%sEditVBox',p.Results.Parent); -label_name=sprintf('%sLabel',tag); -value_name=sprintf('%sEdit',tag); - -this.Gui.(label_name)=annotation(this.Gui.(vbox_name),... - 'textbox',[0.5,0.5,0.3,0.3],... - 'String',p.Results.Title,'Units','Normalized',... - 'HorizontalAlignment','Left','VerticalAlignment','middle',... - 'FontSize',10,'BackgroundColor',p.Results.BackgroundColor); -this.Gui.(value_name)=uicontrol('Parent',this.Gui.(vbox_edit),... - 'Style','edit','String',num2str(p.Results.init_val),... - 'HorizontalAlignment','Right',... - 'FontSize',10,'Enable',p.Results.Enable); - -if ~isempty(this.UserGui.Fields.(tag).Callback) - this.Gui.(value_name).Callback=this.UserGui.Fields.(tag).Callback; -end - -end \ No newline at end of file diff --git a/@MyFit/createUserControls.m b/@MyFit/createUserControls.m new file mode 100644 index 0000000..b6d0f36 --- /dev/null +++ b/@MyFit/createUserControls.m @@ -0,0 +1,80 @@ +% Fill the user panel with control elements using the information in +% UserParamList + +function createUserControls(this, varargin) + p = inputParser(); + addParameter(p, 'background_color', 'w'); + addParameter(p, 'field_hight', 0); + parse(p, varargin{:}); + + bg_color = p.Results.background_color; + field_h = p.Results.field_hight; + + % First, create the main hbox and two vboxes within it, for the display + % of parameter labels and values, respectively. + this.Gui.UserInnerHbox = uix.HBox( ... + 'Parent', this.Gui.UserPanel, ... + 'BackgroundColor', bg_color); + this.Gui.UserParamNameVbox = uix.VBox( ... + 'Parent', this.Gui.UserInnerHbox, ... + 'BackgroundColor', bg_color); + this.Gui.UserParamEditVbox = uix.VBox( ... + 'Parent', this.Gui.UserInnerHbox, ... + 'BackgroundColor', bg_color); + set(this.Gui.UserInnerHbox, 'Widths', [-2,-1]); + + param_names = fieldnames(this.UserParamList); + + for i=1:length(param_names) + S = this.UserParamList.(param_names{i}); + + % Create names for the label and edit field gui elements + lcn = sprintf('%sLabel',param_names{i}); + vcn = sprintf('%sEdit',param_names{i}); + + this.Gui.(lcn) = annotation(this.Gui.UserParamNameVbox, ... + 'textbox', [0.5,0.5,0.3,0.3], ... + 'String', S.title, ... + 'Units', 'Normalized', ... + 'HorizontalAlignment', 'Left', ... + 'VerticalAlignment', 'middle', ... + 'FontSize', 10, ... + 'BackgroundColor', bg_color); + + this.Gui.(vcn) = uicontrol( ... + 'Parent', this.Gui.UserParamEditVbox, ... + 'Style', 'edit', ... + 'HorizontalAlignment', 'Right', ... + 'FontSize', 10, ... + 'Enable', S.editable, ... + 'String', num2str(this.(param_names{i}))); + + if S.editable + this.Gui.(vcn).Callback = ... + createUserParamCallback(this, param_names{i}); + end + + % Create a set method for the dynamic property corresponding to the + % user parameter, which will update the value displayed in GUI if + % property value is changed programmatically. + S.Metaprop.SetMethod = createSetUserParamFcn( ... + param_names{i}, this.Gui.(vcn)); + end + + % Sets the heights of the edit boxes + set(this.Gui.UserParamNameVbox, ... + 'Heights', field_h*ones(1, length(param_names))); + set(this.Gui.UserParamEditVbox, ... + 'Heights', field_h*ones(1, length(param_names))); +end + +% Subroutine for the creation of dynamic set methods +function f = createSetUserParamFcn(param_name, GuiElement) + function setUserParam(this, val) + this.(param_name) = val; + GuiElement.String = num2str(val); + end + + f = @setUserParam; +end + diff --git a/@MyG/MyG.m b/@MyG/MyG.m deleted file mode 100644 index fc17a0b..0000000 --- a/@MyG/MyG.m +++ /dev/null @@ -1,193 +0,0 @@ -classdef MyG < handle - properties (Access=public) - %Name or tag of instance - name - %Trace of mechanical resonance - MechTrace; - %Trace of calibration tone - CalTrace; - - temp; - beta; - end - - properties (GetAccess=public, SetAccess=private) - %Struct containing Gui handles - Gui; - %Gui flag - enable_gui; - %Stores value of g0 - g0; - gamma_m; - mech_freq; - q_m; - k_b=1.38e-23; - h=6.63e-34; - end - - properties (Access=private) - %Struct containing variable names corresponding to Gui edits - VarStruct; - %Contains inputParser - Parser; - end - - properties (Dependent=true) - var_tags; - end - - methods - function this=MyG(varargin) - createVarStruct(this); - createParser(this); - parse(this.Parser,varargin{:}) - - this.MechTrace=this.Parser.Results.MechTrace; - this.CalTrace=this.Parser.Results.CalTrace; - this.beta=this.Parser.Results.beta; - this.temp=this.Parser.Results.temp; - this.enable_gui=this.Parser.Results.enable_gui; - - if this.enable_gui - this.Gui=guihandles(eval('GuiGCal')); - initGui(this); - end - end - - %Class deletion function - function delete(this) - set(this.Gui.figure1,'CloseRequestFcn',''); - %Deletes the figure - delete(this.Gui.figure1); - %Removes the figure handle to prevent memory leaks - this.Gui=[]; - end - - %Creates class input parser - function createParser(this) - p=inputParser; - validateTrace=@(x) validateattributes(x,{'MyTrace'},... - {'nonempty'}); - addParameter(p,'MechTrace',MyTrace(),validateTrace); - addParameter(p,'CalTrace',MyTrace(),validateTrace); - addParameter(p,'name','placeholder',@ischar); - addParameter(p,'enable_gui',true); - cellfun(@(x) addParameter(p, this.VarStruct.(x).var,... - this.VarStruct.(x).default), this.var_tags); - this.Parser=p; - end - - %Creates the variable struct which contains variable names and - %default values - function createVarStruct(this) - addVar(this,'Temp','temp',295); - addVar(this,'Beta','beta',0); - end - - %Adds a variable to the VarStruct - function addVar(this,name,var,default) - this.VarStruct.(name).var=var; - this.VarStruct.(name).default=default; - end - - %Initializes the GUI - function initGui(this) - cellfun(@(x) set(this.Gui.([x,'Edit']),'Callback',... - @(hObject,~) editCallback(this,hObject)),... - this.var_tags); - this.Gui.CopyButton.Callback=@(~,~) copyCallback(this); - this.Gui.figure1.CloseRequestFcn=@(~,~) closeFigure(this); - this.Gui.AnalyzeButton.Callback=@(~,~) calcG(this); - end - - function calcG(this) - %Conditions the caltrace by doing background subtraction, then - %finds the area - cal_bg=mean([this.CalTrace.y(1:5);this.CalTrace.y((end-4):end)]); - this.CalTrace.y=this.CalTrace.y-cal_bg; - cal_area=integrate(this.CalTrace); - v_rms_eom=sqrt(cal_area); - - %Conditions the mechtrace by doing background subtraction, then - %finds the area - mech_bg=mean([this.MechTrace.y(1:5);this.MechTrace.y((end-4):end)]); - this.MechTrace.y=this.MechTrace.y-mech_bg; - mech_area=integrate(this.MechTrace); - v_rms_mech=sqrt(mech_area); - - %Finds the mechanical frequency and the fwhm - [~,this.mech_freq]=max(this.MechTrace); - this.gamma_m=calcFwhm(this.MechTrace); - - - %Defines constants and finds mechanical phononon number - - n_m=this.k_b*this.temp/(this.h*this.mech_freq); - - %Calculates g_0 - this.g0=(v_rms_mech/v_rms_eom)*... - this.beta*this.mech_freq/sqrt(4*n_m); - - if this.enable_gui - set(this.Gui.MechFreq,'String',num2str(this.mech_freq/1e6,4)); - set(this.Gui.Q,'String',num2str(this.q_m,6)); - set(this.Gui.Linewidth,'String',num2str(this.gamma_m,5)); - set(this.Gui.g0,'String',num2str(this.g0,5)); - end - end - - %The close figure function calls the deletion method. - function closeFigure(this) - delete(this); - end - - %Generic editbox callback which sets the appropriate property of - %the class - function editCallback(this, hObject) - tag_str=erase(get(hObject,'Tag'),'Edit'); - var_str=this.VarStruct.(tag_str).var; - this.(var_str)=str2double(get(hObject,'String')); - calcG(this); - end - - %Callback function for copying values to clipboard - function copyCallback(this) - copy_string=sprintf('%s \t %s \t %s \t %s',... - this.mech_freq,this.q_m,this.gamma_m,this.g0); - clipboard('copy',copy_string); - end - end - - %% Set functions - methods - function set.beta(this,beta) - this.beta=beta; - if this.enable_gui - this.Gui.BetaEdit.String=num2str(this.beta); %#ok - end - end - - function set.temp(this,temp) - this.temp=temp; - if this.enable_gui - this.Gui.TempEdit.String=num2str(this.temp); - end - - end - end - - %% Get functions - methods - function var_tags=get.var_tags(this) - var_tags=fieldnames(this.VarStruct); - end - - function q_m=get.q_m(this) - try - q_m=this.mech_freq/this.gamma_m; - catch - q_m=NaN; - end - end - end -end diff --git a/@MyGuiCont/MyGuiCont.m b/@MyGuiCont/MyGuiCont.m new file mode 100644 index 0000000..88ae51a --- /dev/null +++ b/@MyGuiCont/MyGuiCont.m @@ -0,0 +1,53 @@ +% Class that provides basic functionality for handling App-based GUIs + +classdef MyGuiCont < handle + + properties (Access = public) + + % GUI object that is stored for reference only. Should include the + % main figure. + Gui + + % Name of the GUI class that can be used with the instrument + gui_name char + end + + methods (Access = public) + function this = MyGuiCont() + + % Create default name of the GUI based on the class name + class_name = class(this); + + % Optionally remove 'My' in front of the class name + tok = regexp(class_name, '(My)?(.+)','tokens'); + + try + if ~isempty(tok) + this.gui_name = ['Gui' tok{1}{2}]; + end + catch ME + warning(['Could not create default GUI name for ' ... + 'the class ' class_name '. Error:' ME.message]); + end + end + + function createGui(this) + assert(~isempty(this.gui_name), ['GUI name is not ' ... + 'specified for the instrument class ' class(this)]); + + if isempty(this.Gui) || ~isvalid(this.Gui) + this.Gui = feval(this.gui_name, this); + end + end + end + + methods + function set.Gui(this, Val) + assert(~isempty(findFigure(Val)), ... + 'Value assigned to Gui property must include a figure'); + + this.Gui = Val; + end + end +end + diff --git a/@MyGuiSync/MyGuiSync.m b/@MyGuiSync/MyGuiSync.m new file mode 100644 index 0000000..e2a3c48 --- /dev/null +++ b/@MyGuiSync/MyGuiSync.m @@ -0,0 +1,787 @@ +% A mechanism to implement synchronization between parameters and GUI +% elements in app-based GUIs + +classdef MyGuiSync < handle + + properties (GetAccess = public, SetAccess = protected) + Listeners = struct() + + % Link structures + Links = struct( ... + 'reference', {}, ... % reference to the link target + 'GuiElement', {}, ... % graphics object + 'gui_element_prop', {}, ... + 'inputProcessingFcn', {}, ... % applied after a value is + ... % inputed to GUI + 'outputProcessingFcn', {}, ... % applied before a new value is + ... % displayed in GUI + 'getTargetFcn', {}, ... + 'setTargetFcn', {}, ... + 'Listener', {} ... % PostSet listener (optional) + ); + + % List of objects to be deleted when App is deleted + cleanup_list = {} + end + + properties (Access = protected) + App + updateGuiFcn + end + + methods (Access = public) + function this = MyGuiSync(App, varargin) + p = inputParser(); + + addRequired(p, 'App', ... + @(x)assert(isa(x, 'matlab.apps.AppBase'), ... + 'App must be a Matlab app.')); + + % Deletion of kernel object triggers the delition of app + addParameter(p, 'KernelObj', []); + + % Optional function, executed after an app parameter has been + % updated (either externally of internally) + addParameter(p, 'updateGuiFcn', [], ... + @(x)isa(x, 'function_handle')); + + parse(p, App, varargin{:}); + + this.updateGuiFcn = p.Results.updateGuiFcn; + + this.App = App; + this.Listeners.AppDeleted = addlistener(App, ... + 'ObjectBeingDestroyed', @(~, ~)delete(this)); + + % Kernel objects usually represent objects for which the app + % provides user interface. Kernel objects are deleted with + % the app and the app is deleted if a kernel object is. + if ~isempty(p.Results.KernelObj) + if iscell(p.Results.KernelObj) + + % A cell containing the list kernel objects is supplied + cellfun(this.addKernelObj, p.Results.KernelObj); + else + + % A single kernel object is supplied + addKernelObj(this, p.Results.KernelObj); + end + end + end + + function delete(this) + + % Delete generic listeners + try + lnames = fieldnames(this.Listeners); + for i=1:length(lnames) + try + delete(this.Listeners.(lnames{i})); + catch + fprintf(['Could not delete the listener to ' ... + '''%s'' event.\n'], lnames{i}) + end + end + catch + fprintf('Could not delete listeners.\n'); + end + + % Delete link listeners + for i=1:length(this.Links) + try + delete(this.Links(i).Listener); + catch ME + warning(['Could not delete listener for a GUI ' ... + 'link. Error: ' ME.message]) + end + end + + % Delete the content of cleanup list + for i = 1:length(this.cleanup_list) + Obj = this.cleanup_list{i}; + try + if isa(Obj, 'timer') + + % Stop if object is a timer + try + stop(Obj); + catch + end + end + + % Check if the object has an appropriate delete method. + % This is a safety measure to never delete a file by + % accident. + if ismethod(Obj, 'delete') + delete(Obj); + else + fprintf(['Object of class ''%s'' ' ... + 'does not have ''delete'' method.\n'], ... + class(Obj)) + end + catch + fprintf(['Could not delete an object of class ' ... + '''%s'' from the cleanup list.\n'], class(Obj)) + end + end + end + + % Establish a correspondence between the value of a GUI element and + % some other property of the app + % + % Elem - graphics object + % prop_ref - reference to a content of app, e.g. 'var1.subprop(3)' + function addLink(this, Elem, prop_ref, varargin) + + % Parse function inputs + p = inputParser(); + p.KeepUnmatched = true; + + % The decision whether to create ValueChangedFcn and + % a PostSet callback is made automatically by this function, + % but the parameters below enforce these functions to be *not* + % created. + addParameter(p, 'create_elem_callback', true, @islogical); + addParameter(p, 'event_update', true, @islogical); + + % Option, relevent when Elem is a menu and its chldren items + % represent mutually exclusive multiple choices for the value + % of reference + addParameter(p, 'submenu_choices', {}, @iscell); + + parse(p, varargin{:}); + + % Make the list of unmatched name-value pairs for subroutine + sub_varargin = struct2namevalue(p.Unmatched); + + if strcmpi(Elem.Type, 'uimenu') && ... + ~ismember('submenu_choices', p.UsingDefaults) + + % The children of menu item represent multiple choices, + % create separate links for all of them + + choises = p.Results.submenu_choices; + assert(length(choises) == length(Elem.Children), ... + ['The length of the list of supplied multiple ' ... + 'choices must be the same as the number of menu ' ... + 'children.']) + + for i = 1:length(Elem.Children) + addLink(this, Elem.Children(i), prop_ref, ... + 'outputProcessingFcn', ... + @(x)isequal(x, choises{i}), ... + 'inputProcessingFcn', @(x)choises{i}); + end + + return + end + + % Find the handle object which the end property belongs to, + % the end property name and, possibly, further subscripts + [Hobj, hobj_prop, RelSubs] = parseReference(this, prop_ref); + + % Check if the specified target is accessible for reading + try + if isempty(RelSubs) + Hobj.(hobj_prop); + else + subsref(Hobj.(hobj_prop), RelSubs); + end + catch + disp(['Property referenced by ' prop_ref ... + ' is not accessible, the corresponding GUI ' ... + 'element will be not linked and will be disabled.']) + Elem.Enable = 'off'; + return + end + + % Create the basis of link structure (everything except for + % set/get functions) + Link = createLinkBase(this, Elem, prop_ref, sub_varargin{:}); + + % Do additional link processing in the case of + % MyInstrument commands + if isa(Hobj, 'MyInstrument') && ... + ismember(hobj_prop, Hobj.command_names) + Link = extendMyInstrumentLink(this, Link, Hobj, hobj_prop); + end + + % Assign the function that returns the value of reference + Link.getTargetFcn = createGetTargetFcn(this, Hobj, ... + hobj_prop, RelSubs); + + % Check if ValueChanged or another callback needs to be created + elem_prop = Link.gui_element_prop; + + cb_name = findElemCallbackType(this, Elem, elem_prop, ... + Hobj, hobj_prop); + + if p.Results.create_elem_callback && ~isempty(cb_name) + + % Assign the function that sets new value to reference + Link.setTargetFcn = createSetTargetFcn(this, Hobj, ... + hobj_prop, RelSubs); + + switch cb_name + case 'ValueChangedFcn' + Elem.ValueChangedFcn = ... + createValueChangedCallback(this, Link); + case 'MenuSelectedFcn' + Elem.MenuSelectedFcn = ... + createMenuSelectedCallback(this, Link); + otherwise + error('Unknown callback name %s', cb_name) + end + end + + % Attempt creating a callback to PostSet event for the target + % property. If such callback is not created, the link needs to + % be updated manually. + if p.Results.event_update + try + Link.Listener = addlistener(Hobj, hobj_prop, ... + 'PostSet', createPostSetCallback(this, Link)); + catch + Link.Listener = event.proplistener.empty(); + end + end + + % Store the link structure + ind = length(this.Links)+1; + this.Links(ind) = Link; + + % Update the value of GUI element + updateElementByIndex(this, ind); + end + + % Change link reference for a given element or update the functions + % that get and set the value of the existing reference. + function reLink(this, Elem, prop_ref) + + % Find the index of link structure corresponding to Elem + ind = ([this.Links.GuiElement] == Elem); + ind = find(ind, 1); + + if isempty(ind) + return + end + + if ~exist('prop_ref', 'var') + + % If the reference is not supplied, update existing + prop_ref = this.Links(ind).reference; + end + + this.Links(ind).reference = prop_ref; + + if ~isempty(this.Links(ind).Listener) + + % Delete and clear the existing listener + delete(this.Links(ind).Listener); + this.Links(ind).Listener = []; + end + + [Hobj, hobj_prop, RelSubs] = parseReference(this, prop_ref); + + this.Links(ind).getTargetFcn = createGetTargetFcn(this, ... + Hobj, hobj_prop, RelSubs); + + if ~isempty(this.Links(ind).setTargetFcn) + + % Create a new ValueChanged callback + this.Links(ind).setTargetFcn = createSetTargetFcn(this, ... + Hobj, hobj_prop, RelSubs); + + this.Links(ind).GuiElement.ValueChangedFcn = ... + createValueChangedCallback(this, this.Links(ind)); + end + + % Attempt creating a new listener + try + this.Links(ind).Listener = addlistener(Hobj, hobj_prop, ... + 'PostSet', createPostSetCallback(this, ... + this.Links(ind))); + catch + this.Links(ind).Listener = event.proplistener.empty(); + end + + % Update the value of GUI element according to the new + % reference + updateElementByIndex(this, ind); + end + + function updateAll(this) + for i = 1:length(this.Links) + + % Only update those elements for which listeners do not + % exist + if isempty(this.Links(i).Listener) + updateElementByIndex(this, i); + end + end + + % Optionally execute the update function defined within the App + if ~isempty(this.updateGuiFcn) + this.updateGuiFcn(); + end + end + + % Update the value of one linked GUI element. + function updateElement(this, Elem) + ind = ([this.Links.GuiElement] == Elem); + ind = find(ind); + + if isempty(ind) + warning('No link found for the GUI element below.'); + disp(Elem); + + return + elseif length(ind) > 1 + warning('Multiple links found for the GUI element below.'); + disp(Elem); + + return + end + + updateElementByIndex(this, ind); + end + + function addToCleanup(this, Obj) + + % Prepend the new object so that the objects which are added + % first would be deleted last + this.cleanup_list = [{Obj}, this.cleanup_list]; + end + end + + methods (Access = protected) + function addKernelObj(this, KernelObj) + assert( ... + ismember('ObjectBeingDestroyed', events(KernelObj)), ... + ['Object must define ''ObjectBeingDestroyed'' event ' ... + 'to be an app kernel.']) + + addToCleanup(this, KernelObj); + + this.Listeners.KernelObjDeleted = addlistener(KernelObj,... + 'ObjectBeingDestroyed', @this.kernelDeletedCallback); + end + + function kernelDeletedCallback(this, ~, ~) + + % Switch off the AppBeingDeleted callback in order to prevent + % an infinite loop + this.Listeners.AppDeleted.Enabled = false; + + delete(this.App); + delete(this); + end + + function f = createPostSetCallback(this, Link) + function postSetCallback(~,~) + val = Link.getTargetFcn(); + + if ~isempty(Link.outputProcessingFcn) + val = Link.outputProcessingFcn(val); + end + + setIfChanged(Link.GuiElement, Link.gui_element_prop, val); + + % Optionally execute the update function defined within + % the App + if ~isempty(this.updateGuiFcn) + this.updateGuiFcn(); + end + end + + f = @postSetCallback; + end + + % Callback that is assigned to graphics elements as ValueChangedFcn + function f = createValueChangedCallback(this, Link) + function valueChangedCallback(~, ~) + val = Link.GuiElement.Value; + + if ~isempty(Link.inputProcessingFcn) + val = Link.inputProcessingFcn(val); + end + + if ~isempty(Link.Listener) + + % Switch the listener off + Link.Listener.Enabled = false; + + % Set the value + Link.setTargetFcn(val); + + % Switch the listener on again + Link.Listener.Enabled = true; + else + Link.setTargetFcn(val); + end + + % Update non event based links + updateAll(this); + end + + f = @valueChangedCallback; + end + + % MenuSelected callbacks are different from ValueChanged in that + % the state needs to be toggled manually + function f = createMenuSelectedCallback(this, Link) + function menuSelectedCallback(~, ~) + + % Toggle the menu state + if strcmpi(Link.GuiElement.Checked, 'on') + Link.GuiElement.Checked = 'off'; + val = 'off'; + else + Link.GuiElement.Checked = 'on'; + val = 'on'; + end + + if ~isempty(Link.inputProcessingFcn) + val = Link.inputProcessingFcn(val); + end + + if ~isempty(Link.Listener) + + % Switch the listener off + Link.Listener.Enabled = false; + + % Set the value + Link.setTargetFcn(val); + + % Switch the listener on again + Link.Listener.Enabled = true; + else + Link.setTargetFcn(val); + end + + % Update non event based links + updateAll(this); + end + + f = @menuSelectedCallback; + end + + function f = createGetTargetFcn(~, Obj, prop_name, S) + function val = refProp() + val = Obj.(prop_name); + end + + function val = subsrefProp() + val = subsref(Obj, S); + end + + if isempty(S) + + % Faster way to access property + f = @refProp; + else + + % More general way to access property + S = [substruct('.', prop_name), S]; + f = @subsrefProp; + end + end + + function f = createSetTargetFcn(~, Obj, prop_name, S) + function assignProp(val) + Obj.(prop_name) = val; + end + + function subsasgnProp(val) + Obj = subsasgn(Obj, S, val); + end + + if isempty(S) + + % Faster way to assign property + f = @assignProp; + else + + % More general way to assign property + S = [substruct('.', prop_name), S]; + f = @subsasgnProp; + end + end + + % Update the value of one linked GUI element given the index of + % corresponding link + function updateElementByIndex(this, ind) + Link = this.Links(ind); + + val = Link.getTargetFcn(); + if ~isempty(Link.outputProcessingFcn) + val = Link.outputProcessingFcn(val); + end + + % Setting value to a matlab app elemen is time consuming, + % so first check if the value has actually changed + setIfChanged(Link.GuiElement, Link.gui_element_prop, val); + end + + %% Subroutines of addLink + + % Parse input and create the base of Link structure + function Link = createLinkBase(this, Elem, prop_ref, varargin) + + % Parse function inputs + p = inputParser(); + + % GUI control element + addRequired(p, 'Elem'); + + % Target to which the value of GUI element will be linked + % relative to the App itself + addRequired(p, 'prop_ref', @ischar); + + % Linked property of the GUI element (can be e.g. 'Color') + addParameter(p, 'elem_prop', 'Value', @ischar); + + % If input_prescaler is given, the value assigned to the + % instrument propery is related to the value x displayed in + % GUI as x/input_presc. + addParameter(p, 'input_prescaler', 1, @isnumeric); + + % Arbitrary processing functions can be specified for input and + % output. outputProcessingFcn is applied before assigning + % the new value to gui elements and inputProcessingFcn is + % applied before assigning to the new value to reference. + addParameter(p, 'outputProcessingFcn', [], ... + @(f)isa(f,'function_handle')); + addParameter(p, 'inputProcessingFcn', [], ... + @(f)isa(f,'function_handle')); + + % Parameters relevant for uilamps + addParameter(p, 'lamp_on_color', MyAppColors.lampOn(), ... + @iscolor); + addParameter(p, 'lamp_off_color', MyAppColors.lampOff(), ... + @iscolor); + + % Option which allows converting a binary choice into a logical + % value + addParameter(p, 'map', {}, @this.validateMapArg); + + parse(p, Elem, prop_ref, varargin{:}); + + assert(all([this.Links.GuiElement] ~= p.Results.Elem), ... + ['Another link for the same GUI element that is ' ... + 'attempted to be linked to ' prop_ref ' already exists.']) + + % Create a new link structure + Link = struct( ... + 'reference', prop_ref, ... + 'GuiElement', p.Results.Elem, ... + 'gui_element_prop', p.Results.elem_prop, ... + 'inputProcessingFcn', p.Results.inputProcessingFcn, ... + 'outputProcessingFcn', p.Results.outputProcessingFcn, ... + 'getTargetFcn', [], ... + 'setTargetFcn', [], ... + 'Listener', [] ... + ); + + % Lamp indicators is a special case. It is often convenient to + % make a lamp indicate on/off state. If a lamp is being linked + % to a logical-type variable we therefore assign a dedicated + % OutputProcessingFcn that puts logical values in + % corresponcence with colors + if strcmpi(Elem.Type, 'uilamp') + Link.gui_element_prop = 'Color'; + + % Select between the on and off colors. + Link.outputProcessingFcn = @(x)select(x, ... + p.Results.lamp_on_color, p.Results.lamp_off_color); + return + end + + % Treat the special case of uimenus + if strcmpi(Elem.Type, 'uimenu') + Link.gui_element_prop = 'Checked'; + end + + if ~ismember('map', p.UsingDefaults) + ref_vals = p.Results.map{1}; + gui_vals = p.Results.map{2}; + + % Assign input and output processing functions that convert + % a logical value into one of the options and back + Link.inputProcessingFcn = @(x)select( ... + isequal(x, gui_vals{1}), ref_vals{:}); + Link.outputProcessingFcn = @(x)select( ... + isequal(x, ref_vals{1}), gui_vals{:}); + end + + % Simple scaling is a special case of value processing + % functions. + if ~ismember('input_prescaler', p.UsingDefaults) + if isempty(Link.inputProcessingFcn) && ... + isempty(Link.outputProcessingFcn) + + Link.inputProcessingFcn = ... + @(x) (x/p.Results.input_prescaler); + Link.outputProcessingFcn = ... + @(x) (x*p.Results.input_prescaler); + else + warning(['input_prescaler is ignored for target ' ... + prop_ref 'as inputProcessingFcn or ' ... + 'outputProcessingFcn has been already ' ... + 'assigned instead.']); + end + end + end + + function Link = extendMyInstrumentLink(~, Link, Instrument, tag) + Cmd = Instrument.CommandList.(tag); + + % If supplied command does not have read permission, issue a + % warning. + if isempty(Cmd.readFcn) + fprintf('Instrument property ''%s'' is nor readable\n', ... + tag); + + % Try switching the color of the gui element to orange + try + Link.GuiElement.BackgroundColor = MyAppColors.warning(); + catch + try + Link.GuiElement.FontColor = MyAppColors.warning(); + catch + end + end + end + + % Generate Items and ItemsData for dropdown menues if they were + % not initialized manually + if isequal(Link.GuiElement.Type, 'uidropdown') && ... + isempty(Link.GuiElement.ItemsData) + + str_value_list = cell(length(Cmd.value_list), 1); + + for i=1:length(Cmd.value_list) + if ischar(Cmd.value_list{i}) + + % Capitalized displayed names for beauty + str = Cmd.value_list{i}; + str_value_list{i} = [upper(str(1)), ... + lower(str(2:end))]; + else + + % Items in a dropdown should be strings + str_value_list{i} = num2str(Cmd.value_list{i}); + end + end + + Link.GuiElement.Items = str_value_list; + + % Assign the list of unprocessed values as ItemsData + Link.GuiElement.ItemsData = Cmd.value_list; + end + + % Add tooltip + if isprop(Link.GuiElement, 'Tooltip') && ... + isempty(Link.GuiElement.Tooltip) + Link.GuiElement.Tooltip = Cmd.info; + end + end + + % Decide what kind of callback (if any) needs to be created for + % the GUI element. Options: 'ValueChangedFcn', 'MenuSelectedFcn' + function callback_name = findElemCallbackType(~, ... + Elem, elem_prop, Hobj, hobj_prop) + + % Check the reference object property attributes + Mp = findprop(Hobj, hobj_prop); + prop_write_accessible = strcmpi(Mp.SetAccess,'public') && ... + (~Mp.Constant) && (~Mp.Abstract); + + % Check if the GUI element enabled and editable + try + gui_element_editable = strcmpi(Elem.Enable, 'on'); + catch + gui_element_editable = true; + end + + % A check for editability is only meaningful for uieditfieds. + % Drop-downs also have 'Editable' property, but it corresponds + % to the editability of elements and should not have an effect + % on assigning callbacks. + if (strcmpi(Elem.Type, 'uinumericeditfield') || ... + strcmpi(Elem.Type, 'uieditfield')) ... + && strcmpi(Elem.Editable, 'off') + gui_element_editable = false; + end + + if ~(prop_write_accessible && gui_element_editable) + callback_name = ''; + return + end + + % Do not create a new callback if one already exists (typically + % it means that a callback was manually defined in AppDesigner) + if strcmp(elem_prop, 'Value') && ... + isprop(Elem, 'ValueChangedFcn') && ... + isempty(Elem.ValueChangedFcn) + + % This is the most typical type of callback + callback_name = 'ValueChangedFcn'; + elseif strcmpi(Elem.Type, 'uimenu') && ... + strcmp(elem_prop, 'Checked') && ... + isempty(Elem.MenuSelectedFcn) + + callback_name = 'MenuSelectedFcn'; + else + callback_name = ''; + end + end + + % Extract the top-most handle object in the reference, the end + % property name and any further subreference + function [Hobj, prop_name, Subs] = parseReference(this, prop_ref) + + % Make sure the reference starts with a dot and convert to + % subreference structure + if prop_ref(1)~='.' + PropSubs = str2substruct(['.', prop_ref]); + else + PropSubs = str2substruct(prop_ref); + end + + % Find the handle object to which the end property belongs as + % well as the end property name + Hobj = this.App; + + Subs = PropSubs; % Subreference relative to Hobj.(prop) + prop_name = PropSubs(1).subs; + + for i=1:length(PropSubs)-1 + testvar = subsref(this.App, PropSubs(1:end-i)); + if isa(testvar, 'handle') + Hobj = testvar; + + Subs = PropSubs(end-i+2:end); + prop_name = PropSubs(end-i+1).subs; + + break + end + end + end + + % Validate the value of 'map' optional argument in createLinkBase + function validateMapArg(~, arg) + try + is_map_arg = iscell(arg) && length(arg)==2 && ... + length(arg{1})==2 && length(arg{2})==2; + catch + is_map_arg = false; + end + + assert(is_map_arg, ['The value must be a cell of the form ' ... + '{{reference value 1, reference value 2}, ' ... + '{GUI dispaly value 1, GUI dispaly value 2}}.']) + end + end +end + diff --git a/@MyInstrument/MyInstrument.m b/@MyInstrument/MyInstrument.m index dcd9bf9..7da80e7 100644 --- a/@MyInstrument/MyInstrument.m +++ b/@MyInstrument/MyInstrument.m @@ -1,237 +1,295 @@ -% Generic class to implement communication with instruments - -classdef MyInstrument < dynamicprops & MyDataSource - - % Access for these variables is 'protected' and in addition - % granted to MyClassParser in order to use construction parser. - % Granting access to MyInstrument explicitly is needed to make - % these properties accessible for subclasses. - properties (GetAccess=public, SetAccess={?MyClassParser,?MyInstrument}) - interface=''; - address=''; - end - - properties (Access=public) - Device %Device communication object - end - - properties (GetAccess=public, SetAccess=protected) - idn_str=''; % identification string - end - - properties (Constant=true) - % Default parameters for device connection - DEFAULT_INP_BUFF_SIZE = 1e7; % buffer size bytes - DEFAULT_OUT_BUFF_SIZE = 1e7; % buffer size bytes - DEFAULT_TIMEOUT = 10; % Timeout in s - end - - events - PropertyRead - end - - methods (Access=public) - function this=MyInstrument(interface, address, varargin) - P=MyClassParser(); - addRequired(P,'interface',@ischar); - addRequired(P,'address',@ischar); - addParameter(P,'name','',@ischar); - processInputs(P, this, interface, address, varargin{:}); - - % Create an empty trace - this.Trace=MyTrace(); - - % Create dummy device object that supports properties - this.Device=struct(); - this.Device.Status='not connected'; - - % Interface and address can correspond to an entry in the list - % of local instruments. Read this entry in such case. - if strcmpi(interface, 'instr_list') - % load the InstrumentList structure - InstrumentList = getLocalSettings('InstrumentList'); - % In this case 'address' is the instrument name in - % the list - instr_name = address; - if ~isfield(InstrumentList, instr_name) - error('%s is not a field of InstrumentList',... - instr_name); - end - if ~isfield(InstrumentList.(instr_name), 'interface') - error(['InstrumentList entry ', instr_name,... - ' has no ''interface'' field']); - else - this.interface = InstrumentList.(instr_name).interface; - end - if ~isfield(InstrumentList.(instr_name), 'address') - error(['InstrumentList entry ', instr_name,... - ' has no ''address'' field']); - else - this.address = InstrumentList.(instr_name).address; - end - % Assign name automatically, but not overwrite if - % already specified - if isempty(this.name) - this.name = instr_name; - end - end - - % Connecting device creates a communication object, - % but does not attempt communication - connectDevice(this); - end - - function delete(this) - %Closes the connection to the device - closeDevice(this); - %Deletes the device object - try - delete(this.Device); - catch - end - end - - %Triggers event for property read from device - function triggerPropertyRead(this) - notify(this,'PropertyRead') - end - - % Read all the relevant instrument properties and return as a - % MyMetadata object. - % Dummy method that needs to be re-defined by a parent class - function Hdr=readHeader(this) - Hdr=MyMetadata(); - % Instrument name is a valid Matalb identifier as ensured by - % its set method (see the superclass) - addField(Hdr, this.name); - % Add identification string as parameter - addParam(Hdr, this.name, 'idn', this.idn_str); - end - - - %% Connect, open, configure, identificate and close the device - % Connects to the device, explicit indication of interface and - % address is for ability to handle instr_list as interface - function connectDevice(this) - int_list={'constructor','visa','tcpip','serial'}; - if ~ismember(lower(this.interface), int_list) - warning(['Device is not connected, unknown interface ',... - this.interface,'. Valid interfaces are ',... - '''constructor'', ''visa'', ''tcpip'' and ''serial''']) - return - end - try - switch lower(this.interface) - % Use 'constructor' interface to connect device with - % more that one parameter, specifying its address - case 'constructor' - % in this case the 'address' is a command - % (ObjectConstructorName), e.g. as returned by the - % instrhwinfo, that creates communication object - % when executed - this.Device=eval(this.address); - case 'visa' - % visa brand is 'ni' by default - this.Device=visa('ni', this.address); - case 'tcpip' - % Works only with default socket. Use 'constructor' - % if socket or other options need to be specified - this.Device=tcpip(this.address); - case 'serial' - this.Device=serial(this.address); - otherwise - error('Unknown interface'); - end - configureDeviceDefault(this); - catch - warning(['Device is not connected, ',... - 'error while creating communication object.']); - end - end - - % Opens the device if it is not open. Does not throw error if - % device is already open for communication with another object, but - % tries to close existing connections instead. - function openDevice(this) - if ~isopen(this) - try - fopen(this.Device); - catch - % try to find and close all the devices with the same - % VISA resource name - try - instr_list=instrfind('RsrcName',this.Device.RsrcName); - fclose(instr_list); - fopen(this.Device); - warning('Multiple instrument objects of address %s exist',... - this.address); - catch - error('Could not open device') - end - end - end - end - - % Closes the connection to the device - function closeDevice(this) - if isopen(this) - fclose(this.Device); - end - end - - function configureDeviceDefault(this) - dev_prop_list = properties(this.Device); - if ismember('OutputBufferSize',dev_prop_list) - this.Device.OutputBufferSize = this.DEFAULT_OUT_BUFF_SIZE; - end - if ismember('InputBufferSize',dev_prop_list) - this.Device.InputBufferSize = this.DEFAULT_INP_BUFF_SIZE; - end - if ismember('Timeout',dev_prop_list) - this.Device.Timeout = this.DEFAULT_TIMEOUT; - end - end - - % Checks if the connection to the device is open - function bool=isopen(this) - try - bool=strcmp(this.Device.Status, 'open'); - catch - warning('Cannot access device Status property'); - bool=false; - end - end - - %% Identification - % Attempt communication and identification of the device - function [str, msg]=idn(this) - was_open=isopen(this); - try - openDevice(this); - [str,~,msg]=query(this.Device,'*IDN?'); - catch ErrorMessage - str=''; - msg=ErrorMessage.message; - end - this.idn_str=str; - % Leave device in the state it was in the beginning - if ~was_open - try - closeDevice(this); - catch - end - end - end - - end - - %% Set and get methods - methods - function set.idn_str(this, str) - % Remove carriage return and new line symbols from the string - newline_smb={sprintf('\n'),sprintf('\r')}; %#ok - str=replace(str, newline_smb,' '); - this.idn_str=str; - end - end -end \ No newline at end of file +% Generic instrument superclass +% +% Undefined/dummy methods: +% queryString(this, cmd) +% +% These methods are intentionally not introduced as abstract as under +% some conditions they are not necessary + +classdef MyInstrument < dynamicprops & matlab.mixin.CustomDisplay + + properties (Access = public) + + % Synchronize all properties after setting new value to one + auto_sync = true + end + + properties (SetAccess = protected, GetAccess = public) + CommandList = struct() + + % Identification string + idn_str = '' + end + + properties (Dependent = true) + command_names + end + + properties (Access = protected) + + % Copying existing metadata is much faster than creating a new one + Metadata = MyMetadata.empty() + + % Logical variables that determine if writing to the instrument + % takes place when property is assigned new value + CommandWriteEnabled = struct() + end + + methods (Access = public) + + % Read all parameters of the physical device + function sync(this) + read_ind = structfun(@(x) ~isempty(x.readFcn), ... + this.CommandList); + read_cns = this.command_names(read_ind); + + for i=1:length(read_cns) + tag = read_cns{i}; + read_value = this.CommandList.(tag).readFcn(); + + % Compare to the previous value and update if different. + % Comparison prevents overhead for objects that listen to + % the changes of property values. + if ~isequal(this.CommandList.(tag).last_value, read_value) + + % Assign value without writing to the instrument + this.CommandWriteEnabled.(tag) = false; + this.(tag) = read_value; + this.CommandWriteEnabled.(tag) = true; + end + end + end + + function addCommand(this, tag, varargin) + p = inputParser(); + + % Name of the command + addRequired(p,'tag', @(x)isvarname(x)); + + % Functions for reading and writing the property value to the + % instrument + addParameter(p, 'readFcn', function_handle.empty(), ... + @(x)isa(x, 'function_handle')); + addParameter(p, 'writeFcn', function_handle.empty(), ... + @(x)isa(x, 'function_handle')); + + % Function applied before writeFcn + addParameter(p, 'validationFcn', function_handle.empty(), ... + @(x)isa(x, 'function_handle')); + + % Function or list of functions executed after updating the + % class property value + addParameter(p, 'postSetFcn', function_handle.empty(), ... + @(x)isa(x, 'function_handle')); + + addParameter(p, 'value_list', {}, @iscell); + addParameter(p, 'default', 0); + addParameter(p, 'info', '', @ischar); + + parse(p,tag,varargin{:}); + + assert(~isprop(this, tag), ['Property named ' tag ... + ' already exists in the class.']); + + for fn = fieldnames(p.Results)' + this.CommandList.(tag).(fn{1}) = p.Results.(fn{1}); + end + + this.CommandList.(tag).info = ... + toSingleLine(this.CommandList.(tag).info); + + vl = this.CommandList.(tag).value_list; + if ~isempty(vl) && isempty(p.Results.validationFcn) + this.CommandList.(tag).validationFcn = ... + createListValidationFcn(this, vl); + end + + % Assign default value from the list if not given explicitly + if ~isempty(vl) && ismember('default', p.UsingDefaults) + default = vl{1}; + else + default = p.Results.default; + end + + % Create and configure a dynamic property + H = addprop(this, tag); + H.GetAccess = 'public'; + H.SetObservable = true; + H.SetMethod = createCommandSetFcn(this, tag); + + % Assign the default value with post processing but without + % writing to the instrument + this.CommandWriteEnabled.(tag) = false; + this.(tag) = default; + this.CommandWriteEnabled.(tag) = true; + + if ~isempty(this.CommandList.(tag).writeFcn) + H.SetAccess = 'public'; + else + H.SetAccess = {'MyInstrument'}; + end + end + + % Identification + function [str, msg] = idn(this) + assert(ismethod(this, 'queryString'), ['The instrument ' ... + 'class must define queryString method in order to ' ... + 'attempt identification.']) + + try + str = queryString(this,'*IDN?'); + catch ME + str = ''; + msg = ME.message; + end + this.idn_str = str; + end + + % Measurement header + function Mdt = readSettings(this) + if isempty(this.Metadata) + createMetadata(this); + end + + % Ensure that instrument parameters are up to data + sync(this); + + % Exclude idn from parameter names + param_names = setdiff(fieldnames(this.Metadata.ParamList), ... + {'idn'}); + + for i = 1:length(param_names) + tag = param_names{i}; + this.Metadata.ParamList.(tag) = this.(tag); + end + + Mdt = copy(this.Metadata); + end + + % Write new settings to the physical instrument + function writeSettings(this, Mdt) + assert(isa(Mdt, 'MyMetadata'), ... + 'Settings must be provided as MyMetadata object.'); + + % Synchronize the instrument object and write only the settings + % which new values are different from present + sync(this); + + param_names = setdiff(fieldnames(Mdt.ParamList), {'idn'}); + + for i = 1:length(param_names) + tag = param_names{i}; + new_val = Mdt.ParamList.(tag); + + if ismember(tag, this.command_names) + + % Set command value under the condition that it is + % write accessible and that that the new value is + % different from present + if ~isempty(this.CommandList.(tag).writeFcn) && ... + ~isequal(this.(tag), new_val) + + this.(tag) = new_val; + end + + continue + end + + if isprop(this, tag) && ~isequal(this.(tag), new_val) + this.(tag) = new_val; + end + end + end + end + + methods (Access = protected) + function createMetadata(this) + this.Metadata = MyMetadata('title', class(this)); + + % Add identification string + addParam(this.Metadata, 'idn', this.idn_str); + + for i = 1:length(this.command_names) + cmd = this.command_names{i}; + addObjProp(this.Metadata, this, cmd, ... + 'comment', this.CommandList.(cmd).info); + end + end + + % Create set methods for dynamic properties + function f = createCommandSetFcn(~, tag) + function commandSetFcn(this, val) + + % Validate new value + vFcn = this.CommandList.(tag).validationFcn; + if ~isempty(vFcn) + vFcn(val); + end + + % Store the unprocessed value for quick reference in + % the future and value change tracking + this.CommandList.(tag).last_value = val; + + % Assign the value after post processing to the property + pFcn = this.CommandList.(tag).postSetFcn; + if ~isempty(pFcn) + val = pFcn(val); + end + + this.(tag) = val; + + if this.CommandWriteEnabled.(tag) + + % Write the new value to the instrument + this.CommandList.(tag).writeFcn(this.(tag)); + + if this.auto_sync + + % Confirm the changes by reading the state + sync(this); + end + end + end + + f = @commandSetFcn; + end + + function f = createListValidationFcn(~, value_list) + function listValidationFcn(val) + assert( ... + any(cellfun(@(y) isequal(val, y), value_list)), ... + ['Value must be one from the following list:', ... + newline, var2str(value_list)]); + end + + f = @listValidationFcn; + end + + % Overload a method of matlab.mixin.CustomDisplay in order to + % modify the display of object. This serves two purposes + % a) separate commands from other properties + % b) order commands in a systematic way + function PrGroups = getPropertyGroups(this) + cmds = this.command_names; + + % We separate the display of non-command properties from the + % rest + props = setdiff(properties(this), cmds); + + PrGroups = [matlab.mixin.util.PropertyGroup(props), ... + matlab.mixin.util.PropertyGroup(cmds)]; + end + end + + %% Set and Get methods + methods + function val = get.command_names(this) + val = fieldnames(this.CommandList); + end + + function set.idn_str(this, str) + this.idn_str = toSingleLine(str); + end + end +end + diff --git a/@MyLakeshore336/MyLakeshore336.m b/@MyLakeshore336/MyLakeshore336.m deleted file mode 100644 index 7ffc1a9..0000000 --- a/@MyLakeshore336/MyLakeshore336.m +++ /dev/null @@ -1,286 +0,0 @@ -% Class communication with Lakeshore Model 336 temperature controller. -classdef MyLakeshore336 < MyInstrument - - properties (Access=public) - temp_unit = 'K'; % temperature unit, K or C - end - - properties (SetAccess=protected, GetAccess=public) - temp = {0,0,0,0}; % cell array of temperatures - setpoint = {0,0,0,0}; - inp_sens_name = {'','','',''}; % input sensor names - heater_rng = {0,0,0,0}; % cell array of heater range codes - % output modes{{mode, cntl_inp, powerup_en},...} - out_mode = {[0,0,0],[0,0,0],[0,0,0],[0,0,0]}; - end - - properties (SetAccess=private, GetAccess=public) - % Correspondense lists. Indexing starts from 0 - inp_list = {'None','A','B','C','D'}; - out_mode_list = {'Off','Closed loop PID','Zone',... - 'Open loop','Monitor out','Warmup supply'}; - heater12_rng_list = {'Off','Low','Medium','High'}; - heater34_rng_list = {'Off','On'}; - end - - properties (Dependent=true) - heater_rng_str % heater range - temp_str % temperatures with measurement unit - out_mode_str % - cntl_inp_str % - powerup_en_str % - end - - methods (Access=public) - function this=MyLakeshore336(interface, address, varargin) - this@MyInstrument(interface, address, varargin{:}); - end - - % read - function temp_arr = readAllHedged(this) - was_open = isopen(this); - openDevice(this); - - temp_arr = readTemperature(this); - readHeaterRange(this); - readSetpoint(this); - readInputSensorName(this); - readOutMode(this); - - % Leave device in the state it was in the beginning - if ~was_open - closeDevice(this); - end - end - - % Re-define readHeader function - function Hdr=readHeader(this) - Hdr=readHeader@MyInstrument(this); - % Hdr should contain single field - fn=Hdr.field_names{1}; - readAllHedged(this); - - addParam(Hdr, fn, 'temp_unit', this.temp_unit); - - % Add properties without comments - props = {'temp','setpoint','inp_sens_name','heater_rng_str',... - 'out_mode_str', 'cntl_inp_str', 'powerup_en_str'}; - for i=1:length(props) - tag = props{i}; - for j = 1:4 - indtag = sprintf('%s%i', tag, j); - addParam(Hdr, fn, indtag, this.(tag){j}); - end - end - end - - function temp_arr = readTemperature(this) - % unit = C or K; - tu = this.temp_unit; - cmd_str = [tu,'RDG? A;',tu,'RDG? B;',tu,'RDG? C;',tu,'RDG? D']; - resp_str = query(this.Device, cmd_str); - resp_split = strsplit(resp_str,';','CollapseDelimiters',false); - % convert to numbers - this.temp = cellfun(@str2num,resp_split,'UniformOutput',false); - % create an output array replacing missing readings with NaN - temp_arr = [NaN, NaN, NaN, NaN]; - for i=1:4 - if ~isempty(this.temp{i}) - temp_arr(i) = this.temp{i}; - end - end - % Trigger event notification - triggerPropertyRead(this); - end - - % out_channel is 1-4, in_channel is A-D - function ret = readHeaterRange(this) - cmd_str = 'RANGE? 1;RANGE? 2;RANGE? 3;RANGE? 4'; - resp_str = query(this.Device, cmd_str); - resp_split = strsplit(resp_str,';','CollapseDelimiters',false); - this.heater_rng = cellfun(@(s)sscanf(s, '%i'),... - resp_split,'UniformOutput',false); - ret = this.heater_rng; - % Trigger event notification - triggerPropertyRead(this); - end - - function writeHeaterRange(this, out_channel, val) - if isHeaterRangeOk(this, out_channel, val) - cmd = sprintf('RANGE %i,%i', out_channel, val); - fprintf(this.Device, cmd); - end - end - - function ret = readSetpoint(this) - cmd_str = 'SETP? 1;SETP? 2;SETP? 3;SETP? 4'; - resp_str = query(this.Device, cmd_str); - resp_split = strsplit(resp_str,';','CollapseDelimiters',false); - this.setpoint = cellfun(@(s)sscanf(s, '%e'),... - resp_split,'UniformOutput',false); - ret = this.setpoint; - % Trigger event notification - triggerPropertyRead(this); - end - - function writeSetpoint(this, out_channel, val) - cmd_str = sprintf('SETP %i,%.3f', out_channel, val); - fprintf(this.Device, cmd_str); - end - - function ret = readInputSensorName(this) - cmd_str = 'INNAME? A;INNAME? B;INNAME? C;INNAME? D'; - resp_str = query(this.Device, cmd_str); - this.inp_sens_name = strtrim(strsplit(resp_str,';',... - 'CollapseDelimiters',false)); - ret = this.inp_sens_name; - % Trigger event notification - triggerPropertyRead(this); - end - - function writeInputSensorName(this, in_channel, name) - fprintf(this.Device, ['INNAME ',in_channel, name]); - ch_n = inChannelToNumber(this, in_channel); - if ~strcmpi(this.inp_sens_name{ch_n}, name) - warning(['Name of input sensor ',in_channel,... - ' could not be changed']) - end - end - - function ret = readOutMode(this) - cmd_str = 'OUTMODE? 1;OUTMODE? 2;OUTMODE? 3;OUTMODE? 4'; - resp_str = query(this.Device, cmd_str); - resp_split = strsplit(resp_str,';','CollapseDelimiters',false); - this.out_mode = cellfun(@(s)sscanf(s, '%i,%i,%i'),... - resp_split,'UniformOutput',false); - ret = this.out_mode; - % Trigger event notification - triggerPropertyRead(this); - end - - function writeOutMode(this,out_channel,mode,cntl_inp,powerup_en) - cmd_str = sprintf('OUTMODE %i,%i,%i,%i',out_channel,... - mode,cntl_inp,powerup_en); - fprintf(this.Device, cmd_str); - end - end - - %% auxiliary method - methods (Access=private) - % check if the heater range code takes a proper value, which is - % channel-dependent - function bool = isHeaterRangeOk(~, out_channel, val) - bool = false; - switch out_channel - case {1,2} - if val>=0 && val <=3 - bool = true; - else - warning(['Wrong heater range. Heater range for '... - 'channels 1 or 2 can '... - 'take only integer values between 0 and 3']) - end - case {3,4} - if val>=0 && val <=1 - bool = true; - else - warning(['Wrong heater range. Heater range for '... - 'channels 3 or 4 can '... - 'take only values 1 or 2.']) - end - end - end - - function num = inChannelToNumber(~,in_channel) - switch in_channel - case 'A' - num = int32(1); - case 'B' - num = int32(2); - case 'C' - num = int32(3); - case 'D' - num = int32(4); - otherwise - error('Input channel should be A, B, C or D.') - end - end - end - - %% Set and get methods - methods - function str_cell = get.heater_rng_str(this) - str_cell = {'','','',''}; - % Channels 1-2 and 3-4 have different possible states - for i=1:4 - if ~isempty(this.heater_rng{i}) - ind = int32(this.heater_rng{i}+1); - else - ind=0; - end - if i<=2 - str_cell{i} = this.heater12_rng_list{ind}; - else - str_cell{i} = this.heater34_rng_list{ind}; - end - end - end - - function str_cell = get.temp_str(this) - str_cell = {'','','',''}; - for i=1:4 - if ~isempty(this.temp{i}) - str_cell{i} = sprintf('%.3f %s', this.temp{i},... - this.temp_unit); - end - end - end - - function str_cell = get.out_mode_str(this) - str_cell = {'','','',''}; - try - for i=1:4 - ind = int32(this.out_mode{i}(1)+1); - str_cell{i} = this.out_mode_list{ind}; - end - catch - warning(['Output mode could not be interpreted ',... - 'from code. Code should be between 0 and 5.']) - end - end - - function str_cell = get.cntl_inp_str(this) - str_cell = {'','','',''}; - try - for i=1:4 - ind = int32(this.out_mode{i}(2)+1); - str_cell{i} = this.inp_list{ind}; - end - catch - warning(['Input channel could not be interpreted ',... - 'from index. Index should be between 0 and 4.']) - end - end - - function str_cell = get.powerup_en_str(this) - str_cell = {'','','',''}; - for i=1:4 - if this.out_mode{i}(3) - str_cell{i} = 'On'; - else - str_cell{i} = 'Off'; - end - end - end - - function set.temp_unit(this, val) - if strcmpi(val,'K')||strcmpi(val,'C') - this.temp_unit = upper(val); - else - warning(['Temperature unit needs to be K or C, ',... - 'value has not been changed']) - end - end - end -end - diff --git a/@MyLog/MyLog.m b/@MyLog/MyLog.m index 5837ddf..8ad9a59 100644 --- a/@MyLog/MyLog.m +++ b/@MyLog/MyLog.m @@ -1,737 +1,863 @@ % 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. % If instantiated as MyLog(load_path) then % the content is loaded from file classdef MyLog < matlab.mixin.Copyable - properties (Access=public) + properties (Access = public, SetObservable = true) + % 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: %.3g' - % Data column and line separators column_sep = '\t' - line_sep='\r\n' + 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' + % Formatting options for the metadata + metadata_opts = {} + 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 % Format for string representation of timestamps datetime_fmt = 'yyyy-MMM-dd HH:mm:ss' + + save_cont = false % If true changes are continuously saved 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 + + properties (GetAccess = public, SetAccess = protected, ... + SetObservable = true) - % Structure array that stores all the axes the log is plotted in - PlotList; + timestamps % Times at which data was aqcuired + data % Array of measurements - % Information about the log in saveable format, - % including time labels and data headers - Metadata + % Structure array that stores labeled time marks + TimeLabels = struct( ... + 'time', {}, ... % datetime object + 'time_str', {}, ... % time in text format + 'text_str', {}); % message string + + % Structure array that stores all the axes the log is plotted in + PlotList = struct( ... + 'Axes', {}, ... % axes handles + 'DataLines',{}, ... % data line handles + 'LbLines', {}, ... % label line handles + 'BgLines', {}); % label line background handles + + % First timestamp that was saved in the current file. This value + % is used to decide which time labels to retain under when the log + % data is being trimmed. + FirstSaveTime = datetime.empty() end - properties (Dependent=true) - data_line_fmt % Format specifier for one data row to be printed + properties (Dependent = true) + channel_no % Number of data colums - column_headers % Time column header + data column headers + data_line_fmt % Format specifier for one data row to be printed - data_file_name % File name with extension for data saving - meta_file_name % File name with extension for metadata saving + column_headers % Time column header + data column headers - timestamps_num % timestamps converted to numeric format + 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 + methods (Access = public) function this = MyLog(varargin) - P=MyClassParser(this); - % options for MeasHeaders - addParameter(P, 'metadata_opts',{},@iscell); - - if mod(length(varargin),2)==1 - % odd number of elements in varargin - interpret the first - % element as file name and the rest as name-value pairs - load_path=varargin{1}; - assert(ischar(load_path)&&isvector(load_path),... - '''file_name'' must be a vector of characters'); - processInputs(P, this, varargin{2:end}); - this.file_name=load_path; - else - % Parse varargin as a list of name-value pairs - processInputs(P, this, varargin{:}); - load_path=[]; - end - - this.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 ~isempty(load_path) - load(this, load_path); - end - + P = MyClassParser(this); + processInputs(P, this, varargin{:}); end - %% Save and load functionality % save the entire data record - function save(this, fname) - % Verify that the data can be saved - assertDataMatrix(this); + function save(this, filename) % File name can be either supplied explicitly or given as the % file_name property - if nargin()<2 - fname = this.file_name; + if nargin() < 2 + filename = this.file_name; else - this.file_name=fname; + this.file_name = filename; end - assert(~isempty(fname), 'File name is not provided.'); + assert(~isempty(filename), 'File name is not provided.'); - datfname=this.data_file_name; - metfname=this.meta_file_name; + datfname = this.data_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); + stat = createFile(datfname); + if ~stat + return end - end - - - % Load log from file - function load(this, fname) - if nargin()==2 - this.file_name=fname; + + fid = fopen(datfname, 'w'); + + % Write column headers + str = printDataHeaders(this); + fprintf(fid, '%s', str); + + % Write the bulk of data + fmt = this.data_line_fmt; + for i = 1:length(this.timestamps) + fprintf(fid, fmt, this.timestamps_num(i), this.data(i,:)); 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.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.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, ... - 'Format', this.datetime_fmt); - 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','Format',this.datetime_fmt); - end + this.FirstSaveTime = this.timestamps(1); + + % Save time labels in a separate file + saveMetadata(this); 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(); + 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'),... + addOptional(p, 'Axes', [], @(x)assert(isaxes(x),... '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; + if ~isempty(p.Results.Axes) + Axes = p.Results.Axes; else - Ax=gca(); + Axes = gca(); end % Find out if the log was already plotted in these axes. If % not, appned Ax to the PlotList. - ind=findPlotInd(this, Ax); + ind = findPlotInd(this, Axes); if isempty(ind) - l=length(this.PlotList); - this.PlotList(l+1).Axes=Ax; - ind=l+1; + l = length(this.PlotList); + this.PlotList(l+1).Axes = Axes; + 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; + Pls = line(Axes, 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 + Pls = this.PlotList(ind).DataLines; + for i = 1:length(Pls) + + % Set new data and timestamps at the same time to + % prevent a conflict of array sizes + set(Pls(i), ... + 'XData', this.timestamps, ... + 'YData', this.data(:,i)); end end % Set the visibility of lines - if ~ismember('isdisp',p.UsingDefaults) - for i=1:ncols - Pls(i).Visible=p.Results.isdisp(i); + 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); + if p.Results.time_labels + + % Plot time labels + plotTimeLabels(this, ind); + else + + % Hide existing time labels + try + set(this.PlotList(ind).LbLines, 'Visible', 'off'); + set(this.PlotList(ind).BgLines, 'Visible', 'off'); + catch ME + warning(ME.message) + end 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'); + + % Add legend + if isempty(Axes.Legend) + legend(Axes, 'Location', 'southwest'); + end + + if p.Results.legend && ~isempty(this.data_headers) && ... + ~isempty(this.data) + + % Display the legend + Axes.Legend.Visible = 'on'; + Axes.Legend.String = this.data_headers; + + % Include only those lines that are visible + for i = 1:ncols + Pls(i).Annotation.LegendInformation.IconDisplayStyle = ... + Pls(i).Visible; + end + else + + % Hide the legend + Axes.Legend.Visible = 'off'; 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{:}); + function appendData(this, Time, val) % Format checks on the input data - assert(isa(time,'datetime')||isnumeric(time),... + 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.'); + + assert(isrow(val) && isnumeric(val), ... + '''val'' argument must be a numeric row vector.'); if ~isempty(this.data) - [~, ncols]=size(this.data); - assert(length(val)==ncols,['Length of ''val'' ',... + [~, ncols] = size(this.data); + assert(length(val) == ncols,['Length of ''val'' ' ... 'does not match the number of data columns']); end % Ensure time format - if isa(time,'datetime') - time.Format=this.datetime_fmt; + if isa(Time,'datetime') + Time.Format = this.datetime_fmt; end % Append new data and time stamps - this.timestamps=[this.timestamps; time]; - this.data=[this.data; val]; + 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 + if this.save_cont try - exstat = exist(this.data_file_name,'file'); - if exstat==0 - % if the file does not exist, create it and write + if exist(this.data_file_name, 'file') == 2 + + % Otherwise open for appending + fid = fopen(this.data_file_name, 'a'); + else + + % 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'); + fid = fopen(this.data_file_name, 'w'); + + str = printDataHeaders(this); + fprintf(fid, '%s', str); end + % Convert the new timestamps to numeric form for saving - if isa(time,'datetime') - time_num=posixtime(time); + if isa(Time, 'datetime') + time_num = posixtime(Time); else - time_num=time; + time_num = Time; end + % Append new data points to file fprintf(fid, this.data_line_fmt, time_num, val); fclose(fid); - % Save metadata with time labels - if ~isempty(this.TimeLabels) && ... - exist(this.meta_file_name, 'file')==0 - save(this.Metadata, this.meta_file_name, ... - 'overwrite', true); + if isempty(this.FirstSaveTime) + this.FirstSaveTime = Time; end catch warning(['Logger cannot save data at time = ',... datestr(datetime('now', ... - 'Format',this.datetime_fmt))]); + 'Format', this.datetime_fmt))]); + % 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 + % Add label + function addTimeLabel(this, varargin) + p = inputParser(); - % Remove existing labels - eraseTimeLabels(this, Ax); + addParameter(p, 'Time', datetime('now'), ... + @(x)assert(isa(x, 'datetime'), ['''Time'' must be of ' ... + 'the type datetime.'])) - % Define marker lines to span over the entire plot - ymin=Ax.YLim(1); - ymax=Ax.YLim(2); - markline = linspace(ymin, ymax, 2); + addParameter(p, 'text', '', ... + @(x)assert(iscellstr(x) || ischar(x) || isstring(x), ... + '''text'' must be a string or cell array of strings.')) - % 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, ymin+0.95*(ymax-ymin), T.text_str,... - 'Units','data',... - 'HorizontalAlignment','right',... - 'VerticalAlignment','top',... - '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', 'Format', this.datetime_fmt), ... - @(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 'index' is specified, then the function configures an + % existing time label instead of adding a new one + addParameter(p, 'index', [], ... + @(x)assert(floor(x)==x && x<=length(this.TimeLabels), ... + ['Index must be a positive integer not exceeding the ' ... + 'number of time labels.'])) + + parse(p, varargin{:}) - if any(ismember({'time','str'}, p.UsingDefaults)) - % Invoke a dialog to add the label time and name + Time = p.Results.Time; + Time.Format = this.datetime_fmt; + + if ismember('text', p.UsingDefaults) + + % Invoke a dialog to input label time and name + if ismember('index', p.UsingDefaults) + + % Default dialog field values + dlg_args = {'', datestr(Time)}; + else + + % Provide existing values for modification + Tlb = this.TimeLabels(p.Results.index); + dlg_args = {char(Tlb.text_str), Tlb.time_str}; + end + answ = inputdlg({'Label text', 'Time'},'Add time label',... - [2 40; 1 40],{'',datestr(p.Results.time)}); + [2 40; 1 40], dlg_args); - if isempty(answ)||isempty(answ{1}) + % If 'Cancel' button is pressed or no input is provided, do + % not modify or add a new label + if isempty(answ) || isempty(answ{1}) return - else - % Conversion of the inputed value to datetime to - % ensure proper format - time=datetime(answ{2}, 'Format', this.datetime_fmt); - % 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); + + % Convert the inputed value to datetime with proper format + Time = datetime(answ{2}, 'Format', this.datetime_fmt); - this.TimeLabels(l+1).time=time; - this.TimeLabels(l+1).time_str=datestr(time); - this.TimeLabels(l+1).text_str=str; + % Store multiple lines as cell array + str = cellstr(answ{1}); + else + str = cellstr(p.Results.text); + end - % Order time labels by ascending time - sortTimeLabels(this); + if ~isempty(this.timestamps) && (Time < this.timestamps(1)) + warning(['The time of label ''%s'' is earlier than ' ... + 'the first timestamp in memory. Such time label ' ... + 'will not be plotted but will be saved in ' ... + 'metadata.'], str{1}); + end - if p.Results.save==true - % Save metadata with new time labels - save(this.Metadata, this.meta_file_name, ... - 'overwrite', true); + if ~isempty(this.FirstSaveTime) && (Time < this.FirstSaveTime) + warning(['The time of label ''%s'' is earlier than ' ... + 'the first timestamp in the current data file. ' ... + 'Such time label will not be automatically saved ' ... + 'in metadata.'], str{1}); 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', 'Format', this.datetime_fmt), ... - @(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 - % Convert the input value to datetime and ensure - % proper format - time=datetime(answ{2}, 'Format', this.datetime_fmt); - % Store multiple lines as cell array - str=cellstr(answ{1}); - end + + % Find the index of time label. We do not supply a default + % value for the end of the list in case the length was changed + % over the course of function execution. + if ismember('index', p.UsingDefaults) + ind = length(this.TimeLabels)+1; + else + ind = p.Results.index; end - this.TimeLabels(ind).time=time; - this.TimeLabels(ind).time_str=datestr(time); - this.TimeLabels(ind).text_str=str; + 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); + if this.save_cont + saveMetadata(this); 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}; + function deleteTimeLabel(this, ind) + this.TimeLabels(ind) = []; + + if this.save_cont + saveMetadata(this); + end + end + + % Show the list of labels in a readable format + function lst = printTimeLabels(this) + if isempty(this.TimeLabels) + lst = {}; + return + end + + if ~isempty(this.timestamps) + + % Select only those time labels that are within + % the interval of time of currently stored data + t_ind = ([this.TimeLabels.time] >= this.timestamps(1)); + Tl = this.TimeLabels(t_ind); + else + Tl = this.TimeLabels; + end + + % The returned output is a list of character strings + lst = cell(length(Tl), 1); + + for i = 1:length(Tl) + if ~isempty(Tl(i).text_str) + + % Display the first line of label + lbl = Tl(i).text_str{1}; + else + lbl = ''; end - lst{i}=[this.TimeLabels(i).time_str,' ', tmpstr]; + lst{i} = [Tl(i).time_str, ' ', lbl]; end end %% Misc public functions % Clear log data and time labels function clear(this) + % Clear while preserving the array types - this.TimeLabels(:)=[]; + 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); + delete(this.PlotList(i).BgLines); end - this.PlotList(:)=[]; + this.PlotList(:) = []; % Clear data and its type this.timestamps = []; this.data = []; + + % Switch off continuous saving to prevent the overwriting of + % previously saved data + this.save_cont = false; 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.']); + % Convert the log channel record to trace + function Trace = toTrace(this, varargin) + p = inputParser(); + + addParameter(p, 'channel', 1, ... + @(x)assert(x>0 && floor(x)==x && x<=this.channel_no, ... + ['Channel number must be an integer between 1 and ' ... + num2str(this.channel_no) '.'])); + + addParameter(p, 'index', []); + + % If false, the beginning of x data in the trace is shifted to + % zero + addParameter(p, 'absolute_time', false, @islogical); + + parse(p, varargin{:}); + + n_ch = p.Results.channel; + + Trace = MyTrace(); + + if ismember('index', p.UsingDefaults) + Trace.x = this.timestamps_num; + Trace.y = this.data(:, n_ch); + else + Trace.x = this.timestamps_num(p.Results.index); + Trace.y = this.data(p.Results.index, n_ch); + end + + Trace.name_x = 'Time'; + Trace.unit_x = 's'; + + if ~p.Results.absolute_time + + % Shift the beginning of data to zero + Trace.x = Trace.x-Trace.x(1); + end + + if length(this.data_headers) >= n_ch + + % Try extracting the name and unit from the column header. + % The regexp will match anything of the format 'x (y)' with + % any types of brackets, (), [] or {}. + tokens = regexp(this.data_headers{n_ch}, ... + '(.*)[\(\[\{](.*)[\)\]\}]', 'tokens'); + + if ~isempty(tokens) + + % Removes leading and trailing whitespaces + tokens = strtrim(tokens{1}); + + Trace.name_y = tokens{1}; + Trace.unit_y = tokens{2}; + else + Trace.name_y = this.data_headers{n_ch}; + end + end end + end + + methods (Access = public, Static = true) - % Display last reading - function str = printLastReading(this) - if isempty(this.timestamps) - str = ''; + % Load log from file. Formatting parameters can be supplied as + % varargin + function L = load(filename, varargin) + assert(exist(filename, 'file') == 2, ... + ['File ''', filename, ''' is not found.']) + + L = MyLog(varargin{:}); + L.file_name = filename; + + % Load metadata if file is found + if exist(L.meta_file_name, 'file') == 2 + Mdt = MyMetadata.load(L.meta_file_name,L.metadata_opts{:}); + setMetadata(L, Mdt); 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 + disp(['Log metadata file is not found, continuing ' ... + 'without it.']); + end + + % Read column headers from the data file + fid = fopen(filename,'r'); + dat_col_heads = strsplit(fgetl(fid), L.column_sep, ... + 'CollapseDelimiters', true); + fclose(fid); + + % Assign column headers + if length(dat_col_heads) > 1 + L.data_headers = dat_col_heads(2:end); + end + + % Read data as delimiter-separated values and convert to cell + % array, skip the first line containing column headers + fulldata = dlmread(filename, L.column_sep, 1, 0); + + L.data = fulldata(:, 2:end); + L.timestamps = fulldata(:, 1); + + % Convert time stamps to datetime if the time column header + % is 'posixtime' + if ~isempty(dat_col_heads) && ... + contains(dat_col_heads{1}, 'posix', 'IgnoreCase', true) + L.timestamps = datetime(L.timestamps, ... + 'ConvertFrom', 'posixtime', 'Format', L.datetime_fmt); end + + L.FirstSaveTime = L.timestamps(1); end end - methods (Access=private) - %% Auxiliary private functions + methods (Access = protected) + + % Add time labels on the plot given the plot index in PlotList + function plotTimeLabels(this, ind) + Axes = this.PlotList(ind).Axes; + + if ~isempty(this.timestamps) && ~isempty(this.TimeLabels) + + % Select for plotting only those time labels that are within + % the interval of time of currently stored data + t_ind = ([this.TimeLabels.time] >= this.timestamps(1)); + Tl = this.TimeLabels(t_ind); + else + Tl = this.TimeLabels; + end + + % Plot labels + for i = 1:length(Tl) + T = Tl(i); + n_lines = max(length(T.text_str), 1); + + try + Lbl = this.PlotList(ind).LbLines(i); + + % Update the existing label line + Lbl.Value = T.time; + Lbl.Label = T.text_str; + Lbl.Visible = 'on'; + + % Update the background width - font size times the + % number of lines + Bgl = this.PlotList(ind).BgLines(i); + Bgl.Value = T.time; + Bgl.LineWidth = Lbl.FontSize*n_lines; + Bgl.Visible = 'on'; + catch + + % Add new background line + this.PlotList(ind).BgLines(i) = xline(Axes, T.time, ... + 'LineWidth', 10*n_lines, ... + 'Color', [1, 1, 1]); + + % Add new label line + this.PlotList(ind).LbLines(i) = xline(Axes, T.time, ... + '-', T.text_str, ... + 'LineWidth', 0.5, ... + 'LabelHorizontalAlignment', 'center', ... + 'FontSize', 10); + end + end + + % Remove redundant markers if any + n_tlbl = length(Tl); + + if length(this.PlotList(ind).LbLines) > n_tlbl + delete(this.PlotList(ind).LbLines(n_tlbl+1:end)); + this.PlotList(ind).LbLines(n_tlbl+1:end) = []; + end + + if length(this.PlotList(ind).BgLines) > n_tlbl + delete(this.PlotList(ind).BgLines(n_tlbl+1:end)); + this.PlotList(ind).BgLines(n_tlbl+1:end) = []; + end + end % 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)=[]; + len = length(this.timestamps); + + if len <= this.length_lim + return end + + % Remove data points beyond the length limit + dn = len-this.length_lim; + this.timestamps(1:dn) = []; + this.data(1:dn, :) = []; + + if isempty(this.TimeLabels) + return + end + + % Remove only the time labels which times fall outside the + % range of both a) trimmed data b) data in the current file + BeginTime = min(this.timestamps(1), this.FirstSaveTime); + ind = ([this.TimeLabels.time] < BeginTime); + this.TimeLabels(ind) = []; end % Print column names to a string - function str=printDataHeaders(this) - cs=this.column_sep; - str=sprintf(['%s',cs], this.column_headers{:}); - str=[str,sprintf(this.line_sep)]; + function str = printDataHeaders(this) + 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) + 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); + ind_b = ([this.PlotList.Axes] == Ax); + + % Find the index of first match + ind = find(ind_b, 1); else - ind=[]; + 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); + times = [this.TimeLabels.time]; + [~,ind] = sort(times); + this.TimeLabels = this.TimeLabels(ind); + end + + % Create metadata from log properties + function Mdt = getMetadata(this) + if ~isempty(this.FirstSaveTime) + + % Select for saving only those time labels that are within + % the interval of time of data in the current file + ind = ([this.TimeLabels.time] >= this.FirstSaveTime); + Tl = this.TimeLabels(ind); + else + Tl = this.TimeLabels; + end + + if ~isempty(Tl) + + % Add the textual part of TimeLabels structure + Mdt = MyMetadata(this.metadata_opts{:}, ... + 'title', 'TimeLabels'); + + Lbl = struct('time_str', {Tl.time_str}, ... + 'text_str', {Tl.text_str}); + addParam(Mdt, 'Lbl', Lbl); + else + Mdt = MyMetadata.empty(); + end + end + + % Save log metadata, owerwriting existing + function saveMetadata(this, Mdt) + if exist('Mdt', 'var') == 0 + Mdt = getMetadata(this); + end + + if isempty(Mdt) + return + end + + metfilename = this.meta_file_name; + + % Create or clear the file + stat = createFile(metfilename, 'overwrite', true); + if ~stat + return + end + + save(Mdt, metfilename); + end + + % Process metadata + function setMetadata(this, Mdt) + + % Assign time labels + Tl = titleref(Mdt, 'TimeLabels'); + if ~isempty(Tl) + Lbl = Tl.ParamList.Lbl; + for i=1:length(Lbl) + this.TimeLabels(i).time_str = Lbl(i).time_str; + this.TimeLabels(i).time = datetime(Lbl(i).time_str, ... + 'Format', this.datetime_fmt); + this.TimeLabels(i).text_str = Lbl(i).text_str; + end + end end end - %% set and get methods + %% 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)); + 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; + + this.data_headers = val; + end + + function set.save_cont(this, val) + this.save_cont = logical(val); + end + + function set.file_name(this, val) + assert(ischar(val),'''file_name'' must be a character string.') + + if ~isequal(this.file_name, val) + + % Reset the first saved timestamp marker + this.FirstSaveTime = datetime.empty(); %#ok + end + + this.file_name = 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; + 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; + ext = this.meta_file_ext; fname = fullfile(filepath,[name,ext]); end - function data_line_fmt=get.data_line_fmt(this) - cs=this.column_sep; - nl=this.line_sep; + function data_line_fmt = get.data_line_fmt(this) + cs = this.column_sep; + nl = this.line_sep; if isempty(this.data) - l=0; + l = 0; else - [~,l]=size(this.data); + [~,l] = size(this.data); end data_line_fmt = this.time_fmt; - for i=1:l + 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) + 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]; + hdrs = [time_title_str, this.data_headers]; end - function time_num_arr=get.timestamps_num(this) + function time_num_arr = get.timestamps_num(this) + % Convert time stamps to numbers - if isa(this.timestamps,'datetime') - time_num_arr=posixtime(this.timestamps); + if isa(this.timestamps, 'datetime') + time_num_arr = posixtime(this.timestamps); else - time_num_arr=this.timestamps; + 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) - - if ~isempty(this.TimeLabels) - % 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) + function val = get.channel_no(this) + if isempty(this.data) + val = length(this.data_headers); + else + [~, val] = size(this.data); end end end end diff --git a/@MyLogger/MyLogger.m b/@MyLogger/MyLogger.m deleted file mode 100644 index f2135b6..0000000 --- a/@MyLogger/MyLogger.m +++ /dev/null @@ -1,119 +0,0 @@ -% Generic logger that executes MeasFcn according to MeasTimer, stores the -% results and optionally continuously saves them. -% MeasFcn should be a function with no arguments. -% MeasFcn need to return a row vector of numbers in order to save the log -% in text format or display it. With other kinds of returned values the -% log can still be recorded, but not saved or dispalyed. - -classdef MyLogger < handle - - properties (Access=public) - % timer object - MeasTimer - - % Function that provides data to be recorded - MeasFcn = @()0 - - save_cont = false - - % MyLog object to store the recorded data - Record - end - - properties (SetAccess=protected, GetAccess=public) - % 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 (Access=public) - function this = MyLogger(varargin) - P=MyClassParser(this); - processInputs(P, this, varargin{:}); - - this.Record=MyLog(P.unmatched_nv{:}); - - % Create and confitugure timer - 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 - % function execution delays - this.MeasTimer.ExecutionMode = 'fixedSpacing'; - this.MeasTimer.TimerFcn = @(~,event)LoggerFcn(this,event); - end - - function delete(this) - %stop and delete the timer - stop(this.MeasTimer); - delete(this.MeasTimer); - end - - - % Redefine start/stop functions for the brevity of use - function start(this) - start(this.MeasTimer); - end - - function stop(this) - stop(this.MeasTimer); - end - - end - - methods (Access=protected) - % Perform measurement and append point to the log - function LoggerFcn(this, event) - time = datetime(event.Data.time); - try - meas_result = this.MeasFcn(); - this.last_meas_stat=1; % last measurement ok - catch - warning(['Logger cannot take measurement at time = ',... - datestr(time)]); - this.last_meas_stat=0; % last measurement not ok - end - - if this.last_meas_stat==1 - % append measurement result together with time stamp - appendData(this.Record, time, meas_result,... - 'save', this.save_cont); - triggerNewData(this); - end - end - - %Triggers event for acquired data - function triggerNewData(this) - notify(this,'NewData') - end - end - - %% Set and get functions - methods - function set.MeasFcn(this, val) - assert(isa(val,'function_handle'), ... - '''MeasFcn'' must be a function handle.'); - this.MeasFcn=val; - end - - function set.Record(this, val) - assert(isa(val,'MyLog'), '''Record'' must be a MyLog object') - this.Record=val; - end - - function set.save_cont(this, val) - this.save_cont=logical(val); - end - - function set.MeasTimer(this, val) - assert(isa(val,'timer'), '''MeasTimer'' must be a timer object') - this.MeasTimer=val; - end - end -end - diff --git a/@MyMetadata/MyMetadata.m b/@MyMetadata/MyMetadata.m index b4711fc..222a252 100644 --- a/@MyMetadata/MyMetadata.m +++ b/@MyMetadata/MyMetadata.m @@ -1,462 +1,460 @@ -% 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 +% MyMetadata stores parameter-value pairs and can be saved in a readable +% format. +% +% Metadata 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(load_path) -% 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=15 - end - - properties (Access=private) - PropHandles %Used to store the handles of the dynamic properties - end - - properties (Dependent=true) - field_names - end +classdef MyMetadata < handle & matlab.mixin.CustomDisplay & ... + matlab.mixin.SetGet & matlab.mixin.Copyable - methods - function [this,varargout]=MyMetadata(varargin) - P=MyClassParser(this); + properties (Access = public) + + % Header sections are separated by [hdr_spec, title, hdr_spec] + title = '' + hdr_spec = '==' - if mod(length(varargin),2)==1 - % odd number of elements in varargin - interpret the first - % element as file name and the rest as name-value pairs - load_path=varargin{1}; - assert(ischar(load_path)&&isvector(load_path),... - '''file_name'' must be a vector of characters'); - processInputs(P, this, varargin{2:end}); - else - % Parse varargin as a list of name-value pairs - processInputs(P, this, varargin{:}); - load_path=[]; - end - - this.PropHandles=struct(); - - if ~isempty(load_path) - varargout{1}=load(this, load_path); - end - end + % Columns are separated by this symbol (space-tab by default) + column_sep = ' \t' - %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 + % Comments start from this symbol + comment_sep = '%' + line_sep = '\r\n' - %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 + % Limit for column padding. Variables which take more space than + % this limit are ignored when calculating the padding length. + pad_lim = 15 - %Clears the object of all fields - function clearFields(this) - cellfun(@(x) deleteField(this, x), this.field_names) - end + % Metadata parameter values + ParamList = struct() + % Options for metadata parameters + ParamOptList = struct() + end + + methods (Access = public) - % 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 + function this = MyMetadata(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); 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{:}); + % Adds a new metadata parameter. + function addParam(this, param_name, value, varargin) + assert(isvarname(param_name), ['Parameter name must be a ' ... + 'valid variable name.']); - S=p.Results.SubStruct; - comment=p.Results.comment; + p = inputParser(); - %Making sure that the comment does not - %contain new line or carriage return characters, which would - %mess up formating when saving metadata + % Format specifier for printing the value + addParameter(p, 'format', '', @ischar); - newline_smb={sprintf('\n'),sprintf('\r')}; %#ok + % Comment to be added to the line + addParameter(p, 'comment', '', @ischar); - 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,' '); + addParameter(p, 'SubStruct', struct('type',{},'subs',{}),... + @isstruct) + parse(p, varargin{:}); + + % Make sure that the comment does not contain new line or + % carriage return characters, which would mess up formating + % when saving the metadata + [comment, is_mod] = toSingleLine(p.Results.comment); + this.ParamOptList.(param_name).comment = comment; + + if is_mod + warning(['Comment string for ''%s'' has been ' ... + 'converted to single line.'], param_name); end + + this.ParamOptList.(param_name).format = p.Results.format; - this.(field_name).(param_name).comment=comment; - + S = p.Results.SubStruct; if isempty(S) - % Assign value directly - this.(field_name).(param_name).value=value; + this.ParamList.(param_name) = value; else - % Assign using subref structure - tmp=feval([class(value),'.empty']); - this.(field_name).(param_name).value=subsasgn(tmp,S,value); + + % Initialize property with an empty variable of the same + % class as value, this is important if SubStruct is an + % array index. + this.ParamList.(param_name) = ... + feval([class(value),'.empty']); + + % Construct full subscript reference with respect to 'this' + S = [struct('type', '.', 'subs', param_name), S]; + + % Assign the value of parameter + this.ParamList = subsasgn(this.ParamList, S, value); end - - this.(field_name).(param_name).fmt_spec=p.Results.fmt_spec; end - % The function below is useful to ensure the correspondence between - % metadata parameter names and object property names. It spares - % some lines of code. Works only with single-field metadata. - function addObjProp(this, Obj, tag, varargin) - assert(length(this.field_names)==1, ['Metadata has to ' ... - 'contain a single field in order to add object property.']); - fn=this.field_names{1}; - addParam(this, fn, tag, Obj.(tag), varargin{:}); + function bool = isparam(this, param_name) + bool = isfield(this.ParamList, param_name); end - % Save in a readable format - function save(this, filename, varargin) - createFile(filename, varargin{:}); - for i=1:length(this.field_names) - printField(this, this.field_names{i}, filename); - end - printEndMarker(this, filename); + % Alias for addParam that is useful to ensure the correspondence + % between metadata parameter names and object property names. + function addObjProp(this, Obj, tag, varargin) + addParam(this, tag, Obj.(tag), varargin{:}); 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; + % Print metadata in a readable form + function str = mdt2str(this) + + % Make the function spannable over arrays + if isempty(this) + str = ''; + return + elseif length(this) > 1 + str_arr = arrayfun(@mdt2str, this, 'UniformOutput', false); + + % Add an extra line between sections + str = strjoin(str_arr, this(1).line_sep); + return + end + + cs = this.column_sep; + ls = this.line_sep; - ParStruct=this.(field_name); + % Make the output string. Start by printing the title. + str = sprintf([this.hdr_spec, this.title, this.hdr_spec, ls]); - %Compose the list of parameter names expanded over subscripts - %except for those which are already character arrays - par_names=fieldnames(ParStruct); + % Compose the list of parameter names expanded over subscripts + % except for those which are already character arrays + par_names = fieldnames(this.ParamList); + + if isempty(par_names) + return + end + + % Expand parameters over subscripts, except for the arrays of + % characters + exp_par_names = cell(1, length(par_names)); + max_nm_arr = zeros(1, length(par_names)); - %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})); + % Expand parameter subscripts + exp_par_names{i} = printSubs( ... + this.ParamList.(par_names{i}), ... + 'own_name', par_names{i}, ... + 'expansion_test', @(y) ~ischar(y)); + + % Max name length for this parameter including subscripts + max_nm_arr(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); + % Calculate width of the name column + name_pad_length = max(max_nm_arr); - %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; + % Compose a list of parameter values converted to char strings + val_strs = cell(1, length(par_names)); + + % Width of the values column will be the maximum parameter + % string width for i=1:length(par_names) - TmpPar=ParStruct.(par_names{i}); + tmp_nm = par_names{i}; + + % Iterate over parameter indices 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 + tmp_exp_nm = exp_par_names{i}{j}; + TmpS = str2substruct(tmp_exp_nm); - %Do check to detect unsupported data type - if ischar(tmpval)&&~isvector(tmpval)&&~isempty(tmpval) + % Get the indexed value of parameter + TmpS = [struct('type','.','subs', tmp_nm), TmpS]; %#ok + tmp_val = subsref(this.ParamList, TmpS); + + %Do the check to detect unsupported data type + if ischar(tmp_val) && ~isvector(tmp_val) && ... + ~isempty(tmp_val) 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) + 'strings.'], tmp_exp_nm) + % Flatten - tmpval=tmpval(:); + tmp_val = tmp_val(:); 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')},' '); + % Check for new line symbols in strings + if (ischar(tmp_val) || isstring(tmp_val)) && ... + any(ismember({newline, sprintf('\r')},tmp_val)) + warning(['String values must not contain ' ... + '''\\n'' and ''\\r'' symbols, replacing ' ... + 'them with '' ''.']); + + tmp_val = replace(tmp_val, ... + {newline, sprintf('\r')}, ' '); end - if isempty(TmpPar.fmt_spec) + format = this.ParamOptList.(tmp_nm).format; + if isempty(format) + % Convert to string with format specifier % extracted from the varaible calss - par_strs{i}{j}=var2str(tmpval); + val_strs{i}{j} = var2str(tmp_val); 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}{j}); - if (val_pad_length else - fprintf(fileID, [data_fmt_spec,ls],... - exp_par_names{i}{j}, par_strs{i}{j}); + str = [str, sprintf([par_format, ls],... + exp_par_names{i}{j}, val_strs{i}{j})]; %#ok end end end + end + + % Save metadata to a file + function save(this, filename, varargin) + p = inputParser(); + addParameter(p, 'overwrite', false, @islogical) + parse(p, varargin{:}); + + if p.Results.overwrite + + % Open and delete any existing content of the file + fileID = fopen(filename, 'w'); + else + + % Open for appending + fileID = fopen(filename, 'a'); + end - %Prints an extra line separator at the end - fprintf(fileID, ls); + fprintf(fileID, '%s', mdt2str(this)); fclose(fileID); end - %Print terminator that separates header from data - function printEndMarker(this, filename) - fileID=fopen(filename,'a'); - fprintf(fileID,... - [this.hdr_spec, this.end_header, ... - this.hdr_spec, this.line_sep]); - fclose(fileID); + % Select objects with titles listed in varargin from the array + function SubArr = titleref(this, varargin) + ind = ismember({this.title}, varargin); + SubArr = this(ind); end - %Adds time header - %Second optional argument is the name of the field, i.e - %addTimeField(this, 'TimeField') - function addTimeField(this, t_field_name) - if nargin()>1 - assert(ischar(t_field_name)&&isvector(t_field_name),... - 'Time field name must be a character vector') - else - t_field_name='Time'; - end + % Remove objects with titles listed in varargin from the array + function SubArr = rmtitle(this, varargin) + ind = ~ismember({this.title}, varargin); + SubArr = this(ind); + end + end + + methods (Access = public, Static = true) + + % Create metadata indicating the present moment of time + function TimeMdt = time(varargin) + TimeMdt = MyMetadata(varargin{:}); - if ismember(t_field_name, this.field_names) - deleteField(this, t_field_name) + if isempty(TimeMdt.title) + TimeMdt.title = 'Time'; end - dv=datevec(datetime('now')); - 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'); + dv = datevec(datetime('now')); + addParam(TimeMdt, 'Year', dv(1), 'format', '%i'); + addParam(TimeMdt, 'Month', dv(2), 'format', '%i'); + addParam(TimeMdt, 'Day', dv(3), 'format', '%i'); + addParam(TimeMdt, 'Hour', dv(4), 'format', '%i'); + addParam(TimeMdt, 'Minute', dv(5), 'format', '%i'); + addParam(TimeMdt, 'Second', floor(dv(6)), 'format', '%i'); + addParam(TimeMdt, 'Millisecond',... + round(1000*(dv(6)-floor(dv(6)))), 'format', '%i'); end - function n_end_header=load(this, filename) - %Before we load, we clear all existing fields - clearFields(this); - - fileID=fopen(filename,'r'); + % Load metadata from file. Return all the entries found and + % the number of the last line read. + function [MdtArr, n_end_line] = load(filename, varargin) + fileID = fopen(filename, 'r'); - title_exp=[this.hdr_spec,'(\w.*)',this.hdr_spec]; + MasterMdt = MyMetadata(varargin{:}); - %Loop initialization - line_no=0; - curr_title=''; + % Loop initialization + MdtArr = MyMetadata.empty(); + line_no = 0; - %Loop continues until we reach the next header or we reach - %the end of the file + % 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.') + + % Grabs the current line + curr_line = fgetl(fileID); + + % Give a warning if the file is empty, i.e. if fgetl + % returns -1 + if curr_line == -1 + disp(['Read empty file ', filename, '.']); + break end - %Skips if current line is empty - if isempty(curr_line) + + % Skips if the current line is empty + if isempty(deblank(curr_line)) + line_no = line_no+1; 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 + S = parseLine(MasterMdt, curr_line); + + switch S.type + case 'title' + + % Add new a new metadata to the output list + TmpMdt = MyMetadata(varargin{:}, 'title', S.match); + MdtArr = [MdtArr, TmpMdt]; %#ok + case 'paramval' + + % Add a new parameter-value pair to the current + % metadata + [name, value, format, comment, Subs] = S.match{:}; + + if ~isparam(TmpMdt, name) + + % Add new parameter + addParam(TmpMdt, name, value, ... + 'format', format, 'SubStruct', Subs, ... + 'comment', comment); + else + + % Assign the value to a new subscript of + % an existing parameter + Subs = [substruct('.', name), Subs]; %#ok + TmpMdt.ParamList = subsasgn( ... + TmpMdt.ParamList, Subs, value); + end + otherwise + + % Exit + break end + + % Increment the counter + line_no = line_no+1; end + + n_end_line = line_no; fclose(fileID); - if isempty(this.field_names) - warning('No metadata found, continuing without metadata.') - n_end_header=1; + end + end + + methods (Access = protected) + + % Parse string and determine the type of string + function S = parseLine(this, str) + S = struct( ... + 'type', '', ... % title, paramval, other + 'match', []); % parsed output + + % Check if the line contains a parameter - value pair. + % First separate the comment if present + pv_token = regexp(str, this.comment_sep, 'split', 'once'); + + if length(pv_token)>1 + comment = pv_token{2}; % There is a comment else - n_end_header=line_no; + comment = ''; % There is no comment end - end - % Need a custom copy method as the one provided by - % matlab.mixin.Copyable does not re-create the handles of dynamic - % properties stored in this.PropHandles - function NewMet=copy(this) - NewMet=MyMetadata(); - Mc=metaclass(NewMet); + % Then process name-value pair. Regard everything after + % the first column separator as value. + pv_token = regexp(pv_token{1}, this.column_sep, 'split', ... + 'once'); - % 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); + % Remove leading and trailing blanks + pv_token = strtrim(pv_token); + + if length(pv_token)>=2 + + % A candidate for parameter-value pair is found. Infer + % the variable name and subscript reference. + [Subs, name] = str2substruct(pv_token{1}); + + if isvarname(name) + + % Attempt converting the value to a number + [value, format] = str2doubleHedged(pv_token{2}); + + S.type = 'paramval'; + S.match = {name, value, format, comment, Subs}; + return 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); + % Check if the line contains a title + title_exp = [this.hdr_spec, '(\w.*)', this.hdr_spec]; + title_token = regexp(str, title_exp, 'once', 'tokens'); + + if ~isempty(title_token) + + % Title expression found + S.type = 'title'; + S.match = title_token{1}; + return + end + + % No match found + S.type = 'other'; + S.match = {}; end + % Make custom footer for command line display + % (see matlab.mixin.CustomDisplay) + function str = getFooter(this) + if length(this) == 1 + + % For a single object display its properties + str = ['Content:', newline, newline, ... + replace(mdt2str(this), sprintf(this.line_sep), ... + newline)]; + elseif length(this) > 1 + + % For a non-empty array of objects display titles + str = sprintf('\tTitles:\n\n'); + + for i = 1:length(this) + str = [str, sprintf('%i\t%s\n', i, this(i).title)]; %#ok + end + + str = [str, newline]; + else + + % For an empty array display nothing + str = ''; + end + end end -end \ No newline at end of file +end + diff --git a/@MyNa/MyNa.m b/@MyNa/MyNa.m deleted file mode 100644 index 0e2591b..0000000 --- a/@MyNa/MyNa.m +++ /dev/null @@ -1,187 +0,0 @@ -% The class for communication with Agilent E5061B Network Analyzer -classdef MyNa < MyScpiInstrument - - properties(Access=public) - Trace1 - Trace2 - - transf_n=1; % trace that triggers NewData event - end - - properties (SetAccess=protected, GetAccess=public) - active_trace = -1; % manipulating with active traces seems unavoidable - % for selecting the data format. -1 stands for unknown - - % data formats for the traces 1-2, options: - % 'PHAS', 'SLIN', 'SLOG', 'SCOM', 'SMIT', 'SADM', 'MLOG', 'MLIN', - %'PLIN', 'PLOG', 'POL' - form1 = 'MLOG'; - form2 = 'PHAS'; - end - - methods - function this=MyNa(interface, address, varargin) - this@MyScpiInstrument(interface, address, varargin{:}); - this.Trace1 = MyTrace(); - this.Trace2 = MyTrace(); - this.Trace1.unit_x = 'Hz'; - this.Trace1.name_x = 'Frequency'; - this.Trace2.unit_x = 'Hz'; - this.Trace2.name_x = 'Frequency'; - end - - % Generate a new data event with header collection suppressed - function transferTrace(this, n_trace) - trace_tag = sprintf('Trace%i', n_trace); - % Assign either Trace1 or 2 to Trace while keeping the metadata - this.(trace_tag).MeasHeaders=copy(this.Trace.MeasHeaders); - this.Trace=copy(this.(trace_tag)); - - triggerNewData(this,'new_header',false); - end - - function data = readTrace(this, n_trace) - writeActiveTrace(this, n_trace); - freq_str = strsplit(query(this.Device,':SENS1:FREQ:DATA?'),','); - data_str = strsplit(query(this.Device,':CALC1:DATA:FDAT?'),','); - data = struct(); - data.x = str2double(freq_str); - % In the returned string there is in general 2 values for each - % frequency point. In the Smith data format this can be used to - % transfer magnitude and phase of the signal in one trace. With - % MLOG, MLIN and PHAS format settings every 2-nd element should - % be 0 - data.y1 = str2double(data_str(1:2:end)); - data.y2 = str2double(data_str(2:2:end)); - - % set the Trace properties - trace_tag = sprintf('Trace%i', n_trace); - this.(trace_tag).x = data.x; - this.(trace_tag).y = data.y1; - - if this.transf_n==n_trace - this.Trace=copy(this.(trace_tag)); - triggerNewData(this); - end - end - - function writeActiveTrace(this, n_trace) - fprintf(this.Device, sprintf(':CALC1:PAR%i:SEL',n_trace)); - this.active_trace = n_trace; - end - - function writeTraceFormat(this, n_trace, fmt) - this.writeActiveTrace(n_trace); - n_str = num2str(n_trace); - this.(['form',n_str]) = fmt; - fprintf(this.Device, sprintf(':CALC1:FORM %s', fmt)); - end - - function singleSweep(this) - openDevice(this); - writeProperty(this,'cont_trig', true); - % Set the triger source to remote control - writeProperty(this,'trig_source', 'BUS'); - % Start a sweep cycle - fprintf(this.Device,':TRIG:SING'); - % Wait for the sweep to finish (for the query to return 1) - query(this.Device,'*OPC?'); - closeDevice(this); - end - - function startContSweep(this) - openDevice(this); - writeProperty(this,'cont_trig', true); - % Set the triger source to be internal - writeProperty(this,'trig_source', 'INT'); - closeDevice(this); - end - - function abortSweep(this) - openDevice(this); - writeProperty(this, 'trig_source', 'BUS'); - fprintf(this.Device,':ABOR'); - closeDevice(this); - end - end - - %% Protected functions - methods (Access=protected) - % Command attributes are {class, attributtes} accepted by - % validateattributes() - function createCommandList(this) - addCommand(this,... - 'cent_freq',':SENS1:FREQ:CENT', 'default',1.5e6,... - 'fmt_spec','%e',... - 'info','(Hz)'); - addCommand(this,... - 'start_freq',':SENS1:FREQ:START', 'default',1e6,... - 'fmt_spec','%e',... - 'info','(Hz)'); - addCommand(this,... - 'stop_freq',':SENS1:FREQ:STOP', 'default',2e6,... - 'fmt_spec','%e',... - 'info','(Hz)'); - addCommand(this,... - 'span',':SENS1:FREQ:SPAN', 'default',1e6,... - 'fmt_spec','%e',... - 'info','(Hz)'); - % IF bandwidth - addCommand(this,... - 'ifbw',':SENS1:BAND', 'default',100,... - 'fmt_spec','%e',... - 'info','IF bandwidth (Hz)'); - % number of points in the sweep - addCommand(this,... - 'point_no',':SENS1:SWE:POIN', 'default',1000,... - 'fmt_spec','%i'); - % number of averages - addCommand(this,... - 'average_no',':SENS1:AVER:COUN', 'default',1,... - 'fmt_spec','%i'); - % number of traces - addCommand(this,... - 'trace_no',':CALC1:PAR:COUN', 'default',1,... - 'fmt_spec','%i',... - 'info','Number of traces'); - % linear or log sweep - addCommand(this,... - 'sweep_type',':SENS1:SWE:TYPE', 'default','LIN',... - 'fmt_spec','%s',... - 'info','Linear or log sweep'); - % switch the output signal on/off - addCommand(this,... - 'enable_out',':OUTP', 'default',0,... - 'fmt_spec','%b',... - 'info','output signal on/off'); - % probe power [dB] - addCommand(this,... - 'power',':SOUR:POW:LEV:IMM:AMPL', 'default',-10,... - 'fmt_spec','%e',... - 'info','Probe power (dB)'); - % windows arrangement on the display, e.g 'D1' - addCommand(this,... - 'disp_type',':DISP:WIND1:SPL', 'default','D1',... - 'fmt_spec','%s',... - 'info','Window arrangement'); - % Continuous sweep triggering - addCommand(this,... - 'cont_trig',':INIT1:CONT', 'default', 0,... - 'fmt_spec','%b'); - addCommand(this,... - 'trig_source', ':TRIG:SOUR', 'default', 'BUS',... - 'fmt_spec','%s') - - % Parametric commands for traces, i can be extended to 4 - for i = 1:2 - % measurement parameters for the traces 1-2, e.g. 'S21' - i_str = num2str(i); - addCommand(this,... - ['meas_par',i_str],[':CALC1:PAR',i_str,':DEF'],... - 'default','S21',... - 'fmt_spec','%s',... - 'info','Measurement parameter'); - end - end - end -end diff --git a/@MyNewAnalysisTraceEvent/MyNewAnalysisTraceEvent.m b/@MyNewAnalysisTraceEvent/MyNewAnalysisTraceEvent.m new file mode 100644 index 0000000..c705b5c --- /dev/null +++ b/@MyNewAnalysisTraceEvent/MyNewAnalysisTraceEvent.m @@ -0,0 +1,19 @@ +%Class for NewData events that are generated by MyDataSource and its +%subclasses, including MyInstrument + +classdef MyNewAnalysisTraceEvent < event.EventData + + properties (Access = public) + analysis_type char + Trace MyTrace + trace_name char + end + + methods (Access = public) + function this = MyNewAnalysisTraceEvent(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + end + end +end + diff --git a/@MyNewDataEvent/MyNewDataEvent.m b/@MyNewDataEvent/MyNewDataEvent.m index adbba7c..a70f17e 100644 --- a/@MyNewDataEvent/MyNewDataEvent.m +++ b/@MyNewDataEvent/MyNewDataEvent.m @@ -1,68 +1,63 @@ %Class for NewData events that are generated by MyDataSource and its %subclasses, including MyInstrument classdef MyNewDataEvent < event.EventData - properties (Access=public) + properties (Access = public) + % Name of the instrument that triggered the event. Usefult for % passing the event data forward, e.g. by triggering % NewDataWithHeaders src_name = 'UnknownInstrument' % New acquired trace. Should be passed by value in order to prevent % race condition when multiple NewData events are triggered by % the same instrument in a short period of time. Passing by value % makes sure that the trace is not modified before it is received % by Daq. Trace % If false then MyCollector does not acquire new measurement % headers for this trace. Setting new_header = false allows % transferring an existing trace to Daq by triggering NewData. new_header = true - % If the new data should be automatically saved by Daq. + % If the new data should be automatically saved by Daq save = false - % If 'save' is true and 'filename_ending' is not empty, Daq appends - % the ending to the file name before saving. In this way the - % default way of generating unique file names by Daq can be - % overwritten. - filename_ending = '' - + % Trace name to be used by Daq + trace_name char end methods % Use parser to process properties supplied as name-value pairs via % varargin - function this=MyNewDataEvent(varargin) - P=MyClassParser(this); - addParameter(P, 'Trace', []); + function this = MyNewDataEvent(varargin) + P = MyClassParser(this); processInputs(P, this, varargin{:}); end - end %% Set and get methods methods % Ensures that the source name is a valid Matlab variable function set.src_name(this, str) assert(ischar(str), ['The value assigned to ''src_name'' ' ... 'must be char']) if ~isempty(str) str=matlab.lang.makeValidName(str); else str='UnknownInstrument'; end this.src_name=str; end function set.Trace(this, Val) assert(isa(Val, 'MyTrace'), ['Trace must be a derivative ' ... 'of MyTrace class.']) this.Trace=Val; end end end \ No newline at end of file diff --git a/@MyNewpUsbComm/MyNewpUsbComm.m b/@MyNewpUsbComm/MyNewpUsbComm.m new file mode 100644 index 0000000..7e2ec6b --- /dev/null +++ b/@MyNewpUsbComm/MyNewpUsbComm.m @@ -0,0 +1,84 @@ +classdef MyNewpUsbComm < MySingleton + + properties (GetAccess = public, SetAccess = private) + + % Driver in use + isbusy = false + end + + properties (Access = public) + + % An instance of Newport.USBComm.USB class + Usb + end + + methods(Access = private) + + % The constructor of a singleton class should only be invoked from + % the instance method. + function this = MyNewpUsbComm() + disp(['Creating a new instance of ' class(this)]) + loadLib(this); + end + end + + methods(Access = public) + + % Load dll + function loadLib(this) + dll_path = which('UsbDllWrap.dll'); + if isempty(dll_path) + error(['UsbDllWrap.dll is not found. This library ',... + 'is a part of Newport USB driver and needs ',... + 'to be present on Matlab path.']) + end + NetAsm = NET.addAssembly(dll_path); + + % Create an instance of Newport.USBComm.USB class + Type = GetType(NetAsm.AssemblyHandle,'Newport.USBComm.USB'); + this.Usb = System.Activator.CreateInstance(Type); + end + + function str = query(this, addr, cmd) + + % Check if the driver is already being used by another process. + % A race condition with various strange consequences is + % potentially possible if it is. + assert(~this.isbusy, 'Newport Usb Comm is already in use.') + + this.isbusy = true; + + % Send query using QueryData buffer. A new buffer needs to be + % created every time to ensure the absence of interference + % between different queries. + QueryData = System.Text.StringBuilder(64); + + stat = Query(this.Usb, addr, cmd, QueryData); + + if stat == 0 + str = char(ToString(QueryData)); + else + str = ''; + warning('Query to Newport usb driver was unsuccessful.'); + end + + this.isbusy = false; + end + end + + methods(Static) + + % Concrete implementation of the singleton constructor. + function this = instance() + persistent UniqueInstance + + if isempty(UniqueInstance) || ~isvalid(UniqueInstance) + this = MyNewpUsbComm(); + UniqueInstance = this; + else + this = UniqueInstance; + end + end + end +end + diff --git a/@MyNewportUsbComm/MyNewportUsbComm.m b/@MyNewportUsbComm/MyNewportUsbComm.m deleted file mode 100644 index c8c3c29..0000000 --- a/@MyNewportUsbComm/MyNewportUsbComm.m +++ /dev/null @@ -1,73 +0,0 @@ -classdef MyNewportUsbComm < MySingleton - - properties (GetAccess=public, SetAccess=private) - isbusy = false % driver in use - QueryData % query buffer - end - - properties (Access=public) - Usb % Instance of Newport.USBComm.USB class - end - - methods(Access=private) - % The constructor of a singleton class should only be invoked from - % the instance method. - function this = MyNewportUsbComm() - this.QueryData=System.Text.StringBuilder(64); - loadLib(this); - end - end - - methods(Access=public) - - % Load dll - function loadLib(this) - dll_path = which('UsbDllWrap.dll'); - if isempty(dll_path) - error(['UsbDllWrap.dll is not found. This library ',... - 'is a part of Newport USB driver and needs ',... - 'to be present on Matlab path.']) - end - NetAsm=NET.addAssembly(dll_path); - % Create an instance of Newport.USBComm.USB class - Type=GetType(NetAsm.AssemblyHandle,'Newport.USBComm.USB'); - this.Usb=System.Activator.CreateInstance(Type); - end - - function str=query(this, addr, cmd) - % Check if the driver is already being used by another process. - % A race condition with various strange consequences is - % potentially possible if it is. - if this.isbusy - warning('NewportUsbComm is already in use') - end - - this.isbusy=true; - % Send query using the QueryData buffer - stat = Query(this.Usb, addr, cmd, this.QueryData); - if stat==0 - str = char(ToString(this.QueryData)); - else - str=''; - warning('Query to Newport usb driver was unsuccessful.'); - end - this.isbusy=false; - end - end - - methods(Static) - % Concrete implementation of the singleton constructor. - function this = instance() - persistent UniqueInstance - - if isempty(UniqueInstance)||(~isvalid(UniqueInstance)) - disp('Creating new instance of NewportUsbComm') - this = MyNewportUsbComm(); - UniqueInstance = this; - else - this = UniqueInstance; - end - end - end -end - diff --git a/@MyPeakFinder/MyPeakFinder.m b/@MyPeakFinder/MyPeakFinder.m deleted file mode 100644 index 2058209..0000000 --- a/@MyPeakFinder/MyPeakFinder.m +++ /dev/null @@ -1,298 +0,0 @@ -classdef MyPeakFinder < handle - properties - Trace; - Peaks; - end - - methods - function this=MyPeakFinder(varargin) - p=inputParser; - addParameter(p,'Trace',MyTrace()); - parse(p,varargin{:}) - - this.Trace=p.Results.Trace; - this.Peaks=struct('Location',[],'Width',[],'Prominence',[],... - 'Value',[]); - end - - %Checks if a peak exists within the given limits - function bool=checkPeakExist(this,x_min,x_max) - assert(isnumeric(x_min) & isnumeric(x_max),... - 'x_min and x_max must be numbers'); - assert(x_max>x_min,['x_max must be greater than x_min,',... - ' currently x_min is %e while x_max is %e'],x_min,x_max); - bool=any([this.Peaks.Location]>x_min & [this.Peaks.Location] 0); - isnumber=@(x) isnumeric(x) && isscalar(x); - - valid_sorts={'none','ascend','descend'}; - sortstrcheck=@(x) any(validatestring(x,valid_sorts)); - - addParameter(p,'FindMinima',false); - addParameter(p,'MinPeakDistance',0,ispositive); - addParameter(p,'MinPeakHeight',-Inf,isnumber); - addParameter(p,'MinPeakWidth',0,ispositive); - addParameter(p,'MaxPeakWidth',Inf,ispositive); - addParameter(p,'MinPeakProminence',0.1,isnumber); - addParameter(p,'SortStr','none',sortstrcheck); - addParameter(p,'Threshold',0); - addParameter(p,'ClearPeaks',true); - addParameter(p,'Limits',[min(this.Trace.x),max(this.Trace.x)]) - addParameter(p,'NPeaks',0); - addParameter(p,'WidthReference','halfprom') - parse(p,varargin{:}); - - %Sets the indices to be searched - x_lim=p.Results.Limits; - ind=(this.Trace.xx_lim(1)); - - %Sets the minimum peak prominence, which is always normalized - min_peak_prominence=p.Results.MinPeakProminence*... - peak2peak(this.Trace.y(ind)); - - %We must condition the Trace such that x is increasing and has - %no NaNs - nan_ind=isnan(this.Trace.x) | isnan(this.Trace.y); - this.Trace.x(nan_ind)=[]; - this.Trace.y(nan_ind)=[]; - ind(nan_ind)=[]; - - if ~issorted(this.Trace.x) - this.Trace.x=flipud(this.Trace.x); - this.Trace.y=flipud(this.Trace.y); - %If it is still not sorted, we sort it - if ~issorted(this.Trace.x) - 'Sorting'; - [this.Trace.x,sort_ind]=sort(this.Trace.x); - this.Trace.y=this.Trace.y(sort_ind); - end - end - - %If we are looking for minima, we invert the trace - if p.Results.FindMinima - y=-this.Trace.y; - else - y=this.Trace.y; - end - - %As there is no way to tell it to look for infinite numbers of - %peaks when you specify NPeaks, we only specify this parameter - %if we need to - if p.Results.NPeaks - extra_args={'NPeaks',p.Results.NPeaks}; - else - extra_args={}; - end - - %We now search for the peaks using the specified parameters - [pks,locs,wdth,prom]=findpeaks(y(ind),... - this.Trace.x(ind),... - 'MinPeakDistance',p.Results.MinPeakDistance,... - 'MinPeakheight',p.Results.MinPeakHeight,... - 'MinPeakWidth',p.Results.MinPeakWidth,... - 'MaxPeakWidth',p.Results.MaxPeakWidth,... - 'SortStr',p.Results.SortStr,... - 'MinPeakProminence',min_peak_prominence,... - 'Threshold',p.Results.Threshold,... - 'WidthReference',p.Results.WidthReference,... - extra_args{:}); - - %We invert the results back if we are looking for minima. - if p.Results.FindMinima - pks=-pks; - prom=-prom; - end - - PeakStruct=struct('Value',num2cell(pks),... - 'Location',num2cell(locs),... - 'Width',num2cell(wdth),... - 'Prominence',num2cell(prom)); - - %If the clearpeaks flag is set, we delete the previous peaks. - if p.Results.ClearPeaks - clearPeaks(this); - this.Peaks=PeakStruct; - else - this.Peaks=[this.Peaks;PeakStruct]; - end - end - - function loadTrace(this,fullfilename) - %Finds type of file - [~,~,ext]=fileparts(fullfilename); - - switch ext - case '.txt' - load(this.Trace,fullfilename); - case '.mat' - DataStruct=load(fullfilename); - fields=fieldnames(DataStruct); - - %We try to find which vectors are x and y for the data, - %first we find the two longest vectors in the .mat file - %and use these - vec_length=structfun(@(x) length(x), DataStruct); - [~,sort_ind]=sort(vec_length,'descend'); - vec_names=fields(sort_ind(1:2)); - - %Now we do some basic conditioning of these vectors: - %Make column vectors and remove NaNs. - vec{1}=DataStruct.(vec_names{1})(:); - vec{2}=DataStruct.(vec_names{2})(:); - - %If there is a start and stopindex, cut down the - %longest vector to size. - if ismember('startIndex',fields) && ... - ismember('stopIndex',fields) - [~,ind]=max(cellfun(@(x) length(x), vec)); - vec{ind}=vec{ind}(DataStruct.startIndex:... - DataStruct.stopIndex); - end - - nan_ind=isnan(vec{1}) | isnan(vec{2}); - vec{1}(nan_ind)=[]; - vec{2}(nan_ind)=[]; - - %We find what x is by looking for a sorted vector - ind_x=cellfun(@(x) issorted(x,'monotonic'),vec); - - this.Trace.x=vec{ind_x}; - this.Trace.y=vec{~ind_x}; - - if ismember('offsetFrequency',fields) - this.Trace.x=this.Trace.x+DataStruct.offsetFrequency; - end - otherwise - error('File type %s is not supported',ext) - end - end - - function fitAllPeaks(this,varargin) - p=inputParser; - addParameter(p,'FitNames',{'Gorodetsky2000'}); - addParameter(p,'base_dir',pwd); - addParameter(p,'session_name','placeholder'); - addParameter(p,'filename','placeholder'); - parse(p,varargin{:}); - - fit_names=p.Results.FitNames; - - %We instantiate the MyFit objects used for the fitting - Fits=struct(); - for i=1:length(fit_names) - Fits.(fit_names{i})=MyFit('fit_name',fit_names{i},... - 'enable_gui',0); - Fits.(fit_names{i}).base_dir=p.Results.base_dir; - Fits.(fit_names{i}).session_name=p.Results.session_name; - Fits.(fit_names{i}).filename=... - [p.Results.filename,'_',fit_names{i}]; - end - - %We fit the peaks - for i=1:length(this.Peaks) - %First extract the data around the peak - [x_fit,y_fit]=extractPeak(this,i); - - for j=1:length(fit_names) - Fits.(fit_names{j}).Data.x=x_fit; - Fits.(fit_names{j}).Data.y=y_fit; - genInitParams(Fits.(fit_names{j})); - fitTrace(Fits.(fit_names{j})); - saveParams(Fits.(fit_names{j}),... - 'save_user_params',false,... - 'save_gof',true); - end - end - - fprintf('Finished fitting peaks \n'); - end - - function [x_peak,y_peak]=extractPeak(this,peak_no) - loc=this.Peaks(peak_no).Location; - w=this.Peaks(peak_no).Width; - ind=(loc-8*wthis.Trace.x); - x_peak=this.Trace.x(ind)-loc; - y_peak=this.Trace.y(ind); - end - - function save(this,varargin) - %Parse inputs for saving - p=inputParser; - addParameter(p,'filename','placeholder',@ischar); - addParameter(p,'save_dir',pwd,@ischar); - addParameter(p,'overwrite_flag',false); - addParameter(p,'save_prec',15); - parse(p,varargin{:}); - - %Assign shorter names - filename=p.Results.filename; - save_dir=p.Results.save_dir; - overwrite_flag=p.Results.overwrite_flag; - save_prec=p.Results.save_prec; - - %Puts together the full file name - fullfilename=fullfile([save_dir,filename,'.txt']); - - %Creates the file in the given folder - write_flag=createFile(save_dir,fullfilename,overwrite_flag); - - %Returns if the file is not created for some reason - if ~write_flag; return; end - - col_names={'Value','Location','Width','Prominence'}; - n_columns=length(col_names); - %Finds appropriate column width - cw=max([cellfun(@(x) length(x),col_names), save_prec+7]); - cw_vec=repmat(cw,1,4); - - pre_fmt_str=repmat('%%%is\\t',1,n_columns); - fmt_str=sprintf([pre_fmt_str,'\r\n'],cw_vec); - - fileID=fopen(fullfilename,'w'); - fprintf(fileID,fmt_str,col_names{:}); - - pre_fmt_str_nmb=repmat('%%%i.15e\\t',1,n_columns); - %Removes the tab at the end - pre_fmt_str_nmb((end-2):end)=[]; - - nmb_fmt_str=sprintf([pre_fmt_str_nmb,'\r\n'],cw_vec); - - fprintf(fileID,nmb_fmt_str,... - [[this.Peaks.Value];[this.Peaks.Location];... - [this.Peaks.Width];[this.Peaks.Prominence]]); - fclose(fileID); - end - - function loadPeaks(this,fullfilename) - assert(ischar(fullfilename),... - 'File name must be a char, currently it is a %s',... - class(fullfilename)); - if ~exist(fullfilename,'file') - error('File named ''%s'' does not exist, choose a different file',... - fullfilename); - end - - LoadStruct=importdata(fullfilename); - headers=regexp(LoadStruct.textdata{1},'\s*(\w*)\s*','Tokens'); - this.Peaks=struct(headers{1}{1},num2cell(LoadStruct.data(:,1)),... - headers{2}{1},num2cell(LoadStruct.data(:,2)),... - headers{3}{1},num2cell(LoadStruct.data(:,3)),... - headers{4}{1},num2cell(LoadStruct.data(:,4))); - - end - - function clearPeaks(this) - this.Peaks=struct('Location',[],'Width',[],'Prominence',[],... - 'Value',[]); - end - end -end - - \ No newline at end of file diff --git a/@MyPeakFinderGui/MyPeakFinderGui.m b/@MyPeakFinderGui/MyPeakFinderGui.m deleted file mode 100644 index a4e961c..0000000 --- a/@MyPeakFinderGui/MyPeakFinderGui.m +++ /dev/null @@ -1,263 +0,0 @@ -classdef MyPeakFinderGui < handle - properties - PeakFinder - Gui; - axis_handle; - end - - properties (Access=private) - peak_color='r'; - data_color='b'; - peak_handle; - end - - properties (Dependent=true) - trace_handle; - filename; - session_name; - base_dir; - save_dir; - end - - methods - function this=MyPeakFinderGui() - this.PeakFinder=MyPeakFinder(); - createGui(this); - end - - function delete(this) - %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 - - function closeFigure(this, ~, ~) - delete(this); - end - - function analyzeCallback(this, src, ~) - src.BackgroundColor=[1,0,0]; - src.String='Analyzing..'; - - searchPeaks(this.PeakFinder,... - 'MinPeakProminence',str2double(this.Gui.PromEdit.String),... - 'MinPeakDistance',str2double(this.Gui.SepEdit.String),... - 'FindMinima',this.Gui.MinimaCheck.Value,... - 'WidthReference','halfprom'); - plotPeaks(this); - src.BackgroundColor=[0.94,0.94,0.94]; - src.String='Analyze'; - end - - function plotPeaks(this) - delete(this.peak_handle); - this.peak_handle=plot(this.axis_handle,... - [this.PeakFinder.Peaks.Location],... - [this.PeakFinder.Peaks.Value],... - 'Marker','o',... - 'LineStyle','none',... - 'Color',this.peak_color); - end - - function fitPeakCallback(this,~,~) - %Clean up fit names - fit_names=cellfun(@(x) erase(x,' '), this.Gui.SelFitList.String,... - 'UniformOutput',false); - fitAllPeaks(this.PeakFinder,... - 'FitNames',fit_names,... - 'base_dir',this.base_dir,... - 'session_name',this.session_name,... - 'filename',this.filename); - end - - function clickCallback(this,~,~) - switch this.Gui.Window.SelectionType - case 'normal' %Left click - addPeak(this); - case 'alt' %Right click - axis(this.axis_handle,'tight') - case 'extend' %Shift click - coords=this.axis_handle.CurrentPoint; - removePeak(this, coords(1)); - otherwise - end - end - - function windowScrollCallback(this, ~, event) - coords=this.axis_handle.CurrentPoint; - - if event.VerticalScrollCount>0 - %Zoom out - zoomAxis(this,coords(1),0.1) - else - %Zoom in - zoomAxis(this,coords(1),10); - end - end - - function removePeak(this, coord) - [~,ind]=min(abs([this.PeakFinder.Peaks.Location]-coord)); - this.PeakFinder.Peaks(ind)=[]; - plotPeaks(this); - end - - function addPeak(this) - x_lim=[this.axis_handle.XLim(1),... - this.axis_handle.XLim(2)]; - if checkPeakExist(this.PeakFinder,x_lim(1),x_lim(2)) - opts=struct('WindowStyle','modal','Interpreter','tex'); - errordlg(['A peak already exists in the window. ',... - 'To add a new peak, zoom such that there is ',... - 'no peak in the window'],'Error',opts); - return - end - searchPeaks(this.PeakFinder,... - 'ClearPeaks',false,... - 'Limits',x_lim,... - 'MinPeakProminence',str2double(this.Gui.PromEdit.String),... - 'FindMinima',this.Gui.MinimaCheck.Value,... - 'NPeaks',1,... - 'SortStr','descend'); - plotPeaks(this); - end - - function zoomAxis(this,coords,zoom_factor) - curr_width=this.axis_handle.XLim(2)-this.axis_handle.XLim(1); - new_width=curr_width/zoom_factor; - this.axis_handle.XLim=... - [coords(1)-new_width/2,coords(1)+new_width/2]; - end - - function clearCallback(this, ~, ~) - delete(getLineHandle(this.PeakFinder.Trace,this.axis_handle)); - clearData(this.PeakFinder.Trace); - cla(this.axis_handle); - end - - function loadTraceCallback(this, src, ~) - %Window can find all files - [fname,path]=uigetfile('*.*'); - if fname==0 - warning('No file was selected'); - return - end - src.BackgroundColor=[1,0,0]; - src.String='Loading..'; - - loadTrace(this.PeakFinder,[path,fname]); - plot(this.PeakFinder.Trace,this.axis_handle); - this.trace_handle.ButtonDownFcn=... - @(src, event) clickCallback(this, src, event); - - exitLoad(this); - end - - function exitLoad(this) - this.Gui.LoadTraceButton.BackgroundColor=[0.94,0.94,0.94]; - this.Gui.LoadTraceButton.String='Load trace'; - end - - function savePeaksCallback(this,~,~) - save(this.PeakFinder,... - 'save_dir',this.save_dir,... - 'filename',this.filename); - end - - function loadPeaksCallback(this,~,~) - [fname,path]=uigetfile('*.*'); - if fname==0 - warning('No file was selected'); - return - end - loadPeaks(this.PeakFinder,[path,fname]); - plotPeaks(this); - end - - function clearPeaksCallback(this,~,~) - clearPeaks(this.PeakFinder); - delete(this.peak_handle); - end - - %Add a fit to the selected list - function addFitCallback(this,~,~) - val=this.Gui.FitList.Value; - if val==0; return; end - - this.Gui.SelFitList.String{end+1}=this.Gui.FitList.String{val}; - this.Gui.FitList.String(val)=[]; - if val>length(this.Gui.FitList.String) - this.Gui.FitList.Value=length(this.Gui.FitList.String); - end - - if this.Gui.SelFitList.Value==0 - this.Gui.SelFitList.Value=1; - end - end - - %Remove fits from selected fits - function removeFitCallback(this,~,~) - val=this.Gui.SelFitList.Value; - if val==0; return; end - - this.Gui.FitList.String{end+1}=this.Gui.SelFitList.String{val}; - this.Gui.SelFitList.String(val)=[]; - if val>length(this.Gui.SelFitList.String) - this.Gui.SelFitList.Value=length(this.Gui.SelFitList.String); - end - - if this.Gui.FitList.Value==0 - this.Gui.FitList.Value=1; - end - end - end - - methods - function trace_handle=get.trace_handle(this) - trace_handle=getLineHandle(this.PeakFinder.Trace,this.axis_handle); - end - - function base_dir=get.base_dir(this) - try - base_dir=this.Gui.BaseEdit.String; - catch - base_dir=pwd; - end - end - - function set.base_dir(this,base_dir) - this.Gui.BaseEdit.String=base_dir; - end - - function session_name=get.session_name(this) - try - session_name=this.Gui.SessionEdit.String; - catch - session_name=''; - end - end - - function set.session_name(this,session_name) - this.Gui.SessionEdit.String=session_name; - end - - function filename=get.filename(this) - try - filename=this.Gui.FileNameEdit.String; - catch - filename='placeholder'; - end - end - - function set.filename(this,filename) - this.Gui.FileNameEdit.String=filename; - end - - %Get function from save directory - function save_dir=get.save_dir(this) - save_dir=createSessionPath(this.base_dir,this.session_name); - end - end -end \ No newline at end of file diff --git a/@MyPeakFinderGui/createGui.m b/@MyPeakFinderGui/createGui.m deleted file mode 100644 index fb14c83..0000000 --- a/@MyPeakFinderGui/createGui.m +++ /dev/null @@ -1,209 +0,0 @@ -function createGui(this) -fit_list={'Lorentzian','LorentzianGrad','Double Lorentzian',... - 'Gorodetsky2000','Gorodetsky2000Plus'}; -fig_width=1000; -fig_h=800; - -row_height=50; -col_width=120; -x_first_col=50; - -button_size=[100,30]; -edit_size=[100,30]; -file_edit_size=[200,30]; -h_first_row=fig_h-row_height; -h_second_row=fig_h-2*row_height; -h_third_row=fig_h-3*row_height; - -%Name sets the title of the window, NumberTitle turns off the FigureN text -%that would otherwise be before the title, MenuBar is the menu normally on -%the figure, toolbar is the toolbar normally on the figure. -%HandleVisibility refers to whether gcf, gca etc will grab this figure. -this.Gui.Window = figure('Name', 'PeakFinder',... - 'NumberTitle', 'off', ... - 'MenuBar','none',... - 'Toolbar','figure',... - 'HandleVisibility', 'off',... - 'Units','Pixels',... - 'Position',[400,0,fig_width,fig_h],... - 'WindowScrollWheelFcn',@(src, event) windowScrollCallback(this, src, event)); -%Sets the close function (runs when x is pressed) to be class function -set(this.Gui.Window, 'CloseRequestFcn',... - @(src,event) closeFigure(this, src, event)); - -%Creates axis -this.axis_handle=axes(this.Gui.Window,... - 'Box','on',... - 'Units','Pixels',... - 'Position',[50,50,fig_width-100,fig_h-6*row_height]); -hold(this.axis_handle,'on'); -this.axis_handle.ButtonDownFcn=@(src, event) clickCallback(this, src, event); -%Button for doing the analysis -this.Gui.AnalyzeButton=uicontrol(this.Gui.Window,... - 'Style','pushbutton',... - 'Units','Pixels',... - 'Position',[x_first_col,h_first_row,button_size],... - 'String','Analyze',... - 'Callback',@(src, event) analyzeCallback(this, src, event)); - -%Checkbox for finding minima -this.Gui.MinimaCheck=uicontrol(this.Gui.Window,... - 'Style','checkbox',... - 'Units','Pixels',... - 'Position',[x_first_col+90,h_second_row,button_size],... - 'Value',0); -this.Gui.MinimaLabel=uicontrol(this.Gui.Window,... - 'Style','edit',... - 'Units','pixels',... - 'Position',[x_first_col,h_second_row,[80,30]],... - 'String','Find minima',... - 'Enable','off'); - -%Button for clearing the data -this.Gui.FitButton=uicontrol(this.Gui.Window,... - 'Style','pushbutton',... - 'Units','Pixels',... - 'Position',[x_first_col,h_third_row,button_size],... - 'String','Fit peaks',... - 'Callback',@(src, event) fitPeakCallback(this, src, event)); - -%Button for clearing the data -this.Gui.ClearButton=uicontrol(this.Gui.Window,... - 'Style','pushbutton',... - 'Units','Pixels',... - 'Position',[x_first_col+col_width,h_first_row,button_size],... - 'String','Clear',... - 'Callback',@(src, event) clearCallback(this, src, event)); - -%Button for loading the trace -this.Gui.LoadTraceButton=uicontrol(this.Gui.Window,... - 'Style','pushbutton',... - 'Units','Pixels',... - 'Position',[x_first_col+4*col_width,h_first_row,button_size],... - 'String','Load trace',... - 'Callback',@(src, event) loadTraceCallback(this, src, event)); - -this.Gui.PromLabel=uicontrol(this.Gui.Window,... - 'Style','edit',... - 'Units','pixels',... - 'Position',[x_first_col+2*col_width,h_first_row,edit_size],... - 'String','Prominence',... - 'Enable','off'); -%Button for changing the peak threshold -this.Gui.PromEdit=uicontrol(this.Gui.Window,... - 'Style','edit',... - 'Units','Pixels',... - 'Position',[x_first_col+3*col_width,h_first_row,edit_size],... - 'String','0.5'); - -this.Gui.SepLabel=uicontrol(this.Gui.Window,... - 'Style','edit',... - 'Units','pixels',... - 'Position',[x_first_col+2*col_width,h_second_row,edit_size],... - 'String','Res. Separation',... - 'Enable','off'); -%Button for changing the resonance separation -this.Gui.SepEdit=uicontrol(this.Gui.Window,... - 'Style','edit',... - 'Units','Pixels',... - 'Position',[x_first_col+3*col_width,h_second_row,edit_size],... - 'String','1'); - -%Button for saving the peaks -this.Gui.SavePeaksButton=uicontrol(this.Gui.Window,... - 'Style','pushbutton',... - 'Units','Pixels',... - 'Position',[x_first_col+4*col_width,h_second_row,button_size],... - 'String','Save Peaks',... - 'Callback',@(src, event) savePeaksCallback(this, src, event)); - -%Button for loading peaks -this.Gui.LoadPeaksButton=uicontrol(this.Gui.Window,... - 'Style','pushbutton',... - 'Units','Pixels',... - 'Position',[x_first_col+4*col_width,h_third_row,button_size],... - 'String','Load Peaks',... - 'Callback',@(src, event) loadPeaksCallback(this, src, event)); - -%Button for clearing the peaks -this.Gui.ClearPeaksButton=uicontrol(this.Gui.Window,... - 'Style','pushbutton',... - 'Units','Pixels',... - 'Position',[x_first_col+col_width,h_second_row,button_size],... - 'String','Clear peaks',... - 'Callback',@(src, event) clearPeaksCallback(this, src, event)); - -this.Gui.BaseLabel=uicontrol(this.Gui.Window,... - 'Style','edit',... - 'Units','pixels',... - 'Position',[x_first_col+5*col_width,h_first_row,edit_size],... - 'String','Base directory',... - 'Enable','off'); -%Button for changing the peak threshold -this.Gui.BaseEdit=uicontrol(this.Gui.Window,... - 'Style','edit',... - 'Units','Pixels',... - 'Position',[x_first_col+6*col_width,h_first_row,file_edit_size],... - 'String',pwd); - - -this.Gui.SessionLabel=uicontrol(this.Gui.Window,... - 'Style','edit',... - 'Units','pixels',... - 'Position',[x_first_col+5*col_width,h_second_row,edit_size],... - 'String','Session name',... - 'Enable','off'); -%Editbox for changing the session name -this.Gui.SessionEdit=uicontrol(this.Gui.Window,... - 'Style','edit',... - 'Units','Pixels',... - 'Position',[x_first_col+6*col_width,h_second_row,file_edit_size],... - 'String','placeholder'); - - -this.Gui.FileNameLabel=uicontrol(this.Gui.Window,... - 'Style','edit',... - 'Units','pixels',... - 'Position',[x_first_col+5*col_width,h_third_row,edit_size],... - 'String','File name',... - 'Enable','off'); -%Editbox for changing the filename -this.Gui.FileNameEdit=uicontrol(this.Gui.Window,... - 'Style','edit',... - 'Units','Pixels',... - 'Position',[x_first_col+6*col_width,h_third_row,file_edit_size],... - 'String','placeholder'); - -%For selecting fits -this.Gui.FitList=uicontrol(this.Gui.Window,... - 'Style','listbox',... - 'Units','Pixels',... - 'String',fit_list,... - 'Position',[x_first_col+col_width,h_third_row-1.5*row_height,... - col_width,2*row_height]); - -%For selecting fits -this.Gui.SelFitList=uicontrol(this.Gui.Window,... - 'Style','listbox',... - 'Units','Pixels',... - 'Position',[x_first_col+2.5*col_width,h_third_row-1.5*row_height,... - col_width,2*row_height]); - -%Add fit -this.Gui.RemoveFitButton=uicontrol(this.Gui.Window,... - 'Style','pushbutton',... - 'Units','Pixels',... - 'Position',[x_first_col+2.05*col_width,h_third_row-0.9*row_height,50,20],... - 'String','<<',... - 'Callback',@(src, event) removeFitCallback(this, src, event)); - -%Remove fit -this.Gui.RemoveFitButton=uicontrol(this.Gui.Window,... - 'Style','pushbutton',... - 'Units','Pixels',... - 'Position',[x_first_col+2.05*col_width,h_third_row-0.4*row_height,50,20],... - 'String','>>',... - 'Callback',@(src, event) addFitCallback(this, src, event)); - - -end \ No newline at end of file diff --git a/@MyPm/MyPm.m b/@MyPm/MyPm.m deleted file mode 100644 index 007e093..0000000 --- a/@MyPm/MyPm.m +++ /dev/null @@ -1,65 +0,0 @@ -% Class for Thorlabs PM100D powermeters -classdef MyPm < MyScpiInstrument - %% Constructor and destructor - methods (Access=public) - function this=MyPm(interface, address, varargin) - this@MyScpiInstrument(interface, address, varargin{:}); - this.Device.Timeout=1; % reading from powermeter is quick - end - - %% Low-level functions for reading and writing textual data to the device - % Appantly, this device sometimemes fails if it receives very long - % commands, so query them one by one - - function writeCommand(this, varargin) - % Send commands to device one by one - for i=1:length(varargin) - cmd_str=varargin{i}; - fprintf(this.Device, cmd_str); - end - end - - % Query commands and return resut as cell array of strings - function res_list=queryCommand(this, varargin) - % Send commands to device one by one - ncmd=length(varargin); - res_list=cell(1,ncmd); - for i=1:ncmd - cmd_str=varargin{i}; - res_list{i}=query(this.Device, cmd_str); - end - end - end - - %% Protected functions - methods (Access=protected) - - function createCommandList(this) - % Sensor name and info - addCommand(this, 'sensor',':SYSTem:SENSor:IDN',... - 'fmt_spec','%s',... - 'default','',... - 'access','r'); - addCommand(this, 'average_no',':SENSe:AVERage:COUNt',... - 'fmt_spec','%i',... - 'default',1,... - 'info','Number of averages, 1 sample takes approx. 3ms'); - addCommand(this, 'wl', ':SENSe:CORRection:WAVelength',... - 'fmt_spec','%e',... - 'default',700,... - 'info','Operation wavelength (nm)') - addCommand(this, 'auto_pow_rng', ':SENSe:POWer:DC:RANGe:AUTO',... - 'fmt_spec','%b',... - 'default',true,... - 'info','Auto power range') - addCommand(this, 'power_unit', ':SENSe:POWer:DC:UNIT',... - 'fmt_spec','%s',... - 'default','W',... - 'val_list',{'W', 'DBM'}) - addCommand(this, 'power', ':MEASure:POWer',... - 'default',0,... - 'fmt_spec','%e','access','r') - end - - end -end diff --git a/@MyRsa/MyRsa.m b/@MyRsa/MyRsa.m deleted file mode 100644 index 7f72514..0000000 --- a/@MyRsa/MyRsa.m +++ /dev/null @@ -1,137 +0,0 @@ -% Class for controlling Tektronix RSA5103 and RSA5106 spectrum analyzers - -classdef MyRsa < MyScpiInstrument - - properties (SetAccess=protected, GetAccess=public) - acq_trace=[] % Last read trace - end - - %% Constructor and destructor - methods (Access=public) - function this=MyRsa(interface, address, varargin) - this@MyScpiInstrument(interface, address, varargin{:}); - - this.Trace.unit_x='Hz'; - this.Trace.unit_y='$\mathrm{V}^2/\mathrm{Hz}$'; - this.Trace.name_y='Power'; - this.Trace.name_x='Frequency'; - end - end - - %% Protected functions - methods (Access=protected) - - function createCommandList(this) - % Resolution bandwidth (Hz) - addCommand(this, 'rbw',':DPX:BAND:RES',... - 'default',1e3,'fmt_spec','%e',... - 'info','Resolution bandwidth (Hz)'); - % If the rbw is auto-set - addCommand(this, 'auto_rbw',':DPX:BAND:RES:AUTO',... - 'default',true,'fmt_spec','%b'); - addCommand(this, 'span', ':DPX:FREQ:SPAN',... - 'default',1e6,'fmt_spec','%e',... - 'info','(Hz)'); - addCommand(this, 'start_freq',':DPX:FREQ:STAR',... - 'default',1e6,'fmt_spec','%e',... - 'info','(Hz)'); - addCommand(this, 'stop_freq',':DPX:FREQ:STOP',... - 'default',2e6,'fmt_spec','%e',... - 'info','(Hz)'); - addCommand(this, 'cent_freq',':DPX:FREQ:CENT',... - 'default',1.5e6,'fmt_spec','%e',... - 'info','(Hz)'); - % Initiate and abort data acquisition, don't take arguments - addCommand(this, 'abort_acq',':ABORt', 'access','w',... - 'fmt_spec',''); - addCommand(this, 'init_acq',':INIT', 'access','w',... - 'fmt_spec',''); - % Continuous triggering - addCommand(this, 'init_cont',':INIT:CONT','default',true,... - 'fmt_spec','%b',... - 'info','Continuous triggering on/off'); - % Number of points in trace - addCommand(this, 'point_no',':DPSA:POIN:COUN',... - 'default',10401, 'val_list',{801,2401,4001,10401},... - 'fmt_spec','P%i'); - % Reference level (dB) - addCommand(this, 'ref_level',':INPut:RLEVel','default',0,... - 'fmt_spec','%e',... - 'info','(dB)'); - % Display scale per division (dBm/div) - addCommand(this, 'disp_y_scale',':DISPlay:DPX:Y:PDIVision',... - 'default',10,'fmt_spec','%e',... - 'info','(dBm/div)'); - % Display vertical offset (dBm) - addCommand(this, 'disp_y_offset',':DISPLAY:DPX:Y:OFFSET',... - 'default',0,'fmt_spec','%e',... - 'info','(dBm)'); - - % Parametric commands - for i = 1:3 - i_str = num2str(i); - % Display trace - addCommand(this, ['disp_trace',i_str],... - [':TRAC',i_str,':DPX'],... - 'default',false,'fmt_spec','%b',... - 'info','on/off'); - % Trace Detection - addCommand(this, ['det_trace',i_str],... - [':TRAC',i_str,':DPX:DETection'],... - 'val_list',{'AVER','AVERage','NEG','NEGative',... - 'POS','POSitive'},... - 'default','AVER','fmt_spec','%s'); - % Trace Function - addCommand(this, ['func_trace',i_str],... - [':TRAC',i_str,':DPX:FUNCtion'],... - 'val_list',{'AVER','AVERage','HOLD','NORM','NORMal'},... - 'default','AVER','fmt_spec','%s'); - % Number of averages - addCommand(this, ['average_no',i_str],... - [':TRAC',i_str,':DPX:AVER:COUN'],... - 'default',1,'fmt_spec','%i'); - % Count completed averages - addCommand(this, ['cnt_trace',i_str],... - [':TRACe',i_str,':DPX:COUNt:ENABle'],... - 'default',false,'fmt_spec','%b',... - 'info','Count completed averages'); - end - end - end - - %% Public functions including callbacks - methods (Access=public) - - function readSingle(this, n_trace) - fetch_cmd = sprintf('fetch:dpsa:res:trace%i?', n_trace); - fwrite(this.Device, fetch_cmd); - data = binblockread(this.Device,'float'); - readProperty(this, 'start_freq','stop_freq','point_no'); - x_vec=linspace(this.start_freq,this.stop_freq,... - this.point_no); - %Calculates the power spectrum from the data, which is in dBm. - %Output is in V^2/Hz - readProperty(this,'rbw'); - power_spectrum = (10.^(data/10))/this.rbw*50*0.001; - %Trace object is created containing the data and its units - this.Trace.x = x_vec; - this.Trace.y = power_spectrum; - - this.acq_trace=n_trace; - - %Trigger acquired data event (inherited from MyInstrument) - triggerNewData(this); - end - - % Extend readHeader function - function Hdr=readHeader(this) - %Call parent class method and then append parameters - Hdr=readHeader@MyScpiInstrument(this); - %Hdr should contain single field - addParam(Hdr, Hdr.field_names{1}, ... - 'acq_trace', this.acq_trace, ... - 'comment', 'Last read trace'); - end - end -end - diff --git a/@MyScpiInstrument/MyScpiInstrument.m b/@MyScpiInstrument/MyScpiInstrument.m index d385723..8c6c13b 100644 --- a/@MyScpiInstrument/MyScpiInstrument.m +++ b/@MyScpiInstrument/MyScpiInstrument.m @@ -1,488 +1,352 @@ -% Class for instruments supporting SCPI, features specialized framework for -% read/write commands +% Class featuring a specialized framework for instruments supporting SCPI +% +% Undefined/dummy methods: +% queryString(this, cmd) +% writeString(this, cmd) + classdef MyScpiInstrument < MyInstrument - properties (SetAccess=protected, GetAccess=public) - %Contains a list of the commands available for the instrument as - %well as the default values and input requirements - CommandList - %Parses commands using an inputParser object - CommandParser - - en_set_cb=true; %enable set callback - end - - properties (Dependent=true) - command_names - command_no - write_commands - read_commands - end - - methods (Access=public) - %% Class constructor - function this=MyScpiInstrument(interface, address, varargin) - this@MyInstrument(interface, address, varargin{:}); - this.CommandList=struct(); - createCommandList(this); - createCommandParser(this); - end - - %% Low-level functions for reading and writing textual data to the device - % These functions can be overloaded if the instrument does not - % support visa communication or use non-standard command separators + methods (Access = public) - function writeCommand(this, varargin) - if ~isempty(varargin) - % Concatenate commands and send to the device - cmd_str=join(varargin,';'); - cmd_str=cmd_str{1}; - fprintf(this.Device, cmd_str); - end - end - - % Query commands and return resut as cell array of strings - function res_list=queryCommand(this, varargin) - if ~isempty(varargin) - % Concatenate commands and send to the device - cmd_str=join(varargin,';'); - cmd_str=cmd_str{1}; - res_str=query(this.Device, cmd_str); - % Drop the end-of-the-string symbol and split - res_list=split(deblank(res_str),';'); + % Extend the functionality of base class method + function addCommand(this, tag, command, varargin) + p = inputParser(); + p.KeepUnmatched = true; + addRequired(p, 'command', @ischar); + + addParameter(p, 'access', 'rw', @ischar); + addParameter(p, 'format', '%e', @ischar); + addParameter(p, 'value_list', {}, @iscell); + addParameter(p, 'validationFcn', function_handle.empty(), ... + @(x)isa(x, 'function_handle')); + addParameter(p, 'default', 0); + + % Command ending for reading + addParameter(p, 'read_ending', '?', @ischar); + + % Command ending for writing, e.g. '%10e' + addParameter(p, 'write_ending', '', @ischar); + parse(p, command, varargin{:}); + + % Create a list of remaining parameters to be supplied to + % the base class method + sub_varargin = struct2namevalue(p.Unmatched); + + % Introduce variables for brevity + format = p.Results.format; + write_ending = p.Results.write_ending; + + if ismember('format', p.UsingDefaults) && ... + ~ismember('write_ending', p.UsingDefaults) + + % Extract format specifier and symbol from the write ending + [smb, format] = parseFormat(this, write_ending); else - res_list={}; + + % Extract format symbol + smb = parseFormat(this, format); end - end - - %% Read and write commands - - %Writes properties to device. Can take multiple inputs. With the - %option all, the function writes default to all the - %available writeable parameters. - function writeProperty(this, varargin) - set_cb_was_enabled = this.en_set_cb; - %Switch PostSet callbacks off to prevent infinite recursion - this.en_set_cb=false; - %Parses the inputs using the CommandParser - parse(this.CommandParser, varargin{:}); - - if this.CommandParser.Results.all - % If the 'all' is true, write all the commands - exec=this.write_commands; - else - % Check which commands were passed values - ind_val=cellfun(@(x)... - (~ismember(x, this.CommandParser.UsingDefaults)),... - this.write_commands); - exec=this.write_commands(ind_val); + if ismember('b', smb) + + % '%b' is a non-MATLAB format specifier that is introduced + % to designate logical variables + format = replace(format, '%b', '%i'); + write_ending = replace(write_ending, '%b', '%i'); end + this.CommandList.(tag).format = format; - % Create a list of textual strings to be sent to device - exec_commands=cell(1,length(exec)); - for i=1:length(exec) - %Create command using the right string spec - cmd=[this.CommandList.(exec{i}).command,... - ' ',this.CommandList.(exec{i}).fmt_spec]; - val=this.CommandParser.Results.(exec{i}); - exec_commands{i}=sprintf(cmd, val); + % Add the full read form of the command, e.g. ':FREQ?' + if contains(p.Results.access, 'r') + read_command = [p.Results.command, p.Results.read_ending]; + readFcn = ... + @()sscanf(queryString(this, read_command), format); + sub_varargin = [sub_varargin, {'readFcn', readFcn}]; + else + read_command = ''; end - % Sends commands to device - writeCommand(this, exec_commands{:}); - for i=1:length(exec) - %Assign written values to instrument properties - this.(exec{i})=this.CommandParser.Results.(exec{i}); + this.CommandList.(tag).read_command = read_command; + + % Add the full write form of the command, e.g. ':FREQ %e' + if contains(p.Results.access,'w') + if ismember('write_ending', p.UsingDefaults) + write_command = [p.Results.command, ' ', format]; + else + write_command = [p.Results.command, write_ending]; + end + writeFcn = ... + @(x)writeString(this, sprintf(write_command, x)); + sub_varargin = [sub_varargin, {'writeFcn', writeFcn}]; + else + write_command = ''; end + this.CommandList.(tag).write_command = write_command; - % Leave en_set_cb in the same state it was found - this.en_set_cb=set_cb_was_enabled; - end - - % Wrapper for writeProperty that opens and closes the device - function writePropertyHedged(this, varargin) - was_open = isopen(this); - if ~was_open - openDevice(this); + % If the value list contains textual values, extend it with + % short forms and add a postprocessing function + value_list = p.Results.value_list; + validationFcn = p.Results.validationFcn; + if ~isempty(value_list) + if any(cellfun(@ischar, value_list)) + + % Put only unique full-named values in the value list + [long_vl, short_vl] = splitValueList(this, value_list); + value_list = long_vl; + + % For validation, use an extended list made of full and + % abbreviated name forms and case-insensitive + % comparison + validationFcn = createScpiListValidationFcn(this, ... + [long_vl, short_vl]); + + postSetFcn = createToStdFormFcn(this, tag, long_vl); + + sub_varargin = [sub_varargin, ... + {'postSetFcn', postSetFcn}]; + end end - try - writeProperty(this, varargin{:}); - catch - warning('Error while writing the properties:'); - disp(varargin); + % Assign validation function based on the value format + if isempty(validationFcn) + validationFcn = createArrayValidationFcn(this, smb); end - readProperty(this, 'all'); - % Leave device in the state it was in the beginning - if ~was_open - closeDevice(this); + + sub_varargin = [sub_varargin, { ... + 'value_list', value_list, ... + 'validationFcn', validationFcn}]; + + % Assign default based on the format of value + if ~ismember('default', p.UsingDefaults) + default = p.Results.default; + elseif ~isempty(value_list) + default = value_list{1}; + else + default = createValidValue(this, smb); end + + sub_varargin = [sub_varargin, {'default', default}]; + + % Execute the base class method + addCommand@MyInstrument(this, tag, sub_varargin{:}); end - function result=readProperty(this, varargin) - set_cb_was_enabled = this.en_set_cb; - %Switch PostSet callbacks off to prevent infinite recursion - this.en_set_cb=false; + % Redefine the base class method to use a single read operation for + % faster communication + function sync(this) + cns = this.command_names; + ind_r = structfun(@(x) ~isempty(x.read_command), ... + this.CommandList); - result = struct(); - read_all_flag = any(strcmp('all',varargin)); - if read_all_flag - % Read all the commands with read access - exec=this.read_commands; - else - ind_r=ismember(varargin,this.read_commands); - exec=varargin(ind_r); - if any(~ind_r) - % Issue warnings for commands not in the command_names - warning('The following are not valid read commands:'); - disp(varargin(~ind_r)); - end - end - % Create a list of textual strings to be sent to device - exec_commands=cellfun(... - @(cmd)[this.CommandList.(cmd).command,'?'],exec,... + read_cns = cns(ind_r); % List of names of readable commands + + read_commands = cellfun(... + @(x) this.CommandList.(x).read_command, read_cns,... 'UniformOutput',false); - % Query device - res_list=queryCommand(this, exec_commands{:}); - query_successful=(length(exec)==length(res_list)); - if query_successful + res_list = queryStrings(this, read_commands{:}); + + if length(read_cns)==length(res_list) + % Assign outputs to the class properties - for i=1:length(exec) - result.(exec{i})=sscanf(res_list{i},... - this.CommandList.(exec{i}).fmt_spec); - %Assign the values to the MyInstrument properties - this.(exec{i})=result.(exec{i}); + for i=1:length(read_cns) + tag = read_cns{i}; + + val = sscanf(res_list{i}, ... + this.CommandList.(tag).format); + + if ~isequal(this.CommandList.(tag).last_value, val) + + % Assign value without writing to the instrument + this.CommandWriteEnabled.(tag) = false; + this.(tag) = val; + this.CommandWriteEnabled.(tag) = true; + end end else warning(['Not all the properties could be read, ',... 'instrument class values are not updated.']); end - - % Leave en_set_cb in the same state it was found - this.en_set_cb=set_cb_was_enabled; - if query_successful - % Trigger notification abour new properties read - % need to do it after resetting en_set_cb - triggerPropertyRead(this); - end end + + %% Write/query + % These methods implement handling multiple SCPI commands. Unless + % overloaded, they rely on write/readString methods for + % communication with the device, which particular subclasses must + % implement or inherit separately. - % Wrapper for readProperty that opens and closes the device - function result=readPropertyHedged(this, varargin) - was_open = isopen(this); - if ~was_open - openDevice(this); - end - - try - result = readProperty(this, varargin{:}); - catch - warning('Error while reading the properties:'); - disp(varargin); - end - % Leave device in the state it was in the beginning - if ~was_open - closeDevice(this); + % Write command strings listed in varargin + function writeStrings(this, varargin) + if ~isempty(varargin) + + % Concatenate commands and send to the device + cmd_str = join(varargin,';'); + cmd_str = cmd_str{1}; + writeString(this, cmd_str); end end - %% Identification - % Overload the basic MyInstrument function to use a generalized way - % of querying commands avaliable in MyScpiInstrument - function [str, msg]=idn(this) - was_open=isopen(this); - try - openDevice(this); - str=queryCommand(this,'*IDN?'); - assert(~isempty(str), ['IDN query to the instrument ' ... - 'did not return a result']) - str=str{1}; - catch ErrorMessage - str=''; - msg=ErrorMessage.message; - end - this.idn_str=str; - % Leave device in the state it was in the beginning - if ~was_open - try - closeDevice(this); - catch - end + % Query commands and return the resut as cell array of strings + function res_list = queryStrings(this, varargin) + if ~isempty(varargin) + + % Concatenate commands and send to the device + cmd_str = join(varargin,';'); + cmd_str = cmd_str{1}; + res_str = queryString(this, cmd_str); + + % Drop the end-of-the-string symbol and split + res_list = split(deblank(res_str),';'); + else + res_list = {}; end end + end + + methods (Access = protected) + %% Misc utility methods - %% Measurements header functionality - - function Hdr=readHeader(this) - %Call parent class method and then append parameters - Hdr=readHeader@MyInstrument(this); - %Hdr should contain a single field - readPropertyHedged(this,'all'); - for i=1:length(this.read_commands) - cmd = this.read_commands{i}; - addParam(Hdr, Hdr.field_names{1}, cmd, this.(cmd), ... - 'comment', this.CommandList.(cmd).info); + % Split the list of string values into a full-form list and a + % list of abbreviations, where the abbreviated forms are inferred + % based on case. For example, the value that has the full name + % 'AVErage' has the short form 'AVE'. + function [long_vl, short_vl] = splitValueList(~, vl) + long_str_vl = {}; % Full forms of string values + short_vl = {}; % Abbreviated forms of string values + num_vl = {}; % Numeric values + + % Iterate over the list of values + for i = 1:length(vl) + + % Short forms exist only for string values + if ischar(vl{i}) + long_str_vl{end+1} = vl{i}; %#ok + + % Add short form + idx = isstrprop(vl{i},'upper'); + short_form = vl{i}(idx); + if ~isequal(vl{i}, short_form) && ~isempty(short_form) + short_vl{end+1} = short_form; %#ok + end + else + num_vl{end+1} = vl{i}; %#ok + end end + + % Remove duplicates + short_vl = unique(lower(short_vl)); + + % Make the list of full forms + long_vl = [num_vl, ... + setdiff(lower(long_str_vl), short_vl, 'stable')]; end - %% Processing of the class variable values - % Extend the property value based on val_list - function std_val = standardizeValue(this, cmd, varargin) - if ~ismember(cmd,this.command_names) - warning('%s is not a valid command',cmd); - std_val = ''; - return - end - vlist = this.CommandList.(cmd).val_list; - % The value to normalize can be explicitly passed as - % varargin{1}, otherwise use this.cmd as value - if isempty(varargin) - val = this.(cmd); - else - val = varargin{1}; - end - % find matching commands - ismatch = false(1,length(vlist)); - for i=1:length(vlist) - n = min([length(val), length(vlist{i})]); - % compare first n symbols disregarding case - ismatch(i) = strncmpi(val, vlist{i},n); - end - % out of matching names pick the longest - if any(ismatch) - mvlist = vlist(ismatch); - %Finds the length of each element of mvlist - n_el=cellfun(@(x) length(x), mvlist); - %Sets std_val to the longest element - std_val=mvlist{n_el==max(n_el)}; + % Create a function that returns the long form of value from + % value_list + function f = createToStdFormFcn(this, cmd, value_list) + function std_val = toStdForm(val) - % sets the property if value was not given explicitly - if isempty(varargin) - this.(cmd) = std_val; + % Standardization is applicable to char-valued properties + % which have value list + if isempty(value_list) || ~ischar(val) + std_val = val; + return end - else - warning(['The value %s is not present in the val_list ',... - 'of command %s.'], val, cmd) - std_val = val; + + % find matching values + n = length(val); + ismatch = cellfun(@(x) strncmpi(val, x, ... + min([n, length(x)])), value_list); + + assert(any(ismatch), ... + sprintf(['%s is not present in the list of values ' ... + 'of command %s.'], val, cmd)); + + % out of the matching values pick the longest + mvals = value_list(ismatch); + n_el = cellfun(@(x) length(x), mvals); + std_val = mvals{n_el==max(n_el)}; end + + assert(ismember(cmd, this.command_names), ['''' cmd ... + ''' is not an instrument command.']) + + f = @toStdForm; end - % Return the list of long command values excluding abbreviations - function std_val_list = stdValueList(this, cmd) - if ~ismember(cmd,this.command_names) - warning('%s is not a valid command',cmd); - std_val_list = {}; - return - end - vlist = this.CommandList.(cmd).val_list; - % Select the commands, which appear only once in the beginnings - % of the strings in val_list - long_val_ind = cellfun(... - @(x)(sum(startsWith(vlist,x,'IgnoreCase',true))==1),vlist); - std_val_list = vlist(long_val_ind); + % Find the format specifier symbol and options + function [smb, format] = parseFormat(~, fmt_spec) + [start, stop, tok] = regexp(fmt_spec, '%([\d\.]*)([a-z])', ... + 'start', 'end', 'tokens'); + + assert(~isempty(tok) && ~isempty(tok{1}{2}), ... + ['Format symbol is not found in ' fmt_spec]); + + % The first cell index corresponds to different matches if + % there are more than one specifier + smb = cellfun(@(x)x{2}, tok); + + % Return a substring that includes all the specifiers + format = fmt_spec(min(start):max(stop)); end - %% Auxiliary functions for auto format assignment to commands - function fmt_spec=formatSpecFromAttributes(~,classes,attributes) - if ismember('char',classes) - fmt_spec='%s'; - elseif ismember('logical',classes)||... - (ismember('numeric',classes)&&... - ismember('integer',attributes)) - fmt_spec='%i'; - else - %assign default value, i.e. double - fmt_spec='%e'; + % List validation function with case-insensitive comparison + function f = createScpiListValidationFcn(~, value_list) + function listValidationFcn(val) + val = lower(val); + assert( ... + any(cellfun(@(y) isequal(val, y), value_list)), ... + ['Value must be one from the following list:', ... + newline, var2str(value_list)]); end + + f = @listValidationFcn; end - function [class,attribute]=attributesFromFormatSpec(~, fmt_spec) - % find index of the first letter after the % sign - ind_p=strfind(fmt_spec,'%'); - ind=ind_p+find(isletter(fmt_spec(ind_p:end)),1)-1; - fmt_spec_letter=fmt_spec(ind); - switch fmt_spec_letter - case {'d','f','e','g'} - class={'numeric'}; - attribute={}; - case 'i' - class={'numeric'}; - attribute={'integer'}; - case 's' - class={'char'}; - attribute={}; - case 'b' - class={'logical'}; - attribute={}; - otherwise - % Class not specified, any of the below classes - % will pass the check - class={'numeric','char','logical'}; - attribute={}; + % smb is an array of format specifier symbols + function f = createArrayValidationFcn(~, smb) + function validateNumeric(val) + assert((length(val) == length(smb)) && isnumeric(val), ... + sprintf(['Value must be a numeric array of length ' ... + '%i.'], length(smb))) end - end - end - - methods (Access=protected) - %% Command list handling - %Adds a command to the CommandList - function addCommand(this, tag, command, varargin) - p=inputParser(); - addRequired(p,'tag',@ischar); - addRequired(p,'command',@ischar); - addParameter(p,'default',[]); - addParameter(p,'classes',{},@iscell); - addParameter(p,'attributes',{},@iscell); - addParameter(p,'fmt_spec','%e',@ischar); - % list of the values the variable can take, {} means no - % restriction - addParameter(p,'val_list',{},@iscell); - addParameter(p,'access','rw',@ischar); - % information about the command - addParameter(p,'info','',@ischar); - parse(p,tag,command,varargin{:}); - - %Adds the command to be sent to the device - this.CommandList.(tag).command=command; - this.CommandList.(tag).access=p.Results.access; - this.CommandList.(tag).write_flag=contains(p.Results.access,'w'); - this.CommandList.(tag).read_flag=contains(p.Results.access,'r'); - this.CommandList.(tag).default=p.Results.default; - this.CommandList.(tag).info=p.Results.info; - %Add the list of values, if needed extending it to include - %short forms. For example, for the allowed value 'AVErage' - %its short form 'AVE' also will be added. - vl=p.Results.val_list; - short_vl={}; - for i=1:length(vl) - if ischar(vl{i}) - idx = isstrprop(vl{i},'upper'); - short_form=vl{i}(idx); - % Add the short form to the list of values if it was - % not included explicitly - if ~ismember(short_form, vl) - short_vl{end+1}=short_form; %#ok - end - end + function validateInteger(val) + assert((length(val) == length(smb)) && ... + all(floor(val) == val), sprintf(['Value must be ' ... + 'an integer array of length %i.'], length(smb))) end - this.CommandList.(tag).val_list=[vl,short_vl]; - % Adds the string specifier to the list. if the format - % specifier is not given explicitly, try to infer - if ismember('fmt_spec', p.UsingDefaults) - this.CommandList.(tag).fmt_spec=... - formatSpecFromAttributes(this,p.Results.classes... - ,p.Results.attributes); - elseif strcmp(p.Results.fmt_spec,'%b') - % b is a non-system specifier to represent the - % logical type - this.CommandList.(tag).fmt_spec='%i'; - else - this.CommandList.(tag).fmt_spec=p.Results.fmt_spec; - end - % Adds the attributes for the input to the command. If not - % given explicitly, infer from the format specifier - if ismember('classes',p.UsingDefaults) - [this.CommandList.(tag).classes,... - this.CommandList.(tag).attributes]=... - attributesFromFormatSpec(this, p.Results.fmt_spec); - else - this.CommandList.(tag).classes=p.Results.classes; - this.CommandList.(tag).attributes=p.Results.attributes; + function validateLogical(val) + assert((length(val) == length(smb)) && ... + all(val==1 | val==0), ['Value must be a logical ' ... + 'array of length ' length(smb) '.']) end - % Adds a property to the class corresponding to the tag - if ~isprop(this,tag) - h=addprop(this,tag); - % Enable PreSet and PostSet events - h.SetObservable=true; - % No AbortSet is the lesser of evils - h.AbortSet=false; - % Store property handle - this.CommandList.(tag).prop_handle=h; + function valudateCharacterString(val) + assert(ischar(val), 'Value must be a character string.'); end - this.(tag)=p.Results.default; - % Add callback to PostSet event that writes the value, assigned - % to the instrument class property, to the prysical devices and - % reads it back. There is no point in storing listeners to - % clean them up as here the generating and receiving objects - % are the same and listeners are destroyed with the generating - % object. - addlistener(this,tag,'PostSet',@(src,~)propPostSetCb(this,src)); - end - - %Creates inputParser using the command list - function p = createCommandParser(this) - %Use input parser - %Requires input of the appropriate class - p=inputParser; - p.StructExpand=0; - %Flag for whether the command should initialize the device with - %defaults - addParameter(p, 'all',false,@islogical); - for i=1:length(this.write_commands) - %Adds optional inputs for each command, with the - %appropriate default value from the command list and the - %required attributes for the command input. - tag=this.write_commands{i}; - % Create validation function based on properties: - % class, attributes and list of values - if ~isempty(this.CommandList.(tag).val_list) - if all(cellfun(@ischar, this.CommandList.(tag).val_list)) - % for textual values use case insentice string comparison - v_func = @(x) any(cellfun(@(y) strcmpi(y, x),... - this.CommandList.(tag).val_list)); - else - % for everything else compare as it is - v_func = @(x) any(cellfun(@(y) isequal(y, x),... - this.CommandList.(tag).val_list)); - end - else - v_func = @(x) validateattributes(x,... - this.CommandList.(tag).classes,... - this.CommandList.(tag).attributes); - end - addParameter(p, tag,... - this.CommandList.(tag).default, v_func); + % Determine the type of validation function + if all(smb == 's' | smb == 'c') + f = @valudateCharacterString; + elseif all(smb == 'b') + f = @validateLogical; + elseif all(smb == 'b' | smb == 'i') + f = @validateInteger; + else + f = @validateNumeric; end - this.CommandParser=p; - end - - %Dummy empty function that needs to be redefined in a subclass to - %incorporate addCommand statements - function createCommandList(~) end - %% Property PostSet callback - function propPostSetCb(this,src) - if this.en_set_cb - writePropertyHedged(this,src.Name,this.(src.Name)); + function val = createValidValue(~, smb) + if all(smb == 's' | smb == 'c') + val = ''; + elseif all(smb == 'b') + val = false(length(smb), 1); + else + val = zeros(length(smb), 1); end end - end - - %% Get functions - methods - function command_names=get.command_names(this) - command_names=fieldnames(this.CommandList); - end - - function write_commands=get.write_commands(this) - ind_w=structfun(@(x) x.write_flag, this.CommandList); - write_commands=this.command_names(ind_w); - end - - function read_commands=get.read_commands(this) - ind_r=structfun(@(x) x.read_flag, this.CommandList); - read_commands=this.command_names(ind_r); - end - - function command_no=get.command_no(this) - command_no=length(this.command_names); - end - end -end \ No newline at end of file +end + diff --git a/@MySingleton/MySingleton.m b/@MySingleton/MySingleton.m index c79d462..bdb200f 100644 --- a/@MySingleton/MySingleton.m +++ b/@MySingleton/MySingleton.m @@ -1,13 +1,12 @@ % This an abstract class used to derive subclasses only one instance of % which can exist at a time. % See https://ch.mathworks.com/matlabcentral/fileexchange/24911-design-pattern-singleton-creational % for more information. classdef MySingleton < handle - methods(Abstract, Static) + methods (Abstract, Static) this = instance(); end - end diff --git a/@MyTbs/MyTbs.m b/@MyTbs/MyTbs.m deleted file mode 100644 index 397108a..0000000 --- a/@MyTbs/MyTbs.m +++ /dev/null @@ -1,176 +0,0 @@ -% Class for controlling 4-channel Tektronix TBS scopes. -% Tested with TBS2074 - -classdef MyTbs < MyScpiInstrument - properties (SetAccess=protected, GetAccess=public) - % List of the physical knobs, which can be rotated programmatically - knob_list = {'GPKNOB','HORZPos','HORZScale',... - 'TRIGLevel','VERTPOS','VERTSCALE'}; - end - - properties (Constant=true) - N_CHANNELS = 4; % number of channels - end - - methods (Access=public) - function this=MyTbs(interface, address, varargin) - this@MyScpiInstrument(interface, address, varargin{:}); - % 4e7 is the maximum trace size of DPO4034-3034 - %(20 mln point of 2-byte integers) - this.Device.InputBufferSize = 4.1e7; %byte - this.Trace.name_x='Time'; - this.Trace.name_y='Voltage'; - - this.Device.ByteOrder='bigEndian'; - end - - function readTrace(this) - %set data format to be signed integer, reversed byte order - fprintf(this.Device,':WFMInpre:ENCdg BINary'); - %2 bytes per measurement point - % read the entire trace - fprintf(this.Device,... - ':DATA:WIDTH 2;STARt 1;STOP %i',this.point_no); - fprintf(this.Device,':CURVE?'); - y_data = int16(binblockread(this.Device,'int16')); - - % read the terminating character - fscanf(this.Device,'%s'); - - % Reading the relevant parameters from the scope - readProperty(this,'unit_y','unit_x',... - 'step_x','step_y','x_zero','y_zero','y_offset'); - - % Calculating the y data - y = (double(y_data)-this.y_offset)*this.step_y+this.y_zero; - n_points=length(y); - % Calculating the x axis - x = linspace(this.x_zero,... - this.x_zero+this.step_x*(n_points-1),n_points); - - this.Trace.x = x; - this.Trace.y = y; - % Discard "" when assiging the Trace labels - this.Trace.unit_x = this.unit_x(2:end-1); - this.Trace.unit_y = this.unit_y(2:end-1); - triggerNewData(this); - end - - function acquireContinuous(this) - openDevice(this); - fprintf(this.Device,... - ':ACQuire:STOPAfter RUNSTop;:ACQuire:STATE ON'); - closeDevice(this); - end - - function acquireSingle(this) - openDevice(this); - fprintf(this.Device,... - ':ACQuire:STOPAfter SEQuence;:ACQuire:STATE ON'); - closeDevice(this); - end - - function turnKnob(this,knob,nturns) - openDevice(this); - fprintf(this.Device, sprintf(':FPAnel:TURN %s,%i',knob,nturns)); - closeDevice(this); - end - end - - %% Protected functions - methods (Access=protected) - function createCommandList(this) - addCommand(this,'channel',':DATa:SOUrce',... - 'default',1,... - 'fmt_spec','CH%i',... - 'info','Channel from which the data is transferred'); - addCommand(this, 'ctrl_channel', ':SELect:CONTROl',... - 'default',1,... - 'fmt_spec','CH%i',... - 'info','Channel currently selected in the scope display'); - % units and scale for x and y waveform data - addCommand(this,'unit_x',':WFMOutpre:XUNit','access','r',... - 'classes',{'char'}); - addCommand(this,'unit_y',':WFMOutpre:YUNit','access','r',... - 'classes',{'char'}); - addCommand(this,'step_y',':WFMOutpre:YMUlt','access','r',... - 'classes',{'numeric'}); - addCommand(this,'step_x',':WFMOutpre:XINcr','access','r',... - 'classes',{'numeric'}); - addCommand(this,'x_zero',':WFMOutpre:XZEro','access','r',... - 'classes',{'numeric'}); - addCommand(this,'y_zero',':WFMOutpre:YZEro','access','r',... - 'classes',{'numeric'}); - addCommand(this,'y_offset',':WFMOutpre:YOFf','access','r',... - 'classes',{'numeric'}); - addCommand(this, 'point_no',':HORizontal:RECOrdlength',... - 'default', 200000,... - 'val_list', {2000, 20000, 200000, 2000000, 20000000},... - 'fmt_spec','%i',... - 'info','Numbers of points in the scope trace'); - % time scale in s per div - addCommand(this, 'time_scale',':HORizontal:SCAle',... - 'default',10E-3,... - 'fmt_spec','%e',... - 'info','Time scale (s/div)'); - % trigger level - addCommand(this, 'trig_lev', ':TRIGger:A:LEVel',... - 'default',1,... - 'fmt_spec','%e'); - % trigger slope - addCommand(this, 'trig_slope', ':TRIGger:A:EDGE:SLOpe',... - 'default', 'RISe', 'val_list',{'RISe','FALL'},... - 'fmt_spec','%s'); - % trigger source - addCommand(this, 'trig_source', ':TRIGger:A:EDGE:SOUrce',... - 'default', 'AUX', 'val_list', {'CH1','CH2','CH3','CH4',... - 'LINE'},... - 'fmt_spec','%s'); - % trigger mode - addCommand(this, 'trig_mode', ':TRIGger:A:MODe',... - 'default', 'AUTO', 'val_list',{'AUTO','NORMal'},... - 'fmt_spec','%s'); - % state of the data acquisition by the scope - addCommand(this, 'acq_state', ':ACQuire:STATE',... - 'default',true,... - 'fmt_spec','%b',... - 'info','State of data acquisition by the scope'); - % acquisition mode - addCommand(this, 'acq_mode', ':ACQuire:MODe',... - 'default', 'HIRes', ... - 'val_list',{'SAMple','PEAKdetect','HIRes','AVErage'},... - 'fmt_spec','%s',... - 'info',['Acquisition mode: sample, peak detect, ',... - 'high resolution, average']); - - % Parametric commands - for i = 1:this.N_CHANNELS - i_str = num2str(i); - % coupling, AC, DC or GND - addCommand(this,... - ['cpl',i_str],[':CH',i_str,':COUP'],... - 'default','DC', 'val_list', {'AC','DC','GND'},... - 'fmt_spec','%s',... - 'info','Channel coupling: AC, DC or GND'); - % offset - addCommand(this,... - ['offset',i_str],[':CH',i_str,':OFFSet'], ... - 'default',0,... - 'fmt_spec','%e',... - 'info','(V)'); - % scale, V/Div - addCommand(this,... - ['scale',i_str],[':CH',i_str,':SCAle'], ... - 'default',1,... - 'fmt_spec','%e',... - 'info','Channel y scale (V/div)'); - % channel enabled - addCommand(this,... - ['enable',i_str],[':SELect:CH',i_str], ... - 'default',true,... - 'fmt_spec','%b',... - 'info','Channel enabled'); - end - end - end -end \ No newline at end of file diff --git a/@MyTds/MyTds.m b/@MyTds/MyTds.m deleted file mode 100644 index 7f4afc0..0000000 --- a/@MyTds/MyTds.m +++ /dev/null @@ -1,161 +0,0 @@ -% Class for controlling 2-channel Tektronix TDS scopes. -classdef MyTds % line feed \n - ENQ = char(5); % enquiry - ACK = char(6); % acknowledge - NAK = char(21); % negative acknowledge - end - - properties (SetAccess=protected, GetAccess=public) - pressure1 = 0; % numeric values of pressure - pressure2 = 0; - stat1; - stat2; - gauge_id1; - gauge_id2; - pressure_unit = ''; - end - - properties (Dependent=true) - pressure_str1; % display string with measurement unit - pressure_str2; - end - - methods (Access=public) - %% Constructor and destructor - function this = MyTpg(interface, address, varargin) - this@MyInstrument(interface, address, varargin{:}); - - % Create MyLogger object and configure it to measure pressure - initLogger(this); - - % Configure trace to store pressure vs time recorded by Logger - this.Trace.name_x='Time'; - this.Trace.unit_x='s'; - end - - % Delete method that cleans up logger. Superclass delete method as - % usual is executed after this one. - function delete(this) - % Stop and delete logger. Destructor should never throw errors. - try - stop(this.Lg) - delete(this.Lg); - catch - end - end - - %% Communication commands - % read pressure from a single channel or both channels at a time - function p_arr = readPressure(this) - query(this.Device,['PRX',this.CR,this.LF]); - str = query(this.Device,this.ENQ); - % Extract pressure and gauge status from reading. - arr = sscanf(str,'%i,%e,%i,%e'); - p_arr=transpose(arr(2:2:end)); - this.pressure1 = p_arr(1); - this.pressure2 = p_arr(2); - % Status codes: - % 0 –> Measurement data okay - % 1 –> Underrange - % 2 –> Overrange - % 3 –> Sensor error - % 4 –> Sensor off (IKR, PKR, IMR, PBR) - % 5 –> No sensor (output: 5,2.0000E-2 [hPa]) - % 6 –> Identification error - this.stat1 = gaugeStatusFromCode(this, arr(1)); - this.stat2 = gaugeStatusFromCode(this, arr(3)); - % Trigger event notification - triggerPropertyRead(this); - end - - function pu = readPressureUnit(this) - query(this.Device,['UNI',this.CR,this.LF]); - str = query(this.Device,this.ENQ); - % Pressure units correspondence table: - % 0 –> mbar/bar - % 1 –> Torr - % 2 –> Pascal - % 3 –> Micron - % 4 –> hPascal (default) - % 5 –> Volt - pu_code = sscanf(str,'%i'); - pu = pressureUnitFromCode(this, pu_code); - this.pressure_unit = pu; - % Trigger event notification - triggerPropertyRead(this); - end - - function id_list = readGaugeId(this) - query(this.Device,['TID',this.CR,this.LF]); - str = query(this.Device,this.ENQ); - id_list = deblank(strsplit(str,{','})); - this.gauge_id1 = id_list{1}; - this.gauge_id2 = id_list{2}; - % Trigger event notification - triggerPropertyRead(this); - end - - function p_arr = readAllHedged(this) - was_open = isopen(this); - if ~was_open - openDevice(this); - end - try - p_arr = readPressure(this); - readPressureUnit(this); - readGaugeId(this); - catch - p_arr = [0,0]; - warning('Error while communicating with gauge controller') - end - % Leave device in the state it was in the beginning - if ~was_open - closeDevice(this); - end - end - - - function code_list = turnGauge(this) - query(this.Device,['SEN',char(1,1),this.CR,this.LF]); - str = query(this.Device,this.ENQ); - code_list = deblank(strsplit(str,{','})); - end - - %% Overloading MyInstrument functions - - % Implement instrument-specific readHeader function - function Hdr=readHeader(this) - Hdr=readHeader@MyInstrument(this); - % Hdr should contain single field - fn=Hdr.field_names{1}; - readAllHedged(this); - addParam(Hdr, fn, 'pressure_unit', this.pressure_unit); - addParam(Hdr, fn, 'pressure1', this.pressure1); - addParam(Hdr, fn, 'pressure2', this.pressure2); - addParam(Hdr, fn, 'stat1', this.stat1); - addParam(Hdr, fn, 'stat2', this.stat2); - addParam(Hdr, fn, 'gauge_id1', this.gauge_id1); - addParam(Hdr, fn, 'gauge_id2', this.gauge_id2); - end - - % Attempt communication and identification of the device - function [str, msg]=idn(this) - was_open=isopen(this); - try - openDevice(this); - query(this.Device,['AYT',this.CR,this.LF]); - [str,~,msg]=query(this.Device,this.ENQ); - catch ErrorMessage - str=''; - msg=ErrorMessage.message; - end - % Remove carriage return and new line symbols from the string - newline_smb={sprintf('\n'),sprintf('\r')}; %#ok - str=replace(str, newline_smb,' '); - - this.idn_str=str; - % Leave device in the state it was in the beginning - if ~was_open - try - closeDevice(this); - catch - end - end - end - - %% Logging functionality - - % Init or reset logger - function initLogger(this, varargin) - if isa(this.Lg, 'MyLogger') - delete(this.Lg); - end - this.Lg = MyLogger('MeasFcn', @()readAllHedged(this), ... - varargin{:}); - if isempty(this.Lg.Record.data_headers) &&... - (~isempty(this.pressure_unit)) - pu = this.pressure_unit; - this.Lg.Record.data_headers=... - {['P ch1 (',pu,')'],['P ch2 (',pu,')']}; - end - end - - % Trigger NewData event that sends trace to Daq - function transferTrace(this, n_ch) - if nargin==1 - % Channel number is 1 if not specified explicitly - n_ch=1; - end - - if isa(this.Lg, 'MyLogger')&&isvalid(this.Lg) - time_arr=this.Lg.Record.timestamps_num; - % Shift time origin to 0 - this.Trace.x=time_arr-time_arr(1); - this.Trace.y=this.Lg.Record.data(:,n_ch); - this.Trace.name_y=sprintf('P Ch%i',n_ch); - this.Trace.unit_y=this.pressure_unit; - triggerNewData(this); - else - warning(['Cannot transfer trace from the logger, ',... - 'missing or invalid MyLogger object.']) - end - end - - %% Functions for convertion between numerical codes and strings - - % Convert numerical code for gauge status to a string - function str = gaugeStatusFromCode(~, code) - switch int8(code) - case 0 - str = 'Measurement data ok'; - case 1 - str = 'Underrange'; - case 2 - str = 'Overrange'; - case 3 - str = 'Sensor error'; - case 4 - str = 'Sensor off'; - case 5 - str = 'No sensor'; - case 6 - str = 'Identification error'; - otherwise - str = ''; - warning('Unknown gauge status code %i', code); - end - end - - % Convert numerical code for pressure unit to a string - function str = pressureUnitFromCode(~, code) - switch int8(code) - case 0 - str = 'mbar'; - case 1 - str = 'Torr'; - case 2 - str = 'Pa'; - case 3 - str = 'Micron'; - case 4 - str = 'hPa'; - case 5 - str = 'Volt'; - otherwise - str = ''; - warning('unknown pressure unit, code=%i',pu_num) - end - end - end - - %% Get functions - methods - function p_str = get.pressure_str1(this) - p_str = sprintf('%.2e %s', this.pressure1, this.pressure_unit); - end - - function p_str = get.pressure_str2(this) - p_str = sprintf('%.2e %s', this.pressure2, this.pressure_unit); - end - end -end - diff --git a/@MyTrace/MyTrace.m b/@MyTrace/MyTrace.m index 0e23644..05579dc 100644 --- a/@MyTrace/MyTrace.m +++ b/@MyTrace/MyTrace.m @@ -1,486 +1,584 @@ % Class for XY data representation with labelling, plotting and % saving/loading functionality -% If instantiated as MyTrace(load_path) then -% the content is loaded from file classdef MyTrace < handle & matlab.mixin.Copyable & matlab.mixin.SetGet - properties (Access=public) - x=[]; - y=[]; - name_x='x'; - name_y='y'; - unit_x=''; - unit_y=''; - % MyMetadata storing information about how the trace was taken - MeasHeaders - file_name=''; - - % Data column and line separators - column_sep = '\t' - line_sep='\r\n' - - %Cell that contains handles the trace is plotted in - hlines={}; + 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); - % options for MeasHeaders - addParameter(P, 'metadata_opts',{},@iscell); - - if mod(length(varargin),2)==1 - % odd number of elements in varargin - interpret the first - % element as file name and the rest as name-value pairs - load_path=varargin{1}; - assert(ischar(load_path)&&isvector(load_path),... - '''file_name'' must be a vector of characters'); - processInputs(P, this, varargin{2:end}); - this.file_name=load_path; - else - % Parse varargin as a list of name-value pairs - processInputs(P, this, varargin{:}); - load_path=[]; - end - - this.MeasHeaders=MyMetadata(P.Results.metadata_opts{:}); + methods (Access = public) + function this = MyTrace(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + end + + function delete(this) - if ~isempty(load_path) - load(this, load_path); - end + % Delete lines from all the axes the trace is plotted in + deleteLine(this); end - %Defines the save function for the class. Note that this is only - %used when we want to write only the data with its associated - %trace, rather than just the trace. To write just the trace with - %fewer input checks, use the writeData function. - function save(this, varargin) - %Parse inputs for saving - p=inputParser; - addParameter(p,'save_prec',15); - addParameter(p,'overwrite',false); - - if mod(length(varargin),2)==1 - % odd number of elements in varargin - interpret the first - % element as file name and the rest as name-value pairs - fname=varargin{1}; - assert(ischar(fname)&&isvector(fname),... - '''filename'' must be a vector of characters'); - this.file_name=fname; - parse(p,varargin{2:end}); - else - % Parse varargin as a list of name-value pairs and take - % file name from the class property - fname=this.file_name; - parse(p,varargin{:}); - end + %Defines the save function for the class. + function save(this, filename, varargin) - %Creates the file in the given folder - stat=createFile(fname, 'overwrite', p.Results.overwrite); + % Parse inputs for saving + p = inputParser; + addParameter(p, 'overwrite', false); + parse(p, varargin{:}); - %Returns if the file is not created for some reason - if stat - %We now write the data to the file - writeData(this, fname, 'save_prec', p.Results.save_prec); - else - warning('File not created, returned write_flag %i',stat); - end + assert(ischar(filename) && isvector(filename), ... + '''filename'' must be a character vector.') + this.file_name = filename; - 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{:}); + % Create the file in the given folder + stat = createFile(filename, 'overwrite', p.Results.overwrite); - fullfilename=p.Results.fullfilename; - save_prec=p.Results.save_prec; + % Returns if the file is not created for some reason + if ~stat + warning('File not created, returned write_flag %i.', stat); + return + end - %Writes the metadata header - Mdt=makeMetadata(this); - save(Mdt, fullfilename); + % Create metadata header + Mdt = getMetadata(this); + + save(Mdt, filename); + + % Write the data + fileID = fopen(filename,'a'); - fileID=fopen(fullfilename,'a'); - %Pads the vectors if they are not equal length - diff=length(this.x)-length(this.y); + % 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']); + 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']); + 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 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]'); + % 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=[]; + this.x = []; + this.y = []; end - - function load(this, file_path, varargin) - p=inputParser; - addParameter(p,'hdr_spec',... - this.MeasHeaders.hdr_spec,@ischar); - addParameter(p,'end_header',... - this.MeasHeaders.end_header,@ischar); - parse(p,varargin{:}); - - this.MeasHeaders.hdr_spec=p.Results.hdr_spec; - this.MeasHeaders.end_header=p.Results.end_header; - - if ~exist(file_path,'file') - error('File does not exist, please choose a different load path') - end - - %Read metadata. We get the line number we want to read - %the main data from as an output. - end_line_no=load(this.MeasHeaders, file_path); - - %Tries to assign units and names and then delete the Info field - %from MeasHeaders - try - setFromMetadata(this, this.MeasHeaders); - deleteField(this.MeasHeaders,'Info'); - catch - warning(['No trace 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=''; - this.unit_y=''; - end - - %Reads x and y data - data_array=dlmread(file_path, this.column_sep, ... - end_line_no,0); - this.x=data_array(:,1); - this.y=data_array(:,2); - - this.file_name=file_path; - end - - % 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=makeMetadata(this) - %First we update the trace information - Mdt=MyMetadata(); - addField(Mdt,'Info'); - addParam(Mdt,'Info','Name1',this.name_x); - addParam(Mdt,'Info','Name2',this.name_y); - addParam(Mdt,'Info','Unit1',this.unit_x); - addParam(Mdt,'Info','Unit2',this.unit_y); - - addMetadata(Mdt,this.MeasHeaders); - end - % Assign trace parameters from metadata - function setFromMetadata(this, Mdt) - if isfield(Mdt.Info, 'Unit1') - this.unit_x=Mdt.Info.Unit1.value; - end - if isfield(Mdt.Info, 'Unit2') - this.unit_y=Mdt.Info.Unit2.value; - end - if isfield(Mdt.Info, 'Name1') - this.name_x=Mdt.Info.Name1.value; - end - if isfield(Mdt.Info, 'Name2') - this.name_y=Mdt.Info.Name2.value; - end - end - %Plots the trace on the given axes, using the class variables to %define colors, markers, lines and labels. Takes all optional %parameters of the class as inputs. - function plot(this, varargin) + function Line = plot(this, varargin) + % Do nothing if there is no data in the trace - if isempty(this) + if isDataEmpty(this) return end - %Checks that x and y are the same size - assert(validatePlot(this),... + % 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(); - - % 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); + % Parses inputs + p = inputParser(); + p.KeepUnmatched = true; - validateMarker=@(x) assert(ismarker(x),... - 'Input must be a valid marker. See ismarker function'); - addParameter(p,'Marker','none',validateMarker); + % Axes in which the trace should be plotted + addOptional(p, 'Axes', [], @(x)assert(isaxes(x),... + 'Argument must be axes or uiaxes.')); - validateLine=@(x) assert(isline(x),... - 'Input must be a valid linestyle. See isline function'); - addParameter(p,'LineStyle','-',validateLine); + addParameter(p, 'make_labels', true, @islogical); - addParameter(p,'MarkerSize',6,... - @(x) validateattributes(x,{'numeric'},{'positive'})); + validateInterpreter = @(x) assert( ... + ismember(x, {'none', 'tex', 'latex'}),... + 'Interpreter must be none, tex or latex'); + addParameter(p, 'Interpreter', 'latex', validateInterpreter); - addParameter(p,'make_labels',false,@islogical); + parse(p, varargin{:}); - 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{:}); + line_opts = struct2namevalue(p.Unmatched); %If axes are not supplied get current - if ~isempty(p.Results.plot_axes) - plot_axes=p.Results.plot_axes; + if ~isempty(p.Results.Axes) + Axes = p.Results.Axes; else - plot_axes=gca(); + Axes = gca(); end - ind=findLineInd(this, plot_axes); + ind = findLineInd(this, Axes); + if ~isempty(ind) && any(ind) - set(this.hlines{ind},'XData',this.x,'YData',this.y); + set(this.plot_lines{ind},'XData',this.x,'YData',this.y); else - this.hlines{end+1}=plot(plot_axes,this.x,this.y); - ind=length(this.hlines); + this.plot_lines{end+1} = plot(Axes, this.x, this.y); + ind = length(this.plot_lines); 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); + 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 - 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); + 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, plot_axes, bool) + function setVisible(this, Axes, bool) if bool vis='on'; else vis='off'; end - ind=findLineInd(this, plot_axes); + ind=findLineInd(this, Axes); if ~isempty(ind) && any(ind) - set(this.hlines{ind},'Visible',vis) + set(this.plot_lines{ind},'Visible',vis) end end %Defines addition of two MyTrace objects - function sum=plus(this,b) + function Sum=plus(this,b) checkArithmetic(this,b); - sum=MyTrace('x',this.x,'y',this.y+b.y, ... + 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) + function Diff=minus(this,b) checkArithmetic(this,b); - diff=MyTrace('x',this.x,'y',this.y-b.y, ... + 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(validatePlot(this),['MyTrace object must contain',... + 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(validatePlot(this),['MyTrace object must contain',... + 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 - %Integrates the trace numerically - function area=integrate(this,varargin) - assert(validatePlot(this),['MyTrace object must contain',... + 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']) - %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; + 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 contained in the indexed part. - area=trapz(this.x(ind),this.y(ind)); + % 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 downsample(this, n, opt) - n0=ceil(n/2); - if nargin()==3 && (strcmpi(opt,'average') || strcmpi(opt,'vg')) + % 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, 'Endpoints', 'shrink'); + tmpy = movmean(this.y, n, 'Endpoints', 'shrink'); - this.x=this.x(n0:n:end); - this.y=tmpy(n0:n:end); + new_x = this.x(n0:n:end); + new_y = tmpy(n0:n:end); else + % Downsample without averaging - this.x=this.x(n0:n:end); - this.y=this.y(n0:n:end); + 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=isempty(this) - bool=isempty(this.x) && isempty(this.y); + function bool = isDataEmpty(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)... + %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 hline=getLineHandle(this,ax) - ind=findLineInd(this,ax); + function Line = getLine(this, Ax) + ind = findLineInd(this, Ax); if ~isempty(ind) - hline=this.hlines{ind}; + Line = this.plot_lines{ind}; + else + Line = []; + end + end + + % deleteLine(this, Ax) - Delete trace line from an axes + % deleteLine(this) - Delete all trace lines + function deleteLine(this, Ax) + if nargin() == 2 + + % 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 - hline=[]; + + % 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=private) + 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 to add,',... + ['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 MyTrace classes must have the same units for arithmetic') - assert(length(this.x)==length(this.y)==... - length(this.x)==length(this.y),... - 'The length of x and y must be equal for arithmetic'); + '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 MyTrace objects must have identical x-axis for arithmetic') + '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, plot_axes) - if ~isempty(this.hlines) - ind=cellfun(@(x) ismember(x,findall(plot_axes,... - 'Type','Line')),this.hlines); + % 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=[]; + 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 + %% Set and get methods + methods - %Set function for MeasHeaders - function set.MeasHeaders(this, MeasHeaders) - assert(isa(MeasHeaders,'MyMetadata'),... - ['MeasHeaders must be an instance of MyMetadata, ',... - 'it is %s'],class(MeasHeaders)); - this.MeasHeaders=MeasHeaders; - end %Set function for x, checks if it is a vector of doubles and %reshapes into a column vector function set.x(this, x) assert(isnumeric(x),... 'Data must be of class double'); this.x=x(:); end %Set function for y, checks if it is a vector of doubles and %reshapes into a column vector function set.y(this, y) assert(isnumeric(y),... 'Data must be of class double'); this.y=y(:); end %Set function for unit_x, checks if input is a string. function set.unit_x(this, unit_x) assert(ischar(unit_x),'Unit must be a char, not a %s',... class(unit_x)); this.unit_x=unit_x; end %Set function for unit_y, checks if input is a string function set.unit_y(this, unit_y) assert(ischar(unit_y),'Unit must be a char, not a %s',... class(unit_y)); this.unit_y=unit_y; end %Set function for name_x, checks if input is a string function set.name_x(this, name_x) assert(ischar(name_x),'Name must be a char, not a %s',... class(name_x)); this.name_x=name_x; end %Set function for name_y, checks if input is a string function set.name_y(this, name_y) assert(ischar(name_y),'Name must be a char, not a %s',... class(name_y)); this.name_y=name_y; end - function set.file_name(this, file_name) - assert(ischar(file_name),'File path must be a char, not a %s',... - class(file_name)); - this.file_name=file_name; - end - %Get function for label_x, creates label from name_x and unit_x. function label_x=get.label_x(this) label_x=sprintf('%s (%s)', this.name_x, this.unit_x); end %Get function for label_y, creates label from name_y and unit_y. function label_y=get.label_y(this) label_y=sprintf('%s (%s)', this.name_y, this.unit_y); end end end diff --git a/@MyZiLi/MyZiLi.m b/@MyZiLi/MyZiLi.m deleted file mode 100644 index ca9db62..0000000 --- a/@MyZiLi/MyZiLi.m +++ /dev/null @@ -1,88 +0,0 @@ -% A generic class for programs based on Zurich Instruments UHFLI and MFLI -% lock-in amplifiers - -classdef MyZiLi < handle - - properties (GetAccess=public, SetAccess={?MyClassParser, ?MyZiLi}) - dev_serial='dev4090' - - % The string that specifies the device name as appears - % in the server's node tree. Can be the same as dev_serial. - dev_id - - % Device information string containing the data returned by - % ziDAQ('discoveryGet', ... - idn_str - - % Device clock frequency, i.e. the number of timestamps per second - clockbase - end - - events - NewSetting % Device settings changed - end - - methods - function this=MyZiLi(dev_serial) - - % Check the ziDAQ MEX (DLL) and Utility functions can be found in Matlab's path. - if ~(exist('ziDAQ', 'file') == 3) && ~(exist('ziCreateAPISession', 'file') == 2) - fprintf('Failed to either find the ziDAQ mex file or ziDevices() utility.\n') - fprintf('Please configure your path using the ziDAQ function ziAddPath().\n') - fprintf('This can be found in the API subfolder of your LabOne installation.\n'); - fprintf('On Windows this is typically:\n'); - fprintf('C:\\Program Files\\Zurich Instruments\\LabOne\\API\\MATLAB2012\\\n'); - return - end - - % Do not throw errors in the constructor to allow creating an - % instance when the physical device is disconnected - try - % Create an API session and connect to the correct Data - % Server. This is a high level function that uses - % ziDAQ('connect',.. and ziDAQ('connectDevice', ... when - % necessary - apilevel=6; - [this.dev_id,~]=ziCreateAPISession(dev_serial, apilevel); - - % Read the divice clock frequency - this.clockbase = ... - double(ziDAQ('getInt',['/',this.dev_id,'/clockbase'])); - catch ME - warning(ME.message) - end - - end - - function str=idn(this) - DevProp=ziDAQ('discoveryGet', this.dev_id); - str=this.dev_id; - if isfield(DevProp, 'devicetype') - str=[str,'; device type: ', DevProp.devicetype]; - end - if isfield(DevProp, 'options') - % Print options from the list as comma-separated values and - % discard the last comma. - opt_str=sprintf('%s,',DevProp.options{:}); - str=[str,'; options: ', opt_str(1:end-1)]; - end - if isfield(DevProp, 'serverversion') - str=[str,'; server version: ', DevProp.serverversion]; - end - this.idn_str=str; - end - - function Hdr=readHeader(this) - Hdr=MyMetadata(); - % name is always a valid variable as ensured by its set method - addField(Hdr, this.name); - % Instrument identification - addParam(Hdr, this.name, 'idn', this.idn_str); - addObjProp(Hdr, this, 'clockbase', 'comment', ... - ['Device clock frequency, i.e. the number of ', ... - 'timestamps per second']); - end - - end -end - diff --git a/@MyZiRingdown/MyZiRingdown.m b/@MyZiRingdown/MyZiRingdown.m deleted file mode 100644 index c9a4e2e..0000000 --- a/@MyZiRingdown/MyZiRingdown.m +++ /dev/null @@ -1,861 +0,0 @@ -% Class for performing ringdown measurements of mechanical oscillators -% using Zurich Instruments UHF or MF lock-in. -% -% Operation: sweep the driving tone (drive_osc) using the sweep module -% in LabOne web user interface, when the magnitude of the demodulator -% signal exceeds trig_threshold the driving tone is switched off and -% the recording of demodulated signal is started, the signal is recorded -% for the duration of record_time. -% -% Features: -% -% Adaptive measurement oscillator frequency -% -% Averaging -% -% Auto saving -% -% Auxiliary output signal: If enable_aux_out=true -% then after a ringdown is started a sequence of pulses is applied -% to the output consisting of itermittent on and off periods -% starting from on. - -classdef MyZiRingdown < MyZiLi & MyDataSource - - properties (Access=public) - % Ringdown is recorded if the signal in the triggering demodulation - % channel exceeds this value - trig_threshold=1e-3 % V - - % Duration of the recorded ringdown - record_time=1 % (s) - - % If enable_acq is true, then the drive is on and the acquisition - % of record is triggered when signal exceeds trig_threshold - enable_acq=false - - % Auxiliary output signal during ringdown. - enable_aux_out=false % If auxiliary output is applied - % time during which the output is in aux_out_on_lev state - aux_out_on_t=1 % (s) - % time during which the output is in aux_out_off_lev state - aux_out_off_t=1 % (s) - - aux_out_on_lev=1 % (V), output trigger on level - aux_out_off_lev=0 % (V), output trigger off level - - % Average the trace over n points to reduce amount of stored data - % while keeping the demodulator bandwidth large - downsample_n=1 - - fft_length=128 - - auto_save=false % if all ringdowns should be automatically saved - - % In adaptive measurement oscillator mode the oscillator frequency - % is continuously changed to follow the signal frequency during - % ringdown acquisition. This helps against the oscillator frequency - % drift. - adaptive_meas_osc=false - end - - % The properties which are read or set only once during the class - % initialization - properties (GetAccess=public, SetAccess={?MyClassParser}) - % enumeration for demodulators, oscillators and output starts from 1 - demod=1 % demodulator used for both triggering and measurement - - % Enumeration in the node structure starts from 0, so, for example, - % the default path to the trigger demodulator refers to the - % demodulator #1 - demod_path='/dev4090/demods/0' - - drive_osc=1 - meas_osc=2 - - % Signal input, integers above 1 correspond to main inputs, aux - % input etc. (see the user interface for device-specific details) - signal_in=1 - - drive_out=1 % signal output used for driving - - % Number of an auxiliary channel used for the output of triggering - % signal, primarily intended to switch the measurement apparatus - % off during a part of the ringdown and thus allow for free - % evolution of the oscillator during that period. - aux_out=1 - - % Poll duration of 1 ms practically means that ziDAQ('poll', ... - % returns immediately with the data accumulated since the - % previous function call. - poll_duration=0.001 % s - poll_timeout=50 % ms - - % Margin for adaptive oscillator frequency adjustment - oscillator - % follows the signal if the dispersion of frequency in the - % demodulator band is below ad_osc_margin times the demodulation - % bandwidth (under the condition that adaptive_meas_osc=true) - ad_osc_margin=0.1 - end - - % Internal variables - properties (GetAccess=public, SetAccess=protected) - recording=false % true if a ringdown is being recorded - - % true if adaptive measurement oscillator mode is on and if the - % measurement oscillator is actually actively following. - ad_osc_following=false - - % Reference timestamp at the beginning of measurement record. - % Stored as uint64. - t0 - - elapsed_t=0 % Time elapsed since the last recording was started - - DemodSpectrum % MyTrace object to store FFT of the demodulator data - end - - % Setting or reading the properties below automatically invokes - % communication with the device - properties (Dependent=true) - drive_osc_freq - meas_osc_freq - drive_on % true when the dirive output is on - - % demodulator sampling rate (as transferred to the computer) - demod_rate - - % The properties below are only used within the program to display - % the information about device state. - drive_amp % (V), peak-to-peak amplitude of the driving tone - lowpass_order % low-pass filter order - lowpass_bw % low-pass filter bandwidth - end - - % Other dependent variables that are dont device properties - properties (Dependent=true) - % Downsample the measurement record to reduce the amount of data - % while keeping the large demodulation bandwidth. - % (samples/s), sampling rate of the trace after avraging - downsampled_rate - - % number of the oscillator presently in use with the demodulator - current_osc - - % true/false, true if the signal output from aux out is in on state - aux_out_on - - % Provides public access to properties of private AvgTrace - n_avg % number of ringdowns to be averaged - avg_count % the average counter - - fft_rbw % resolution bandwidth of fft - end - - % Keeping handle objects fully private is the only way to restrict set - % access to their properties - properties (Access=private) - PollTimer - - AuxOutOffTimer % Timer responsible for switching the aux out off - AuxOutOnTimer % Timer responsible for switching the aux out on - - % Demodulator samples z(t) stored to continuously calculate - % spectrum, values of z are complex here, z=x+iy. - % osc_freq is the demodulation frequency - DemodRecord=struct('t',[],'z',[],'osc_freq',[]) - - AvgTrace % MyAvgTrace object used for averaging ringdowns - end - - events - NewDemodSample % New demodulator samples received - RecordingStarted % Acquisition of a new trace triggered - end - - methods (Access=public) - - %% Constructor and destructor - function this = MyZiRingdown(dev_serial, varargin) - this=this@MyZiLi(dev_serial); - - P=MyClassParser(this); - addRequired(P, dev_serial, @ischar) - % Poll timer period - addParameter(P,'poll_period',0.042,@isnumeric); - processInputs(P, this, dev_serial, varargin{:}); - - % Create and configure trace objects - % Trace is inherited from the superclass - this.Trace=MyTrace(... - 'name_x','Time',... - 'unit_x','s',... - 'name_y','Magnitude r',... - 'unit_y','V'); - this.DemodSpectrum=MyTrace(... - 'name_x','Frequency',... - 'unit_x','Hz',... - 'name_y','PSD',... - 'unit_y','V^2/Hz'); - this.AvgTrace=MyAvgTrace(); - - % Set up the poll timer. Using a timer for anyncronous - % data readout allows to use the wait time for execution - % of other programs. - % Fixed spacing is preferred as it is the most robust mode of - % operation when keeping the intervals between callbacks - % precisely defined is not the biggest concern. - this.PollTimer=timer(... - 'ExecutionMode','fixedSpacing',... - 'Period',P.Results.poll_period,... - 'TimerFcn',@(~,~)pollTimerCallback(this)); - - % Aux out timers use fixedRate mode for more precise timing. - % The two timers are executed periodically with a time lag. - % The first timer switches the auxiliary output off - this.AuxOutOffTimer=timer(... - 'ExecutionMode','fixedRate',... - 'TimerFcn',@(~,~)auxOutOffTimerCallback(this)); - % The second timer switches the auxiliary output on - this.AuxOutOnTimer=timer(... - 'ExecutionMode','fixedRate',... - 'TimerFcn',@(~,~)auxOutOnTimerCallback(this)); - - this.demod_path = sprintf('/%s/demods/%i', this.dev_id, ... - this.demod-1); - end - - function delete(this) - % delete function should never throw errors, so protect - % statements with try-catch - try - stopPoll(this) - catch - warning(['Could not usubscribe from the demodulator ', ... - 'or stop the poll timer.']) - end - % Delete timers to prevent them from running indefinitely in - % the case of program crash - try - delete(this.PollTimer) - catch - warning('Could not delete the poll timer.') - end - try - stop(this.AuxOutOffTimer); - delete(this.AuxOutOffTimer); - catch - warning('Could not stop and delete AuxOutOff timer.') - end - try - stop(this.AuxOutOnTimer); - delete(this.AuxOutOnTimer); - catch - warning('Could not stop and delete AuxOutOn timer.') - end - end - - %% Other methods - - function startPoll(this) - % Configure the oscillators, demodulator and driving output - % -1 accounts for the difference in enumeration conventions - % in the software names (starting from 1) and node numbers - % (starting from 0). - % First, update the demodulator path - this.demod_path = sprintf('/%s/demods/%i', ... - this.dev_id, this.demod-1); - - % Set the data transfer rate so that it satisfies the Nyquist - % criterion (>x2 the bandwidth of interest) - this.demod_rate=4*this.lowpass_bw; - - % Configure the demodulator. Signal input: - ziDAQ('setInt', ... - [this.demod_path,'/adcselect'], this.signal_in-1); - % Oscillator: - ziDAQ('setInt', ... - [this.demod_path,'/oscselect'], this.drive_osc-1); - % Enable data transfer from the demodulator to the computer - ziDAQ('setInt', [this.demod_path,'/enable'], 1); - - % Configure the signal output - disable all the oscillator - % contributions excluding the driving tone - path = sprintf('/%s/sigouts/%i/enables/*', ... - this.dev_id, this.drive_out-1); - ziDAQ('setInt', path, 0); - path = sprintf('/%s/sigouts/%i/enables/%i', ... - this.dev_id, this.drive_out-1, this.drive_osc-1); - ziDAQ('setInt', path, 1); - - % Enable output - this.drive_on=true; - - % By convention, we start form 'enable_acq=false' state - this.enable_acq=false; - - % Configure the auxiliary trigger output - put it in the manual - % mode so it does not output demodulator readings - path=sprintf('/%s/auxouts/%i/outputselect', ... - this.dev_id, this.aux_out-1); - ziDAQ('setInt', path, -1); - % The convention is that aux out is on by default - this.aux_out_on=true; - - % Subscribe to continuously receive samples from the - % demodulator. Samples accumulated between timer callbacks - % will be read out using ziDAQ('poll', ... - ziDAQ('subscribe',[this.demod_path,'/sample']); - - % Start continuous polling - start(this.PollTimer) - end - - function stopPoll(this) - stop(this.PollTimer) - ziDAQ('unsubscribe',[this.demod_path,'/sample']); - end - - % Main function that polls data from the device demodulator - function pollTimerCallback(this) - - % ziDAQ('poll', ... with short poll_duration returns - % immediately with the data accumulated since the last timer - % callback - Data = ziDAQ('poll', this.poll_duration, this.poll_timeout); - - if ziCheckPathInData(Data, [this.demod_path,'/sample']) - % Demodulator returns data - DemodSample= ... - Data.(this.dev_id).demods(this.demod).sample; - - % Append new samples to the record and recalculate spectrum - appendSamplesToBuff(this, DemodSample); - calcfft(this); - - if this.recording - % If recording is under way, append the new samples to - % the trace - rec_finished = appendSamplesToTrace(this, DemodSample); - - % Recording can be manually stopped by setting - % enable_acq=false - if ~this.enable_acq - rec_finished=true; - end - - % Update elapsed time - this.elapsed_t=this.Trace.x(end); - - % If the adaptive measurement frequency mode is on, - % update the measurement oscillator frequency. - % Make sure that the demodulator record actually - % contains signal by comparing the dispersion of - % frequency to demodulator bandwidth. - if this.adaptive_meas_osc - [df_avg, df_dev]=calcfreq(this); - if df_dev < this.ad_osc_margin*this.lowpass_bw - this.meas_osc_freq=df_avg; - % Change indicator - this.ad_osc_following=true; - else - this.ad_osc_following=false; - end - else - this.ad_osc_following=false; - end - else - r=sqrt(DemodSample.x.^2+DemodSample.y.^2); - if this.enable_acq && max(r)>this.trig_threshold - % Start acquisition of a new trace if the maximum - % of the signal exceeds threshold - this.recording=true; - - % Find index at which the threshold was - % exceeded - ind0=find(r>this.trig_threshold,1,'first'); - - this.t0=DemodSample.timestamp(ind0); - this.elapsed_t=0; - - % Switch the drive off - this.drive_on=false; - - % Set the measurement oscillator frequency to be - % the frequency at which triggering occurred - this.meas_osc_freq=this.drive_osc_freq; - - % Switch the oscillator - this.current_osc=this.meas_osc; - - % Optionally start the auxiliary output timers - if this.enable_aux_out - % Configure measurement periods and delays - T=this.aux_out_on_t+this.aux_out_off_t; - this.AuxOutOffTimer.Period=T; - this.AuxOutOnTimer.Period=T; - - this.AuxOutOffTimer.startDelay=... - this.aux_out_on_t; - this.AuxOutOnTimer.startDelay=T; - - % Start timers - start(this.AuxOutOffTimer) - start(this.AuxOutOnTimer) - end - - % Clear trace and append new data starting from the - % index, at which triggering occurred. - % Theoretically, a record can be finished with - % this one portion if the record time is set small. - clearData(this.Trace); - - rec_finished = ... - appendSamplesToTrace(this, DemodSample, ind0); - - notify(this, 'RecordingStarted'); - else - rec_finished=false; - end - - % Indicator for adaptive measurement is off, since - % recording is not under way - this.ad_osc_following=false; - end - - notify(this,'NewDemodSample'); - - % Stop recording if a record was completed - if rec_finished - % stop recording - this.recording=false; - this.ad_osc_following=false; - - % Stop auxiliary timers - stop(this.AuxOutOffTimer); - stop(this.AuxOutOnTimer); - - % Return the drive and aux out to the default state - this.aux_out_on=true; - this.current_osc=this.drive_osc; - this.drive_on=true; - - % Downsample the trace to reduce the amount of data - downsample(this.Trace, this.downsample_n, 'avg'); - - % Do trace averaging. If the new data length is not of - % the same size as the length of the existing data - % (which should happen only when the record period was - % changed during recording or when recording was - % manually stopped), truncate to the minimum length - if ~isempty(this.AvgTrace) && ... - (length(this.AvgTrace.y)~=length(this.Trace.y)) - l=min(length(this.AvgTrace.y),length(this.Trace.y)); - this.AvgTrace.y=this.AvgTrace.y(1:l); - this.AvgTrace.x=this.AvgTrace.x(1:l); - this.Trace.y=this.Trace.y(1:l); - this.Trace.x=this.Trace.x(1:l); - disp('Ringdown record was truncated') - end - avg_compl=addAverage(this.AvgTrace, this.Trace); - - % Trigger NewData - if this.n_avg>1 - end_str=sprintf('_%i', this.AvgTrace.avg_count); - else - end_str=''; - end - triggerNewData(this, 'save', this.auto_save, ... - 'filename_ending', end_str); - - % If the ringdown averaging is complete, disable - % further triggering to exclude data overwriting - if avg_compl - this.enable_acq=false; - - if this.n_avg>1 - end_str='_avg'; - % Trigger one more time to transfer the average - % trace. - % A new measurement header is not necessary - % as the delay since the last triggering is - % minimum. - triggerNewData(this, ... - 'Trace', copy(this.AvgTrace), ... - 'save', this.auto_save, ... - 'filename_ending', end_str); - end - end - end - end - end - - % Append timestamps vs r=sqrt(x^2+y^2) to the measurement record. - % Starting index can be supplied as varargin. - % The output variable tells if the record is finished. - function isfin = appendSamplesToTrace(this, DemodSample, varargin) - if isempty(varargin) - startind=1; - else - startind=varargin{1}; - end - - r=sqrt(DemodSample.x(startind:end).^2 + ... - DemodSample.y(startind:end).^2); - % Subtract the reference time, convert timestamps to seconds - ts=double(DemodSample.timestamp(startind:end) -... - this.t0)/this.clockbase; - - % Check if recording should be stopped - isfin=(ts(end)>=this.record_time); - if isfin - % Remove excess data points from the new data - ind=(tsflen - this.DemodRecord.t = this.DemodRecord.t(end-flen+1:end); - this.DemodRecord.z = this.DemodRecord.z(end-flen+1:end); - this.DemodRecord.osc_freq = ... - this.DemodRecord.osc_freq(end-flen+1:end); - end - end - - function calcfft(this) - flen=min(this.fft_length, length(this.DemodRecord.t)); - [freq, spectr]=xyFourier( ... - this.DemodRecord.t(end-flen+1:end), ... - this.DemodRecord.z(end-flen+1:end)); - this.DemodSpectrum.x=freq; - this.DemodSpectrum.y=abs(spectr).^2; - end - - % Calculate the average frequency and dispersion of the demodulator - % signal - function [f_avg, f_dev]=calcfreq(this) - if ~isempty(this.DemodSpectrum) - norm=sum(this.DemodSpectrum.y); - - % Calculate the center frequency of the spectrum - f_avg=dot(this.DemodSpectrum.x, this.DemodSpectrum.y)/norm; - - f_dev=sqrt(dot(this.DemodSpectrum.x.^2, ... - this.DemodSpectrum.y)/norm-f_avg^2); - - % Shift the FFT center by the demodulation frequency to - % output absolute value - f_avg=f_avg+mean(this.DemodRecord.osc_freq); - else - f_avg=[]; - f_dev=[]; - end - end - - % Provide restricted access to private AvgTrace - function resetAveraging(this) - % Clear data and reset counter - clearData(this.AvgTrace); - notify(this,'NewSetting'); - end - - function auxOutOffTimerCallback(this) - this.aux_out_on=false; - end - - function auxOutOnTimerCallback(this) - this.aux_out_on=true; - end - - %% measurement header functionality - - function Hdr=readHeader(this) - Hdr=readHeader@MyZiLi(this); - - % Demodulator parameters - addObjProp(Hdr, this, 'demod', 'comment', ... - 'Number of the demodulator in use (starting from 1)'); - addObjProp(Hdr, this, 'demod_rate', 'comment', ... - '(samples/s), demodulator data transfer rate'); - addObjProp(Hdr, this, 'lowpass_order', 'comment', ... - 'Order of the demodulator low-pass filter'); - addObjProp(Hdr, this, 'lowpass_bw', 'comment', ... - ['(Hz), 3 dB bandwidth of the demodulator low-pass ', ... - 'filter']); - addObjProp(Hdr, this, 'meas_osc', 'comment', ... - 'Measurement oscillator number'); - addObjProp(Hdr, this, 'meas_osc_freq', 'comment', '(Hz)'); - - % Signal input - addObjProp(Hdr, this, 'signal_in', 'comment', ... - 'Singnal input number'); - - % Drive parameters - addObjProp(Hdr, this, 'drive_out', 'comment', ... - 'Driving output number'); - addObjProp(Hdr, this, 'drive_osc', 'comment', ... - 'Swept oscillator number'); - addObjProp(Hdr, this, 'drive_amp', 'comment', ... - '(V) peak to peak'); - - % Parameters of the auxiliary output - addObjProp(Hdr, this, 'aux_out', 'comment', ... - 'Auxiliary output number'); - addObjProp(Hdr, this, 'enable_aux_out', 'comment', ... - 'Auxiliary output is applied during ringdown'); - addObjProp(Hdr, this, 'aux_out_on_lev', 'comment', '(V)'); - addObjProp(Hdr, this, 'aux_out_off_lev', 'comment', '(V)'); - addObjProp(Hdr, this, 'aux_out_on_t', 'comment', '(s)'); - addObjProp(Hdr, this, 'aux_out_off_t', 'comment', '(s)'); - - % Software parameters - addObjProp(Hdr, this, 'trig_threshold', 'comment', ... - '(V), threshold for starting a ringdown record'); - addObjProp(Hdr, this, 'record_time', 'comment', '(s)'); - addObjProp(Hdr, this, 'n_avg', 'comment', ... - 'Number of ringdowns to be averaged'); - addObjProp(Hdr, this, 'downsampled_rate', 'comment', ... - ['(samples/s), rate to which a ringown trace is ', ... - 'downsampled with averaging after acquisition']); - addObjProp(Hdr, this, 'auto_save', 'comment', '(s)'); - - % Adaptive measurement oscillator - addObjProp(Hdr, this, 'adaptive_meas_osc', 'comment', ... - ['If true the measurement oscillator frequency is ', ... - 'adjusted during ringdown']); - addObjProp(Hdr, this, 'ad_osc_margin'); - addObjProp(Hdr, this, 'fft_length', 'comment', '(points)'); - - % Timer poll parameters - addParam(Hdr, this.name, 'poll_period', ... - this.PollTimer.Period, 'comment', '(s)'); - addObjProp(Hdr, this, 'poll_duration', 'comment', '(s)'); - addObjProp(Hdr, this, 'poll_timeout', 'comment', '(ms)'); - end - end - - %% Set and get methods. - methods - - function freq=get.drive_osc_freq(this) - path=sprintf('/%s/oscs/%i/freq', this.dev_id, this.drive_osc-1); - freq=ziDAQ('getDouble', path); - end - - function set.drive_osc_freq(this, val) - assert(isfloat(val), ... - 'Oscillator frequency must be a floating point number') - path=sprintf('/%s/oscs/%i/freq', this.dev_id, this.drive_osc-1); - ziDAQ('setDouble', path, val); - notify(this,'NewSetting'); - end - - function freq=get.meas_osc_freq(this) - path=sprintf('/%s/oscs/%i/freq', this.dev_id, this.meas_osc-1); - freq=ziDAQ('getDouble', path); - end - - function set.meas_osc_freq(this, val) - assert(isfloat(val), ... - 'Oscillator frequency must be a floating point number') - path=sprintf('/%s/oscs/%i/freq', this.dev_id, this.meas_osc-1); - ziDAQ('setDouble', path, val); - notify(this,'NewSetting'); - end - - function set.drive_on(this, val) - path=sprintf('/%s/sigouts/%i/on',this.dev_id,this.drive_out-1); - % Use double() to convert from logical type - ziDAQ('setInt', path, double(val)); - notify(this,'NewSetting'); - end - - function bool=get.drive_on(this) - path=sprintf('/%s/sigouts/%i/on',this.dev_id,this.drive_out-1); - bool=logical(ziDAQ('getInt', path)); - end - - function set.current_osc(this, val) - assert((val==this.drive_osc) || (val==this.meas_osc), ... - ['The number of current oscillator must be that of ', ... - 'the drive or measurement oscillator, not ', num2str(val)]) - ziDAQ('setInt', [this.demod_path,'/oscselect'], val-1); - notify(this,'NewSetting') - end - - function osc_num=get.current_osc(this) - osc_num=double(ziDAQ('getInt', ... - [this.demod_path,'/oscselect']))+1; - end - - function amp=get.drive_amp(this) - path=sprintf('/%s/sigouts/%i/amplitudes/%i', ... - this.dev_id, this.drive_out-1, this.drive_osc-1); - amp=ziDAQ('getDouble', path); - end - - function set.drive_amp(this, val) - path=sprintf('/%s/sigouts/%i/amplitudes/%i', ... - this.dev_id, this.drive_out-1, this.drive_osc-1); - ziDAQ('setDouble', path, val); - notify(this,'NewSetting'); - end - - function set.lowpass_order(this, val) - assert(any(val==[1,2,3,4,5,6,7,8]), ['Low-pass filter ', ... - 'order must be an integer between 1 and 8']) - ziDAQ('setInt', [this.demod_path,'/order'], val); - notify(this,'NewSetting'); - end - - function n=get.lowpass_order(this) - n=ziDAQ('getInt', [this.demod_path,'/order']); - end - - function bw=get.lowpass_bw(this) - tc=ziDAQ('getDouble', [this.demod_path,'/timeconstant']); - bw=ziBW2TC(tc, this.lowpass_order); - end - - function set.lowpass_bw(this, val) - tc=ziBW2TC(val, this.lowpass_order); - ziDAQ('setDouble', [this.demod_path,'/timeconstant'], tc); - notify(this,'NewSetting'); - end - - function rate=get.demod_rate(this) - rate=ziDAQ('getDouble', [this.demod_path,'/rate']); - end - - function set.demod_rate(this, val) - ziDAQ('setDouble', [this.demod_path,'/rate'], val); - notify(this,'NewSetting'); - end - - function set.downsample_n(this, val) - n=round(val); - assert(n>=1, ['Number of points for trace averaging must ', ... - 'be greater than 1']) - this.downsample_n=n; - notify(this,'NewSetting'); - end - - function set.aux_out_on(this, bool) - path=sprintf('/%s/auxouts/%i/offset', ... - this.dev_id, this.aux_out-1); - if bool - out_offset=this.aux_out_on_lev; - else - out_offset=this.aux_out_off_lev; - end - ziDAQ('setDouble', path, out_offset); - notify(this,'NewSetting'); - end - - function bool=get.aux_out_on(this) - path=sprintf('/%s/auxouts/%i/offset', ... - this.dev_id, this.aux_out-1); - val=ziDAQ('getDouble', path); - % Signal from the auxiliary output is continuous, we make the - % binary decision about the output state depending on if - % the signal is closer to the ON or OFF level - bool=(abs(val-this.aux_out_on_lev) < ... - abs(val-this.aux_out_off_lev)); - end - - function set.downsampled_rate(this, val) - dr=this.demod_rate; - if val>dr - % Downsampled rate should not exceed the data transfer rate - val=dr; - end - % Round so that the averaging is done over an integer number of - % points - this.downsample_n=round(dr/val); - notify(this,'NewSetting'); - end - - function val=get.downsampled_rate(this) - val=this.demod_rate/this.downsample_n; - end - - function set.fft_length(this, val) - if val<1 - val=1; - end - % Round val to the nearest 2^n to make the calculation of - % Fourier transform efficient - n=round(log2(val)); - this.fft_length=2^n; - notify(this,'NewSetting'); - end - - function val=get.fft_rbw(this) - val=this.demod_rate/this.fft_length; - end - - function set.fft_rbw(this, val) - assert(val>0,'FFT resolution bandwidth must be greater than 0') - % Rounding of fft_length to the nearest integer is handled by - % its own set method - this.fft_length=this.demod_rate/val; - notify(this,'NewSetting'); - end - - function set.n_avg(this, val) - this.AvgTrace.n_avg=val; - notify(this,'NewSetting'); - end - - function val=get.n_avg(this) - val=this.AvgTrace.n_avg; - end - - function val=get.avg_count(this) - val=this.AvgTrace.avg_count; - end - - function set.aux_out_on_t(this, val) - assert(val>0.001, ... - 'Aux out on time must be greater than 0.001 s.') - this.aux_out_on_t=val; - notify(this,'NewSetting'); - end - - function set.aux_out_off_t(this, val) - assert(val>0.001, ... - 'Aux out off time must be greater than 0.001 s.') - this.aux_out_off_t=val; - notify(this,'NewSetting'); - end - - function set.enable_acq(this, val) - this.enable_acq=logical(val); - notify(this,'NewSetting'); - end - end -end - diff --git a/Fit classes/@MyDoubleLorentzianFit/MyDoubleLorentzianFit.m b/Fit classes/@MyDoubleLorentzianFit/MyDoubleLorentzianFit.m new file mode 100644 index 0000000..5847473 --- /dev/null +++ b/Fit classes/@MyDoubleLorentzianFit/MyDoubleLorentzianFit.m @@ -0,0 +1,145 @@ +classdef MyDoubleLorentzianFit < MyFit + properties (Access=public) + tot_spacing; + end + + %Public methods + methods (Access=public) + %Constructor function + function this=MyDoubleLorentzianFit(varargin) + this@MyFit(... + 'fit_name','DoubleLorentzian',... + 'fit_function','1/pi*b/2*a/((x-c)^2+(b/2)^2)+1/pi*e/2*d/((x-f)^2+(e/2)^2)+g',... + 'fit_tex','$$\frac{a}{\pi}\frac{b/2}{(x-c)^2+(b/2)^2}+\frac{d}{\pi}\frac{e/2}{(x-f)^2+(e/2)^2}+g$$',... + 'fit_params', {'a','b','c','d','e','f','g'},... + 'fit_param_names',{'Amplitude 1','Width 1','Center 1','Amplitude 2',... + 'Width 2','Center 2','Offset'},... + varargin{:}); + end + end + + methods (Access=protected) + function [p_in,lim_lower,lim_upper]=calcInitParams(this) + x = this.Data.x; + y = this.Data.y; + + %Assumes form a/pi*b/2/((x-c)^2+(b/2)^2)+d/pi*e/2/((x-f)^2+(e/2)^2))+g + + lim_upper=[Inf,Inf,Inf,Inf,Inf,Inf,Inf]; + lim_lower=[-Inf,0,-Inf,-Inf,0,-Inf,-Inf]; + + %Finds peaks on the positive signal (max 2 peaks) + [~,locs{1},widths{1},proms{1}]=findpeaks(y,x,... + 'MinPeakDistance',0.01*range(x),'SortStr','descend','NPeaks',2); + + %Finds peaks on the negative signal (max 2 peaks) + [~,locs{2},widths{2},proms{2}]=findpeaks(-y,x,... + 'MinPeakDistance',0.001*range(x),'SortStr','descend','NPeaks',2); + + %If the prominence of the peak in the positive signal is greater, we adapt + %our limits and parameters accordingly, if negative signal has a greater + %prominence, we use this for fitting. + if isempty(proms{2}) || proms{1}(1)>proms{2}(1) + ind=1; + lim_lower(1)=0; + lim_lower(4)=0; + p_in(7)=min(y); + else + lim_upper(1)=0; + lim_upper(4)=0; + ind=2; + p_in(7)=max(y); + proms{2}=-proms{2}; + end + + p_in(2)=widths{ind}(1); + %Calculates the amplitude, as when x=c, the amplitude is 2a/(pi*b) + p_in(1)=proms{ind}(1)*pi*p_in(2)/2; + p_in(3)=locs{ind}(1); + if length(locs{ind})==2 + p_in(5)=widths{ind}(2); + p_in(4)=proms{ind}(2)*pi*p_in(5)/2; + p_in(6)=locs{ind}(2); + else + p_in(5)=widths{ind}(1); + p_in(4)=proms{ind}(1)*pi*p_in(5)/2; + p_in(6)=locs{ind}(1); + end + + %If one of the lorentzians is found to be much smaller than the other, we + %instead fit using only the greater lorentzian's parameters. This is an + %adaption for very closely spaced lorentzians. + if abs(p_in(1))>abs(10*p_in(4)) + p_in(1)=p_in(1)/2; + p_in(5)=p_in(2); + p_in(6)=p_in(3); + p_in(4)=p_in(1); + end + + lim_lower(2)=0.01*p_in(2); + lim_upper(2)=100*p_in(2); + + lim_lower(5)=0.01*p_in(5); + lim_upper(5)=100*p_in(5); + end + + %Calculates user-defined parameters + function calcUserParams(this) + this.opt_lw1=convOptFreq(this,this.coeffs(2)); + this.opt_lw2=convOptFreq(this,this.coeffs(5)); + splitting=abs(this.coeffs(6)-this.coeffs(3)); + this.mode_split=convOptFreq(this,splitting); + end + + %This function is used to convert the x-axis to frequency. + function real_freq=convOptFreq(this,freq) + real_freq=freq*this.line_spacing*this.line_no/this.tot_spacing; + end + + function createUserGuiStruct(this) + createUserGuiStruct@MyFit(this); + this.UserGui.Tabs.Opt.tab_title='Optical'; + this.UserGui.Tabs.Opt.Children={}; + addUserField(this,'Opt','line_spacing',... + 'Line Spacing (MHz)',1e6,'conv_factor',1e6,... + 'Callback', @(~,~) calcUserParams(this)); + addUserField(this,'Opt','line_no','Number of lines',10,... + 'Callback', @(~,~) calcUserParams(this)); + addUserField(this,'Opt','opt_lw1','Linewidth 1 (MHz)',1e6,... + 'enable_flag','off','conv_factor',1e6); + addUserField(this,'Opt','opt_lw2','Linewidth 2 (MHz)',1e6,... + 'enable_flag','off','conv_factor',1e6); + addUserField(this,'Opt','mode_split',... + 'Modal splitting (MHz)',1e6,... + 'enable_flag','off','conv_factor',1e6); + end + + function genSliderVecs(this) + genSliderVecs@MyFit(this); + + if validateData(this) + %We choose to have the slider go over the range of + %the x-values of the plot for the center of the + %Lorentzian. + this.slider_vecs{3}=... + linspace(this.x_vec(1),this.x_vec(end),101); + %Find the index closest to the init parameter + [~,ind]=... + min(abs(this.init_params(3)-this.slider_vecs{3})); + %Set to ind-1 as the slider goes from 0 to 100 + set(this.Gui.(sprintf('Slider_%s',... + this.fit_params{3})),'Value',ind-1); + + %Same for the other center + this.slider_vecs{6}=... + linspace(this.x_vec(1),this.x_vec(end),101); + %Find the index closest to the init parameter + [~,ind]=... + min(abs(this.init_params(6)-this.slider_vecs{6})); + %Set to ind-1 as the slider goes from 0 to 100 + set(this.Gui.(sprintf('Slider_%s',... + this.fit_params{6})),'Value',ind-1); + end + end + end +end \ No newline at end of file diff --git a/Fit classes/@MyExponentialFit/MyExponentialFit.m b/Fit classes/@MyExponentialFit/MyExponentialFit.m new file mode 100644 index 0000000..a6f2aa2 --- /dev/null +++ b/Fit classes/@MyExponentialFit/MyExponentialFit.m @@ -0,0 +1,101 @@ +classdef MyExponentialFit < MyFitParamScaling + + methods (Access = public) + function this = MyExponentialFit(varargin) + this@MyFitParamScaling( ... + 'fit_name', 'Exponential',... + 'fit_function', 'a*exp(b*x)+c',... + 'fit_tex', '$$ae^{bx}+c$$',... + 'fit_params', {'a','b','c'},... + 'fit_param_names', {'Amplitude','Rate','Offset'},... + varargin{:}); + end + end + + methods (Access = protected) + + function calcInitParams(this) + ind=this.data_selection; + + x=this.Data.x(ind); + y=this.Data.y(ind); + + %Setting upper and lower limits + [amp_max,ind_max]=max(y); + [amp_min,ind_min]=min(y); + + this.lim_upper=[Inf,Inf,Inf]; + this.lim_lower=-this.lim_upper; + + %Fix to avoid unphysical offsets on data where all y values + %exceed 0. + if all(y>0) + this.lim_lower(3)=0; + end + + if abs(amp_max)>abs(amp_min) + this.lim_upper(1)=Inf; + this.lim_lower(1)=0; + else + this.lim_upper(1)=0; + this.lim_lower(1)=-Inf; + end + + if (ind_max>ind_min && abs(amp_max)>abs(amp_min))... + || (ind_maxabs(amp_min) + p_in(3)=amp_min; + else + p_in(3)=amp_max; + end + + this.param_vals = p_in; + end + end + + methods (Access = private) + function sc_vals = scaleFitParams(~, vals, scaling_coeffs) + [mean_x,std_x,mean_y,std_y]=scaling_coeffs{:}; + + sc_vals(2)=vals(2)*std_x; + sc_vals(1)=vals(1)*exp(sc_vals(2)*mean_x/std_x)/std_y; + sc_vals(3)=(vals(3)-mean_y)/std_y; + end + + function vals = unscaleFitParams(~, sc_vals, scaling_coeffs) + [mean_x,std_x,mean_y,std_y]=scaling_coeffs{:}; + + vals(1)=exp(-sc_vals(2)*mean_x/std_x)*sc_vals(1)*std_y; + + %Edge case for limits + if isnan(vals(1)) + vals(1)=0; + end + + vals(2)=sc_vals(2)/std_x; + vals(3)=std_y*sc_vals(3)+mean_y; + end + end +end \ No newline at end of file diff --git a/Fit classes/@MyFitParamScaling/MyFitParamScaling.m b/Fit classes/@MyFitParamScaling/MyFitParamScaling.m new file mode 100644 index 0000000..2c06f96 --- /dev/null +++ b/Fit classes/@MyFitParamScaling/MyFitParamScaling.m @@ -0,0 +1,45 @@ +% Class that adds the capability of normalizing the data by z-score before +% performing the fit to improve numeric performance. +% Scaling/unscaling functions for the parameters must be defined in +% the end classes. + +classdef (Abstract) MyFitParamScaling < MyFit + + methods (Access = public) + function this = MyFitParamScaling(varargin) + this@MyFit(varargin{:}); + end + end + + methods (Access = protected) + + % Overload the doFit function to fit scaled data. + function fitted_vals = doFit(this, x, y, init_vals, lim_lower, ... + lim_upper) + + % Scale x and y data + [scaled_x, mean_x, std_x] = zscore(x); + [scaled_y, mean_y, std_y] = zscore(y); + + % Scaling coefficients + sc = {mean_x, std_x, mean_y, std_y}; + + scaled_fitted_vals = doFit@MyFit(this, scaled_x, scaled_y, ... + scaleFitParams(this, init_vals, sc), ... + scaleFitParams(this, lim_lower, sc), ... + scaleFitParams(this, lim_upper, sc)); + + fitted_vals = unscaleFitParams(this, scaled_fitted_vals, sc); + end + end + + methods (Access = protected, Abstract) + + % Functions that define scaling and unscaling of the fit parameters + % by zscore + sc_vals = scaleFitParams(~, vals, xy_zscore) + + vals = unscaleFitParams(~, sc_vals, xy_zscore) + end +end + diff --git a/Fit classes/@MyGaussianFit/MyGaussianFit.m b/Fit classes/@MyGaussianFit/MyGaussianFit.m new file mode 100644 index 0000000..841ce74 --- /dev/null +++ b/Fit classes/@MyGaussianFit/MyGaussianFit.m @@ -0,0 +1,62 @@ +classdef MyGaussianFit < MyFit + %Public methods + methods (Access=public) + %Constructor function + function this=MyGaussianFit(varargin) + this@MyFit(... + 'fit_name','Gaussian',... + 'fit_function','a*exp(-((x-c)/b)^2/2)+d',... + 'fit_tex', '$$ae^{-\frac{(x-c)^2}{2b^2}}+d$$',... + 'fit_params',{'a','b','c','d'},... + 'fit_param_names',{'Amplitude','Width','Center','Offset'},... + varargin{:}); + end + end + methods (Access=protected) + function [init_params,lim_lower,lim_upper]=calcInitParams(this) + x = this.Data.x; + y = this.Data.y; + + %Assumes a*exp(-((x-c)/b)^2/2)+d - remember matlab orders the fit + %parameters alphabetically + + bg=median(y); + y=y-bg; + + [amp_max,ind_max]=max(y); + [amp_min,ind_min]=min(y); + + lim_upper=[Inf,Inf,Inf,Inf]; + lim_lower=-lim_upper; + + if abs(amp_max)>abs(amp_min) + amp=amp_max; + center=x(ind_max); + lim_upper(1)=Inf; + lim_lower(1)=0; + else + amp=amp_min; + center=x(ind_min); + lim_upper(1)=0; + lim_lower(1)=-Inf; + end + + ind1=find(y>amp/2,1,'first'); + ind2=find(y>amp/2,1,'last'); + fwhm=x(ind2)-x(ind1); + width=fwhm/2.35482; + + %Sets the lower limit on width to zero + lim_lower(2)=0; + + %Sets the upper limit on width to 100 times the range of the data + lim_upper(2)=100*range(x); + + %Sets upper and lower limit on the center + lim_lower(3)=min(x)/2; + lim_upper(3)=max(x)*2; + + init_params=[amp,width,center, bg]; + end + end +end \ No newline at end of file diff --git a/Fit classes/@MyGorodetsky2000Fit/MyGorodetsky2000Fit.m b/Fit classes/@MyGorodetsky2000Fit/MyGorodetsky2000Fit.m new file mode 100644 index 0000000..560d22f --- /dev/null +++ b/Fit classes/@MyGorodetsky2000Fit/MyGorodetsky2000Fit.m @@ -0,0 +1,71 @@ +classdef MyGorodetsky2000Fit < MyFit + %Public methods + methods (Access=public) + %Constructor function + function this=MyGorodetsky2000Fit(varargin) + this@MyFit(... + 'fit_name','Gorodetsky2000',... + 'fit_function',['a*abs( (k0^2/4 - kex^2/4 + gamma^2/4 - (x-b).^2 + i*k0.*(x-b))',... + './( (k0 + kex)^2/4 + gamma^2/4 - (x-b).^2 + i.*(x-b)*(k0 + kex) )).^2+c*(x-b)'],... + 'fit_tex',['$$a\left|\frac{\kappa_0^2/4-\kappa_{ex}^2/4+\gamma^2/4-(x-b)^2+i\kappa_0(x-b)/2}',... + '{(\kappa_0+\kappa_{ex})^2/4+\gamma^2/4-(x-b)^2+i(x-b)(\kappa_0+\kappa_{ex})}\right|^2$$+c(x-b)'],... + 'fit_params', { 'a','b','c','gamma','k0', 'kex'},... + 'fit_param_names',{'Background','Center','BG Slope','Mode splitting',... + 'Intrinsic','Extrinsic'},... + varargin{:}); + end + end + + methods (Access=protected) + %Calculates the initial parameters using an external function. + function [p_in,lim_lower,lim_upper]=calcInitParams(this) + x = this.Data.x; + y = this.Data.y; + + % { 'a','b','c', 'gamma','k0', 'kex'},... + lim_upper=[Inf,Inf,Inf,Inf,Inf,Inf]; + lim_lower=[-Inf,-Inf,-Inf,0,0,0]; + + + %Finds peaks on the negative signal (max 2 peaks) + [~,locs,widths,~]=findpeaks(-y,x,... + 'MinPeakDistance',0.001*range(x),'SortStr','descend','NPeaks',2); + + + p_in(1)=max(y); + + %position + p_in(2)=mean(locs); + + p_in(3)=(y(end)-y(1))/(x(end)-x(1)); + + if length(locs)==2 + p_in(4)=abs(diff(locs))/2; + else + p_in(4)=0; + end + + p_in(5)=mean(widths)/2; + %Assume critical coupling + p_in(6)=p_in(4); + end + + function genSliderVecs(this) + genSliderVecs@MyFit(this); + + if validateData(this) + %We choose to have the slider go over the range of + %the x-values of the plot for the center of the + %Lorentzian. + this.slider_vecs{2}=... + linspace(this.x_vec(1),this.x_vec(end),101); + %Find the index closest to the init parameter + [~,ind]=... + min(abs(this.init_params(2)-this.slider_vecs{2})); + %Set to ind-1 as the slider goes from 0 to 100 + set(this.Gui.(sprintf('Slider_%s',... + this.fit_params{2})),'Value',ind-1); + end + end + end +end \ No newline at end of file diff --git a/Fit classes/@MyLinearFit/MyLinearFit.m b/Fit classes/@MyLinearFit/MyLinearFit.m new file mode 100644 index 0000000..c0d8167 --- /dev/null +++ b/Fit classes/@MyLinearFit/MyLinearFit.m @@ -0,0 +1,22 @@ +classdef MyLinearFit < MyFit + methods (Access = public) + function this = MyLinearFit(varargin) + this@MyFit(... + 'fit_name','Linear',... + 'fit_function','a*x+b',... + 'fit_tex','$$ax+bx$$',... + 'fit_params',{'a','b'},... + 'fit_param_names',{'Gradient','Offset'},... + varargin{:}); + end + end + + methods (Access = protected) + + %Overload the doFit function to do polyFit instead of nonlinear + %fitting + function fitted_vals = doFit(~, x, y, varargin) + fitted_vals = polyfit(x, y, 1); + end + end +end \ No newline at end of file diff --git a/Fit classes/@MyLorentzianFit/MyLorentzianFit.m b/Fit classes/@MyLorentzianFit/MyLorentzianFit.m new file mode 100644 index 0000000..a0137ee --- /dev/null +++ b/Fit classes/@MyLorentzianFit/MyLorentzianFit.m @@ -0,0 +1,119 @@ +classdef MyLorentzianFit < MyFitParamScaling + + methods (Access = public) + function this = MyLorentzianFit(varargin) + this@MyFitParamScaling( ... + 'fit_name', 'Lorentzian', ... + 'fit_function', '1/pi*a*b/2/((x-c)^2+(b/2)^2)+d', ... + 'fit_tex', '$$\frac{a}{\pi}\frac{b/2}{(x-c)^2+(b/2)^2}+d$$', ... + 'fit_params', {'a','b','c','d'}, ... + 'fit_param_names', {'Amplitude','Width','Center','Offset'}, ... + varargin{:}); + end + end + + methods (Access = protected) + + function calcInitParams(this) + ind = this.data_selection; + + x = this.Data.x(ind); + y = this.Data.y(ind); + + this.lim_upper=[Inf,Inf,Inf,Inf]; + this.lim_lower=[-Inf,0,-Inf,-Inf]; + + %Finds peaks on the positive signal (max 1 peak) + try + [~,locs(1),widths(1),proms(1)]=findpeaks(y,x,... + 'MinPeakDistance',range(x)/2,'SortStr','descend',... + 'NPeaks',1); + catch + proms(1)=0; + end + + %Finds peaks on the negative signal (max 1 peak) + try + [~,locs(2),widths(2),proms(2)]=findpeaks(-y,x,... + 'MinPeakDistance',range(x)/2,'SortStr','descend',... + 'NPeaks',1); + catch + proms(2)=0; + end + + if proms(1)==0 && proms(2)==0 + warning(['No peaks were found in the data, giving ' ... + 'default initial parameters to fit function']) + this.param_vals=[1,1,1,1]; + this.lim_lower=-[Inf,0,Inf,Inf]; + this.lim_upper=[Inf,Inf,Inf,Inf]; + return + end + + %If the prominence of the peak in the positive signal is + %greater, we adapt our limits and parameters accordingly, + %if negative signal has a greater prominence, we use this + %for fitting. + if proms(1)>proms(2) + ind=1; + p_in(4)=min(y); + else + ind=2; + p_in(4)=max(y); + proms(2)=-proms(2); + end + + p_in(2)=widths(ind); + + %Calculates the amplitude, as when x=c, the amplitude + %is 2a/(pi*b) + p_in(1)=proms(ind)*pi*p_in(2)/2; + p_in(3)=locs(ind); + + this.param_vals = p_in; + this.lim_lower(2)=0.01*p_in(2); + this.lim_upper(2)=100*p_in(2); + end + + function genSliderVecs(this) + genSliderVecs@MyFit(this); + + try + + %We choose to have the slider go over the range of + %the x-values of the plot for the center of the + %Lorentzian. + this.slider_vecs{3}=... + linspace(this.Fit.x(1),this.Fit.x(end),101); + %Find the index closest to the init parameter + [~,ind]=... + min(abs(this.param_vals(3)-this.slider_vecs{3})); + %Set to ind-1 as the slider goes from 0 to 100 + set(this.Gui.(sprintf('Slider_%s',... + this.fit_params{3})),'Value',ind-1); + catch + end + end + end + + methods (Access = protected) + function sc_vals = scaleFitParams(~, vals, scaling_coeffs) + [mean_x,std_x,mean_y,std_y]=scaling_coeffs{:}; + + sc_vals(1)=vals(1)/(std_y*std_x); + sc_vals(2)=vals(2)/std_x; + sc_vals(3)=(vals(3)-mean_x)/std_x; + sc_vals(4)=(vals(4)-mean_y)/std_y; + end + + %Converts scaled coefficients to real coefficients + function vals = unscaleFitParams(~, sc_vals, scaling_coeffs) + [mean_x,std_x,mean_y,std_y]=scaling_coeffs{:}; + + vals(1)=sc_vals(1)*std_y*std_x; + vals(2)=sc_vals(2)*std_x; + vals(3)=sc_vals(3)*std_x+mean_x; + vals(4)=sc_vals(4)*std_y+mean_y; + end + end +end \ No newline at end of file diff --git a/Fit classes/@MyLorentzianGradFit/MyLorentzianGradFit.m b/Fit classes/@MyLorentzianGradFit/MyLorentzianGradFit.m new file mode 100644 index 0000000..6328108 --- /dev/null +++ b/Fit classes/@MyLorentzianGradFit/MyLorentzianGradFit.m @@ -0,0 +1,93 @@ +classdef MyLorentzianGradFit < MyFit + %Public methods + methods (Access=public) + %Constructor function + function this=MyLorentzianGradFit(varargin) + this@MyFit(... + 'fit_name','LorentzianGrad',... + 'fit_function','1/pi*a*b/2/((x-c)^2+(b/2)^2)+d*(x-c)+e',... + 'fit_tex','$$\frac{a}{\pi}\frac{b/2}{(x-c)^2+(b/2)^2}+d(x-c)+e$$',... + 'fit_params', {'a','b','c','d','e'},... + 'fit_param_names',{'Amplitude','Width','Center','Gradient','Offset'},... + varargin{:}); + end + end + + methods (Access=protected) + function [p_in,lim_lower,lim_upper]=calcInitParams(this) + x = this.Data.x; + y = this.Data.y; + + %Assumes form a/pi*b/2/((x-c)^2+(b/2)^2)+d + + lim_upper=[Inf,Inf,Inf,Inf,Inf]; + lim_lower=[-Inf,0,-Inf,-Inf,-Inf]; + + %Finds peaks on the positive signal (max 1 peak) + try + [~,locs(1),widths(1),proms(1)]=findpeaks(y,x,... + 'MinPeakDistance',range(x)/2,'SortStr','descend',... + 'NPeaks',1); + catch + proms(1)=0; + end + + %Finds peaks on the negative signal (max 1 peak) + try + [~,locs(2),widths(2),proms(2)]=findpeaks(-y,x,... + 'MinPeakDistance',range(x)/2,'SortStr','descend',... + 'NPeaks',1); + catch + proms(2)=0; + end + + if proms(1)==0 && proms(2)==0 + warning('No peaks were found in the data, giving default initial parameters to fit function') + p_in=[1,1,1,1,1]; + lim_lower=-[Inf,0,Inf,Inf]; + lim_upper=[Inf,Inf,Inf,Inf]; + return + end + + %If the prominence of the peak in the positive signal is greater, we adapt + %our limits and parameters accordingly, if negative signal has a greater + %prominence, we use this for fitting. + if proms(1)>proms(2) + ind=1; + p_in(5)=min(y); + else + ind=2; + p_in(5)=max(y); + proms(2)=-proms(2); + end + + p_in(2)=widths(ind); + %Calculates the amplitude, as when x=c, the amplitude is 2a/(pi*b) + p_in(1)=proms(ind)*pi*p_in(2)/2; + p_in(3)=locs(ind); + + p_in(4)=(y(end)-y(1))/(x(end)-x(1)); + + lim_lower(2)=0.01*p_in(2); + lim_upper(2)=100*p_in(2); + end + + function genSliderVecs(this) + genSliderVecs@MyFit(this); + + if validateData(this) + %We choose to have the slider go over the range of + %the x-values of the plot for the center of the + %Lorentzian. + this.slider_vecs{3}=... + linspace(this.x_vec(1),this.x_vec(end),101); + %Find the index closest to the init parameter + [~,ind]=... + min(abs(this.init_params(3)-this.slider_vecs{3})); + %Set to ind-1 as the slider goes from 0 to 100 + set(this.Gui.(sprintf('Slider_%s',... + this.fit_params{3})),'Value',ind-1); + end + end + end +end \ No newline at end of file diff --git a/Fit classes/@MyMechExponentialFit/MyMechExponentialFit.m b/Fit classes/@MyMechExponentialFit/MyMechExponentialFit.m new file mode 100644 index 0000000..9a02d7e --- /dev/null +++ b/Fit classes/@MyMechExponentialFit/MyMechExponentialFit.m @@ -0,0 +1,30 @@ +% Exponential fit with user parameters defined for convenient +% characterization of mechanical resonators + +classdef MyMechExponentialFit < MyExponentialFit + + methods (Access = protected) + function createUserParamList(this) + addUserParam(this, 'tau', 'title', '\tau (s)', ... + 'editable', 'off') + addUserParam(this, 'lw', 'title', 'Linewidth (Hz)', ... + 'editable', 'off'); + + % Frequency at must be inputed by user + addUserParam(this, 'freq', 'title', 'Frequency (MHz)', ... + 'editable', 'on', 'default', 1) + addUserParam(this, 'Q', 'title', 'Qualify Factor (x10^6)', ... + 'editable', 'off'); + addUserParam(this, 'Qf', 'title', 'Q\times f (10^{14} Hz)', ... + 'editable', 'off'); + end + + %Function for calculating the parameters shown in the user panel + function calcUserParams(this) + this.tau=abs(1/this.param_vals(2)); + this.lw=abs(this.param_vals(2)/pi); + this.Q=pi*this.freq*this.tau; + this.Qf=this.Q*this.freq; + end + end +end \ No newline at end of file diff --git a/Fit classes/@MyMechLorentzianFit/MyMechLorentzianFit.m b/Fit classes/@MyMechLorentzianFit/MyMechLorentzianFit.m new file mode 100644 index 0000000..335e61e --- /dev/null +++ b/Fit classes/@MyMechLorentzianFit/MyMechLorentzianFit.m @@ -0,0 +1,30 @@ +% Lorenzian fit customized for the characterization of quality factors of +% mechanical resonators + +classdef MyMechLorentzianFit < MyLorentzianFit + methods (Access = protected) + + % Function for calculating the parameters shown in the user panel + function calcUserParams(this) + lw = this.param_vals(2); + freq = this.param_vals(3); + + this.lw = lw; % Linewidth in Hz + this.freq = freq/1e6; % Frequency in MHz + this.Q = (freq/lw)/1e6; % Q in millions + this.Qf = (freq^2/lw)/1e14; % Qf in 10^14 Hz + end + + function createUserParamList(this) + addUserParam(this, 'lw', 'title', 'Linewidth (Hz)', ... + 'editable', 'off') + addUserParam(this, 'freq', 'title', 'Frequency (MHz)', ... + 'editable', 'off') + addUserParam(this, 'Q', 'title', 'Qualify factor (x10^6)', ... + 'editable', 'off'); + addUserParam(this, 'Qf', 'title', 'Q\times f (10^{14} Hz)', ... + 'editable', 'off'); + end + end +end + diff --git a/Fit classes/@MyOpticalLorentzianFit/MyOpticalLorentzianFit.m b/Fit classes/@MyOpticalLorentzianFit/MyOpticalLorentzianFit.m new file mode 100644 index 0000000..3d37a1b --- /dev/null +++ b/Fit classes/@MyOpticalLorentzianFit/MyOpticalLorentzianFit.m @@ -0,0 +1,91 @@ +% Lorenzian fit with additional capabilities for the calibration of optical +% linewidth + +classdef MyOpticalLorentzianFit < MyLorentzianFit + properties (GetAccess = public, SetAccess = protected) + RefCursors MyCursor + end + + methods (Access = public) + function this = MyOpticalLorentzianFit(varargin) + this@MyLorentzianFit(varargin{:}); + + if ~isempty(this.Axes) + + % Add two vertical reference cursors to set the frequency + % scale + xlim = this.Axes.XLim; + x1 = xlim(1)+0.2*(xlim(2)-xlim(1)); + x2 = xlim(2)-0.2*(xlim(2)-xlim(1)); + + this.RefCursors = ... + [MyCursor(this.Axes, ... + 'orientation', 'vertical', ... + 'position', x1, ... + 'Label','Ref 1', 'Color', [0, 0, 0.6]), ... + MyCursor(this.Axes, 'orientation', 'vertical', ... + 'position', x2, ... + 'Label','Ref 2', 'Color', [0, 0, 0.6])]; + end + end + + function delete(this) + if ~isempty(this.RefCursors) + delete(this.RefCursors); + end + end + + function centerCursors(this) + + % Center the range cursors + centerCursors@MyFit(this); + + % Center the ref cursors + if ~isempty(this.Axes) && ~isempty(this.RefCursors) ... + && all(isvalid(this.RefCursors)) + xlim = this.Axes.XLim; + + x1 = xlim(1)+0.2*(xlim(2)-xlim(1)); + x2 = xlim(2)-0.2*(xlim(2)-xlim(1)); + + this.RefCursors(1).value = x1; + this.RefCursors(2).value = x2; + end + end + end + + methods (Access = protected) + function createUserParamList(this) + addUserParam(this, 'line_spacing', ... + 'title', 'Reference line spacing (MHz)', ... + 'editable', 'on', ... + 'default', 1); + addUserParam(this, 'line_no', ... + 'title', 'Number of reference lines', ... + 'editable', 'on', ... + 'default', 1); + addUserParam(this, 'lw', ... + 'title', 'Linewidth (MHz)', ... + 'editable', 'off'); + end + + function calcUserParams(this) + raw_lw = this.param_vals(2); + + if ~isempty(this.RefCursors) + + % Get the reference spacing from the position of cursors + xmin = min(this.RefCursors.value); + xmax = max(this.RefCursors.value); + ref_spacing = xmax - xmin; + else + + % Otherwise the reference spacing is the entire data range + ref_spacing = this.Data.x(1)-this.Data.x(end); + end + + this.lw = raw_lw*this.line_spacing*this.line_no/ref_spacing; + end + end +end + diff --git a/Fit classes/@MyQuadraticFit/MyQuadraticFit.m b/Fit classes/@MyQuadraticFit/MyQuadraticFit.m new file mode 100644 index 0000000..04b4244 --- /dev/null +++ b/Fit classes/@MyQuadraticFit/MyQuadraticFit.m @@ -0,0 +1,23 @@ +classdef MyQuadraticFit < MyFit + + methods (Access = public) + function this = MyQuadraticFit(varargin) + this@MyFit(... + 'fit_name','Quadratic',... + 'fit_function','a*x^2+b*x+c',... + 'fit_tex','$$ax^2+bx+c$$',... + 'fit_params',{'a','b','c'},... + 'fit_param_names',{'Quadratic coeff.','Linear coeff.','Offset'},... + varargin{:}); + end + end + + methods (Access = protected) + + %Overload the doFit function to do polyFit instead of nonlinear + %fitting. + function fitted_vals = doFit(~, x, y, varargin) + fitted_vals = polyfit(x, y, 2); + end + end +end \ No newline at end of file diff --git a/GUIs/GuiAgilentNa.mlapp b/GUIs/GuiAgilentNa.mlapp new file mode 100644 index 0000000..9670040 Binary files /dev/null and b/GUIs/GuiAgilentNa.mlapp differ diff --git a/GUIs/GuiBeta.fig b/GUIs/GuiBeta.fig deleted file mode 100644 index bc78599..0000000 Binary files a/GUIs/GuiBeta.fig and /dev/null differ diff --git a/GUIs/GuiBeta.m b/GUIs/GuiBeta.m deleted file mode 100644 index f57997d..0000000 --- a/GUIs/GuiBeta.m +++ /dev/null @@ -1,104 +0,0 @@ -function varargout = GuiBeta(varargin) -% GUIBETA MATLAB code for GuiBeta.fig -% GUIBETA, by itself, creates a new GUIBETA or raises the existing -% singleton*. -% -% H = GUIBETA returns the handle to a new GUIBETA or the handle to -% the existing singleton*. -% -% GUIBETA('CALLBACK',hObject,eventData,handles,...) calls the local -% function named CALLBACK in GUIBETA.M with the given input arguments. -% -% GUIBETA('Property','Value',...) creates a new GUIBETA or raises the -% existing singleton*. Starting from the left, property value pairs are -% applied to the GUI before GuiBeta_OpeningFcn gets called. An -% unrecognized property name or invalid value makes property application -% stop. All inputs are passed to GuiBeta_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 GuiBeta - -% Last Modified by GUIDE v2.5 07-Nov-2017 17:35:50 - -% Begin initialization code - DO NOT EDIT -gui_Singleton = 1; -gui_State = struct('gui_Name', mfilename, ... - 'gui_Singleton', gui_Singleton, ... - 'gui_OpeningFcn', @GuiBeta_OpeningFcn, ... - 'gui_OutputFcn', @GuiBeta_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 GuiBeta is made visible. -function GuiBeta_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 GuiBeta (see VARARGIN) - -% Choose default command line output for GuiBeta -handles.output = hObject; - - -% Update handles structure -guidata(hObject, handles); - -% UIWAIT makes GuiBeta wait for user response (see UIRESUME) -% uiwait(handles.figure1); - - -% --- Outputs from this function are returned to the command line. -function varargout = GuiBeta_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; - - - - -function v_RF_input_Callback(hObject, eventdata, handles) -% hObject handle to v_RF_input (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 v_RF_input as text -% str2double(get(hObject,'String')) returns contents of v_RF_input as a double -Vp_calibration(hObject, eventdata, handles); - -% --- Executes during object creation, after setting all properties. -function v_RF_input_CreateFcn(hObject, eventdata, handles) -% hObject handle to v_RF_input (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 AnalyzeButton. -function AnalyzeButton_Callback(hObject, eventdata, handles) -% hObject handle to AnalyzeButton (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) diff --git a/GUIs/GuiCeCryo.mlapp b/GUIs/GuiCeCryo.mlapp deleted file mode 100644 index c5cd6f1..0000000 Binary files a/GUIs/GuiCeCryo.mlapp and /dev/null differ diff --git a/GUIs/GuiColdEdgeCryo.mlapp b/GUIs/GuiColdEdgeCryo.mlapp new file mode 100644 index 0000000..d293d44 Binary files /dev/null and b/GUIs/GuiColdEdgeCryo.mlapp differ diff --git a/GUIs/GuiDaq.fig b/GUIs/GuiDaq.fig deleted file mode 100644 index a1e337a..0000000 Binary files a/GUIs/GuiDaq.fig and /dev/null differ diff --git a/GUIs/GuiDaq.m b/GUIs/GuiDaq.m deleted file mode 100644 index d364ad7..0000000 --- a/GUIs/GuiDaq.m +++ /dev/null @@ -1,249 +0,0 @@ -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 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 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 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 diff --git a/GUIs/GuiGCal.fig b/GUIs/GuiGCal.fig deleted file mode 100644 index 682988e..0000000 Binary files a/GUIs/GuiGCal.fig and /dev/null differ diff --git a/GUIs/GuiGCal.m b/GUIs/GuiGCal.m deleted file mode 100644 index e8a8965..0000000 --- a/GUIs/GuiGCal.m +++ /dev/null @@ -1,136 +0,0 @@ -function varargout = GuiGCal(varargin) -%GUIGCAL MATLAB code file for GuiGCal.fig -% GUIGCAL, by itself, creates a new GUIGCAL or raises the existing -% singleton*. -% -% H = GUIGCAL returns the handle to a new GUIGCAL or the handle to -% the existing singleton*. -% -% GUIGCAL('Property','Value',...) creates a new GUIGCAL using the -% given property value pairs. Unrecognized properties are passed via -% varargin to GuiGCal_OpeningFcn. This calling syntax produces a -% warning when there is an existing singleton*. -% -% GUIGCAL('CALLBACK') and GUIGCAL('CALLBACK',hObject,...) call the -% local function named CALLBACK in GUIGCAL.M with the given input -% arguments. -% -% *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 GuiGCal - -% Last Modified by GUIDE v2.5 06-Nov-2017 21:07:25 - -% Begin initialization code - DO NOT EDIT -gui_Singleton = 1; -gui_State = struct('gui_Name', mfilename, ... - 'gui_Singleton', gui_Singleton, ... - 'gui_OpeningFcn', @GuiGCal_OpeningFcn, ... - 'gui_OutputFcn', @GuiGCal_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 GuiGCal is made visible. -function GuiGCal_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 unrecognized PropertyName/PropertyValue pairs from the -% command line (see VARARGIN) - -% Choose default command line output for GuiGCal -handles.output = hObject; - -% Update handles structure -guidata(hObject, handles); - -% UIWAIT makes GuiGCal wait for user response (see UIRESUME) -% uiwait(handles.figure1); - - -% --- Outputs from this function are returned to the command line. -function varargout = GuiGCal_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 PEomEdit_CreateFcn(hObject, eventdata, handles) -% hObject handle to PEomEdit (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 VPiEdit_CreateFcn(hObject, eventdata, handles) -% hObject handle to VPiEdit (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 TempEdit_CreateFcn(hObject, eventdata, handles) -% hObject handle to TempEdit (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 BetaEdit_CreateFcn(hObject, eventdata, handles) -% hObject handle to BetaEdit (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 AnalyzeButton. -function AnalyzeButton_Callback(hObject, eventdata, handles) -% hObject handle to AnalyzeButton (see GCBO) -% eventdata reserved - to be defined in a future version of MATLAB -% handles structure with handles and user data (see GUIDATA) diff --git a/GUIs/GuiLakeshore.mlapp b/GUIs/GuiLakeshore.mlapp deleted file mode 100644 index 30b1bf8..0000000 Binary files a/GUIs/GuiLakeshore.mlapp and /dev/null differ diff --git a/GUIs/GuiLakeshore336.mlapp b/GUIs/GuiLakeshore336.mlapp new file mode 100644 index 0000000..95e6d1c Binary files /dev/null and b/GUIs/GuiLakeshore336.mlapp differ diff --git a/GUIs/GuiLogger.mlapp b/GUIs/GuiLogger.mlapp deleted file mode 100644 index 596f251..0000000 Binary files a/GUIs/GuiLogger.mlapp and /dev/null differ diff --git a/GUIs/GuiNa.mlapp b/GUIs/GuiNa.mlapp deleted file mode 100644 index 395fe86..0000000 Binary files a/GUIs/GuiNa.mlapp and /dev/null differ diff --git a/GUIs/GuiNewpTlb.mlapp b/GUIs/GuiNewpTlb.mlapp new file mode 100644 index 0000000..9f653aa Binary files /dev/null and b/GUIs/GuiNewpTlb.mlapp differ diff --git a/GUIs/GuiPfeifferTpg.mlapp b/GUIs/GuiPfeifferTpg.mlapp new file mode 100644 index 0000000..51513e3 Binary files /dev/null and b/GUIs/GuiPfeifferTpg.mlapp differ diff --git a/GUIs/GuiPm.mlapp b/GUIs/GuiPm.mlapp deleted file mode 100644 index bf114dd..0000000 Binary files a/GUIs/GuiPm.mlapp and /dev/null differ diff --git a/GUIs/GuiRsa.mlapp b/GUIs/GuiRsa.mlapp deleted file mode 100644 index ee91760..0000000 Binary files a/GUIs/GuiRsa.mlapp and /dev/null differ diff --git a/GUIs/GuiScope.mlapp b/GUIs/GuiScope.mlapp deleted file mode 100644 index c1dcaf7..0000000 Binary files a/GUIs/GuiScope.mlapp and /dev/null differ diff --git a/GUIs/GuiTekRsa.mlapp b/GUIs/GuiTekRsa.mlapp new file mode 100644 index 0000000..f6a9583 Binary files /dev/null and b/GUIs/GuiTekRsa.mlapp differ diff --git a/GUIs/GuiTekScope.mlapp b/GUIs/GuiTekScope.mlapp new file mode 100644 index 0000000..cb41f2c Binary files /dev/null and b/GUIs/GuiTekScope.mlapp differ diff --git a/GUIs/GuiThorlabsPm.mlapp b/GUIs/GuiThorlabsPm.mlapp new file mode 100644 index 0000000..ba96259 Binary files /dev/null and b/GUIs/GuiThorlabsPm.mlapp differ diff --git a/GUIs/GuiTlb.mlapp b/GUIs/GuiTlb.mlapp deleted file mode 100644 index a34261b..0000000 Binary files a/GUIs/GuiTlb.mlapp and /dev/null differ diff --git a/GUIs/GuiTpg.mlapp b/GUIs/GuiTpg.mlapp deleted file mode 100644 index 86dd172..0000000 Binary files a/GUIs/GuiTpg.mlapp and /dev/null differ diff --git a/GUIs/GuiZiRingdown.mlapp b/GUIs/GuiZiRingdown.mlapp index f970b05..a111177 100644 Binary files a/GUIs/GuiZiRingdown.mlapp and b/GUIs/GuiZiRingdown.mlapp differ diff --git a/GUIs/GuiZiScopeFt.mlapp b/GUIs/GuiZiScopeFt.mlapp index c9dc4ad..026843d 100644 Binary files a/GUIs/GuiZiScopeFt.mlapp and b/GUIs/GuiZiScopeFt.mlapp differ diff --git a/GUIs/aGuiTemplate.mlapp b/GUIs/aGuiTemplate.mlapp deleted file mode 100644 index f3955d1..0000000 Binary files a/GUIs/aGuiTemplate.mlapp and /dev/null differ diff --git a/Instrument classes/@MyAgilentDso/MyAgilentDso.m b/Instrument classes/@MyAgilentDso/MyAgilentDso.m new file mode 100644 index 0000000..6296165 --- /dev/null +++ b/Instrument classes/@MyAgilentDso/MyAgilentDso.m @@ -0,0 +1,182 @@ +% Class for controlling 4-channel Agilent DSO scopes. +% Tested with DSO7034A + +classdef MyAgilentDso < MyScpiInstrument & MyDataSource & MyCommCont ... + & MyGuiCont + + properties (Constant = true) + channel_no = 4 % number of channels + end + + methods (Access = public) + function this = MyAgilentDso(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + + this.Trace.name_x = 'Time'; + this.Trace.name_y = 'Voltage'; + this.Trace.unit_x = 's'; + this.Trace.unit_y = 'V'; + + connect(this); + + % 1.6e7 is the maximum trace size of DSO7034A + %(8 mln point of 2-byte integers) + this.Comm.InputBufferSize = 2e7; %byte + + createCommandList(this); + + % There is high compatibility with Tektronix scope classes + this.gui_name = 'GuiTekScope'; + end + + function readTrace(this) + this.Comm.ByteOrder = 'littleEndian'; + + % Set data format to be signed integer, reversed byte order, + % 2 bytes per measurement point, read the maximum + % available number of points + writeStrings(this, ... + ':WAVeform:BYTeorder LSBFirst', ... + ':WAVeform:FORMat WORD', ... + ':WAVeform:POINts:MODE MAX', ... + ':WAVeform:UNSigned OFF', ... + ':WAVeform:DATA?'); + + % Read the trace data + y_data = int16(binblockread(this.Comm, 'int16')); + + % Read the preamble + pre_str = queryString(this, ':WAVeform:PREamble?'); + + % Drop the end-of-the-string symbol and split + pre = str2double(split(pre_str(1:end-1), ',')); + step_x = pre(5); + step_y = pre(8); + x_zero = pre(6); + y_zero = pre(9); + + % Calculate the y values + y = double(y_data)*step_y + y_zero; + n_points = length(y); + + % Calculate the x axis + x = linspace(x_zero, x_zero + step_x*(n_points-1), n_points); + + this.Trace.x = x; + this.Trace.y = y; + + triggerNewData(this); + end + + function acquireContinuous(this) + writeString(this, ':RUN'); + end + + function acquireSingle(this) + writeString(this, ':SINGLe'); + end + + function stopAcquisition(this) + writeString(this, ':STOP'); + end + + % Emulates the physical knob turning, works with nturns=+-1 + function turnKnob(this, knob, nturns) + switch upper(knob) + case 'HORZSCALE' + + % Timebase is changed + if nturns == -1 + this.time_scale = this.time_scale*2; + elseif nturns == 1 + this.time_scale = this.time_scale/2; + else + return + end + case {'VERTSCALE1', 'VERTSCALE2'} + + % Vertical scale is changed + n_ch = sscanf(upper(knob), 'VERTSCALE%i'); + tag = sprintf('scale%i', n_ch); + if nturns==-1 + this.(tag) = this.(tag)*2; + elseif nturns==1 + this.(tag) = this.(tag)/2; + else + return + end + end + end + end + + methods (Access = protected) + function createCommandList(this) + addCommand(this, 'channel', ':WAVeform:SOURce', ... + 'format', 'CHAN%i', ... + 'info', 'Channel from which the data is transferred'); + + addCommand(this, 'time_scale', ':TIMebase:SCALe',... + 'format', '%e',... + 'info', 'Time scale (s/div)'); + + addCommand(this, 'trig_lev', ':TRIGger:LEVel', ... + 'format', '%e'); + + % trigger slope - works, but incompatible with Tektronix + addCommand(this, 'trig_slope', ':TRIGger:SLOpe', ... + 'format', '%s', ... + 'value_list', {'NEGative', 'POSitive', 'EITHer', ... + 'ALTernate'}); + + addCommand(this, 'trig_source', ':TRIGger:SOUrce', ... + 'format', '%s', ... + 'value_list', {'CHAN1', 'CHAN2', 'CHAN3', 'CHAN4',... + 'EXT','LINE'}); + + % trigger mode + addCommand(this, 'trig_mode', ':TRIGger:SWEep', ... + 'format', '%s', ... + 'value_list', {'AUTO', 'NORMal'}); + + addCommand(this, 'acq_mode', ':ACQuire:TYPE', ... + 'format', '%s', ... + 'info', ['Acquisition mode: normal(sample), ', ... + 'high resolution or average'], ... + 'value_list', {'NORMal', 'AVERage', 'HRESolution', ... + 'PEAK'}); + + % Parametric commands + for i = 1:this.channel_no + i_str = num2str(i); + + addCommand(this, ... + ['cpl' i_str], [':CHANnel' i_str ':COUPling'], ... + 'format', '%s', ... + 'info', 'Channel coupling: AC, DC or GND', ... + 'value_list', {'AC','DC','GND'}); + + addCommand(this, ... + ['imp' i_str], [':CHANnel' i_str ':IMPedance'], ... + 'format', '%s', ... + 'info', 'Channel impedance: 1 MOhm or 50 Ohm', ... + 'value_list', {'FIFty','FIF','ONEMeg','ONEM'}); + + addCommand(this,... + ['offset' i_str], [':CHANnel' i_str ':OFFSet'], ... + 'format', '%e', ... + 'info', '(V)'); + + addCommand(this,... + ['scale' i_str], [':CHANnel' i_str ':SCAle'], ... + 'format', '%e', ... + 'info', 'Channel y scale (V/div)'); + + addCommand(this,... + ['enable' i_str], [':CHANnel' i_str ':DISPlay'], ... + 'format', '%b',... + 'info', 'Channel enabled'); + end + end + end +end \ No newline at end of file diff --git a/Instrument classes/@MyAgilentNa/MyAgilentNa.m b/Instrument classes/@MyAgilentNa/MyAgilentNa.m new file mode 100644 index 0000000..ff86ea7 --- /dev/null +++ b/Instrument classes/@MyAgilentNa/MyAgilentNa.m @@ -0,0 +1,190 @@ +% The class for communication with Agilent E5061B Network Analyzer + +classdef MyAgilentNa < MyScpiInstrument & MyCommCont & MyDataSource ... + & MyGuiCont + + properties(Access = public, SetObservable) + Trace1 + Trace2 + + transf_n = 1 % trace that triggers NewData event + end + + properties (SetAccess = protected, GetAccess = public, SetObservable) + + % Manipulating active traces seems unavoidable for data format + % selection. -1 stands for unknown. + active_trace = -1 + + % data formats for the traces 1-2, options: + % 'PHAS', 'SLIN', 'SLOG', 'SCOM', 'SMIT', 'SADM', 'MLOG', 'MLIN', + %'PLIN', 'PLOG', 'POL' + form1 = 'MLOG' + form2 = 'PHAS' + end + + methods + function this = MyAgilentNa(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + + this.Trace1 = MyTrace(); + this.Trace2 = MyTrace(); + this.Trace1.unit_x = 'Hz'; + this.Trace1.name_x = 'Frequency'; + this.Trace2.unit_x = 'Hz'; + this.Trace2.name_x = 'Frequency'; + + connect(this); + createCommandList(this); + end + + % Generate a new data event with header collection suppressed + function transferTrace(this, n_trace) + trace_tag = sprintf('Trace%i', n_trace); + + % Assign either Trace1 or 2 to Trace while keeping the metadata + this.(trace_tag).UserMetadata = copy(this.Trace.UserMetadata); + this.Trace = copy(this.(trace_tag)); + + triggerNewData(this, 'new_header', false); + end + + function readTrace(this, n_trace) + writeActiveTrace(this, n_trace); + + freq_str = strsplit(queryString(this,':SENS1:FREQ:DATA?'),','); + data_str = strsplit(queryString(this,':CALC1:DATA:FDAT?'),','); + + data_x = str2double(freq_str); + + % In the returned string there is in general 2 values for each + % frequency point. In the Smith data format this can be used to + % transfer magnitude and phase of the signal in one trace. With + % MLOG, MLIN and PHAS format settings every 2-nd element should + % be 0 + data_y1 = str2double(data_str(1:2:end)); + + % set the Trace properties + trace_tag = sprintf('Trace%i', n_trace); + this.(trace_tag).x = data_x; + this.(trace_tag).y = data_y1; + + if this.transf_n == n_trace + this.Trace = copy(this.(trace_tag)); + triggerNewData(this); + end + end + + function writeActiveTrace(this, n_trace) + writeString(this, sprintf(':CALC1:PAR%i:SEL', n_trace)); + this.active_trace = n_trace; + end + + function writeTraceFormat(this, n_trace, fmt) + writeActiveTrace(this, n_trace); + + n_str = num2str(n_trace); + + this.(['form', n_str]) = fmt; + writeString(this, sprintf(':CALC1:FORM %s', fmt)); + end + + function singleSweep(this) + + % Set the triger source to remote control + this.trig_source = 'BUS'; + this.cont_trig = true; + + % Start a sweep cycle + writeString(this, ':TRIG:SING'); + + % Wait for the sweep to finish (for the query to return 1) + queryString(this, '*OPC?'); + end + + function startContSweep(this) + + % Set the triger source to internal + this.trig_source = 'INT'; + this.cont_trig = true; + end + + function abortSweep(this) + this.trig_source = 'BUS'; + writeString(this, ':ABOR'); + end + end + + methods (Access = protected) + function createCommandList(this) + addCommand(this, 'cent_freq', ':SENS1:FREQ:CENT', ... + 'format', '%e', ... + 'info', '(Hz)'); + + addCommand(this, 'start_freq', ':SENS1:FREQ:START', ... + 'format', '%e',... + 'info', '(Hz)'); + + addCommand(this, 'stop_freq', ':SENS1:FREQ:STOP', ... + 'format', '%e',... + 'info', '(Hz)'); + + addCommand(this, 'span', ':SENS1:FREQ:SPAN', ... + 'format', '%e',... + 'info', '(Hz)'); + + addCommand(this, 'ifbw', ':SENS1:BAND', ... + 'format', '%e', ... + 'info', 'IF bandwidth (Hz)'); + + addCommand(this, 'point_no', ':SENS1:SWE:POIN', ... + 'format', '%i'); + + addCommand(this, 'average_no', ':SENS1:AVER:COUN', ... + 'format', '%i'); + + addCommand(this, 'trace_no', ':CALC1:PAR:COUN', ... + 'format', '%i',... + 'info', 'Number of traces', ... + 'value_list', {1, 2}); + + addCommand(this, 'sweep_type', ':SENS1:SWE:TYPE', ... + 'format', '%s',... + 'info', 'Linear or log sweep', ... + 'value_list', {'LIN', 'LOG'}); + + addCommand(this, 'enable_out', ':OUTP', ... + 'format', '%b',... + 'info', 'output signal on/off'); + + addCommand(this, 'power', ':SOUR:POW:LEV:IMM:AMPL', ... + 'format', '%e',... + 'info', 'Probe power (dB)'); + + addCommand(this, 'disp_type', ':DISP:WIND1:SPL',... + 'format', '%s',... + 'info', 'Window arrangement', ... + 'default', 'D1'); + + addCommand(this, 'cont_trig', ':INIT1:CONT', ... + 'format', '%b'); + + addCommand(this, 'trig_source', ':TRIG:SOUR', ... + 'format', '%s', ... + 'default', 'BUS') + + % Parametric commands for traces, i can be extended to 4 + for i = 1:2 + + % measurement parameters for the traces 1-2, e.g. 'S21' + i_str = num2str(i); + addCommand(this,... + ['meas_par',i_str], [':CALC1:PAR',i_str,':DEF'], ... + 'format', '%s',... + 'info', 'Measurement parameter', ... + 'default', 'S21'); + end + end + end +end diff --git a/Instrument classes/@MyColdEdgeCryo/MyColdEdgeCryo.m b/Instrument classes/@MyColdEdgeCryo/MyColdEdgeCryo.m new file mode 100644 index 0000000..e3edace --- /dev/null +++ b/Instrument classes/@MyColdEdgeCryo/MyColdEdgeCryo.m @@ -0,0 +1,194 @@ +% Class for controlling the auto manifold of ColdEdge stinger cryostat. +% The manifold is managed by an Arduino board that communicates with +% computer via serial protocol. + +classdef MyColdEdgeCryo < MyScpiInstrument & MyCommCont & MyGuiCont + + properties (GetAccess = public, SetAccess = protected, ... + SetObservable = true) + + % If a long term operation (e.g. starting a cooldown or pumping the + % capillary) is in progress + operation_in_progress + + % Time for the recirculator to pump down helium from the capillary + % before closing it off + tr = 900 + end + + properties (Access = protected) + Timer + end + + methods (Access = public) + function this = MyColdEdgeCryo(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + + this.Timer = timer(); + + % Buffer size of 64 kByte should be way an overkill. + this.Device.InputBufferSize = 2^16; + this.Device.OutputBufferSize = 2^16; + + connect(this); + createCommandList(this); + end + + function delete(this) + try + stop(this.Timer); + catch ME + warning(ME.message); + end + + try + delete(this.Timer); + catch ME + warning(ME.message); + end + end + + % Abort the current operation + function abort(this) + stop(this.Timer); + this.operation_in_progress = false; + + this.auto_sync = false; + + this.valve1 = false; + this.valve2 = false; + this.valve3 = false; + this.valve4 = false; + this.valve5 = false; + this.valve7 = false; + this.recirc = false; + this.cryocooler = false; + + % Sync once + sync(this); + + % Return to the hedged mode + this.auto_sync = true; + end + + function startCooldown(this) + assert(~this.operation_in_progress, ['Cannot initiate' ... + ' cooldown stop. Another operation is in progress.']) + + this.auto_sync = false; + + this.valve2 = false; + this.valve3 = false; + this.valve5 = false; + this.valve7 = false; + + % Open the recirculator path, starting from the return + this.valve4 = true; + this.valve1 = true; + + % Start the compressors + this.recirc = true; + this.cryocooler = true; + + % Sync once + sync(this); + + % Return to the hedged mode + this.auto_sync = true; + end + + function stopCooldown(this) + function switchRecirculatorOff(~, ~) + this.auto_sync = false; + + % Close the recirculator path, starting from the supply + this.valve1 = false; + this.valve4 = false; + + % Switch off the recirculator after all the valves are + % closed + this.recirc = false; + + sync(this); + this.auto_sync = true; + + this.operation_in_progress = false; + end + + assert(~this.operation_in_progress, ['Cannot initiate' ... + ' cooldown stop. Another operation is in progress.']) + + this.auto_sync = false; + + % Switch off the cryocooler, close the recirculator supply + % valve (1). + this.valve1 = false; + this.cryocooler = false; + + sync(this); + this.auto_sync = true; + + % Wait for the helium to be pumped out of the capillary by the + % recirculator and then switch the recirculator off + this.Timer.ExecutionMode = 'singleShot'; + this.Timer.StartDelay = this.tr; + this.Timer.TimerFcn = @switchRecirculatorOff; + + start(this.Timer); + this.operation_in_progress = true; + end + + % Overload writeSettings method of MyInstrument + function writeSettings(this) + disp(['The settings of ' class(this) ' cannot be loaded ' ... + 'for safety considerations. Please configure the ' ... + 'instrument manually']) + return + end + end + + methods (Access = protected) + function createCommandList(this) + + % Valve states + for i = 1:7 + if i == 6 + continue % There is no valve 6 + end + + tag = sprintf('valve%i',i); + cmd = sprintf(':VALVE%i',i); + + addCommand(this, tag, cmd, ... + 'format', '%b', ... + 'info', 'Valve open(true)/clsed(false)'); + end + + addCommand(this, 'recirc', ':REC', ... + 'format', '%b', ... + 'info', 'Recirculator on/off'); + + addCommand(this, 'cryocooler', ':COOL', ... + 'format', '%b', ... + 'info', 'Cryocooler on/off'); + + addCommand(this, 'press', ':PRES', ... + 'format', '%e', ... + 'access', 'r', ... + 'info', 'Supply pressure (PSI)'); + end + end + + methods + function val = get.operation_in_progress(this) + try + val = strcmpi(this.Timer.Running, 'on'); + catch ME + warning(ME.message); + val = false; + end + end + end +end + diff --git a/@MyHfWs/MyHfWs.m b/Instrument classes/@MyHfWs/MyHfWs.m similarity index 73% rename from @MyHfWs/MyHfWs.m rename to Instrument classes/@MyHfWs/MyHfWs.m index 1795db3..b1776de 100644 --- a/@MyHfWs/MyHfWs.m +++ b/Instrument classes/@MyHfWs/MyHfWs.m @@ -1,265 +1,276 @@ % Class for controlling HighFinesse wavelengthmeter, tested with WS6-200 classdef MyHfWs < handle properties (Access=public) - name = '' - + % Files containg the functions for communication with % wavelengthmeter, dll and header dllname = 'wlmData.dll' headername = 'wlmData.hml' % Timeout for trying to run the wavelengthmeter server app run_server_timeout = 60 % seconds end - properties (GetAccess=public, SetAccess=protected) + properties (GetAccess = public, SetAccess = protected) + % These properties use get methods to read value every time they % are addressed wavelength = 0 % Wavelength in nm frequency = 0 % Frequency in THz idn_str = '' end - properties (Dependent=true) + properties (Access = protected) + Metadata = MyMetadata.empty() + end + + properties (Dependent = true) libname end - methods (Access=public) - %% Constructor and destructor - - % Constructor can accept dummy 'interface' and 'address' arguments, - % they will be stored in P.unmatched_nv + methods (Access = public) function this = MyHfWs(varargin) - P=MyClassParser(this); - P.PartialMatching=false; + P = MyClassParser(this); processInputs(P, this, varargin{:}); % Load dll library and its header loadWlmLib(this); % Check if the wavelengthmeter software is running if ~isServerRunning(this) disp(['Wavelength meter server apptication ', ... 'is not running. Attempting to start.']) runServer(this); startMeas(this); end end - %% Communication methods - function loadWlmLib(this) dll_path = which(this.dllname); if isempty(dll_path) error([this.dllname,' is not found. This library ',... 'needs to be present on Matlab path.']) end header_path = which(this.headername); if isempty(header_path) error([this.headername,' is not found. This header ',... 'file needs to be present on Matlab path.']) end if ~libisloaded(this.libname) fprintf('Loading %s library with %s header\n', ... this.dllname, this.headername); loadlibrary(dll_path, header_path); end end - function ret_val=readWavelength(this, varargin) - p=inputParser(); + function ret_val = readWavelength(this, varargin) + p = inputParser(); addParameter(p, 'n_ch', 1, @(x)assert( (mod(x,1)==0) && ... (x>=1) && (x<=8), ... 'Channel number must be integer between 1 and 8.')) parse(p, varargin{:}) - n_ch=p.Results.n_ch; + n_ch = p.Results.n_ch; % read out the measured wavelength - ret_val = calllib(this.libname,'GetWavelengthNum',n_ch,0); + ret_val = calllib(this.libname, 'GetWavelengthNum', n_ch, 0); end - function ret_val=readFrequency(this, varargin) - p=inputParser(); + function ret_val = readFrequency(this, varargin) + p = inputParser(); addParameter(p, 'n_ch', 1, @(x)assert( (mod(x,1)==0) && ... (x>=1) && (x<=8), ... 'Channel number must be integer between 1 and 8.')) parse(p, varargin{:}) - n_ch=p.Results.n_ch; + n_ch = p.Results.n_ch; % read out the measured wavelength - ret_val = calllib(this.libname,'GetFrequencyNum',n_ch,0); + ret_val = calllib(this.libname, 'GetFrequencyNum', n_ch, 0); end % Run the wavelengthmeter control program function runServer(this) - T=timer('Period',0.5,... + T = timer('Period',0.5,... 'ExecutionMode','fixedDelay',... 'TasksToExecute', ceil(this.run_server_timeout/0.5)); - T.TimerFcn=@(x,y)this.runServerTimerCallback(x,y); + + T.TimerFcn = @(x,y)this.runServerTimerCallback(x,y); % cCtrlWLMShow: 1 - displays the window of wlm server % application if it was hidden and starts the server if it is % not running if calllib(this.libname,'ControlWLM',1,0,0) == 1 start(T); wait(T); else warning('Wavelengthmeter server app could not be started.') end % Clean up delete(T); end - function bool=isServerRunning(this) - bool=calllib(this.libname,'Instantiate',0,0,0,0); + function bool = isServerRunning(this) + bool = calllib(this.libname, 'Instantiate', 0, 0, 0, 0); end % Start continuous measurement on the server - function stat=startMeas(this) - stat=calllib(this.libname,'Operation',hex2dec('0002')); + function stat = startMeas(this) + stat = calllib(this.libname, 'Operation', hex2dec('0002')); end % Stop measurement on the server - function stat=stopMeas(this) - stat=calllib(this.libname,'Operation',hex2dec('0000')); + function stat = stopMeas(this) + stat = calllib(this.libname, 'Operation', hex2dec('0000')); end % Return the identification string of the device function str = idn(this) + % Get the device information % Wavelengthmeter type can be 5 to 8 - type = calllib(this.libname,'GetWLMVersion',0); + type = calllib(this.libname, 'GetWLMVersion', 0); + % Version number - vers = calllib(this.libname,'GetWLMVersion',1); + vers = calllib(this.libname, 'GetWLMVersion', 1); + % Software version - soft_vers = calllib(this.libname,'GetWLMVersion',2); - str=['WS',num2str(type),', Version ',num2str(vers),... + soft_vers = calllib(this.libname, 'GetWLMVersion', 2); + str = ['WS' num2str(type) ', Version ' num2str(vers) ... ', Software version ' num2str(soft_vers)]; + + this.idn_str = str; end - %% Measurement headers - - function Hdr=readHeader(this) - Hdr=MyMetadata(); - % Generate valid field name from instrument name if present and - % class name otherwise - if ~isempty(this.name) - field_name=genvarname(this.name); - else - field_name=class(this); + function Mdt = readSettings(this) + if isempty(this.Metadata) + createMetadata(this); end - addField(Hdr, field_name); - % Add identification string as parameter - addParam(Hdr, field_name, 'idn', this.idn_str); % Calculate wavelength in vacuum from frequency to avoid % vaccum/air ambiguity - f=this.frequency; - c=299792458; % (m/s), speed of light + f = this.frequency; + + c = 299792458; % (m/s), speed of light + % Print with 9 digits of precision which corresponds to % kHz-scale resolution in the visible range. % This should be safely beyond the instrument resolution. - wl=sprintf('%.9f',(c/(f*1e12))*1e9); + f_str = sprintf('%.9f', f); + wl_str = sprintf('%.9f', (c/(f*1e12))*1e9); + if f<=0 + % The last measurement was not ok, so get the error code % instead of the value - wl=readErrorFromCode(this, f); + f_str = readErrorFromCode(this, f); + wl_str = f_str; end - addParam(Hdr, field_name, 'wavelength', wl, ... - 'comment', '(nm), in vacuum'); + + this.Metadata.ParamList.idn = this.idn_str; + this.Metadata.ParamList.frequency = f_str; + this.Metadata.ParamList.wavelength = wl_str; + + Mdt = copy(this.Metadata); end - %% Auxiliary functions - %Convert error codes returned by readWavelength and %readFrequency commands to message function str=readErrorFromCode(~, code) if code>0 str='Measurement ok'; return end switch code case 0 str='No value measured'; case -1 str='The wavelengthmeter has not detected any signal'; case -2 str=['The wavelengthmeter has not detected a ',... 'calculatable signal']; case -3 str='Underexposed'; case -4 str='Overexposed'; case -5 str=['Server application is not running or ', ... 'wavelength meter is not active or available']; case -6 str=['The caller function is not avaliable for ',... 'this version of wavelengthmeter']; case -8 str='ErrNoPulse'; otherwise str='Unknown error'; end end end - %% Auxiliary pivate methods - - methods (Access=private) - + methods (Access = protected) function runServerTimerCallback(this, Timer, ~) if ~isServerRunning(this) + % Check if the timer has reached its limit if Timer.TasksExecuted>=Timer.TasksToExecute warning(['Timeout for running the server ',... 'app (%is) is exceeded.'], this.run_server_timeout) end else stop(Timer); end end + + function createMetadata(this) + this.Metadata = MyMetadata(); + + % Add identification string as parameter + addParam(this.Metadata, 'idn', this.idn_str); + + addParam(this.Metadata, 'frequency', 0, ... + 'comment', '(THz)'); + + addParam(this.Metadata, 'wavelength', 0, ... + 'comment', '(nm), in vacuum'); + end end - - %% Set and get methods + methods - function set.dllname(this, val) assert(ischar(val) && isvector(val), ['''dllname'' must be '... 'a character vector.']) [~,~,ext]=fileparts(this.dllname); assert(strcmpi(ext,'dll'), ['''dllname'' must be a ',... 'dynamic-link library and have extension .dll']) this.dllname=val; end function set.headername(this, val) assert(ischar(val) && isvector(val), ['''headername'' must be '... 'a character vector.']) this.headername=val; end function nm=get.libname(this) + % dll name without extension [~,nm,~]=fileparts(this.dllname); end function wl=get.wavelength(this) wl=readWavelength(this); end function wl=get.frequency(this) wl=readFrequency(this); end end end diff --git a/Instrument classes/@MyLakeshore336/MyLakeshore336.m b/Instrument classes/@MyLakeshore336/MyLakeshore336.m new file mode 100644 index 0000000..ee6487b --- /dev/null +++ b/Instrument classes/@MyLakeshore336/MyLakeshore336.m @@ -0,0 +1,134 @@ +% Class communication with Lakeshore Model 336 temperature controller. + +classdef MyLakeshore336 < MyScpiInstrument & MyCommCont & MyGuiCont + properties (GetAccess = public, ... + SetAccess = {?MyClassParser, ?MyLakeshore336}, SetObservable) + + % Temperature unit, K or C. This variable should be set + % before the command list is created. + temp_unit = 'K' + end + + properties (Constant = true) + + % Correspondence lists for numeric codes in parameter values. + % Indexing starts from 0. These lists are for information only. + + % Values for the output mode, out_mode_n(1). + out_mode_list = {'Off', 'Closed loop PID', 'Zone', ... + 'Open loop', 'Monitor out', 'Warmup supply'}; + + % Control input, out_mode_n(2). + control_input_list = {'None', 'A', 'B', 'C', 'D'}; + + % Heater ranges + heater12_range_list = {'Off','Low','Medium','High'}; + heater34_range_list = {'Off','On'}; + end + + methods (Access = public) + function this = MyLakeshore336(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + + connect(this); + createCommandList(this); + end + + % Create temperature logger + function Lg = createLogger(this, varargin) + function temp = readTemperature() + sync(this); + temp = [this.temp_a,this.temp_b,this.temp_c,this.temp_d]; + end + + Lg = MyLogger(varargin{:}, 'MeasFcn', @readTemperature); + + % Make column headers + inp_ch = {'A', 'B', 'C', 'D'}; + headers = cell(1, 4); + for i = 1:length(inp_ch) + sens_name = sprintf('sens_name_%s', lower(inp_ch{i})); + headers{i} = sprintf('T ch %s %s (%s)', inp_ch{i}, ... + this.(sens_name), this.temp_unit); + end + + if isempty(Lg.Record.data_headers) + Lg.Record.data_headers = headers; + end + end + end + + methods (Access = protected) + function createCommandList(this) + + % Commands for the input channels + inp_ch = {'A', 'B', 'C', 'D'}; + for i = 1:4 + nch = inp_ch{i}; + + addCommand(this, ['sens_name_' lower(nch)], 'INNAME', ... + 'read_ending', ['? ' nch], ... + 'write_ending', [' ' nch ',%s'], ... + 'info', ['Sensor name channel ' nch]); + + info = sprintf('Reading channel %s (%s)', nch, ... + this.temp_unit); + + addCommand(this, ['temp_' lower(nch)], ... + [this.temp_unit 'RDG'], ... + 'format', '%e', ... + 'access', 'r', ... + 'read_ending', ['? ' nch], ... + 'info', info); + end + + % Commands for the output channels + for i = 1:4 + nch = num2str(i); + + addCommand(this, ['setp_' nch], 'SETP', ... + 'read_ending', ['? ' nch], ... + 'write_ending', [' ' nch ',%e'], ... + 'info', ['Output ' nch ' PID setpoint in ' ... + 'preferred units of the sensor']); + + addCommand(this, ['out_mode_' nch], 'OUTMODE', ... + 'read_ending', ['? ' nch], ... + 'write_ending', [' ' nch ',%i,%i,%i'], ... + 'info', ['Output ' nch ' settings: ' ... + '[mode, cntl_input, powerup_enable]'], ... + 'default', [0, 0, 0]); + + if i==1 || i==2 + + % Outputs 1 and 2 have finer range control than than 3 + % and 4 + addCommand(this, ['range_' nch], 'RANGE', ... + 'read_ending', ['? ' nch], ... + 'write_ending', [' ' nch ',%i'], ... + 'info', ['Output ' nch ' range ' ... + '0/1/2/3 -> off/low/medium/high'], ... + 'value_list', {0, 1, 2, 3}); + else + addCommand(this, ['range_' nch], 'RANGE', ... + 'read_ending', ['? ' nch], ... + 'write_ending', [' ' nch ',%i'], ... + 'info', ['Output ' nch ' range ' ... + '0/1 -> off/on'], ... + 'value_list', {0, 1}); + end + end + end + end + + methods + function set.temp_unit(this, val) + assert(strcmpi(val,'K') || strcmpi(val,'C'), ... + 'Temperature unit must be K or C.') + + this.temp_unit = upper(val); + end + end +end + diff --git a/@MyTlb6300/MyTlb6300.m b/Instrument classes/@MyNewpTlb6300/MyNewpTlb6300.m similarity index 59% rename from @MyTlb6300/MyTlb6300.m rename to Instrument classes/@MyNewpTlb6300/MyNewpTlb6300.m index 44ac428..def90e9 100644 --- a/@MyTlb6300/MyTlb6300.m +++ b/Instrument classes/@MyNewpTlb6300/MyNewpTlb6300.m @@ -1,95 +1,102 @@ % Class for communication with NewFocus TLB6300 tunable laser controllers -classdef MyTlb6300 < MyScpiInstrument +classdef MyNewpTlb6300 < MyScpiInstrument & MyCommCont & MyGuiCont - methods (Access=public) + methods (Access = public) + function this = MyNewpTlb6300(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + + connect(this); + createCommandList(this); + + this.gui_name = 'GuiNewpTlb'; + end + % Need to overwrite the standard query function as % TLB6300 does not seem to support concatenation of commands % in queries - function res_list=queryCommand(this, varargin) - if ~isempty(varargin) - % Send queries to the device one by one - n_cmd = length(varargin); - res_list = cell(n_cmd,1); - for i=1:n_cmd - cmd=varargin{i}; - res_list{i}=query(this.Device, cmd); - end - else - res_list={}; + % Query commands and return resut as cell array of strings + function res_list = queryStrings(this, varargin) + + % Send commands to device one by one + ncmd = length(varargin); + res_list = cell(1,ncmd); + + for i = 1:ncmd + cmd_str = varargin{i}; + res_list{i} = queryString(this, cmd_str); end end end - %% Protected functions - methods (Access=protected) + + methods (Access = protected) function createCommandList(this) + % Output wavelength, nm addCommand(this, 'wavelength',':SENS:WAVE',... - 'access','r','default',780,'fmt_spec','%e',... + 'access','r','default',780,'format','%e',... 'info','Output wavelength (nm)'); % Diode current, mA addCommand(this, 'current',':SENS:CURR:DIOD',... - 'access','r','default',1,'fmt_spec','%e',... + 'access','r','default',1,'format','%e',... 'info','Diode current (mA)'); % Diode temperature, C addCommand(this, 'temp_diode',':SENS:TEMP:LEV:DIOD',... - 'access','r','default',10,'fmt_spec','%e',... + 'access','r','default',10,'format','%e',... 'info','Diode temperature (C)'); % Output power, mW addCommand(this, 'power',':SENS:POW:LEV:FRON',... - 'access','r','default',1,'fmt_spec','%e',... + 'access','r','default',1,'format','%e',... 'info','Output power (mW)'); % Wavelength setpoint, nm addCommand(this, 'wavelength_sp',':WAVE',... - 'default',780,'fmt_spec','%e',... + 'default',780,'format','%e',... 'info','Wavelength setpoint (nm)'); % Constant power mode on/off addCommand(this, 'const_power',':CPOW',... - 'access','w','default',true,'fmt_spec','%b',... + 'access','w','default',true,'format','%b',... 'info','Constant power mode on/off'); % Power setpoint, mW addCommand(this, 'power_sp',':POW',... - 'access','w','default',10,'fmt_spec','%e',... + 'access','w','default',10,'format','%e',... 'info','Power setpoint (mW)'); % Current setpoint, mW addCommand(this, 'current_sp',':CURR',... - 'default',100,'fmt_spec','%e',... + 'default',100,'format','%e',... 'info','Current setpoint (mA)'); % Control mode local/remote addCommand(this, 'control_mode',':SYST:MCON',... 'access','w','val_list',{'EXT','INT'},... - 'default','LOC','fmt_spec','%s',... + 'default','LOC','format','%s',... 'info','Control local(EXT)/remote(INT)'); % Output on/off addCommand(this, 'enable_output',':OUTP',... - 'default',false,'fmt_spec','%b',... + 'default',false,'format','%b',... 'info','on/off'); % Wavelength track is not fully remotely controllable with % TLB6300 end - end - %% Public functions including callbacks - methods (Access=public) - + methods (Access = public) function setMaxOutPower(this) + % Depending on if the laser in the constat power or current % mode, set value to max - openDevice(this); if this.const_power + % Actual power is clipped to max practical value - writeCommand(this, ':POW 99'); + writeString(this, ':POW 99'); else + % Maximum current according to specs is 152 mA - writeCommand(this, ':CURR 150'); + writeString(this, ':CURR 150'); end - closeDevice(this); end - end end diff --git a/Instrument classes/@MyNewpTlb6700/MyNewpTlb6700.m b/Instrument classes/@MyNewpTlb6700/MyNewpTlb6700.m new file mode 100644 index 0000000..d7f6441 --- /dev/null +++ b/Instrument classes/@MyNewpTlb6700/MyNewpTlb6700.m @@ -0,0 +1,232 @@ +% Class for communication with NewFocus TLB6700 tunable laser controllers +% Needs UsbDllWrap.dll from Newport USB driver on Matlab path +% +% Start instrument as MyTlb6700('address','USBaddr'), where USBaddr +% is indicated in the instrument menu. Example: MyTlb6700('address', '1'). +% +% This class uses MyNewpUsbComm, an instance of which needs to be shared +% between multiple devices, as the Newport driver, apparently, cannot +% handle concurrent calls. + +classdef MyNewpTlb6700 < MyScpiInstrument & MyGuiCont + + properties (GetAccess = public, ... + SetAccess = {?MyClassParser, ?MyTlb6700}) + + % Interface field is not used in this instrument, but keep it + % for the sake of information + interface = 'usb' + address = '' + end + + properties (GetAccess = public, SetAccess = protected) + + % Instance of Newport.USBComm.USB used for communication. + % Must be shared between the devices + UsbComm + end + + methods (Access = public) + function this = MyNewpTlb6700(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + + % Convert address to number + this.address = str2double(this.address); + + % Get the unique instance of control class for Newport driver + this.UsbComm = MyNewpUsbComm.instance(); + + createCommandList(this); + + this.gui_name = 'GuiNewpTlb'; + end + end + + methods (Access = protected) + function createCommandList(this) + + % Commands for this class do not start from ':', as the + % protocol does not fully comply with SCPI standard + + addCommand(this, 'wavelength', 'SENSe:WAVElength', ... + 'format', '%e', ... + 'info', 'Output wavelength (nm)', ... + 'access', 'r'); + + addCommand(this, 'current', 'SENSe:CURRent:DIODe', ... + 'format', '%e', ... + 'info', 'Diode current (mA)', ... + 'access', 'r'); + + addCommand(this, 'temp_diode', 'SENSe:TEMPerature:DIODe', ... + 'format', '%e', ... + 'info', 'Diode temperature (C)', ... + 'access', 'r'); + + addCommand(this, 'power', 'SENSe:POWer:DIODe', ... + 'format', '%e', ... + 'info', 'Output power (mW)', ... + 'access', 'r'); + + addCommand(this, 'wavelength_sp', 'SOURce:WAVElength', ... + 'format', '%e', ... + 'info', 'Wavelength setpoint (nm)'); + + addCommand(this, 'const_power', 'SOURce:CPOWer', ... + 'format', '%b', ... + 'info', 'Constant power mode on/off'); + + addCommand(this, 'power_sp', 'SOURce:POWer:DIODe', ... + 'format', '%e', ... + 'info', 'Power setpoint (mW)'); + + addCommand(this, 'current_sp', 'SOURce:CURRent:DIODe', ... + 'format', '%e', ... + 'info', 'Current setpoint (mA)'); + + % Control mode local/remote + addCommand(this, 'control_mode', 'SYSTem:MCONtrol', ... + 'format', '%s',... + 'info', 'Control local/remote', ... + 'value_list', {'LOC','REM'}); + + % Output on/off + addCommand(this, 'enable_output', 'OUTPut:STATe', ... + 'format', '%b', ... + 'info', 'on/off'); + + % Wavelength track on/off + addCommand(this, 'wavelength_track', 'OUTPut:TRACk', ... + 'format', '%b', ... + 'info', 'on/off'); + + % Wavelength scan related commands + % Scan start wavelength (nm) + addCommand(this, 'scan_start_wl', 'SOURce:WAVE:START', ... + 'format', '%e', ... + 'info', '(nm)'); + + % Scan stop wavelength (nm) + addCommand(this, 'scan_stop_wl', 'SOURce:WAVE:STOP', ... + 'format', '%e', ... + 'info', '(nm)'); + + % Scan speed (nm/s) + addCommand(this, 'scan_speed', 'SOURce:WAVE:SLEW:FORWard', ... + 'format', '%e', ... + 'info', '(nm/s)'); + + % Maximum scan speed (nm/s) + addCommand(this, 'scan_speed_max', 'SOURce:WAVE:MAXVEL', ... + 'format', '%e', ... + 'info', '(nm/s)', ... + 'access', 'r'); + end + end + + methods (Access = public) + function openComm(this) + + % Opening a single device is not supported by Newport Usb + % Driver, so open all the devices of the given type + OpenDevices(this.UsbComm.Usb, hex2num('100A')); + end + + % Query textual command + function result = queryString(this, str) + try + result = query(this.UsbComm, this.address, str); + catch ME + try + % Attempt re-opening communication + openComm(this); + result = query(this.UsbComm, this.address, str); + catch + rethrow(ME); + end + end + end + + % Redefine queryStrings of MyScpiInstrument + function res_list = queryStrings(this, varargin) + if ~isempty(varargin) + n_cmd = length(varargin); + res_list = cell(n_cmd,1); + + % Query commands one by one as sending one query seems + % to sometimes give errors if the string is very long + for i = 1:n_cmd + cmd = [varargin{i},';']; + res_list{i} = queryString(this, cmd); + end + else + res_list = {}; + end + end + + % Writing is done by sending a command and querying its status. + % Still, redefine writeStrings of MyScpiInstrument for consistency + % and clarity. + function stat = writeString(this, str) + stat = queryString(this, str); + end + + function stat = writeStrings(this, varargin) + stat = queryStrings(this, varargin{:}); + end + + %% Laser power and scan control functions + + function stat = setMaxOutPower(this) + + % Depending on if the laser in the constat power or current + % mode, set value to max + if this.const_power + stat = queryString(this, 'SOURce:POWer:DIODe MAX;'); + else + stat = queryString(this, 'SOURce:CURRent:DIODe MAX;'); + end + end + + % Returns minimum and maximum wavelengths of the laser. There does + % not seem to be a more direct way of doing this with TLB6700 + % other than setting and then reading the min/max values. + function [wl_min, wl_max] = readMinMaxWavelength(this) + tmp = this.scan_start_wl; + + % Read min wavelength of the laser + writeStrings(this, 'SOURce:WAVE:START MIN'); + resp = queryStrings(this, 'SOURce:WAVE:START?'); + wl_min = str2double(resp{1}); + + % Read max wavelength of the laser + writeStrings(this, 'SOURce:WAVE:START MAX'); + resp = queryStrings(this, 'SOURce:WAVE:START?'); + wl_max = str2double(resp{1}); + + % Return scan start to its original value + this.scan_start_wl = tmp; + end + + function configSingleScan(this) + + % Configure: + % Do not switch the laser off during the backward scan, + % Perform a signle scan, + % Return at maximum speed + writeStrings(this,'SOURce:WAVE:SCANCFG 0', ... + 'SOURce:WAVE:DESSCANS 1', ... + 'SOURce:WAVE:SLEW:RETurn MAX'); + end + + function startScan(this) + writeStrings(this, 'OUTPut:SCAN:START'); + end + + function stopScan(this) + writeStrings(this, 'OUTPut:SCAN:STOP'); + end + end +end + diff --git a/Instrument classes/@MyPfeifferTpg/MyPfeifferTpg.m b/Instrument classes/@MyPfeifferTpg/MyPfeifferTpg.m new file mode 100644 index 0000000..744ad51 --- /dev/null +++ b/Instrument classes/@MyPfeifferTpg/MyPfeifferTpg.m @@ -0,0 +1,183 @@ +% Class for communication with Pfeiffer TPG single and dual pressure gauge +% controllers. +% Use 'serial' communication objects instead of 'visa' with this instrument +% Tested with TPG 262 and 362. + +classdef MyPfeifferTpg < MyInstrument & MyCommCont & MyGuiCont + + properties (Constant, Access = protected) + + % Named constants for communication + ETX = char(3); % end of text + CR = char(13); % carriage return \r + LF = char(10); %#ok line feed \n + ENQ = char(5); % enquiry + ACK = char(6); % acknowledge + NAK = char(21); % negative acknowledge + end + + properties (SetAccess = protected, GetAccess = public, SetObservable) + + % Last measurement status + gauge_stat = {'', ''}; + end + + methods (Access = public) + function this = MyPfeifferTpg(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + + connect(this); + createCommandList(this); + end + + % read pressure from a single channel or both channels at a time + function p_arr = readPressure(this) + queryString(this, ['PRX', this.CR, this.LF]); + str = queryString(this, this.ENQ); + + % Extract pressure and gauge status from reading. + arr = sscanf(str,'%i,%e,%i,%e'); + p_arr = transpose(arr(2:2:end)); + + % Status codes: + % 0 –> Measurement data okay + % 1 –> Underrange + % 2 –> Overrange + % 3 –> Sensor error + % 4 –> Sensor off (IKR, PKR, IMR, PBR) + % 5 –> No sensor (output: 5,2.0000E-2 [hPa]) + % 6 –> Identification error + this.gauge_stat = {gaugeStatusFromCode(this, arr(1)), ... + gaugeStatusFromCode(this, arr(3))}; + end + + function pu = readPressureUnit(this) + queryString(this, ['UNI',this.CR,this.LF]); + str = queryString(this, this.ENQ); + + % Pressure units correspondence table: + % 0 –> mbar/bar + % 1 –> Torr + % 2 –> Pascal + % 3 –> Micron + % 4 –> hPascal (default) + % 5 –> Volt + pu_code = sscanf(str,'%i'); + pu = pressureUnitFromCode(this, pu_code); + end + + function id_list = readGaugeId(this) + queryString(this, ['TID',this.CR,this.LF]); + str = queryString(this, this.ENQ); + + id_list = deblank(strsplit(str,{','})); + end + + function code_list = turnGauge(this) + queryString(this, ['SEN',char(1,1),this.CR,this.LF]); + str = queryString(this, this.ENQ); + code_list = deblank(strsplit(str,{','})); + end + + % Attempt communication and identification of the device + function [str, msg] = idn(this) + try + queryString(this, ['AYT', this.CR, this.LF]); + str = queryString(this, this.ENQ); + catch ME + str = ''; + msg = ME.message; + end + + this.idn_str = toSingleLine(str); + end + + % Create pressure logger + function Lg = createLogger(this, varargin) + function p = MeasPressure() + + % Sync the class properties which also will tirgger an + % update of all the guis to which the instrument is linked + sync(this); + p = this.pressure; + end + + Lg = MyLogger(varargin{:}, 'MeasFcn', @MeasPressure); + + pu = this.pressure_unit; + if isempty(Lg.Record.data_headers) && ~isempty(pu) + Lg.Record.data_headers = ... + {['P ch1 (' pu ')'], ['P ch2 (' pu ')']}; + end + end + end + + methods (Access = protected) + function createCommandList(this) + addCommand(this, 'pressure', ... + 'readFcn', @this.readPressure, ... + 'default', [0, 0]); + + addCommand(this, 'pressure_unit', ... + 'readFcn', @this.readPressureUnit, ... + 'default', 'mBar'); + + addCommand(this, 'gauge_id', ... + 'readFcn', @this.readGaugeId, ... + 'default', {'', ''}); + end + + function createMetadata(this) + createMetadata@MyInstrument(this); + + addObjProp(this.Metadata, this, 'gauge_stat', ... + 'comment', 'Last measurement status'); + end + + % Convert numerical code for gauge status to a string + function str = gaugeStatusFromCode(~, code) + switch int8(code) + case 0 + str = 'Measurement data ok'; + case 1 + str = 'Underrange'; + case 2 + str = 'Overrange'; + case 3 + str = 'Sensor error'; + case 4 + str = 'Sensor off'; + case 5 + str = 'No sensor'; + case 6 + str = 'Identification error'; + otherwise + str = ''; + warning('Unknown gauge status code %i', code); + end + end + + % Convert numerical code for pressure unit to a string + function str = pressureUnitFromCode(~, code) + switch int8(code) + case 0 + str = 'mBar'; + case 1 + str = 'Torr'; + case 2 + str = 'Pa'; + case 3 + str = 'Micron'; + case 4 + str = 'hPa'; + case 5 + str = 'Volt'; + otherwise + str = ''; + warning('unknown pressure unit, code=%i',pu_num) + end + end + end +end + diff --git a/Instrument classes/@MyTekDpo/MyTekDpo.m b/Instrument classes/@MyTekDpo/MyTekDpo.m new file mode 100644 index 0000000..258235b --- /dev/null +++ b/Instrument classes/@MyTekDpo/MyTekDpo.m @@ -0,0 +1,113 @@ +% Class for controlling 4-channel Tektronix DPO scopes. +% Tested with DPO4034, DPO3034 + +classdef MyTekDpo < MyTekScope + + methods (Access = public) + function this = MyTekDpo(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + + this.knob_list = lower({'GPKNOB1', 'GPKNOB2', 'HORZPos', ... + 'HORZScale', 'TRIGLevel', 'PANKNOB1', 'ZOOM', ... + 'VERTPOS1', 'VERTPOS2', 'VERTPOS3', 'VERTPOS4', ... + 'VERTSCALE1', 'VERTSCALE2', 'VERTSCALE3', 'VERTSCALE4'}); + + % Create communication object + connect(this); + + % 2e7 is the maximum trace size of DPO4034-3034 + %(10 mln point of 2-byte integers) + this.Comm.InputBufferSize = 2.1e7; %byte + + createCommandList(this); + end + end + + methods (Access = protected) + function createCommandList(this) + addCommand(this, 'channel',':DATa:SOUrce', ... + 'format', 'CH%i', ... + 'info', ['Channel from which the trace ' ... + 'is transferred'], ... + 'value_list', {1, 2, 3, 4}); + + addCommand(this, 'ctrl_channel', ':SELect:CONTROl', ... + 'format', 'CH%i', ... + 'info', ['Channel currently selected in ' ... + 'the scope display'], ... + 'value_list', {1, 2, 3, 4}); + + addCommand(this, 'point_no', ':HORizontal:RECOrdlength', ... + 'format', '%i', ... + 'info', 'Numbers of points in the scope trace', ... + 'value_list', {1000, 10000, 100000, 1000000, 10000000}); + + addCommand(this, 'time_scale', ':HORizontal:SCAle', ... + 'format', '%e', ... + 'info', 'Time scale (s/div)'); + + addCommand(this, 'trig_lev', ':TRIGger:A:LEVel',... + 'format', '%e', ... + 'info', '(V)'); + + addCommand(this, 'trig_slope', ':TRIGger:A:EDGE:SLOpe', ... + 'format', '%s', ... + 'value_list', {'RISe','FALL'}); + + addCommand(this, 'trig_source', ':TRIGger:A:EDGE:SOUrce', ... + 'format', '%s', ... + 'value_list', {'CH1','CH2','CH3','CH4', ... + 'AUX','EXT','LINE'}); + + addCommand(this, 'trig_mode', ':TRIGger:A:MODe', ... + 'format', '%s', ... + 'value_list', {'AUTO', 'NORMal'}); + + addCommand(this, 'acq_state', ':ACQuire:STATE', ... + 'format', '%b',... + 'info', 'State of data acquisition by the scope'); + + addCommand(this, 'acq_mode', ':ACQuire:MODe', ... + 'format', '%s', ... + 'info', ['Acquisition mode: sample, peak ' ... + 'detect, high resolution, average or envelope'], ... + 'value_list', {'SAMple', 'PEAKdetect', 'HIRes', ... + 'AVErage', 'ENVelope'}); + + % Parametric commands + for i = 1:this.channel_no + i_str = num2str(i); + + addCommand(this,... + ['cpl',i_str],[':CH',i_str,':COUP'], ... + 'format', '%s', ... + 'info', 'Channel coupling: AC, DC or GND', ... + 'value_list', {'DC','AC','GND'}); + + % impedances, 1MOhm or 50 Ohm + addCommand(this,... + ['imp', i_str], [':CH', i_str, ':IMPedance'],... + 'format', '%s', ... + 'info', 'Channel impedance: 1 MOhm or 50 Ohm', ... + 'value_list', {'MEG', 'FIFty'}); + + % Offset + addCommand(this, ... + ['offset',i_str], [':CH',i_str,':OFFSet'], ... + 'format', '%e', ... + 'info', '(V)'); + + addCommand(this, ... + ['scale',i_str], [':CH',i_str,':SCAle'], ... + 'format', '%e', ... + 'info', 'Channel y scale (V/div)'); + + addCommand(this,... + ['enable',i_str], [':SEL:CH',i_str], ... + 'format', '%b',... + 'info', 'Channel enabled'); + end + end + end +end \ No newline at end of file diff --git a/Instrument classes/@MyTekMdo/MyTekMdo.m b/Instrument classes/@MyTekMdo/MyTekMdo.m new file mode 100644 index 0000000..1ed6dd3 --- /dev/null +++ b/Instrument classes/@MyTekMdo/MyTekMdo.m @@ -0,0 +1,115 @@ +% Control class for Tektronix MDO scopes, tested with MDO3034 + +classdef MyTekMdo < MyTekScope + + methods (Access = public) + function this = MyTekMdo(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + + this.channel_no = 4; + this.knob_list = lower({'GPKNOB1', 'GPKNOB2', 'HORZPos', ... + 'HORZScale', 'TRIGLevel', 'PANKNOB1', 'ZOOM', ... + 'VERTPOS1', 'VERTPOS2', 'VERTPOS3', 'VERTPOS4', ... + 'VERTSCALE1', 'VERTSCALE2', 'VERTSCALE3', 'VERTSCALE4'}); + + connect(this); + + % 2e7 is the maximum trace size of MDO3034 + %(10 mln point of 2-byte integers) + this.Comm.InputBufferSize = 2.1e7; %byte + + createCommandList(this); + end + end + + methods (Access = protected) + function createCommandList(this) + addCommand(this, 'channel', ':DATa:SOUrce', ... + 'format', 'CH%i', ... + 'info', ['Channel from which the trace is ' ... + 'transferred'], ... + 'value_list', {1, 2, 3, 4}); + + addCommand(this, 'ctrl_channel', ':SELect:CONTROl', ... + 'format', 'CH%i', ... + 'info', ['Channel currently selected in ' ... + 'the scope display'], ... + 'value_list', {1, 2, 3, 4}); + + addCommand(this, 'point_no', ':HORizontal:RECOrdlength', ... + 'format', '%i', ... + 'info', 'Numbers of points in the scope trace', ... + 'value_list', {1000, 10000, 100000, 1000000, 5000000, ... + 10000000}); + + addCommand(this, 'time_scale', ':HORizontal:SCAle', ... + 'format', '%e', ... + 'info', 'Time scale (s/div)'); + + addCommand(this, 'trig_lev', ':TRIGger:A:LEVel',... % debug + 'format', '%e', ... + 'info', '(V)'); + + addCommand(this, 'trig_slope', ':TRIGger:A:EDGE:SLOpe', ... + 'format', '%s', ... + 'value_list', {'RISe', 'FALL', 'EITHer'}); + + addCommand(this, 'trig_source', ':TRIGger:A:EDGE:SOUrce', ... + 'format', '%s', ... + 'value_list', {'CH1', 'CH2', 'CH3', 'CH4', ... + 'AUX', 'LINE', 'RF'}); + + addCommand(this, 'trig_mode', ':TRIGger:A:MODe', ... + 'format', '%s', ... + 'value_list', {'AUTO', 'NORMal'}); + + addCommand(this, 'acq_state', ':ACQuire:STATE', ... + 'format', '%b',... + 'info', 'State of data acquisition by the scope'); + + addCommand(this, 'acq_mode', ':ACQuire:MODe', ... + 'format', '%s', ... + 'info', ['Acquisition mode: sample, peak ' ... + 'detect, high resolution, average or envelope'], ... + 'value_list',{'SAMple', 'PEAKdetect', 'HIRes', ... + 'AVErage', 'ENVelope'}); + + % Parametric commands + for i = 1:this.channel_no + i_str = num2str(i); + + addCommand(this,... + ['cpl',i_str],[':CH',i_str,':COUP'], ... + 'format', '%s', ... + 'info', 'Channel coupling: AC, DC or GND', ... + 'value_list', {'DC', 'DCREJect', 'AC'}); + + % impedances, 1MOhm or 50 Ohm + addCommand(this,... + ['imp', i_str], [':CH', i_str, ':TERmination'], ... + 'format', '%e', ... + 'info', ['Channel impedance: 50 Ohm, ' ... + '75 Ohm or 1 MOhm'], ... + 'value_list', {50, 75, 1e6}); + + % Offset + addCommand(this, ... + ['offset',i_str], [':CH',i_str,':OFFSet'], ... + 'format', '%e', ... + 'info', '(V)'); + + addCommand(this, ... + ['scale',i_str], [':CH',i_str,':SCAle'], ... + 'format', '%e', ... + 'info', 'Channel y scale (V/div)'); + + addCommand(this,... + ['enable',i_str], [':SEL:CH',i_str], ... + 'format', '%b',... + 'info', 'Channel enabled'); + end + end + end +end + diff --git a/Instrument classes/@MyTekRsa/MyTekRsa.m b/Instrument classes/@MyTekRsa/MyTekRsa.m new file mode 100644 index 0000000..3500a24 --- /dev/null +++ b/Instrument classes/@MyTekRsa/MyTekRsa.m @@ -0,0 +1,191 @@ +% Class for controlling Tektronix RSA5103 and RSA5106 spectrum analyzers + +classdef MyTekRsa < MyScpiInstrument & MyDataSource & MyCommCont ... + & MyGuiCont + + properties (SetAccess = protected, GetAccess = public) + acq_trace % The number of last read trace + end + + methods (Access = public) + function this = MyTekRsa(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + + this.Trace.unit_x = 'Hz'; + this.Trace.unit_y = '$\mathrm{V}^2/\mathrm{Hz}$'; + this.Trace.name_y = 'Power'; + this.Trace.name_x = 'Frequency'; + + % Create communication object + connect(this); + + % Set up the list of communication commands + createCommandList(this); + end + end + + methods (Access = protected) + function createCommandList(this) + + % We define commands for both the nominal and actual resolution + % bandwidths as these two are useful in different + % circumstances. The nominal one unlike the actual one takes + % effect immediately after it is set to a new value, whereas + % the actual one is the true rbw if the device does not follow + % the nominal one (usually if the nominal rbw is is too small). + addCommand(this, 'rbw', ':DPX:BANDwidth:RESolution', ... + 'format', '%e', ... + 'info', 'Nominal resolution bandwidth (Hz)'); + + addCommand(this, 'rbw_act', ':DPX:BANDwidth:ACTual', ... + 'format', '%e', ... + 'access', 'r', ... + 'info', 'Actual resolution bandwidth (Hz)'); + + addCommand(this, 'auto_rbw', ':DPX:BAND:RES:AUTO', ... + 'format', '%b'); + + addCommand(this, 'span', ':DPX:FREQ:SPAN', ... + 'format', '%e', ... + 'info', '(Hz)'); + + addCommand(this, 'start_freq', ':DPX:FREQ:STAR',... + 'format', '%e', ... + 'info', '(Hz)'); + + addCommand(this, 'stop_freq', ':DPX:FREQ:STOP',... + 'format', '%e', ... + 'info', '(Hz)'); + + addCommand(this, 'cent_freq', ':DPX:FREQ:CENT',... + 'format', '%e', ... + 'info', '(Hz)'); + + % Continuous triggering + addCommand(this, 'init_cont', ':INIT:CONT', ... + 'format', '%b',... + 'info', 'Continuous triggering on/off'); + + % Number of points in trace + addCommand(this, 'point_no', ':DPSA:POIN:COUN', ... + 'format', 'P%i', ... + 'value_list', {801, 2401, 4001, 10401}); + + % Reference level (dB) + addCommand(this, 'ref_level',':INPut:RLEVel', ... + 'format', '%e',... + 'info', '(dB)'); + + % Display scale per division (dBm/div) + addCommand(this, 'disp_y_scale', ':DISPlay:DPX:Y:PDIVision',... + 'format', '%e', ... + 'info', '(dBm/div)'); + + % Display vertical offset (dBm) + addCommand(this, 'disp_y_offset', ':DISPLAY:DPX:Y:OFFSET', ... + 'format', '%e', ... + 'info', '(dBm)'); + + % Parametric commands + for i = 1:3 + i_str = num2str(i); + + % Display trace + addCommand(this, ['disp_trace',i_str], ... + [':TRAC',i_str,':DPX'], ... + 'format', '%b', ... + 'info', 'on/off'); + + % Trace Detection + addCommand(this, ['det_trace',i_str],... + [':TRAC',i_str,':DPX:DETection'],... + 'format', '%s', ... + 'value_list', {'AVERage', 'NEGative', 'POSitive'}); + + % Trace Function + addCommand(this, ['func_trace',i_str], ... + [':TRAC',i_str,':DPX:FUNCtion'], ... + 'format', '%s', ... + 'value_list', {'AVERage', 'HOLD', 'NORMal'}); + + % Number of averages + addCommand(this, ['average_no',i_str], ... + [':TRAC',i_str,':DPX:AVER:COUN'], ... + 'format', '%i'); + + % Count completed averages + addCommand(this, ['cnt_trace',i_str], ... + [':TRACe',i_str,':DPX:COUNt:ENABle'], ... + 'format', '%b', ... + 'info', 'Count completed averages'); + end + end + end + + + methods (Access = public) + function readTrace(this, varargin) + if ~isempty(varargin) + n_trace = varargin{1}; + else + n_trace = this.acq_trace; + end + + % Ensure that device parameters, especially those that will be + % later used for the calculation of frequency axis, are up to + % date + sync(this); + + writeString(this, sprintf('fetch:dpsa:res:trace%i?', n_trace)); + data = binblockread(this.Comm, 'float'); + + % Calculate the frequency axis + this.Trace.x = linspace(this.start_freq, this.stop_freq,... + this.point_no); + + % Calculates the power spectrum from the data, which is in dBm. + % Output is in V^2/Hz + this.Trace.y = (10.^(data/10))/this.rbw_act*50*0.001; + + this.acq_trace = n_trace; + + % Trigger acquired data event + triggerNewData(this); + end + + % Abort data acquisition + function abortAcq(this) + writeString(this, ':ABORt'); + end + + % Initiate data acquisition + function initAcq(this) + writeString(this, ':INIT'); + end + + % Wait for the current operation to be completed + function val = opc(this) + val = queryString(this, '*OPC?'); + end + + % Extend readSettings function + function Mdt = readSettings(this) + + %Call parent class method and then append parameters + Mdt = readSettings@MyScpiInstrument(this); + + %Hdr should contain single field + addParam(Mdt, 'acq_trace', this.acq_trace, ... + 'comment', 'The number of last read trace'); + end + end + + methods + function set.acq_trace(this, val) + assert((val==1 || val==2 || val==3), ... + 'Acquisition trace number must be 1, 2 or 3.'); + this.acq_trace = val; + end + end +end diff --git a/Instrument classes/@MyTekScope/MyTekScope.m b/Instrument classes/@MyTekScope/MyTekScope.m new file mode 100644 index 0000000..3a1a1b2 --- /dev/null +++ b/Instrument classes/@MyTekScope/MyTekScope.m @@ -0,0 +1,110 @@ +% Generic class for controlling Tektronix scopes + +classdef MyTekScope < MyScpiInstrument & MyDataSource & MyCommCont ... + & MyGuiCont + + properties (GetAccess = public, SetAccess={?MyClassParser,?MyTekScope}) + + % number of channels + channel_no = 4 + + % List of the physical knobs, which can be rotated programmatically + knob_list = {} + end + + methods (Access = public) + function this = MyTekScope(varargin) + + % Set default GUI name + this.gui_name = 'GuiTekScope'; + + this.Trace.name_x = 'Time'; + this.Trace.name_y = 'Voltage'; + end + + function readTrace(this) + + % Read raw y data + y_data = readY(this); + + % Read units, offsets and steps for the scales + parms = queryStrings(this, ... + ':WFMOutpre:XUNit?', ... + ':WFMOutpre:YUNit?', ... + ':WFMOutpre:XINcr?', ... + ':WFMOutpre:YMUlt?', ... + ':WFMOutpre:XZEro?', ... + ':WFMOutpre:YZEro?', ... + ':WFMOutpre:YOFf?'); + + num_params = str2doubleHedged(parms); + [unit_x, unit_y, step_x, step_y, x_zero, ... + y_zero, y_offset] = num_params{:}; + + % Calculating the y data + y = (y_data-y_offset)*step_y+y_zero; + n_points = length(y); + + % Calculating the x data + x = linspace(x_zero, x_zero + step_x*(n_points-1), n_points); + + this.Trace.x = x; + this.Trace.y = y; + + % Discard "" when assiging the Trace labels + this.Trace.unit_x = unit_x(2:end-1); + this.Trace.unit_y = unit_y(2:end-1); + + triggerNewData(this); + end + + function acquireContinuous(this) + writeStrings(this, ... + ':ACQuire:STOPAfter RUNSTop', ... + ':ACQuire:STATE ON'); + end + + function acquireSingle(this) + writeStrings(this, ... + ':ACQuire:STOPAfter SEQuence', ... + ':ACQuire:STATE ON'); + end + + function turnKnob(this, knob, nturns) + writeString(this, sprintf(':FPAnel:TURN %s,%i', knob, nturns)); + end + end + + methods (Access = protected) + + % The default version of this method works for DPO3034-4034 scopes + function y_data = readY(this) + + % Configure data transfer: binary format and two bytes per + % point. Then query the trace. + this.Comm.ByteOrder = 'bigEndian'; + + writeStrings(this, ... + ':DATA:ENCDG RIBinary', ... + ':DATA:WIDTH 2', ... + ':DATA:STARt 1', ... + sprintf(':DATA:STOP %i', this.point_no), ... + ':CURVE?'); + + y_data = double(binblockread(this.Comm, 'int16')); + + % For some reason MDO3000 scope needs to have an explicit pause + % between data reading and any other communication + pause(0.01); + end + end + + methods + function set.knob_list(this, val) + assert(iscellstr(val), ['Value must be a cell array of ' ... + 'character strings.']) %#ok + this.knob_list = val; + end + end +end + diff --git a/Instrument classes/@MyTekTbs/MyTekTbs.m b/Instrument classes/@MyTekTbs/MyTekTbs.m new file mode 100644 index 0000000..1dec631 --- /dev/null +++ b/Instrument classes/@MyTekTbs/MyTekTbs.m @@ -0,0 +1,124 @@ +% Class for controlling 4-channel Tektronix TBS scopes. +% Tested with TBS2074 + +classdef MyTekTbs < MyTekScope + + methods (Access = public) + function this = MyTekTbs(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + + this.knob_list = lower({'GPKNOB','HORZPos','HORZScale', ... + 'TRIGLevel','VERTPOS','VERTSCALE'}); + + connect(this); + + % 4e7 is the maximum trace size of DPO4034-3034 + % (20 mln point of 2-byte integers) + this.Comm.InputBufferSize = 4.1e7; %byte + + createCommandList(this); + end + end + + methods (Access = protected) + function createCommandList(this) + addCommand(this,'channel',':DATa:SOUrce',... + 'format', 'CH%i',... + 'info', 'Channel from which data is transferred', ... + 'default', 1); + + addCommand(this, 'ctrl_channel', ':SELect:CONTROl', ... + 'format', 'CH%i',... + 'info', ['Channel currently selected in ' ... + 'the scope display'], ... + 'default', 1); + + addCommand(this, 'point_no', ':HORizontal:RECOrdlength', ... + 'format', '%i',... + 'info', 'Numbers of points in the scope trace', ... + 'value_list', {2000, 20000, 200000, 2000000, 20000000}); + + % time scale in s per div + addCommand(this, 'time_scale', ':HORizontal:SCAle', ... + 'format', '%e',... + 'info', 'Time scale (s/div)'); + + % trigger level + addCommand(this, 'trig_lev', ':TRIGger:A:LEVel', ... + 'format', '%e'); + + % trigger slope + addCommand(this, 'trig_slope', ':TRIGger:A:EDGE:SLOpe', ... + 'format', '%s', ... + 'value_list', {'RISe','FALL'}); + + % trigger source + addCommand(this, 'trig_source', ':TRIGger:A:EDGE:SOUrce', ... + 'format', '%s', ... + 'value_list', {'CH1', 'CH2', 'CH3', 'CH4', 'LINE'}); + + % trigger mode + addCommand(this, 'trig_mode', ':TRIGger:A:MODe', ... + 'format', '%s', ... + 'value_list', {'AUTO', 'NORMal'}); + + % state of the data acquisition by the scope + addCommand(this, 'acq_state', ':ACQuire:STATE', ... + 'format', '%b', ... + 'info', 'State of data acquisition by the scope'); + + % acquisition mode + addCommand(this, 'acq_mode', ':ACQuire:MODe',... + 'format', '%s', ... + 'info', ['Acquisition mode: sample, ', ... + 'peak detect, high resolution, average'], ... + 'value_list', {'SAMple','PEAKdetect','HIRes','AVErage'}); + + % Parametric commands + for i = 1:this.channel_no + i_str = num2str(i); + + addCommand(this,... + ['cpl',i_str],[':CH',i_str,':COUP'], ... + 'format', '%s',... + 'info', 'Channel coupling: AC, DC or GND', ... + 'value_list', {'AC','DC','GND'}); + + addCommand(this, ... + ['offset',i_str],[':CH',i_str,':OFFSet'], ... + 'format', '%e', ... + 'info', '(V)'); + + addCommand(this,... + ['scale',i_str],[':CH',i_str,':SCAle'], ... + 'format', '%e', ... + 'info', 'Channel y scale (V/div)'); + % channel enabled + addCommand(this,... + ['enable',i_str],[':SELect:CH',i_str], ... + 'format', '%b', ... + 'info', 'Channel enabled'); + end + end + + function y_data = readY(this) + + % Configure data transfer: binary format and two bytes per + % point. Then query the trace. + this.Comm.ByteOrder = 'bigEndian'; + + writeStrings(this, ... + ':WFMInpre:ENCdg BINary', ... + ':DATA:WIDTH 2', ... + ':DATA:STARt 1', ... + sprintf(':DATA:STOP %i', this.point_no), ... + ':CURVE?'); + + y_data = double(binblockread(this.Comm, 'int16')); + + % Read the terminating character + fscanf(this.Comm, '%s'); + end + end +end \ No newline at end of file diff --git a/Instrument classes/@MyTekTds/MyTekTds.m b/Instrument classes/@MyTekTds/MyTekTds.m new file mode 100644 index 0000000..9fd5b95 --- /dev/null +++ b/Instrument classes/@MyTekTds/MyTekTds.m @@ -0,0 +1,115 @@ +% Class for controlling 2-channel Tektronix TDS scopes. + +classdef MyTekTds < MyTekScope + + properties (Constant = true) + point_no = 2500 % number of points is fixed for this device + end + + methods (Access = public) + function this = MyTekTds(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + + this.channel_no = 2; + this.knob_list = lower({'HORZSCALE', 'VERTSCALE1', ... + 'VERTSCALE2'}); + + connect(this); + + % 5e3 is the maximum trace size of TDS2022 + %(2500 point of 2-byte integers) + this.Comm.InputBufferSize = 1e4; % byte + + createCommandList(this); + end + + % Emulates the physical knob turning, works with nturns=+-1 + function turnKnob(this, knob, nturns) + switch upper(knob) + case 'HORZSCALE' + % timebase is changed + if nturns==-1 + sc = this.time_scale*2; + elseif nturns==1 + sc = this.time_scale/2; + else + return + end + + writeString(this, ... + sprintf('HORizontal:MAIn:SCAle %i',sc)); + case {'VERTSCALE1', 'VERTSCALE2'} + + % vertical scale is changed + n_ch = sscanf(upper(knob), 'VERTSCALE%i'); + tag = sprintf('scale%i', n_ch); + if nturns==-1 + sc = this.(tag)*2; + elseif nturns==1 + sc = this.(tag)/2; + else + return + end + + writeString(this, sprintf('CH%i:SCAle %i',n_ch,sc)); + end + end + end + + methods (Access = protected) + function createCommandList(this) + addCommand(this, 'channel', ':DATa:SOUrce', ... + 'format', 'CH%i', ... + 'info', 'Channel from which the data is transferred', ... + 'value_list', {1, 2}); + + addCommand(this, 'time_scale', ':HORizontal:MAIn:SCAle', ... + 'format', '%e', ... + 'info', 'Time scale (s/div)'); + + % Trigger level + addCommand(this, 'trig_lev', ':TRIGger:MAIn:LEVel', ... + 'format', '%e', ... + 'info', '(V)'); + + addCommand(this, 'trig_slope', ':TRIGger:MAIn:EDGE:SLOpe', ... + 'format', '%s', ... + 'value_list', {'RISe', 'FALL'}); + + addCommand(this, 'trig_source', ':TRIGger:MAIn:EDGE:SOUrce',... + 'format', '%s', ... + 'value_list', {'CH1', 'CH2', 'EXT', 'EXT5', 'EXT10', ... + 'AC LINE'}); + + addCommand(this, 'trig_mode', ':TRIGger:MAIn:MODe', ... + 'format', '%s', ... + 'value_list', {'AUTO','NORMal'}); + + addCommand(this, 'acq_state', ':ACQuire:STATE', ... + 'format', '%b', ... + 'info', 'State of data acquisition by the scope'); + + % Parametric commands + for i = 1:this.channel_no + i_str = num2str(i); + + addCommand(this,... + ['cpl',i_str], [':CH',i_str,':COUP'], ... + 'format', '%s', ... + 'info', 'Channel coupling: AC, DC or GND', ... + 'value_list', {'DC', 'AC', 'GND'}); + + addCommand(this,... + ['scale',i_str], [':CH',i_str,':SCAle'], ... + 'format', '%e',... + 'info', 'Channel y scale (V/div)'); + + addCommand(this,... + ['enable',i_str],[':SEL:CH',i_str], ... + 'format', '%b', ... + 'info', 'Channel enabled'); + end + end + end +end \ No newline at end of file diff --git a/Instrument classes/@MyThorlabsPm/MyThorlabsPm.m b/Instrument classes/@MyThorlabsPm/MyThorlabsPm.m new file mode 100644 index 0000000..9773b4b --- /dev/null +++ b/Instrument classes/@MyThorlabsPm/MyThorlabsPm.m @@ -0,0 +1,74 @@ +% Class for Thorlabs PM100D powermeters + +classdef MyThorlabsPm < MyScpiInstrument & MyCommCont & MyGuiCont + + methods (Access = public) + function this = MyThorlabsPm(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + + connect(this); + + % reading from powermeter is quick + this.Comm.Timeout = 1; + + createCommandList(this); + end + + % Appantly, this device sometimemes fails if it receives very long + % commands, so query them one by one + function writeStrings(this, varargin) + + % Send commands to device one by one + for i=1:length(varargin) + cmd_str = varargin{i}; + writeString(this, cmd_str); + end + end + + % Query commands and return resut as cell array of strings + function res_list = queryStrings(this, varargin) + + % Send commands to device one by one + ncmd = length(varargin); + res_list = cell(1,ncmd); + + for i = 1:ncmd + cmd_str = varargin{i}; + res_list{i} = queryString(this, cmd_str); + end + end + end + + methods (Access = protected) + function createCommandList(this) + + % Sensor name and info + addCommand(this, 'sensor', ':SYSTem:SENSor:IDN', ... + 'format', '%s', ... + 'access', 'r'); + + addCommand(this, 'average_no',':SENSe:AVERage:COUNt', ... + 'format', '%i', ... + 'info', ['Number of averages, 1 sample takes ' ... + 'approx. 3ms']); + + addCommand(this, 'wl', ':SENSe:CORRection:WAVelength',... + 'format', '%e', ... + 'info', 'Operation wavelength (nm)'); + + addCommand(this, 'auto_pow_rng', ... + ':SENSe:POWer:DC:RANGe:AUTO', ... + 'format', '%b', ... + 'info', 'Auto power range'); + + addCommand(this, 'power_unit', ':SENSe:POWer:DC:UNIT', ... + 'format', '%s',... + 'value_list', {'W', 'DBM'}) + + addCommand(this, 'power', ':MEASure:POWer', ... + 'format', '%e', ... + 'access', 'r') + end + end +end diff --git a/Instrument classes/@MyZiLockIn/MyZiLockIn.m b/Instrument classes/@MyZiLockIn/MyZiLockIn.m new file mode 100644 index 0000000..d54837a --- /dev/null +++ b/Instrument classes/@MyZiLockIn/MyZiLockIn.m @@ -0,0 +1,112 @@ +% A generic class for programs based on Zurich Instruments UHFLI and MFLI +% lock-in amplifiers + +classdef MyZiLockIn < MyInstrument + + properties (Access = public, SetObservable) + + % Used to establish connection with the instrument + dev_serial = 'dev4090' + end + + properties (GetAccess = public, SetAccess = protected, SetObservable) + + % This string gives the device name as it appears in + % the server's node tree. It is read out during the creation + % of session and is typically the same as dev_serial. + dev_id + + % Device clock frequency, i.e. the number of timestamps per second + clockbase + end + + properties (Access = public, Dependent, Hidden) + + % Address is another alias for dev_serial which is kept for + % compatibility with other instrument classes + address + end + + methods (Access = public) + function createApiSession(this) + + % Check the ziDAQ MEX (DLL) and Utility functions can be found + % in Matlab's path. + if ~(exist('ziDAQ', 'file') == 3) && ... + ~(exist('ziCreateAPISession', 'file') == 2) + fprintf(['Failed to either find the ziDAQ mex file ' ... + 'or ziDevices() utility.\n']) + fprintf(['Please configure your path using the ziDAQ ' ... + 'function ziAddPath().\n']) + fprintf(['This can be found in the API subfolder of ' ... + 'your LabOne installation.\n']); + fprintf('On Windows this is typically:\n'); + fprintf(['C:\\Program Files\\Zurich Instruments' ... + '\\LabOne\\API\\MATLAB2012\\\n']); + return + end + + % Do not throw errors in the constructor to allow creating a + % class instance when the physical device is disconnected + try + + % Create an API session and connect to the correct Data + % Server. This is a high level function that uses + % ziDAQ('connect',.. and ziDAQ('connectDevice', ... when + % necessary + apilevel = 6; + [this.dev_id, ~] = ziCreateAPISession(this.dev_serial, ... + apilevel); + + % Read the divice clock frequency + this.clockbase = ... + double(ziDAQ('getInt',['/',this.dev_id,'/clockbase'])); + catch ME + warning(ME.message) + end + end + + function str = idn(this) + DevProp = ziDAQ('discoveryGet', this.dev_id); + str = this.dev_id; + + if isfield(DevProp, 'devicetype') + str = [str,'; device type: ', DevProp.devicetype]; + end + + if isfield(DevProp, 'options') + + % Print options from the list as comma-separated values and + % discard the last comma. + opt_str = sprintf('%s,',DevProp.options{:}); + str = [str,'; options: ', opt_str(1:end-1)]; + end + if isfield(DevProp, 'serverversion') + str = [str,'; server version: ', DevProp.serverversion]; + end + this.idn_str = str; + end + end + + methods (Access = protected) + function createMetadata(this) + createMetadata@MyInstrument(this); + addObjProp(this.Metadata, this, 'clockbase', 'comment', ... + ['Device clock frequency, i.e. the number of ', ... + 'timestamps per second']); + end + end + + methods + + % Alias for the device serial + function val = get.address(this) + val = this.dev_serial; + end + + function set.address(this, val) + this.dev_serial = val; + end + end +end + diff --git a/Instrument classes/@MyZiRingdown/MyZiRingdown.m b/Instrument classes/@MyZiRingdown/MyZiRingdown.m new file mode 100644 index 0000000..cd5e958 --- /dev/null +++ b/Instrument classes/@MyZiRingdown/MyZiRingdown.m @@ -0,0 +1,939 @@ +% Class for performing ringdown measurements of mechanical oscillators +% using Zurich Instruments UHF or MF lock-in. +% +% Operation: sweep the driving tone (drive_osc) using the sweep module +% in LabOne web user interface, when the magnitude of the demodulator +% signal exceeds trig_threshold the driving tone is switched off and +% the recording of demodulated signal is started, the signal is recorded +% for the duration of record_time. +% +% Features: +% +% Adaptive measurement oscillator frequency +% +% Averaging +% +% Auto saving +% +% Auxiliary output signal: If enable_aux_out=true +% then after a ringdown is started a sequence of pulses is applied +% to the output consisting of intermittent on and off periods +% starting from on. + +classdef MyZiRingdown < MyZiLockIn & MyDataSource & MyGuiCont + + properties (Access = public, SetObservable = true) + + % Ringdown is recorded if the signal in the triggering demodulation + % channel exceeds this value + trig_threshold = 1e-3 % V + + % Duration of the recorded ringdown + record_time = 1 % (s) + + % If enable_acq is true, then the drive is on and the acquisition + % of record is triggered when signal exceeds trig_threshold + enable_acq = false + + % Auxiliary output signal during ringdown. + enable_aux_out = false % If auxiliary output is applied + + % time during which the output is in aux_out_on_lev state + aux_out_on_t = 1 % (s) + + % time during which the output is in aux_out_off_lev state + aux_out_off_t = 1 % (s) + + aux_out_on_lev = 1 % (V), output trigger on level + aux_out_off_lev = 0 % (V), output trigger off level + + % Average the trace over n points to reduce amount of stored data + % while keeping the demodulator bandwidth large + downsample_n = 1 + + fft_length = 128 + + % If all ringdowns should be automatically saved + auto_save = false + + % Name of the ringdown trace. It is used to produce the filename + % in the case if the ringdowns are auto saved + trace_name = 'ringdown' + + % In adaptive measurement oscillator mode the oscillator frequency + % is continuously changed to follow the signal frequency during + % ringdown acquisition. This helps against the oscillator frequency + % drift. + adaptive_meas_osc = false + end + + % The properties which are read or set only once during the class + % initialization + properties (GetAccess = public, SetAccess = {?MyClassParser}, ... + SetObservable = true) + + % enumeration for demodulators, oscillators and output starts from 1 + demod = 1 % demodulator used for both triggering and measurement + + % Enumeration in the node structure starts from 0, so, for example, + % the default path to the trigger demodulator refers to the + % demodulator #1 + demod_path = '/dev4090/demods/0' + + drive_osc = 1 + meas_osc = 2 + + % Signal input, integers above 1 correspond to main inputs, aux + % input etc. (see the user interface for device-specific details) + signal_in = 1 + + drive_out = 1 % signal output used for driving + + % Number of an auxiliary channel used for the output of triggering + % signal, primarily intended to switch the measurement apparatus + % off during a part of the ringdown and thus allow for free + % evolution of the oscillator during that period. + aux_out = 1 + + % Poll duration of 1 ms practically means that ziDAQ('poll', ... + % returns immediately with the data accumulated since the + % previous function call. + poll_duration = 0.001 % s + poll_timeout = 50 % ms + + % Margin for adaptive oscillator frequency adjustment - oscillator + % follows the signal if the dispersion of frequency in the + % demodulator band is below ad_osc_margin times the demodulation + % bandwidth (under the condition that adaptive_meas_osc=true) + ad_osc_margin = 0.1 + end + + % Internal variables + properties (GetAccess = public, SetAccess = protected, SetObservable) + + recording = false % true if a ringdown is being recorded + + % true if adaptive measurement oscillator mode is on and if the + % measurement oscillator is actually actively following. + ad_osc_following = false + + % Reference timestamp at the beginning of measurement record. + % Stored as uint64. + t0 + + elapsed_t = 0 % Time elapsed since the last recording was started + + DemodSpectrum % MyTrace object to store FFT of the demodulator data + end + + % Other dependent variables that are not device properties + properties (Dependent = true) + + % Downsample the measurement record to reduce the amount of data + % while keeping the large demodulation bandwidth. + % (samples/s), sampling rate of the trace after avraging + downsampled_rate + + % Provides public access to properties of private AvgTrace + n_avg % number of ringdowns to be averaged + avg_count % the average counter + + fft_rbw % resolution bandwidth of fft + + poll_period % (s) + end + + % Keeping handle objects fully private is the only way to restrict set + % access to their properties + properties (Access = private) + PollTimer + + AuxOutOffTimer % Timer responsible for switching the aux out off + AuxOutOnTimer % Timer responsible for switching the aux out on + + % Demodulator samples z(t) stored to continuously calculate + % spectrum, the values of z are complex here, z=x+iy. + % osc_freq is the demodulation frequency + DemodRecord = struct('t',[],'z',[],'osc_freq',[]) + + AvgTrace % MyAvgTrace object used for averaging ringdowns + + % Buffers for the acquisition of ringdown trace + ts_buff + r_sq_buff + end + + events + NewDemodSample % New demodulator samples received + RecordingStarted % Acquisition of a new trace triggered + end + + methods (Access = public) + + %% Constructor and destructor + function this = MyZiRingdown(varargin) + P = MyClassParser(this); + addParameter(P, 'poll_period', 0.1, @isnumeric); + processInputs(P, this, varargin{:}); + + % Create and configure trace objects + % Trace is inherited from the superclass + this.Trace = MyTrace(... + 'name_x','Time',... + 'unit_x','s',... + 'name_y','Magnitude r',... + 'unit_y','V'); + + this.DemodSpectrum = MyTrace(... + 'name_x','Frequency',... + 'unit_x','Hz',... + 'name_y','PSD',... + 'unit_y','V^2/Hz'); + + this.AvgTrace = MyAvgTrace(); + + % Set up the poll timer. Using a timer for anyncronous + % data readout allows to use the wait time for execution + % of other programs. + % Fixed spacing is preferred as it is the most robust mode of + % operation when keeping the intervals between callbacks + % precisely defined is not the biggest concern. + % Busy mode is 'drop' - there is no need to accumulate timer + % callbacks as the data is stored in the buffer of zi data + % server since the previous poll. + this.PollTimer = timer(... + 'BusyMode', 'drop',... + 'ExecutionMode', 'fixedSpacing',... + 'Period', P.Results.poll_period,... + 'TimerFcn', @this.pollTimerCallback); + + % Aux out timers use fixedRate mode for more precise timing. + % The two timers are executed periodically with a time lag. + % The first timer switches the auxiliary output off + this.AuxOutOffTimer = timer(... + 'ExecutionMode', 'fixedRate',... + 'TimerFcn', @this.auxOutOffTimerCallback); + + % The second timer switches the auxiliary output on + this.AuxOutOnTimer = timer(... + 'ExecutionMode', 'fixedRate',... + 'TimerFcn', @this.auxOutOnTimerCallback); + + createApiSession(this); + + % After the session is created and device_id is known, create + % the demodulator path. + this.demod_path = sprintf('/%s/demods/%i', this.dev_id, ... + this.demod-1); + + createCommandList(this); + end + + function delete(this) + + % delete function should never throw errors, so protect + % statements with try-catch + try + stopPoll(this) + catch ME + warning(['Could not usubscribe from the demodulator ', ... + 'or stop the poll timer. Error: ' ME.message]) + end + + % Delete timers to prevent them from running indefinitely in + % the case of program crash + try + delete(this.PollTimer) + catch ME + warning(['Could not delete the poll timer.' ME.message]) + end + + try + stop(this.AuxOutOffTimer); + delete(this.AuxOutOffTimer); + catch + warning('Could not stop and delete AuxOutOff timer.') + end + + try + stop(this.AuxOutOnTimer); + delete(this.AuxOutOnTimer); + catch + warning('Could not stop and delete AuxOutOn timer.') + end + end + + %% Other methods + + function startPoll(this) + sync(this); + + % Configure the oscillators, demodulator and driving output + % -1 accounts for the difference in enumeration conventions + % in the software names (starting from 1) and node numbers + % (starting from 0). + % First, update the demodulator path + this.demod_path = sprintf('/%s/demods/%i', ... + this.dev_id, this.demod-1); + + % Set the data transfer rate so that it satisfies the Nyquist + % criterion (>x2 the bandwidth of interest) + this.demod_rate = 4*this.lowpass_bw; + + % Configure the demodulator. Signal input: + ziDAQ('setInt', ... + [this.demod_path,'/adcselect'], this.signal_in-1); + + % Oscillator: + ziDAQ('setInt', ... + [this.demod_path,'/oscselect'], this.drive_osc-1); + + % Enable data transfer from the demodulator to the computer + ziDAQ('setInt', [this.demod_path,'/enable'], 1); + + % Configure the signal output - disable all the oscillator + % contributions excluding the driving tone + path = sprintf('/%s/sigouts/%i/enables/*', ... + this.dev_id, this.drive_out-1); + ziDAQ('setInt', path, 0); + + path = sprintf('/%s/sigouts/%i/enables/%i', ... + this.dev_id, this.drive_out-1, this.drive_osc-1); + ziDAQ('setInt', path, 1); + + % By convention, we start form 'enable_acq=false' state + this.enable_acq = false; + this.drive_on = false; + + % Configure the auxiliary trigger output - put it in the manual + % mode so it does not output demodulator readings + path = sprintf('/%s/auxouts/%i/outputselect', ... + this.dev_id, this.aux_out-1); + ziDAQ('setInt', path, -1); + + % The convention is that aux out is on by default + this.aux_out_on = true; + + % Subscribe to continuously receive samples from the + % demodulator. Samples accumulated between timer callbacks + % will be read out using ziDAQ('poll', ... + ziDAQ('subscribe', [this.demod_path,'/sample']); + + % Start continuous polling + start(this.PollTimer) + end + + function stopPoll(this) + stop(this.PollTimer) + ziDAQ('unsubscribe', [this.demod_path,'/sample']); + end + + % Main function that polls data from the device demodulator + function pollTimerCallback(this, ~, ~) + + % Switch off the hedged mode to reduce latency + this.auto_sync = false; + + % ziDAQ('poll', ... with short poll_duration returns + % immediately with the data accumulated since the last timer + % callback + Data = ziDAQ('poll', this.poll_duration, this.poll_timeout); + + try + + % Get the new demodulator data + DemodSample = Data.(this.dev_id).demods(this.demod).sample; + catch + this.auto_sync = true; + return + end + + % Append new samples to the record and recalculate spectrum + appendSamplesToBuff(this, DemodSample); + calcfft(this); + + if this.recording + + % If the recording has just started, save the start time + if isempty(this.Trace.x) + this.t0 = DemodSample.timestamp(1); + end + + % If recording is under way, append the new samples to + % the trace + rec_finished = appendSamplesToTrace(this, DemodSample); + + % Update elapsed time + if ~isempty(this.Trace.x) + this.elapsed_t = this.Trace.x(end); + else + this.elapsed_t = 0; + end + + % If the adaptive measurement frequency mode is on, + % update the measurement oscillator frequency. + % Make sure that the demodulator record actually + % contains a signal by comparing the dispersion of + % frequency to the demodulator bandwidth. + if this.adaptive_meas_osc + [df_avg, df_dev] = calcfreq(this); + if df_dev < this.ad_osc_margin*this.lowpass_bw + this.meas_osc_freq = df_avg; + + % Change indicator + this.ad_osc_following = true; + else + this.ad_osc_following = false; + end + else + this.ad_osc_following = false; + end + else + r = sqrt(DemodSample.x.^2+DemodSample.y.^2); + if this.enable_acq && max(r)>this.trig_threshold + + % Start acquisition of a new trace if the maximum + % of the signal exceeds threshold + this.recording = true; + this.elapsed_t = 0; + + % Switch the drive off + this.drive_on = false; + + % Set the measurement oscillator frequency to be + % the frequency at which triggering occurred + this.meas_osc_freq = this.drive_osc_freq; + + % Switch the oscillator + this.current_osc = this.meas_osc; + + % Clear the buffer on ZI data server from existing + % demodulator samples, as these samples were + % recorded with drive on + ziDAQ('poll', this.poll_duration, this.poll_timeout); + + % Optionally start the auxiliary output timers + if this.enable_aux_out + + % Configure measurement periods and delays + T = this.aux_out_on_t + this.aux_out_off_t; + this.AuxOutOffTimer.Period = T; + this.AuxOutOnTimer.Period = T; + + this.AuxOutOffTimer.startDelay =... + this.aux_out_on_t; + this.AuxOutOnTimer.startDelay = T; + + % Start timers + start(this.AuxOutOffTimer) + start(this.AuxOutOnTimer) + end + + % Clear trace + clearData(this.Trace); + + notify(this, 'RecordingStarted'); + end + + rec_finished = false; + + % Indicator for adaptive measurement is off, since + % recording is not under way + this.ad_osc_following = false; + end + + notify(this,'NewDemodSample'); + + % Stop recording if a ringdown record was completed + if rec_finished + + % stop recording + this.recording = false; + this.ad_osc_following = false; + + % Stop auxiliary timers + stop(this.AuxOutOffTimer); + stop(this.AuxOutOnTimer); + + % Return the drive and aux out to the default state + this.aux_out_on = true; + this.current_osc = this.drive_osc; + + % Do trace averaging. If the new data length is not of + % the same size as the length of the existing data + % (which should happen only when the record period was + % changed during recording or when recording was + % manually stopped), truncate to the minimum length + if ~isempty(this.AvgTrace.x) && ... + (length(this.AvgTrace.y)~=length(this.Trace.y)) + + l = min(length(this.AvgTrace.y), ... + length(this.Trace.y)); + + this.AvgTrace.y = this.AvgTrace.y(1:l); + this.AvgTrace.x = this.AvgTrace.x(1:l); + this.Trace.y = this.Trace.y(1:l); + this.Trace.x = this.Trace.x(1:l); + + disp('Ringdown record was truncated') + end + avg_compl = addAverage(this.AvgTrace, this.Trace); + + % Trigger NewData + if this.n_avg > 1 + end_str = sprintf('_%i', this.AvgTrace.avg_count); + else + end_str = ''; + end + triggerNewData(this, 'save', this.auto_save, ... + 'trace_name', [this.trace_name, end_str]); + + % If the ringdown averaging is complete, disable + % further triggering to exclude data overwriting + if avg_compl + this.enable_acq = false; + this.drive_on = false; + + if this.n_avg > 1 + end_str = '_avg'; + + % Trigger one more time to transfer the average + % trace. + % A new measurement header is not necessary + % as the delay since the last triggering is + % minimum. + triggerNewData(this, ... + 'Trace', copy(this.AvgTrace), ... + 'new_header', false, ... + 'save', this.auto_save, ... + 'trace_name', [this.trace_name, end_str]); + end + else + + % Continue trying to acquire new ringdowns + this.enable_acq = true; + this.drive_on = true; + end + end + + this.auto_sync = true; + end + + % Append timestamps vs r=sqrt(x^2+y^2) to the measurement record. + % Starting index can be supplied as varargin. + % The output variable tells if the record is finished. + function isfin = appendSamplesToTrace(this, DemodSample) + r_sq = DemodSample.x.^2 + DemodSample.y.^2; + + % Subtract the reference time, convert timestamps to seconds + ts = double(DemodSample.timestamp - this.t0)/this.clockbase; + + % Check if recording should be stopped + isfin = (ts(end) >= this.record_time); + if isfin + + % Remove excess data points from the new data + ind = (tsflen + this.DemodRecord.t = this.DemodRecord.t(end-flen+1:end); + this.DemodRecord.z = this.DemodRecord.z(end-flen+1:end); + this.DemodRecord.osc_freq = ... + this.DemodRecord.osc_freq(end-flen+1:end); + end + end + + function calcfft(this) + flen = min(this.fft_length, length(this.DemodRecord.t)); + [freq, spectr] = xyFourier( ... + this.DemodRecord.t(end-flen+1:end), ... + this.DemodRecord.z(end-flen+1:end)); + this.DemodSpectrum.x = freq; + this.DemodSpectrum.y = abs(spectr).^2; + end + + % Calculate the average frequency and dispersion of the demodulator + % signal + function [f_avg, f_dev] = calcfreq(this) + if ~isempty(this.DemodSpectrum.x) + norm = sum(this.DemodSpectrum.y); + + % Calculate the center frequency of the spectrum + f_avg = dot(this.DemodSpectrum.x, ... + this.DemodSpectrum.y)/norm; + + f_dev = sqrt(dot(this.DemodSpectrum.x.^2, ... + this.DemodSpectrum.y)/norm-f_avg^2); + + % Shift the FFT center by the demodulation frequency to + % output absolute value + f_avg = f_avg + mean(this.DemodRecord.osc_freq); + else + f_avg = []; + f_dev = []; + end + end + + % Provide restricted access to private AvgTrace + function resetAveraging(this) + + % Clear data and reset the counter + clearData(this.AvgTrace); + end + + function auxOutOffTimerCallback(this, ~, ~) + this.aux_out_on = false; + end + + function auxOutOnTimerCallback(this, ~, ~) + this.aux_out_on = true; + end + end + + methods (Access = protected) + function createCommandList(this) + addCommand(this, 'drive_osc_freq', ... + 'readFcn', @this.readDriveOscFreq, ... + 'writeFcn', @this.writeDriveOscFreq, ... + 'info', '(Hz)'); + + addCommand(this, 'meas_osc_freq', ... + 'readFcn', @this.readMeasOscFreq, ... + 'writeFcn', @this.writeMeasOscFreq, ... + 'info', '(Hz)'); + + addCommand(this, 'drive_on', ... + 'readFcn', @this.readDriveOn, ... + 'writeFcn', @this.writeDriveOn); + + addCommand(this, 'current_osc', ... + 'readFcn', @this.readCurrentOsc, ... + 'writeFcn', @this.writeCurrentOsc, ... + 'info', 'measurement or driving'); + + addCommand(this, 'drive_amp', ... + 'readFcn', @this.readDriveAmp, ... + 'writeFcn', @this.writeDriveAmp, ... + 'info', '(Vpk)'); + + addCommand(this, 'lowpass_order', ... + 'readFcn', @this.readLowpassOrder, ... + 'writeFcn', @this.writeLowpassOrder, ... + 'default', 1); + + addCommand(this, 'lowpass_bw', ... + 'readFcn', @this.readLowpassBw, ... + 'writeFcn', @this.writeLowpassBw, ... + 'info', '3 db bandwidth of lowpass filter (Hz)'); + + addCommand(this, 'demod_rate', ... + 'readFcn', @this.readDemodRate, ... + 'writeFcn', @this.writeDemodRate, ... + 'info', ['Rate at which demodulator data is ' ... + 'transferred to computer']); + + addCommand(this, 'aux_out_on', ... + 'readFcn', @this.readAuxOutOn, ... + 'writeFcn', @this.writeAuxOutOn, ... + 'info', 'If aux out in ''on'' state, true/false'); + end + + + function val = readDriveOscFreq(this) + path = sprintf('/%s/oscs/%i/freq', this.dev_id, ... + this.drive_osc-1); + val = ziDAQ('getDouble', path); + end + + function writeDriveOscFreq(this, val) + path = sprintf('/%s/oscs/%i/freq', this.dev_id, ... + this.drive_osc-1); + ziDAQ('setDouble', path, val); + end + + function val = readMeasOscFreq(this) + path = sprintf('/%s/oscs/%i/freq', this.dev_id, ... + this.meas_osc-1); + val = ziDAQ('getDouble', path); + end + + function writeMeasOscFreq(this, val) + path = sprintf('/%s/oscs/%i/freq', this.dev_id, ... + this.meas_osc-1); + ziDAQ('setDouble', path, val); + end + + function val = readDriveOn(this) + path = sprintf('/%s/sigouts/%i/on', this.dev_id, ... + this.drive_out-1); + val = logical(ziDAQ('getInt', path)); + end + + function writeDriveOn(this, val) + path = sprintf('/%s/sigouts/%i/on', this.dev_id, ... + this.drive_out-1); + % Use double() to convert from logical + ziDAQ('setInt', path, double(val)); + end + + function val = readCurrentOsc(this) + val = double(ziDAQ('getInt', ... + [this.demod_path,'/oscselect']))+1; + end + + function writeCurrentOsc(this, val) + assert((val==this.drive_osc) || (val==this.meas_osc), ... + ['The number of current oscillator must be that of ', ... + 'the drive or measurement oscillator, not ', num2str(val)]) + ziDAQ('setInt', [this.demod_path,'/oscselect'], val-1); + end + + function val = readDriveAmp(this) + path = sprintf('/%s/sigouts/%i/amplitudes/%i', ... + this.dev_id, this.drive_out-1, this.drive_osc-1); + val = ziDAQ('getDouble', path); + end + + function writeDriveAmp(this, val) + path=sprintf('/%s/sigouts/%i/amplitudes/%i', ... + this.dev_id, this.drive_out-1, this.drive_osc-1); + ziDAQ('setDouble', path, val); + end + + function n = readLowpassOrder(this) + n = ziDAQ('getInt', [this.demod_path,'/order']); + end + + function writeLowpassOrder(this, val) + assert(any(val==[1,2,3,4,5,6,7,8]), ['Low-pass filter ', ... + 'order must be an integer between 1 and 8']) + ziDAQ('setInt', [this.demod_path,'/order'], val); + end + + function bw = readLowpassBw(this) + tc = ziDAQ('getDouble', [this.demod_path,'/timeconstant']); + bw = ziTC2BW(tc, this.lowpass_order); + end + + function writeLowpassBw(this, val) + tc = ziBW2TC(val, this.lowpass_order); + ziDAQ('setDouble', [this.demod_path,'/timeconstant'], tc); + end + + function val = readDemodRate(this) + val = ziDAQ('getDouble', [this.demod_path,'/rate']); + end + + function writeDemodRate(this, val) + ziDAQ('setDouble', [this.demod_path,'/rate'], val); + end + + function bool = readAuxOutOn(this) + path = sprintf('/%s/auxouts/%i/offset', ... + this.dev_id, this.aux_out-1); + val = ziDAQ('getDouble', path); + + % Signal from the auxiliary output is continuous, we make the + % binary decision about the output state depending on if + % the signal is closer to the ON or OFF level + bool = (abs(val-this.aux_out_on_lev) < ... + abs(val-this.aux_out_off_lev)); + end + + function writeAuxOutOn(this, bool) + path = sprintf('/%s/auxouts/%i/offset', ... + this.dev_id, this.aux_out-1); + if bool + out_offset = this.aux_out_on_lev; + else + out_offset = this.aux_out_off_lev; + end + ziDAQ('setDouble', path, out_offset); + end + + function createMetadata(this) + createMetadata@MyZiLockIn(this); + + % Demodulator parameters + addObjProp(this.Metadata, this, 'demod', 'comment', ... + 'Number of the demodulator in use (starting from 1)'); + addObjProp(this.Metadata, this, 'meas_osc', 'comment', ... + 'Measurement oscillator number'); + + % Signal input + addObjProp(this.Metadata, this, 'signal_in', 'comment', ... + 'Singnal input number'); + + % Drive parameters + addObjProp(this.Metadata, this, 'drive_out', 'comment', ... + 'Driving output number'); + addObjProp(this.Metadata, this, 'drive_osc', 'comment', ... + 'Swept oscillator number'); + + % Parameters of the auxiliary output + addObjProp(this.Metadata, this, 'aux_out', 'comment', ... + 'Auxiliary output number'); + addObjProp(this.Metadata, this, 'enable_aux_out', 'comment',... + 'Auxiliary output is applied during ringdown'); + addObjProp(this.Metadata, this, 'aux_out_on_lev', ... + 'comment', '(V)'); + addObjProp(this.Metadata, this, 'aux_out_off_lev', ... + 'comment', '(V)'); + addObjProp(this.Metadata, this, 'aux_out_on_t', ... + 'comment', '(s)'); + addObjProp(this.Metadata, this, 'aux_out_off_t', ... + 'comment', '(s)'); + + % Software parameters + addObjProp(this.Metadata, this, 'trig_threshold', 'comment',... + '(V), threshold for starting a ringdown record'); + addObjProp(this.Metadata, this, 'record_time', ... + 'comment', '(s)'); + addObjProp(this.Metadata, this, 'downsampled_rate', ... + 'comment', ['(samples/s), rate to which a ringown ', ... + 'trace is downsampled with averaging after acquisition']); + addObjProp(this.Metadata, this, 'auto_save', 'comment', '(s)'); + + % Adaptive measurement oscillator + addObjProp(this.Metadata, this, 'adaptive_meas_osc', ... + 'comment', ['If true the measurement oscillator ', ... + 'frequency is adjusted during ringdown']); + addObjProp(this.Metadata, this, 'ad_osc_margin'); + addObjProp(this.Metadata, this, 'fft_length', ... + 'comment', '(points)'); + + % Timer poll parameters + addParam(this.Metadata, 'poll_period', [],... + 'comment', '(s)'); + addObjProp(this.Metadata, this, 'poll_duration', ... + 'comment', '(s)'); + addObjProp(this.Metadata, this, 'poll_timeout', ... + 'comment', '(ms)'); + end + end + + %% Set and get methods. + methods + function set.downsample_n(this, val) + n = round(val); + assert(n>=1, ['Number of points for trace averaging must ', ... + 'be greater than 1']) + this.downsample_n = n; + end + + function set.downsampled_rate(this, val) + dr = this.demod_rate; + + % Downsampled rate should not exceed the data transfer rate + val = min(val, dr); + + % Round so that the averaging is done over an integer number of + % points + this.downsample_n = round(dr/val); + end + + function val = get.downsampled_rate(this) + val = this.demod_rate/this.downsample_n; + end + + function set.fft_length(this, val) + + % Round val to the nearest 2^n to make the calculation of + % Fourier transform efficient + n = round(log2(max(val, 1))); + this.fft_length = 2^n; + end + + function val = get.fft_rbw(this) + val = this.demod_rate/this.fft_length; + end + + function set.fft_rbw(this, val) + assert(val>0,'FFT resolution bandwidth must be greater than 0') + % Rounding of fft_length to the nearest integer is handled by + % its own set method + + this.fft_length = this.demod_rate/val; + end + + function set.n_avg(this, val) + this.AvgTrace.n_avg = val; + end + + function val = get.n_avg(this) + val = this.AvgTrace.n_avg; + end + + function val = get.avg_count(this) + val = this.AvgTrace.avg_count; + end + + function set.aux_out_on_t(this, val) + assert(val>0.001, ... + 'Aux out on time must be greater than 0.001 s.') + this.aux_out_on_t = val; + end + + function set.aux_out_off_t(this, val) + assert(val>0.001, ... + 'Aux out off time must be greater than 0.001 s.') + this.aux_out_off_t = val; + end + + function set.enable_acq(this, val) + this.enable_acq = logical(val); + end + + function val = get.poll_period(this) + val = this.PollTimer.Period; + end + end +end + diff --git a/@MyZiScopeFt/MyZiScopeFt.m b/Instrument classes/@MyZiScopeFt/MyZiScopeFt.m similarity index 52% rename from @MyZiScopeFt/MyZiScopeFt.m rename to Instrument classes/@MyZiScopeFt/MyZiScopeFt.m index 25f68d0..17a0760 100644 --- a/@MyZiScopeFt/MyZiScopeFt.m +++ b/Instrument classes/@MyZiScopeFt/MyZiScopeFt.m @@ -1,217 +1,250 @@ % Spectrum analyzer based on Zurich Instruments UHFLI or MFLI -classdef MyZiScopeFt < MyZiLi & MyDataSource +classdef MyZiScopeFt < MyZiLockIn & MyDataSource & MyGuiCont - properties (GetAccess=public, SetAccess={?MyClassParser}) - n_scope=1 % number of hardware scope - n_ch=1 % number of scope channel + properties (GetAccess = public, SetAccess = {?MyClassParser}) + n_scope = 1 % number of hardware scope + n_ch = 1 % number of scope channel % Input numbers between 1 and 148 correspond to various signals % including physical inputs, outputs, demodulator channels and - % results of arthmetic operations. See the LabOne user interface - % for the complete list of choices and corresponding numbers. - % This number is shifted by +1 compare to the hardware node - % enumeration as usual. - signal_in=1 + % the results of arthmetic operations. See the LabOne user + % interface for the complete list of choices and corresponding + % numbers. This number is shifted by +1 compare to the hardware + % node enumeration as usual. + signal_in = 1 % Deas time between scope frame acquisitions. Smaller time results % in faster averaging but may not look nice during real time % gui update. - trigholdoff=0.02 % seconds + trigholdoff = 0.02 % seconds end - properties (Access=private) - scope_module % 'handle' (in quotes) to a ZI software scope module + properties (Access = private) + scope_module % 'handle' (in quotes) of a ZI software scope module PollTimer % Timer that regularly reads data drom the scope TmpTrace % Temporary variable used for averaging end - properties (Dependent=true) + properties (Dependent = true) scope_path - - scope_rate % samples/sec - n_pt % length of scope wave fft_rbw % Spacing between fft bins + poll_period end events NewWave % Triggered when the scope acquires new waves end - methods (Access=public) + methods (Access = public) - function this = MyZiScopeFt(dev_serial, varargin) - this=this@MyZiLi(dev_serial); - - P=MyClassParser(this); - addRequired(P, dev_serial, @ischar) - % Poll timer period - addParameter(P,'poll_period',0.042,@isnumeric); - processInputs(P, this, dev_serial, varargin{:}); + function this = MyZiScopeFt(varargin) + P = MyClassParser(this); + addParameter(P, 'poll_period', 0.1, @isnumeric); + processInputs(P, this, varargin{:}); % Trace object in this case is directly used for averaging - this.Trace=MyAvgTrace(... - 'name_x','Time',... - 'unit_x','s',... - 'name_y','Magnitude r',... - 'unit_y','V'); - this.TmpTrace=MyTrace(); - - this.PollTimer=timer(... - 'ExecutionMode','fixedSpacing',... - 'Period',P.Results.poll_period,... - 'TimerFcn',@(~,~)pollTimerCallback(this)); + this.Trace = MyAvgTrace(); + this.TmpTrace = MyTrace(... + 'name_x', 'Frequency',... + 'unit_x', 'Hz',... + 'name_y', 'Power',... + 'unit_y', '$\mathrm{V}^2/\mathrm{Hz}$'); + + this.PollTimer = timer(... + 'ExecutionMode', 'fixedSpacing',... + 'Period', P.Results.poll_period,... + 'TimerFcn', @this.pollTimerCallback); + + createApiSession(this); + createCommandList(this); end function delete(this) + % delete function should never throw errors, so protect % statements with try-catch try stopPoll(this) - catch + catch ME warning(['Could not usubscribe from the scope node ', ... - 'or stop the poll timer.']) + 'or stop the poll timer. Error: ' ME.message]) end + % Clear the module's thread. try ziDAQ('clear', this.scope_module); - catch - warning('Could not clear the scope module.') + catch ME + warning(['Could not clear the scope module. Error: ' ... + ME.message]) end + % Delete timers to prevent them from running indefinitely in % the case of program crash try delete(this.PollTimer) catch warning('Could not delete the poll timer.') end end function startPoll(this) + % Configure hardware scope + % Signal input - path=sprintf('%s/channels/%i/inputselect', ... + path = sprintf('%s/channels/%i/inputselect', ... this.scope_path, this.n_ch); ziDAQ('setInt', path, this.signal_in-1); + + % Enable data transfer from only one scope channel + ziDAQ('setInt', [this.scope_path '/channel'], 1); + % Disable segmented mode of data transfer. This mode is only % useful if records longer than 5Mpts are required. ziDAQ('setInt', [this.scope_path '/segments/enable'], 0); + % Take continuous records ziDAQ('setInt', [this.scope_path '/single'], 0); + % Disable the scope trigger ziDAQ('setInt', [this.scope_path '/trigenable'], 0); + % The scope hold off time inbetween acquiring triggers (still % relevant if triggering is disabled). ziDAQ('setDouble', [this.scope_path '/trigholdoff'], ... this.trigholdoff); + % Enable the scope ziDAQ('setInt', [this.scope_path '/enable'], 1); % Initialize and configure a software Scope Module. this.scope_module = ziDAQ('scopeModule'); + % Do not average ziDAQ('set', this.scope_module, ... 'scopeModule/averager/weight', 1); % Set the Scope Module's mode to return frequency domain data. ziDAQ('set', this.scope_module, 'scopeModule/mode', 3); + % Use rectangular window function. ziDAQ('set', this.scope_module, 'scopeModule/fft/window', 0); ziDAQ('set', this.scope_module, 'scopeModule/fft/power', 1); ziDAQ('set', this.scope_module, ... 'scopeModule/fft/spectraldensity', 1); ziDAQ('subscribe', this.scope_module, ... [this.scope_path '/wave']); ziDAQ('execute', this.scope_module); start(this.PollTimer); end function stopPoll(this) stop(this.PollTimer); ziDAQ('finish', this.scope_module); end - function pollTimerCallback(this) + function pollTimerCallback(this, ~, ~) Data = ziDAQ('read', this.scope_module); if ziCheckPathInData(Data, [this.scope_path,'/wave']) + % Get the list of scope waves recorded since the previous % poll - new_waves=Data.(this.dev_id).scopes(this.n_scope).wave; + new_waves = Data.(this.dev_id).scopes(this.n_scope).wave; + % Add waves to the average trace for i=1:length(new_waves) - dt=new_waves{i}.dt; - n=double(new_waves{i}.totalsamples); + dt = new_waves{i}.dt; + n = double(new_waves{i}.totalsamples); + % Calculate the frequency axis - this.TmpTrace.x=linspace(0, (1-1/n)/(2*dt), n); - this.TmpTrace.y=new_waves{i}.wave; - is_compl=addAverage(this.Trace, this.TmpTrace); + this.TmpTrace.x = linspace(0, (1-1/n)/(2*dt), n); + this.TmpTrace.y = new_waves{i}.wave; + is_compl = addAverage(this.Trace, this.TmpTrace); if is_compl && strcmpi(this.Trace.avg_type, 'lin') triggerNewData(this); end end + notify(this, 'NewWave'); end end + end + + methods (Access = protected) + function createCommandList(this) + addCommand(this, 'scope_rate', ... + 'readFcn', @this.readScopeRate, ... + 'writeFcn', @this.writeScopeRate, ... + 'info', '(samples/s)'); + + addCommand(this, 'n_pt', ... + 'readFcn', @this.readNpt, ... + 'writeFcn', @this.writeNpt, ... + 'info', 'Scope wave length'); + end - function Hdr=readHeader(this) - Hdr=readHeader@MyZiLi(this); - addObjProp(Hdr, this, 'n_scope', 'comment', ... + function createMetadata(this) + createMetadata@MyZiLockIn(this); + + addObjProp(this.Metadata, this, 'n_scope', 'comment', ... 'Hardware scope number'); - addObjProp(Hdr, this, 'n_ch', 'comment', ... + addObjProp(this.Metadata, this, 'n_ch', 'comment', ... 'Scope channel'); - addObjProp(Hdr, this, 'signal_in', 'comment', ... + addObjProp(this.Metadata, this, 'signal_in', 'comment', ... 'Signal input number'); - addObjProp(Hdr, this, 'trigholdoff', 'comment', ... + addObjProp(this.Metadata, this, 'trigholdoff', 'comment', ... ['(s), the scope hold off time inbetween acquiring ' ... 'triggers']); - addParam(Hdr, this.name, 'poll_period', ... + addParam(this.Metadata, 'poll_period', ... this.PollTimer.Period, 'comment', '(s)'); end - end - - %% Set and get methods - - methods - function val=get.scope_path(this) - val=sprintf('/%s/scopes/%i',this.dev_id,this.n_scope-1); - end - function val=get.scope_rate(this) - tn=ziDAQ('getDouble', [this.scope_path '/time']); - val=this.clockbase/(2^tn); + function val = readScopeRate(this) + tn = ziDAQ('getDouble', [this.scope_path '/time']); + val = this.clockbase/(2^tn); end - function set.scope_rate(this, val) - tn=round(log2(this.clockbase/val)); - % Trim to be withn 0 and 16 - tn=max(0,tn); - tn=min(tn, 16); + function writeScopeRate(this, val) + tn = round(log2(this.clockbase/val)); + + % Trim to withn 0 and 16 + tn = max(0,tn); + tn = min(tn, 16); ziDAQ('setDouble', [this.scope_path '/time'], tn); clearData(this.Trace); - notify(this, 'NewSetting'); end - function val=get.fft_rbw(this) - l=length(this.Trace.x); + function val = readNpt(this) + val = ziDAQ('getDouble', [this.scope_path '/length']); + end + + function writeNpt(this, val) + ziDAQ('setDouble', [this.scope_path '/length'], val); + clearData(this.Trace); + end + end + + methods + function val = get.scope_path(this) + val = sprintf('/%s/scopes/%i',this.dev_id,this.n_scope-1); + end + + function val = get.fft_rbw(this) + l = length(this.Trace.x); if l>=2 - val=this.Trace.x(2)-this.Trace.x(1); + val = this.Trace.x(2)-this.Trace.x(1); else - val=Inf; + val = Inf; end end - function val=get.n_pt(this) - val=ziDAQ('getDouble', [this.scope_path '/length']); - end - - function set.n_pt(this, val) - ziDAQ('setDouble', [this.scope_path '/length'], val); - clearData(this.Trace); - notify(this, 'NewSetting'); + function val = get.poll_period(this) + val = this.PollTimer.Period; end end end diff --git a/Local/LocalInstrumentControlSettings.mat b/Local/LocalInstrumentControlSettings.mat index 260b34c..568e72f 100644 Binary files a/Local/LocalInstrumentControlSettings.mat and b/Local/LocalInstrumentControlSettings.mat differ diff --git a/Local/runDaq.m b/Local/runDaq.m deleted file mode 100644 index ce030e5..0000000 --- a/Local/runDaq.m +++ /dev/null @@ -1,8 +0,0 @@ -% General Plot -function runDaq() - name = 'Daq'; - C = MyCollector.instance(); - Daq = MyDaq('collector_handle',C,'global_name',name); - assignin('base',name,Daq); -end - diff --git a/Local/runDiodeLaserScan.m b/Local/runDiodeLaserScan.m deleted file mode 100644 index f2b384d..0000000 --- a/Local/runDiodeLaserScan.m +++ /dev/null @@ -1,17 +0,0 @@ -%show_in_daq=true -function runDiodeLaserScan() - % Get the unique instance of MyCollector - C = MyCollector.instance(); - - name='DiodeLaserScan'; - if ~ismember(name, C.running_instruments) - % Create an instrument instance - Instr=DiodeLaserScan('Scope','DPO4034_2','Laser','ECDL850He3',... - 'name','DiodeLaserScan'); - % Add instrument to Collector - addInstrument(C, Instr); - else - warning('DiodeLaserScan is already running') - end -end - diff --git a/Local/runLogViewer.m b/Local/runLogViewer.m index f360c72..bb0f425 100644 --- a/Local/runLogViewer.m +++ b/Local/runLogViewer.m @@ -1,9 +1,41 @@ -% Start a logger gui in dummy mode, which allows to browse existing logs +% Start a logger gui in dummy mode, which allows to browse existing logs, +% and add it to the collector as an app. function runLogViewer() - name = 'LogViewer'; - Lg=MyLogger(); - Lw=GuiLogger(Lg,'dummy_mode',true); - assignin('base',name,Lw); + name = 'ViewerLogger'; + + Collector = MyCollector.instance(); + + if ismember(name, Collector.running_instruments) + + % If LogViewer is already present in the Collector, do not create + % a new one, but rather bring focus to the existing one. + disp([name, ' is already running.']); + + Lg = getInstrument(Collector, name); + + % Bring the window of existing GUI to the front + try + setFocus(findFigure(Lg.Gui)); + catch ME + warning(ME.message) + end + else + + % Start GuiLogger in dummy mode + Lw = GuiLogger(); + Lw.Lg.Gui = Lw; + addInstrument(Collector, name, Lw.Lg, 'collect_header', false); + + % Display the instrument's name + Fig = findFigure(Lw); + Fig.Name = char(name); + + % Apply color scheme + applyLocalColorScheme(Fig); + + % Move the app figure to the center of the screen + centerFigure(Fig); + end end diff --git a/Local/runNewportTlbScan.m b/Local/runNewportTlbScan.m new file mode 100644 index 0000000..9025dcb --- /dev/null +++ b/Local/runNewportTlbScan.m @@ -0,0 +1,40 @@ +%data_source=true +%title=Ecdl850Scan + +function runNewportTlbScan() + + % Get the unique instance of MyCollector + C = MyCollector.instance(); + + name = 'runNewportTlbScan'; + if ~ismember(name, C.running_instruments) + + % Create an instrument instance + App = NewportTlbScan( ... + 'scope_name', 'Dpo4034nano2', ... + 'laser_name', 'ECDL850He3'); + + % Add instrument to Collector + addInstrument(C, name, App); + + % Display the instrument's name + Fig = findFigure(App); + if ~isempty(Fig) + Fig.Name = char(name); + else + warning('No UIFigure found to assign the name') + end + + % Apply color scheme + applyLocalColorScheme(Fig); + + % Move the app figure to the center of the screen + centerFigure(Fig); + else + disp([name ' is already running']) + App = getInstrument(C, name); + + setFocus(App); + end +end + diff --git a/Logger/@MyLogger/MyLogger.m b/Logger/@MyLogger/MyLogger.m new file mode 100644 index 0000000..5058111 --- /dev/null +++ b/Logger/@MyLogger/MyLogger.m @@ -0,0 +1,301 @@ +% Generic logger that executes measFcn according to MeasTimer, stores the +% results and optionally continuously saves them. +% measFcn should be a function with no arguments. +% measFcn need to return a row vector of numbers in order to save the log +% in text format or display it. With other kinds of returned values the +% log can still be recorded, but not saved or dispalyed. + +classdef MyLogger < MyGuiCont + + properties (Access = public, SetObservable = true) + + % Timer object + MeasTimer timer + + % Function that provides data to be recorded + measFcn = @()0 + + % MyLog object to store the recorded data + Record MyLog + + % Format for displaying readings (column name: value) + disp_fmt = '\t%15s:\t%.5g' + + % Option for daily/weekly creation of a new log file + FileCreationInterval duration + end + + properties (SetAccess = protected, GetAccess = public) + + % If last measurement was succesful + % 0-false, 1-true, 2-never measured + last_meas_stat = 2 + end + + properties (Access = protected) + Metadata = MyMetadata.empty() + end + + events + + % Event that is triggered each time measFcn is successfully + % executed + NewMeasurement + + % Event for transferring data to the collector + NewData + end + + methods (Access = public) + function this = MyLogger(varargin) + P = MyClassParser(this); + addParameter(P, 'log_opts', {}, @iscell); + addParameter(P, 'enable_gui', false); + processInputs(P, this, varargin{:}); + + this.Record = MyLog(P.Results.log_opts{:}); + + % Create and confitugure timer + 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 + % function execution delays + this.MeasTimer.ExecutionMode = 'fixedSpacing'; + this.MeasTimer.TimerFcn = @this.loggerFcn; + + % Create GUI if necessary + this.gui_name = 'GuiLogger'; + if P.Results.enable_gui + createGui(this); + end + end + + function delete(this) + + % Stop and delete the timer + try + stop(this.MeasTimer); + catch ME + warning(['Could not stop measurement timer. Error: ' ... + ME.message]); + end + + try + delete(this.MeasTimer); + catch ME + warning(['Could not delete measurement timer. Error: ' ... + ME.message]); + end + end + + % Redefine start/stop functions for the brevity of use + function start(this) + if ~isempty(this.FileCreationInterval) && ... + isempty(this.Record.FirstSaveTime) + + % If run in the limited length mode, extend the record + % file name + createLogFileName(this); + end + + start(this.MeasTimer); + end + + function stop(this) + stop(this.MeasTimer); + end + + function bool = isrunning(this) + try + bool = strcmpi(this.MeasTimer.running, 'on'); + catch ME + warning(['Cannot check if the measurement timer is on. '... + 'Error: ' ME.message]); + + bool = false; + end + end + + % Trigger an event that transfers the data from one log channel + % to Daq + function triggerNewData(this, varargin) + + % Since the class does not have Trace property, a Trace must be + % supplied explicitly + Trace = toTrace(this.Record, varargin{:}); + EventData = MyNewDataEvent('Trace',Trace, 'new_header',false); + notify(this, 'NewData', EventData); + end + + % Display reading + function str = printReading(this, ind) + if isempty(this.Record.timestamps) + str = ''; + return + end + + % Print the last reading if index is not given explicitly + if nargin()< 2 + ind = length(this.Record.timestamps); + end + + switch ind + case 1 + prefix = 'First reading '; + case length(this.Record.timestamps) + prefix = 'Last reading '; + otherwise + prefix = 'Reading '; + end + + str = [prefix, char(this.Record.timestamps(ind)), newline]; + data_row = this.Record.data(ind, :); + + for i=1:length(data_row) + if length(this.Record.data_headers)>=i + lbl = this.Record.data_headers{i}; + else + lbl = sprintf('data%i', i); + end + str = [str,... + sprintf(this.disp_fmt, lbl, data_row(i)), newline]; %#ok + end + end + + % Generate a new file name for the measurement record + function createLogFileName(this, path, name, ext) + [ex_path, ex_name, ex_ext] = fileparts(this.Record.file_name); + + if ~exist('path', 'var') + path = ex_path; + end + + if ~exist('name', 'var') + name = ex_name; + end + + if ~exist('ext', 'var') + if ~isempty(ex_ext) + ext = ex_ext; + else + ext = this.Record.data_file_ext; + end + end + + % Remove the previous time stamp from the file name if exists + token = regexp(name, ... + '\d\d\d\d-\d\d-\d\d \d\d-\d\d (.*)', 'tokens'); + if ~isempty(token) + name = token{1}{1}; + end + + % Prepend a new time stamp + name = [datestr(datetime('now'),'yyyy-mm-dd HH-MM '), name]; + + file_name = fullfile(path, [name, ext]); + + % Ensure that the generated file name is unique + file_name = createUniqueFileName(file_name); + + this.Record.file_name = file_name; + end + + function Mdt = readSettings(this) + if isempty(this.Metadata) + this.Metadata = MyMetadata('title', class(this)); + + addParam(this.Metadata, 'meas_period', [], 'comment', ... + 'measurement period (s)'); + + addParam(this.Metadata, 'save_cont', [], 'comment', ... + 'If measurements are continuously saved (true/false)'); + + addParam(this.Metadata, 'file_creation_interval', [], ... + 'comment', ['The interval over which new data ' ... + 'files are created when saving continuously ' ... + '(days:hours:min:sec)']); + + addParam(this.Metadata, 'log_length_limit', [], ... + 'comment', ['The maximum number of points kept ' ... + 'in the measurement record']); + end + + % Update parameter values + this.Metadata.ParamList.meas_period = this.MeasTimer.Period; + this.Metadata.ParamList.save_cont = this.Record.save_cont; + this.Metadata.ParamList.file_creation_interval = ... + char(this.FileCreationInterval); + this.Metadata.ParamList.log_length_limit = ... + this.Record.length_lim; + + Mdt = copy(this.Metadata); + end + + % Configure the logger settings from metadata + function writeSettings(this, Mdt) + + % Stop the logger if it is presently running + stop(this); + + if isparam(Mdt, 'meas_period') + this.MeasTimer.Period = Mdt.ParamList.meas_period; + end + + if isparam(Mdt, 'save_cont') + this.Record.save_cont = Mdt.ParamList.save_cont; + end + + if isparam(Mdt, 'file_creation_interval') + this.FileCreationInterval = ... + duration(Mdt.ParamList.file_creation_interval); + end + + if isparam(Mdt, 'log_length_limit') + this.Record.length_lim = Mdt.ParamList.log_length_limit; + end + end + end + + methods (Access = protected) + + % Perform measurement and append point to the log + function loggerFcn(this, ~, event) + Time = datetime(event.Data.time); + try + meas_result = this.measFcn(); + this.last_meas_stat = 1; % last measurement ok + catch ME + warning(['Logger cannot take measurement at time = ',... + datestr(Time) '.\nError: ' ME.message]); + this.last_meas_stat = 0; % last measurement not ok + return + end + + if this.Record.save_cont && ... + ~isempty(this.FileCreationInterval) && ... + ~isempty(this.Record.FirstSaveTime) && ... + (Time - this.Record.FirstSaveTime) >= ... + this.FileCreationInterval + + % Switch to a new data file + createLogFileName(this); + end + + % Append measurement result together with time stamp + appendData(this.Record, Time, meas_result); + notify(this, 'NewMeasurement'); + end + end + + %% Set and get functions + methods + function set.measFcn(this, val) + assert(isa(val, 'function_handle'), ... + '''measFcn'' must be a function handle.'); + this.measFcn = val; + end + end +end + diff --git a/Logger/GuiLogger.mlapp b/Logger/GuiLogger.mlapp new file mode 100644 index 0000000..031011f Binary files /dev/null and b/Logger/GuiLogger.mlapp differ diff --git a/Measurement and analysis routines/AmplModCal/GuiAmplModCal.mlapp b/Measurement and analysis routines/AmplModCal/GuiAmplModCal.mlapp new file mode 100644 index 0000000..f1434fc Binary files /dev/null and b/Measurement and analysis routines/AmplModCal/GuiAmplModCal.mlapp differ diff --git a/Measurement and analysis routines/AmplModCal/MyAmplModCal.m b/Measurement and analysis routines/AmplModCal/MyAmplModCal.m new file mode 100644 index 0000000..807844c --- /dev/null +++ b/Measurement and analysis routines/AmplModCal/MyAmplModCal.m @@ -0,0 +1,226 @@ +% Routine for the calibration of amplitude modulation depth, alpha, +% using heterodyne signal spectrum. +% +% Alpha is defined in the following expression for amplitude-modulated +% complex amplitude of light: +% +% E_0(t) = A*(1+\alpha \cos(\Omega_{cal} t)) + +classdef MyAmplModCal < MyAnalysisRoutine + + properties (Access = public, SetObservable = true) + Data MyTrace + + % Minimum thereshold for peak search. If MinHeightCursor exists, it + % has priority over the programmatically set value. + min_peak_height double + + mod_freq = 0 % modulation frequency (Hz) + end + + properties (Access = public, Dependent = true, SetObservable = true) + enable_cursor + end + + properties (GetAccess = public, SetAccess = protected, ... + SetObservable = true) + Axes + Gui + + MinHeightCursor MyCursor + + alpha = 0 % Phase modulation depth + end + + properties (Access = protected) + + % Line that displays the positions of peaks found + PlottedPeaks + end + + methods (Access = public) + function this = MyAmplModCal(varargin) + p = inputParser(); + addParameter(p, 'Data', MyTrace()); + addParameter(p, 'Axes', [], @isaxes); + addParameter(p, 'enable_cursor', true, @islogical); + addParameter(p, 'enable_gui', true, @islogical); + parse(p, varargin{:}); + + this.Data = p.Results.Data; + this.Axes = p.Results.Axes; + + if ~isempty(this.Axes) && isvalid(this.Axes) + ylim = this.Axes.YLim; + pos = min(ylim(1)+0.1*(ylim(2)-ylim(1)), 10*ylim(1)); + + this.MinHeightCursor = MyCursor(this.Axes, ... + 'orientation', 'horizontal', ... + 'position', pos, ... + 'Label', 'Peak threshold', ... + 'Color', [0.6, 0, 0]); + + this.min_peak_height = pos; + + this.enable_cursor = p.Results.enable_cursor; + else + this.min_peak_height = 1e-12; + end + + % Gui is created right before the construction of object + % is over + if p.Results.enable_gui + this.Gui = GuiAmplModCal(this); + end + end + + function delete(this) + if ~isempty(this.PlottedPeaks) + delete(this.PlottedPeaks) + end + if ~isempty(this.MinHeightCursor) + delete(this.MinHeightCursor) + end + end + + % Calculate the depth of phase modulation from the hights of peaks + % in the spectrum + function calcAlpha(this) + min_y = this.min_peak_height; + + % Find peaks above the given threshold + % Returned values: [y, x, widths, prominences] + [peak_y, peak_x, peak_w, ~] = findpeaks( ... + this.Data.y, this.Data.x, 'MinPeakHeight', min_y); + + n_peaks = length(peak_y); + + assert(n_peaks >= 3, ['Less than 3 peaks are found in '... + 'the data with given threshold (' num2str(min_y) '). ' ... + 'Amplitude modulation depth cannot be calculated.']) + + % Find the central peak, which is not necessarily the highest + mean_freq = sum(peak_x.*peak_y)/sum(peak_y); + [~, cent_ind] = min(abs(peak_x-mean_freq)); + + % Take the integration width to be a few times the width of the + % central peak. + int_w = 6*peak_w(cent_ind); + + if isempty(this.mod_freq) || this.mod_freq<=0 + + % If an approximate value of modulation frequency is not + % known, take the central peak plus another two highest + % peaks of the spectrum + [~, sort_ind] = sort(peak_y, 'descend'); + + % Remove the central peak index from the set + sort_ind = setdiff(sort_ind, cent_ind, 'stable'); + + pm_sb_ind = sort(sort_ind(1:2), 'ascend'); + + sb_ind = [pm_sb_ind(1), cent_ind, pm_sb_ind(2)]; + else + + % Select two sidebands spaced by one modulation frequency + % from the central peak + mod_f = this.mod_freq; + scaled_peak_x = (peak_x - peak_x(cent_ind))/mod_f; + + % Find sidebands + [err_p, ind_p] = min(abs(scaled_peak_x - 1)); + [err_m, ind_m] = min(abs(scaled_peak_x + 1)); + + % Specify the tolerance to the mismatch between the + % frequencies of the found peaks and pre-defined mod_freq + mod_peak_tol = 0.1; + + if (err_p < mod_peak_tol) && (err_m < mod_peak_tol) + + % Add the found indices to the list of sideband + % peak indices + sb_ind = [ind_m, cent_ind, ind_p]; + else + + % Prompt user to specify approximate modulation + % frequency. Show warning dialog if running in a gui + % mode or output warning in the command line otherwise. + msg = ['Cannot identify modulation sidebands. ' ... + 'Please check the approximate value of ' ... + 'modulation frequency and try again.']; + + if ~isempty(this.Gui) + Wd = warndlg(msg, 'Warning'); + centerFigure(Wd); + else + warning(msg); + end + end + end + + peak_x = peak_x(sb_ind); + peak_y = peak_y(sb_ind); + + mod_f = (peak_x(3)-peak_x(1))/2; + + % Display the found peaks + if ~isempty(this.Axes) && isvalid(this.Axes) + if ~isempty(this.PlottedPeaks)&&isvalid(this.PlottedPeaks) + set(this.PlottedPeaks,'XData',peak_x,'YData',peak_y); + else + this.PlottedPeaks = line(this.Axes, ... + 'XData', peak_x, 'YData', peak_y, 'Color', 'r', ... + 'LineStyle', 'none', 'Marker', 'o'); + end + end + + % Calculate areas under the peaks + n_peaks = 3; + peak_int = zeros(1, n_peaks); + for i = 1:n_peaks + peak_int(i) = integrate(this.Data, peak_x(i)-int_w/2, ... + peak_x(i)+int_w/2); + end + + % Calculate alpha + this.alpha = sqrt(2*(peak_int(1)+peak_int(3))/peak_int(2)); + this.mod_freq = mod_f; + end + + function clearPeakDisp(this) + if ~isempty(this.PlottedPeaks) + delete(this.PlottedPeaks) + end + end + end + + % Set and get methods + methods + function set.enable_cursor(this, val) + if ~isempty(this.MinHeightCursor) && ... + isvalid(this.MinHeightCursor) + this.MinHeightCursor.Line.Visible = val; + end + end + + function val = get.enable_cursor(this) + if ~isempty(this.MinHeightCursor) && ... + isvalid(this.MinHeightCursor) + val = strcmpi(this.MinHeightCursor.Line.Visible, 'on'); + else + val = false; + end + end + + % The value of min_peak_hight is taken from the cursor if it is + % enabled, or it can be set programmatically if there is no cursor + function val = get.min_peak_height(this) + if this.enable_cursor + val = this.MinHeightCursor.value; + else + val = this.min_peak_height; + end + end + end +end + diff --git a/Measurement and analysis routines/AmplModCal/alpha_def.png b/Measurement and analysis routines/AmplModCal/alpha_def.png new file mode 100644 index 0000000..d7034ac Binary files /dev/null and b/Measurement and analysis routines/AmplModCal/alpha_def.png differ diff --git a/Measurement and analysis routines/AmplNoiseCal/GuiAmplNoiseCal.mlapp b/Measurement and analysis routines/AmplNoiseCal/GuiAmplNoiseCal.mlapp new file mode 100644 index 0000000..bb0181c Binary files /dev/null and b/Measurement and analysis routines/AmplNoiseCal/GuiAmplNoiseCal.mlapp differ diff --git a/Measurement and analysis routines/AmplNoiseCal/MyAmplNoiseCal.m b/Measurement and analysis routines/AmplNoiseCal/MyAmplNoiseCal.m new file mode 100644 index 0000000..ff0ee4d --- /dev/null +++ b/Measurement and analysis routines/AmplNoiseCal/MyAmplNoiseCal.m @@ -0,0 +1,174 @@ +% Routine for the calibration of Relative Intensity Noise (RIN) spectrum +% using a calibration tone with known amplitude modulation depth, alpha, +% defined in the following expression for the complex amplitude: +% +% E_0(t) = A*(1+\alpha \cos(\Omega_{cal} t)) + +classdef MyAmplNoiseCal < MyAnalysisRoutine + + properties (Access = public, SetObservable = true) + + % Thermomechanical spectrum + Data MyTrace + + % RIN spectrum + AmplSpectrum MyTrace + + % Cursors for the selection of calibration tone + CalCursors MyCursor + cal_range = [0, 0] + + % Amplitude modulation depth of the calibration tone, supplied + % externally + alpha = 0.1 + end + + properties (GetAccess = public, SetAccess = protected, ... + SetObservable = true) + Axes + Gui + + % Frequency of the calibration tone, found in the cal_range + cal_freq + + % Conversion factor between S_V and RIN defined such that + % RIN = cf*S_V + cf = 1 + end + + methods (Access = public) + function this = MyAmplNoiseCal(varargin) + p = inputParser(); + addParameter(p, 'Data', MyTrace()); + addParameter(p, 'Axes', [], @isaxes); + addParameter(p, 'enable_gui', true, @islogical); + parse(p, varargin{:}); + + this.Data = p.Results.Data; + this.Axes = p.Results.Axes; + + this.AmplSpectrum = MyTrace(); + + if ~isempty(this.Axes) + + % Add two sets of vertical cursors for the selection of + % integration ranges + xlim = this.Axes.XLim; + x1 = xlim(1)+0.4*(xlim(2)-xlim(1)); + x2 = xlim(1)+0.45*(xlim(2)-xlim(1)); + + this.CalCursors = ... + [MyCursor(this.Axes, ... + 'orientation', 'vertical', 'position', x1, ... + 'Label','Cal 1', 'Color', [0, 0, 0.6]), ... + MyCursor(this.Axes, ... + 'orientation', 'vertical', 'position', x2, ... + 'Label','Cal 2', 'Color', [0, 0, 0.6])]; + end + + % Gui is created right before the construction of object + % is over + if p.Results.enable_gui + this.Gui = GuiAmplNoiseCal(this); + end + end + + function delete(this) + if ~isempty(this.CalCursors) + delete(this.CalCursors); + end + end + + % Calculates cf + function calcConvFactor(this) + if isempty(this.Data) || isDataEmpty(this.Data) + warning('Data is empty'); + return + end + + cr = this.cal_range; + + % Find the frequency of calibration tone + ind = (this.Data.x>cr(1) & this.Data.x<=cr(2)); + freq = sum(this.Data.x(ind) .* this.Data.y(ind))/ ... + sum(this.Data.y(ind)); + + this.cal_freq = freq; + + % Calculate area under the calibration tone peak + area = integrate(this.Data, cr(1), cr(2)); + + % Average square of relative intensity excursions due to the + % calibration tone + vSqCt = 2*this.alpha^2; + + this.cf = vSqCt/area; + end + + % Convert data using pre-calculated conversion factor + function convertSpectrum(this) + if isempty(this.Data) || isDataEmpty(this.Data) + warning('Data is empty'); + return + end + + this.AmplSpectrum.x = this.Data.x; + this.AmplSpectrum.y = this.Data.y*this.cf; + + this.AmplSpectrum.name_x = this.Data.name_x; + this.AmplSpectrum.unit_x = 'Hz'; + + this.AmplSpectrum.name_y = 'RIN'; + this.AmplSpectrum.unit_y = '1/Hz'; + + % Update metadata + this.AmplSpectrum.UserMetadata = createMetadata(this); + + triggerNewAnalysisTrace(this, ... + 'Trace', copy(this.AmplSpectrum), 'analysis_type', 'rin'); + end + end + + methods (Access = protected) + function Mdt = createMetadata(this) + Mdt = MyMetadata('title', 'CalibrationParameters'); + + if ~isempty(this.Data.file_name) + + % If data file has name, indicate it + addParam(Mdt, 'source', this.Data.file_name, ... + 'comment', 'File containing raw data'); + end + + addParam(Mdt, 'alpha', this.alpha, ... + 'comment', 'Amplitude modulation depth'); + addParam(Mdt, 'cal_freq', this.cal_freq, ... + 'comment', ['Calibration tone frequency (' ... + this.Data.unit_x ')']); + addParam(Mdt, 'cf', this.cf, ... + 'comment', 'Conversion factor RIN = cf*S_V'); + end + end + + % Set and get methods + methods + + % Get the integration range for the calibration tone + function val = get.cal_range(this) + if ~isempty(this.CalCursors) && all(isvalid(this.CalCursors)) + + % If cursors exist, return the range between the + % cursors + xmin = min(this.CalCursors.value); + xmax = max(this.CalCursors.value); + + val = [xmin, xmax]; + else + + % Otherwise the value can be set programmatically + val = this.cal_range; + end + end + end +end + diff --git a/Measurement and analysis routines/DiodeLaserScan.mlapp b/Measurement and analysis routines/DiodeLaserScan.mlapp deleted file mode 100644 index be6311e..0000000 Binary files a/Measurement and analysis routines/DiodeLaserScan.mlapp and /dev/null differ diff --git a/Measurement and analysis routines/FreqNoiseCal/GuiFreqNoiseCal.mlapp b/Measurement and analysis routines/FreqNoiseCal/GuiFreqNoiseCal.mlapp new file mode 100644 index 0000000..36c493b Binary files /dev/null and b/Measurement and analysis routines/FreqNoiseCal/GuiFreqNoiseCal.mlapp differ diff --git a/Measurement and analysis routines/FreqNoiseCal/MyFreqNoiseCal.m b/Measurement and analysis routines/FreqNoiseCal/MyFreqNoiseCal.m new file mode 100644 index 0000000..8302c85 --- /dev/null +++ b/Measurement and analysis routines/FreqNoiseCal/MyFreqNoiseCal.m @@ -0,0 +1,208 @@ +% Routine for the calibration of frequency noise spectrum using a +% calibration tone with known phase modulation depth, beta, defined as +% follows: +% +% E_0(t) = A*Exp(-i\beta \cos(\Omega_{cal} t)) + +classdef MyFreqNoiseCal < MyAnalysisRoutine + + properties (Access = public, SetObservable = true) + + % Thermomechanical spectrum + Data MyTrace + + % Spectrum, calibrated in frequency units. It can be + % S_\omega + % S_f = S_\omega/(2pi)^2 + % sqrt(S_f) + FreqSpectrum MyTrace + + % The type of calculated frequency spectrum, + % S_\omega, S_f or sqrt(S_f) + spectrum_type = 'S_f' + + % Cursors for the selection of calibration tone + CalCursors MyCursor + cal_range = [0, 0] + + % Phase modulation depth of the calibration tone, supplied + % externally + beta = 0.1 + end + + properties (GetAccess = public, SetAccess = protected, ... + SetObservable = true) + Axes + Gui + + % Frequency of the calibration tone, found in the cal_range + cal_freq + + % Conversion factor between S_V and S_\omega defined such that + % S_\omega = cf*S_V + cf = 1 + end + + methods (Access = public) + function this = MyFreqNoiseCal(varargin) + p = inputParser(); + addParameter(p, 'Data', MyTrace()); + addParameter(p, 'Axes', [], @isaxes); + addParameter(p, 'enable_gui', true, @islogical); + parse(p, varargin{:}); + + this.Data = p.Results.Data; + this.Axes = p.Results.Axes; + + this.FreqSpectrum = MyTrace(); + + if ~isempty(this.Axes) + + % Add two sets of vertical cursors for the selection of + % integration ranges + xlim = this.Axes.XLim; + x1 = xlim(1)+0.4*(xlim(2)-xlim(1)); + x2 = xlim(1)+0.45*(xlim(2)-xlim(1)); + + this.CalCursors = ... + [MyCursor(this.Axes, ... + 'orientation', 'vertical', 'position', x1, ... + 'Label','Cal 1', 'Color', [0, 0, 0.6]), ... + MyCursor(this.Axes, ... + 'orientation', 'vertical', 'position', x2, ... + 'Label','Cal 2', 'Color', [0, 0, 0.6])]; + end + + % Gui is created right before the construction of object + % is over + if p.Results.enable_gui + this.Gui = GuiFreqNoiseCal(this); + end + end + + function delete(this) + if ~isempty(this.CalCursors) + delete(this.CalCursors); + end + end + + % Calculates cf + function calcConvFactor(this) + if isempty(this.Data) || isDataEmpty(this.Data) + warning('Data is empty'); + return + end + + cr = this.cal_range; + + % Find the frequency of calibration tone + ind = (this.Data.x>cr(1) & this.Data.x<=cr(2)); + freq = sum(this.Data.x(ind) .* this.Data.y(ind))/ ... + sum(this.Data.y(ind)); + + this.cal_freq = freq; + + % Calculate area under the calibration tone peak + area = integrate(this.Data, cr(1), cr(2)); + + % Average square of frequency excursions due to calibration + % tone + vSqCt = this.beta^2*(2*pi*freq)^2/2; + + this.cf = vSqCt/area; + end + + function convertSpectrum(this) + if isempty(this.Data) || isDataEmpty(this.Data) + warning('Data is empty'); + return + end + + this.FreqSpectrum.x = this.Data.x; + this.FreqSpectrum.name_x = this.Data.name_x; + this.FreqSpectrum.unit_x = 'Hz'; + + switch this.spectrum_type + case 'S_\omega' + this.FreqSpectrum.name_y = '$S_{\omega}$'; + this.FreqSpectrum.unit_y = 'rad$^2$/Hz'; + + this.FreqSpectrum.y = this.Data.y*this.cf; + case 'S_f' + this.FreqSpectrum.name_y = '$S_{\omega}/(2\pi)^2$'; + this.FreqSpectrum.unit_y = 'Hz$^2$/Hz'; + + this.FreqSpectrum.y = ... + this.Data.y*this.cf/(2*pi)^2; + case 'sqrt(S_f)' + this.FreqSpectrum.name_y ='$\sqrt{S_{\omega}}/(2\pi)$'; + this.FreqSpectrum.unit_y ='Hz/$\sqrt{\mathrm{Hz}}$'; + + this.FreqSpectrum.y = ... + sqrt(this.Data.y*this.cf)/(2*pi); + otherwise + error(['Unknown frequency spectrum type ' ... + this.spectrum_type]) + end + + % Update metadata + this.FreqSpectrum.UserMetadata = createMetadata(this); + + triggerNewAnalysisTrace(this, 'Trace', ... + copy(this.FreqSpectrum), 'analysis_type', 'freq_noise'); + end + end + + methods (Access = protected) + function Mdt = createMetadata(this) + Mdt = MyMetadata('title', 'CalibrationParameters'); + + if ~isempty(this.Data.file_name) + + % If data file has name, indicate it + addParam(Mdt, 'source', this.Data.file_name, ... + 'comment', 'File containing raw data'); + end + + addParam(Mdt, 'spectrum_type', this.spectrum_type); + addParam(Mdt, 'beta', this.beta, ... + 'comment', 'Phase modulation depth'); + addParam(Mdt, 'cal_freq', this.cal_freq, ... + 'comment', ['Calibration tone frequency (' ... + this.Data.unit_x ')']); + addParam(Mdt, 'cf', this.cf, ... + 'comment', 'Conversion factor S_\omega = cf*S_V'); + end + end + + % Set and get methods + methods + + % Get the integration range for the calibration tone + function val = get.cal_range(this) + if ~isempty(this.CalCursors) && all(isvalid(this.CalCursors)) + + % If cursors exist, return the range between the + % cursors + xmin = min(this.CalCursors.value); + xmax = max(this.CalCursors.value); + + val = [xmin, xmax]; + else + + % Otherwise the value can be set programmatically + val = this.cal_range; + end + end + + function set.spectrum_type(this, val) + val_list = {'S_\omega', 'S_f', 'sqrt(S_f)'}; + + assert(ismember(val, val_list), ['The value of ' ... + 'spectrum_type must be one of the following: ' ... + var2str(val_list)]) + this.spectrum_type = val; + end + end +end + diff --git a/Measurement and analysis routines/MyPeakInspector.mlapp b/Measurement and analysis routines/MyPeakInspector.mlapp deleted file mode 100644 index b896158..0000000 Binary files a/Measurement and analysis routines/MyPeakInspector.mlapp and /dev/null differ diff --git a/Measurement and analysis routines/MyTraceAdder.mlapp b/Measurement and analysis routines/MyTraceAdder.mlapp new file mode 100644 index 0000000..3a98ba5 Binary files /dev/null and b/Measurement and analysis routines/MyTraceAdder.mlapp differ diff --git a/Measurement and analysis routines/PhaseModCal/GuiPhaseModCal.mlapp b/Measurement and analysis routines/PhaseModCal/GuiPhaseModCal.mlapp new file mode 100644 index 0000000..1f8bb7d Binary files /dev/null and b/Measurement and analysis routines/PhaseModCal/GuiPhaseModCal.mlapp differ diff --git a/Measurement and analysis routines/PhaseModCal/MyPhaseModCal.m b/Measurement and analysis routines/PhaseModCal/MyPhaseModCal.m new file mode 100644 index 0000000..81b8b44 --- /dev/null +++ b/Measurement and analysis routines/PhaseModCal/MyPhaseModCal.m @@ -0,0 +1,257 @@ +% Routine for the calibration of beta-factor, characterizing the phase +% modulation of light, using heterodyne signal spectrum. +% +% Beta is defined in the following expression for phase-modulated complex +% amplitude of light: +% +% E_0(t) = A*Exp(-i\beta \cos(\Omega_{cal} t)) + +classdef MyPhaseModCal < MyAnalysisRoutine + + properties (Access = public, SetObservable = true) + Data MyTrace + + % Minimum thereshold for peak search. If MinHeightCursor exists, it + % has priority over the programmatically set value. + min_peak_height double + + mod_freq = 0 % modulation frequency (Hz) + end + + properties (Access = public, Dependent = true, SetObservable = true) + enable_cursor + end + + properties (GetAccess = public, SetAccess = protected, ... + SetObservable = true) + Axes + Gui + + MinHeightCursor MyCursor + + beta = 0 % Phase modulation depth + end + + properties (Access = protected) + + % Line that displays the positions of peaks found + PlottedPeaks + end + + methods (Access = public) + function this = MyPhaseModCal(varargin) + p = inputParser(); + addParameter(p, 'Data', MyTrace()); + addParameter(p, 'Axes', [], @isaxes); + addParameter(p, 'enable_cursor', true, @islogical); + addParameter(p, 'enable_gui', true, @islogical); + parse(p, varargin{:}); + + this.Data = p.Results.Data; + this.Axes = p.Results.Axes; + + if ~isempty(this.Axes) && isvalid(this.Axes) + ylim = this.Axes.YLim; + pos = min(ylim(1)+0.1*(ylim(2)-ylim(1)), 10*ylim(1)); + + this.MinHeightCursor = MyCursor(this.Axes, ... + 'orientation', 'horizontal', ... + 'position', pos, ... + 'Label', 'Peak threshold', ... + 'Color', [0.6, 0, 0]); + + this.min_peak_height = pos; + + this.enable_cursor = p.Results.enable_cursor; + else + this.min_peak_height = 1e-12; + end + + % Gui is created right before the construction of object + % is over + if p.Results.enable_gui + this.Gui = GuiPhaseModCal(this); + end + end + + function delete(this) + if ~isempty(this.PlottedPeaks) + delete(this.PlottedPeaks) + end + if ~isempty(this.MinHeightCursor) + delete(this.MinHeightCursor) + end + end + + % Calculate the depth of phase modulation from the hights of peaks + % in the spectrum + function calcBeta(this) + min_y = this.min_peak_height; + + % Find peaks above the given threshold + % Returned values: [y, x, widths, prominences] + [peak_y, peak_x, peak_w, ~] = findpeaks( ... + this.Data.y, this.Data.x, 'MinPeakHeight', min_y); + + n_peaks = length(peak_y); + + assert(n_peaks >= 3, ['Less than 3 peaks are found in '... + 'the data with given threshold (' num2str(min_y) '). ' ... + 'Phase modulation depth cannot be calculated.']) + + % Find the central peak, which is not necessarily the highest + mean_freq = sum(peak_x.*peak_y)/sum(peak_y); + [~, cent_ind] = min(abs(peak_x-mean_freq)); + + % Take the integration width to be a few times the width of the + % central peak. + int_w = 6*peak_w(cent_ind); + + % Check if the peaks are rougly equally spaced harmonics, if + % not, use the pre-specified value of modulation frequency to + % select the right peaks. + peak_x_diff = peak_x(2:n_peaks)-peak_x(1:n_peaks-1); + mod_f = mean(peak_x_diff); + + % Specify the tolerance to the mismatch of frequencies between + % the found modulation peaks + mod_peak_tol = 0.1; + + if max(abs(peak_x_diff-mod_f))/mod_f > mod_peak_tol + + % Try using the approximate value of modulation frequency + % that can be specified by the user. + disp(['Distances between the found peaks are not ' ... + 'regular, will use the pre-defined value of ' ... + 'modulation frequency to post select peaks.']); + + if isempty(this.mod_freq) || this.mod_freq<=0 + + % Prompt user to specify approximate modulation + % frequency. Show warning dialog if running in a gui + % mode or output warning in the command line otherwise. + if ~isempty(this.Gui) + Wd = warndlg(['Cannot identify modulation ' ... + 'sidebands automatically. Please input ' ... + 'an approximate value of modulation ' ... + 'frequency and try again.'], 'Warning'); + centerFigure(Wd); + else + warning(['An approximate value on modulation ' ... + 'frequency must be specified by setting ' ... + 'mod_freq property. Please specify the ' ... + 'frequency and try again.']); + end + + return + end + + mod_f = this.mod_freq; + end + + % Delete the peaks that do not appear at the expected + % frequencies of harmonics of the modulation frequency + scaled_peak_x = (peak_x - peak_x(cent_ind))/mod_f; + sb_ind = cent_ind; + for i = 1:ceil(n_peaks/2) + + % Iterate over the sideband index i and find pairs of + % sidebands + [err_p, ind_p] = min(abs(scaled_peak_x - i)); + [err_m, ind_m] = min(abs(scaled_peak_x + i)); + + if (err_p/i < mod_peak_tol) && (err_m/i < mod_peak_tol) + + % Add the found indices to the list of sideband + % peak indices + sb_ind = [ind_m, sb_ind, ind_p]; %#ok + else + break + end + end + + % Out of all peaks select sideband peaks that appear in pairs + peak_y = peak_y(sb_ind); + peak_x = peak_x(sb_ind); + + n_peaks = length(peak_y); + assert(n_peaks >= 3, ['Less than 3 peaks are found. ' ... + 'Phase modulation depth cannot be calculated.']) + + % Re-calculate the modulation frequency + mod_f = (peak_x(end)-peak_x(1))/(n_peaks-1); + + % Display the found peaks + if ~isempty(this.Axes) && isvalid(this.Axes) + if ~isempty(this.PlottedPeaks)&&isvalid(this.PlottedPeaks) + set(this.PlottedPeaks,'XData',peak_x,'YData',peak_y); + else + this.PlottedPeaks = line(this.Axes, ... + 'XData', peak_x, 'YData', peak_y, 'Color', 'r', ... + 'LineStyle', 'none', 'Marker', 'o'); + end + end + + % Calculate areas under the peaks + peak_int = zeros(1, n_peaks); + for i = 1:n_peaks + peak_int(i) = integrate(this.Data, peak_x(i)-int_w/2, ... + peak_x(i)+int_w/2); + end + + % Scale by the maximum area for better fit convergence + peak_int = peak_int/max(peak_int); + + % Find beta value by fitting + Ft = fittype('a*besselj(n, beta)^2', 'independent', 'n', ... + 'coefficients', {'a', 'beta'}); + Opts = fitoptions('Method', 'NonLinearLeastSquares',... + 'StartPoint', [1, 0.1],... + 'MaxFunEvals', 2000,... + 'MaxIter', 2000,... + 'TolFun', 1e-10,... + 'TolX', 1e-10); + + peak_ord = -floor(n_peaks/2):floor(n_peaks/2); + FitResult = fit(peak_ord(:), peak_int(:), Ft, Opts); + + % Store the result in class variables + this.beta = abs(FitResult.beta); + this.mod_freq = mod_f; + end + + function clearPeakDisp(this) + if ~isempty(this.PlottedPeaks) + delete(this.PlottedPeaks) + end + end + end + + % Set and get methods + methods + function set.enable_cursor(this, val) + if ~isempty(this.MinHeightCursor) && ... + isvalid(this.MinHeightCursor) + this.MinHeightCursor.Line.Visible = val; + end + end + + function val = get.enable_cursor(this) + if ~isempty(this.MinHeightCursor) && ... + isvalid(this.MinHeightCursor) + val = strcmpi(this.MinHeightCursor.Line.Visible, 'on'); + else + val = false; + end + end + + function val = get.min_peak_height(this) + if this.enable_cursor + val = this.MinHeightCursor.value; + else + val = this.min_peak_height; + end + end + end +end + diff --git a/Measurement and analysis routines/PhaseModCal/beta_def.png b/Measurement and analysis routines/PhaseModCal/beta_def.png new file mode 100644 index 0000000..9239afd Binary files /dev/null and b/Measurement and analysis routines/PhaseModCal/beta_def.png differ diff --git a/Measurement and analysis routines/g0Cal/Guig0Cal.mlapp b/Measurement and analysis routines/g0Cal/Guig0Cal.mlapp new file mode 100644 index 0000000..396cebb Binary files /dev/null and b/Measurement and analysis routines/g0Cal/Guig0Cal.mlapp differ diff --git a/Measurement and analysis routines/g0Cal/Myg0Cal.m b/Measurement and analysis routines/g0Cal/Myg0Cal.m new file mode 100644 index 0000000..40cac6c --- /dev/null +++ b/Measurement and analysis routines/g0Cal/Myg0Cal.m @@ -0,0 +1,259 @@ +% Calibration routine for the vacuum optomechanical coupling rate g0 + +classdef Myg0Cal < MyAnalysisRoutine & MyGuiCont + + properties (Access = public, SetObservable) + + % Thermomechanical spectrum + Data MyTrace + + % Cursors for the selection of calibration tone + CalCursors MyCursor + cal_range = [0, 0] + + % Cursors for the integration of thermomechanical noise + MechCursors MyCursor + mech_range = [0, 0] + + % Fitting routine used in the method is 'fit' + LorFit MyLorentzianFit + + % method of finding the area under the mechanical noise peak, + % integration or fit + method = 'integration' + + % Calibration parameters set by the user + beta = 0 % Phase modulation depth of the reference tone + T = 300 % Temperature (K) + + % Correction for dynamic backaction. Requires 'fit' method + % and reference quality factor. + correct_dba = false + ref_Q = 0 + end + + properties (GetAccess = public, SetAccess = protected, SetObservable) + Axes = matlab.graphics.axis.Axes.empty() + + % Parameters of the fitted mechanical Lorentzian + Q = 0 + lw = 0 + freq = 0 + + % Calibration result, g0l = g0/2pi + g0l = 0 + end + + properties (Access = protected) + Psl % listener to the PostSet event of 'method' property + end + + methods (Access = public) + function this = Myg0Cal(varargin) + P = MyClassParser(this); + addParameter(P, 'enable_gui', true, @islogical); + processInputs(P, this, varargin{:}); + + if ~isempty(this.Axes) + + % Add two sets of vertical cursors for the selection of + % integration ranges + xlim = this.Axes.XLim; + x1 = xlim(1)+0.4*(xlim(2)-xlim(1)); + x2 = xlim(1)+0.45*(xlim(2)-xlim(1)); + + this.CalCursors = ... + [MyCursor(this.Axes, ... + 'orientation', 'vertical', 'position', x1, ... + 'Label','Cal 1', 'Color', [0, 0, 0.6]), ... + MyCursor(this.Axes, ... + 'orientation', 'vertical', 'position', x2, ... + 'Label','Cal 2', 'Color', [0, 0, 0.6])]; + + x1 = xlim(2)-0.45*(xlim(2)-xlim(1)); + x2 = xlim(2)-0.4*(xlim(2)-xlim(1)); + + this.MechCursors = ... + [MyCursor(this.Axes, ... + 'orientation', 'vertical', 'position', x1, ... + 'Label','Mech 1', 'Color', [0.6, 0, 0]), ... + MyCursor(this.Axes, ... + 'orientation', 'vertical', 'position', x2, ... + 'Label','Mech 2', 'Color', [0.6, 0, 0])]; + end + + % This listener handles the initialization of fitting routine + % when the method is switched to 'fit' for the first time + this.Psl = addlistener(this, 'method', 'PostSet', ... + @(~,~)initFit(this)); + + % Gui is created right before the construction of object + % is over + if P.Results.enable_gui + createGui(this); + end + end + + function delete(this) + if ~isempty(this.Psl) + delete(this.Psl); + end + if ~isempty(this.CalCursors) + delete(this.CalCursors); + end + if ~isempty(this.MechCursors) + delete(this.MechCursors); + end + if ~isempty(this.LorFit) + delete(this.LorFit) + end + end + + % Calculate g0 + function calcg0(this) + if isempty(this.Data) || isDataEmpty(this.Data) + warning('Data is empty'); + return + end + + if this.beta <= 0 + warning('Phase modulation depth beta must be specified') + return + end + + cr = this.cal_range; + + % Find the frequency of calibration tone + ind = (this.Data.x>cr(1) & this.Data.x<=cr(2)); + cal_tone_freq = sum(this.Data.x(ind) .* this.Data.y(ind))/ ... + sum(this.Data.y(ind)); + + % Calculate area under the calibration tone peak + cal_tone_area = integrate(this.Data, cr(1), cr(2)); + + mr = this.mech_range; + + if isequal(this.method, 'integration') + mech_area = integrate(this.Data, mr(1), mr(2)); + + % Estimate mechanical frequency + ind = (this.Data.x>mr(1) & this.Data.x<=mr(2)); + this.freq = sum(this.Data.x(ind) .* this.Data.y(ind))/ ... + sum(this.Data.y(ind)); + elseif isequal(this.method, 'fit') + + % Select data within the mechanics range + ind = (this.Data.x>mr(1) & this.Data.x<=mr(2)); + this.LorFit.Data.x = this.Data.x(ind); + this.LorFit.Data.y = this.Data.y(ind); + + fitTrace(this.LorFit); + + % Extract fitted parameters + this.lw = this.LorFit.param_vals(2); % Linewidth in Hz + this.freq = this.LorFit.param_vals(3); % Frequency in Hz + this.Q = (this.freq/this.lw); % Q in millions + + % Our Lorentzian is normalized such that the area under + % the curve is equal to the amplitude parameter + mech_area = this.LorFit.param_vals(1); + + if this.correct_dba + + % Apply correction for the cooling or amplification + % of mechanical motion by dynamic backaction + mech_area = mech_area*this.ref_Q/this.Q; + end + end + + % Boltzmann and Planck constants + k_b = 1.3806e-23; % J/K + h = 6.62607e-34; % m^2*kg/s + + % Equilibrium thermal occupation of the oscillator + n_th = k_b*this.T/(h*this.freq); + + % Calculate g0 + this.g0l = this.beta*cal_tone_freq* ... + sqrt(mech_area/(cal_tone_area*4*n_th)); + end + end + + methods (Access = protected) + + % Initialize a new fit object if it was not created yet and if the + % method is 'fit' + function initFit(this) + if isequal(this.method, 'fit') + + % Make sure that the fit object is initialized + if isempty(this.LorFit) || ~isvalid(this.LorFit) + + % Select data within the mechanics range + ind = (this.Data.x>this.mech_range(1) & ... + this.Data.x<=this.mech_range(2)); + + this.LorFit = MyLorentzianFit('Axes', this.Axes, ... + 'x', this.Data.x(ind), 'y', this.Data.y(ind), ... + 'enable_range_cursors', false, ... + 'enable_gui', false); + end + + genInitParams(this.LorFit); + else + if ~isempty(this.LorFit) && isvalid(this.LorFit) + + % Remove the fit curve from the plot + clearFit(this.LorFit); + end + end + end + end + + methods + function set.method(this, val) + assert(ischar(val), ... + 'Value assigned as ''method'' must be a character string') + + val = lower(val); + assert(isequal(val, 'integration') || isequal(val, 'fit'), ... + '''method'' must be ''integration'' or ''fit''') + + this.method = val; + end + + % Get the integration range for the calibration tone + function val = get.cal_range(this) + if ~isempty(this.CalCursors) && all(isvalid(this.CalCursors)) + + % If cursors exist, return the range between the + % cursors + xmin = min(this.CalCursors.value); + xmax = max(this.CalCursors.value); + + val = [xmin, xmax]; + else + + % Otherwise the value can be set programmatically + val = this.cal_range; + end + end + + % Get the range for the mechanical peak + function val = get.mech_range(this) + if ~isempty(this.MechCursors) && all(isvalid(this.MechCursors)) + + % If cursors exist, return the range between the + % cursors + xmin = min(this.MechCursors.value); + xmax = max(this.MechCursors.value); + + val = [xmin, xmax]; + else + + % Otherwise the value can be set programmatically + val = this.mech_range; + end + end + end +end diff --git a/Measurement and analysis routines/g0Cal/g0_cal.png b/Measurement and analysis routines/g0Cal/g0_cal.png new file mode 100644 index 0000000..031cd45 Binary files /dev/null and b/Measurement and analysis routines/g0Cal/g0_cal.png differ diff --git a/README.md b/README.md index 68c5993..ca1a0c8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ # Instrument-control The repository contains various MATLAB objects made for controlling instruments. The superclass `MyInstrument` has some basic functionality the subclasses can use. The MyTrace object is made for simple loading, saving and plotting of data and passing the data around with associated units and tags. -## MyFit Dependencies -MyFit needs the MATLAB GUI Layout Toolbox (https://ch.mathworks.com/matlabcentral/fileexchange/47982-gui-layout-toolbox) and the `UICOMPONENT` toolbox (http://ch.mathworks.com/matlabcentral/fileexchange/14583-uicomponent-expands-uicontrol-to-all-java-classes) for its GUI. + +## Dependencies +The software is tested with MATLAB 2019b +* Most instrument classes rely on [Instrument Control Toolbox](https://ch.mathworks.com/products/instrument.html?s_tid=FX_PR_info) + +* `MyFit` classes need [GUI Layout Toolbox](https://ch.mathworks.com/matlabcentral/fileexchange/47982-gui-layout-toolbox), [UICOMPONENT toolbox](http://ch.mathworks.com/matlabcentral/fileexchange/14583-uicomponent-expands-uicontrol-to-all-java-classes) and [Signal Processing Toolbox](https://ch.mathworks.com/products/signal.html) + +* The control class for HighFiness wavelengthmeters needs [MATLAB Support for MinGW-w64 C/C++ Compiler](https://ch.mathworks.com/matlabcentral/fileexchange/52848-matlab-support-for-mingw-w64-c-c-compiler) \ No newline at end of file diff --git a/Required packages/Cursorbar/+graphics/+internal/CursorbarContainer.m b/Required packages/Cursorbar/+graphics/+internal/CursorbarContainer.m deleted file mode 100644 index 5aa1483..0000000 --- a/Required packages/Cursorbar/+graphics/+internal/CursorbarContainer.m +++ /dev/null @@ -1,261 +0,0 @@ -classdef (ConstructOnLoad=true) CursorbarContainer < handle - % CursorbarContainer Container class for a graphics.Cursorbar objects. - % - % Container class for graphics.Cursorbar objects, where de-serialization - % restores the links. - % - % MCOS graphics cannot rely on custom machinery in load to restore - % Cursorbar listeners. Instead, create a CursorbarContainer to wrap the - % Cursorbar, which will restore the listeners when it is de-serialized. - % - % CursorbarContainer requires the new MATLAB graphics system that was - % introduced in R2014b - % - % Usage: - % CursorbarContainer() - Creates a CursorbarContainer - % without linked Cursorbar handle. - % CursorbarContainer(hCursorbar) - Creates a CursorbarContainer for - % the specified Cursorbar handle. - % - % CursorbarContainer Constructor: - % CursorbarContainer - CursorbarContainer constructor. - % - % CursorbarContainer Properties: - % Key - The key to the stored application data - % Target - Targets of the Cursorbar - % PropertyValues - Property-value pairs of the Cursorbar - % CursorbarHandle - The Cursorbar handle - % - % CursorbarContainer Methods: - % saveobj - Serialize the Target and PropertyValues. - % hasCursorbar - Checks if the CursorbarContainer contains - % the current cursorbar. - % - % CursorbarContainer Static Methods: - % loadobj - Restore the Cursorbar listeners on - % de-serialization. - % getCursorbars - Get all the cursorbars from the current - % figure. - % getContainers - Get all the containers from the current - % figure. - % - % Inherited handle Methods: - % addlistener - Add listener for event. - % delete - Delete a handle object. - % eq - Test handle equality. - % findobj - Find objects with specified property - % values. - % findprop - Find property of MATLAB handle object. - % ge - Greater than or equal relation. - % gt - Greater than relation. - % isvalid - Test handle validity. - % le - Less than or equal relation for handles. - % lt - Less than relation for handles. - % ne - Not equal relation for handles. - % notify - Notify listeners of event. - % - % Inherited handle Events: - % ObjectBeingDestroyed - Notifies listeners that a particular - % object has been destroyed. - % Web: - % Undocumented Matlab: Undocumented cursorbar object. - % - % See also: Cursorbar. - % - % Thanks to Yaroslav Don for his assistance in updating cursorbar for - % MATLAB Graphics and for his contribution of new functionality. - - % This class is based on linkaxes and matlab.graphics.internal.LinkAxes. - - % Copyright 2016 The MathWorks, Inc. - - - %% Properties - properties (Constant) - Key = 'GraphicsCursorbarContainer' % The key to the stored application data - end - - % -------------------------------------- - - properties (SetAccess = 'protected') - Target@handle % Targets of the Cursorbar - PropertyValues % Property-value pairs of the Cursorbar - end - - % -------------------------------------- - - properties (Transient) - CursorbarHandle@graphics.Cursorbar scalar % The Cursorbar handle - end - - properties (SetAccess = 'protected', Hidden) - PeerContainer@graphics.internal.CursorbarContainer % A handle to a container's Peer, if Cursorbar also has one - end - - %% Main Methods - - methods - - function hThis = CursorbarContainer(hCursorbar) - % CURSORBARCONTAINER A CursorbarContainer constructor. - % - % See also: CursorbarContainer. - - % Check MATLAB Graphics system version - if verLessThan('matlab','8.4.0') - error('graphics:internal:CursorbarContainer:CursorbarContainer:oldVersion', ... - 'CursorbarContainer requires the new MATLAB graphics system that was introduced in R2014b.'); - end - - % call handle constructor - hThis = hThis@handle; - - % set up the Cursorbar handles - % each container can hold only a single Cursorbar - if nargin==1 - validateattributes(hCursorbar,{'graphics.Cursorbar'},{'nonempty'}); - % - for i=numel(hCursorbar):-1:1 - hThis(i).CursorbarHandle = hCursorbar(i); - end - reshape(hThis,size(hCursorbar)); - end - end - - % -------------------------------------- - - function hThis = saveobj(hThis) - % SAVEOBJ Serialize the Target and PropertyValues. - - % set the target and the property values right before serialization; - if ~isempty(hThis.CursorbarHandle) - hThis.Target = hThis.CursorbarHandle.Target; - hThis.PropertyValues = { ... - 'BottomMarker', hThis.CursorbarHandle.BottomMarker, ... - 'CreateFcn', hThis.CursorbarHandle.CreateFcn, ... - 'CursorLineColor', hThis.CursorbarHandle.CursorLineColor, ... - 'CursorLineStyle', hThis.CursorbarHandle.CursorLineStyle, ... - 'CursorLineWidth', hThis.CursorbarHandle.CursorLineWidth, ... - 'DeleteFcn', hThis.CursorbarHandle.DeleteFcn, ... - 'DisplayName', hThis.CursorbarHandle.DisplayName, ... - 'FigureCallbacks', hThis.CursorbarHandle.FigureCallbacks, ... - 'HitTest', hThis.CursorbarHandle.HitTest, ... - 'Interruptible', hThis.CursorbarHandle.Interruptible, ... - 'Location', hThis.CursorbarHandle.Location, ... - 'Orientation', hThis.CursorbarHandle.Orientation, ... - 'Parent', hThis.CursorbarHandle.Parent, ... - 'Position', hThis.CursorbarHandle.Position, ... - 'SelectionHighlight', hThis.CursorbarHandle.SelectionHighlight, ... - 'ShowText', hThis.CursorbarHandle.ShowText, ... - 'Tag', hThis.CursorbarHandle.Tag, ... - 'TargetIntersections', hThis.CursorbarHandle.TargetIntersections, ... - 'TargetMarkerEdgeColor', hThis.CursorbarHandle.TargetMarkerEdgeColor, ... - 'TargetMarkerFaceColor', hThis.CursorbarHandle.TargetMarkerFaceColor, ... - 'TargetMarkerSize', hThis.CursorbarHandle.TargetMarkerSize, ... - 'TargetMarkerStyle', hThis.CursorbarHandle.TargetMarkerStyle, ... - 'TextDescription', hThis.CursorbarHandle.TextDescription, ... - 'TopMarker', hThis.CursorbarHandle.TopMarker, ... - 'UserData', hThis.CursorbarHandle.UserData, ... - 'Visible', hThis.CursorbarHandle.Visible ... - }; - end - end - - end - - % -------------------------------------- - - methods (Static = true) - function hThis = loadobj(hThis) - % LOADOBJ Restore the Cursorbar listeners on de-serialization. - - % create a new cursorbar if the target is valid - if ~isempty(hThis.Target) && all(isgraphics(hThis.Target)) && ~isempty(hThis.PropertyValues) - - % construct a new Cursorbar - hThat = hThis.PeerContainer; - if ~isempty(hThat) && ~isempty(hThat.CursorbarHandle) - % there is a valid Peer: create a Crossbar and set its values - hThis.CursorbarHandle = drawCrossbar(hThat.CursorbarHandle, hThis.PropertyValues{:}); - else - % create a new Cursorbar and set its values - hThis.CursorbarHandle = graphics.Cursorbar(hThis.Target, hThis.PropertyValues{:}); - end - end - - end - end - - %% Auxiliary Methods - - methods - - function tf = hasCursorbar(hThis,hCursorbar) - % HASCURSORBAR Checks if the CursorbarContainer contains the current cursorbar. - validateattributes(hCursorbar,{'graphics.Cursorbar'},{'scalar'}); - - % compare - tf = [hThis.CursorbarHandle]==hCursorbar; - tf = reshape(tf,size(hThis)); - end - - end - % -------------------------------------- - - methods (Static) - function hCursorbars = getCursorbars(hFig) - % GETCURSORBARS Get all the cursorbars from the current figure. - assert(ishghandle(hFig) && strcmp(hFig.Type,'figure'), ... - 'graphics:internal:CursorbarContainer:getCursorbars:notAFigure', ... - 'Input must be a valid figure handle.'); - % - hContainers = CursorbarContainer.getContainers(hFig); - if ~isempty(hContainers) - hCursorbars = [hContainers.CursorbarHandle]; - else - hCursorbars = graphics.GraphicsPlaceholder.empty; - end - end - - % -------------------------------------- - - function hContainers = getContainers(hFig) - % GETCONTAINERS Get all the containers from the current figure. - assert(ishghandle(hFig) && strcmp(hFig.Type,'figure'), ... - 'graphics:internal:CursorbarContainer:getContainers:notAFigure', ... - 'Input must be a valid figure handle.'); - % - hContainers = getappdata(hFig, graphics.internal.CursorbarContainer.Key); - end - end - - %% Protected Methods - - methods (Access = {?graphics.Cursorbar, ?graphics.internal.CursorbarContainer}) - - function setPeer(hThis,hThat) - % SETPEER Sets peer container handle. - - % set only the handle links - % Cursorbar is responsible for all the listener mechanisms - validateattributes(hThis,{'graphics.internal.CursorbarContainer'},{'scalar'}); - validateattributes(hThat,{'graphics.internal.CursorbarContainer'},{'scalar'}); - hThis.PeerContainer = hThat; - hThat.PeerContainer = hThis; - end - - % -------------------------------------- - - function removePeer(hThis) - % REMOVEPEER Removes peer container handle. - - % remove only the handle links - % Cursorbar is responsible for all the listener mechanisms - hThis.PeerContainer.PeerContainer = graphics.internal.CursorbarContainer.empty; - hThis.PeerContainer = graphics.internal.CursorbarContainer.empty; - end - - end - - -end diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/Cursorbar.m b/Required packages/Cursorbar/+graphics/@Cursorbar/Cursorbar.m deleted file mode 100644 index 6707ac4..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/Cursorbar.m +++ /dev/null @@ -1,1677 +0,0 @@ -classdef Cursorbar < graphics.Graphics & matlab.mixin.SetGet - % CURSORBAR Creates a cursor line attached to an axes or lines. - % - % The Cursorbar can be dragged interactively across the axes. If - % attached to a plot, the cursor points are updated as well. The - % Cursorbar can be either horizontal or vertical. - % - % Cursorbar requires the new MATLAB graphics system that was - % introduced in R2014b - % - % Usage: - % graphics.Cursorbar(hTarget) - Creates a Cursorbar on a target - % Axes or Chart. - % graphics.Cursorbar(hTarget, ...) - Creates a Cursorbar on a target - % Axes or Chart with additional - % property-value pairs. - % Example: - % x = linspace(0,20,101); - % y = sin(x); - % % - % h = plot(x,y); - % graphics.Cursorbar(h); - % - % Cursorbar Constructor: - % Cursorbar - Cursorbar constructor - % - % Cursorbar Properties: - % Annotation - Legend icon display style - % BeingDeleted - Deletion status of group - % BottomHandle - Handle to the bottom (left) edge marker - % BottomMarker - Top (right) edge marker shape - % BusyAction - Callback queuing - % ButtonDownFcn - Mouse-click callback - % Children - Children of Cursorbar - % CreateFcn - Creation callback - % CursorLineColor - Cursor line's color - % CursorLineStyle - Cursor line's style - % CursorLineWidth - Cursor line's width - % CursorbarOrientation - Permitted Cursorbar Orientation options - % CursorbarShowText - Permitted Cursorbar ShowText options - % DataCursorHandle - Handle to the Data Cursor - % DeleteFcn - Deletion callback - % DisplayHandle - Display text handle - % DisplayName - Text used by the legend - % FigureCallbacks - Additional Figure callbacks - % HandleVisibility - Visibility of object handle {'on','off'} - % HitTest - Response to mouse clicks captured by - % children - % Interruptible - Callback interruption - % Location - Location is a single value which is used - % to set the Position, based on the - % Orientation - % Orientation - Orientation of Cursorbar - % {'vertical','horizontal'} - % Parent - Parent of Cursorbar - % PermittedChartTargets - Classes of permitted targets - % PickableParts - Children that can capture mouse clicks - % Position - Position is used to set the location of - % main marker for the intersection - % Selected - Selection state - % SelectionHighlight - Display of selection handles when - % selected - % ShowText - Showing the Cursorbar Text {'on','off'} - % Tag - Tag to associate with the Cursorbar - % Target - Handle to the Target - % TargetIntersections - How many intersections are plotted - % {'multiple','single'} - % TargetMarkerEdgeColor - Target's marker outline color - % TargetMarkerFaceColor - Target's marker fill color - % TargetMarkerSize - Target's marker size - % TargetMarkerStyle - Target's marker style - % TextDescription - Type of text description - % {'short','long'} - % TopHandle - Handle to the top (right) edge marker - % TopMarker - Top (right) edge marker shape - % Type - Type of graphics object - % UIContextMenu - Context menu - % UpdateFcn - Update callback - % UserData - Data to associate with the Cursorbar - % object - % Visible - Visibility of Cursorbar {'on','off'} - % - % Cursorbar Methods: - % drawCrossbar - Draws a linked perpendicular bar - % duplicate - Duplicate the cursorbar to an identical - % one - % getCursorInfo - Get DataCursor info from Cursorbar - % getMarkerLocations - Return x,y position of the Cursorbar's - % intersection markers - % ishandle - Checks on self if valid handle - % isTargetAxes - Is the Target an axes - % isTargetChart - Is the Target a permitted chart object - % - % Inherited matlab.mixin.SetGet Methods: - % set - Set MATLAB object property values. - % get - Get MATLAB object properties. - % setdisp - Specialized MATLAB object property - % display. - % getdisp - Specialized MATLAB object property - % display. - % - % Inherited handle Methods: - % addlistener - Add listener for event. - % delete - Delete a handle object. - % eq - Test handle equality. - % findobj - Find objects with specified property - % values. - % findprop - Find property of MATLAB handle object. - % ge - Greater than or equal relation. - % gt - Greater than relation. - % isvalid - Test handle validity. - % le - Less than or equal relation for handles. - % lt - Less than relation for handles. - % ne - Not equal relation for handles. - % notify - Notify listeners of event. - % - % Inherited matlab.mixin.CustomDisplay Methods: - % details - Fully detailed formal object display. - % disp - Simple informal object display. - % display - Print variable name and display object. - % - % Inherited matlab.mixin.Heterogeneous Methods: - % cat - Concatenation for heterogeneous arrays. - % horzcat - Horizontal concatenation for - % heterogeneous arrays. - % vertcat - Vertical concatenation for - % heterogeneous arrays. - % - % Cursorbar Events: - % BeginDrag - Notifies listeners that the dragging of - % the Cursorbar has begun. - % EndDrag - Notifies listeners that the dragging of - % the Cursorbar has ended. - % UpdateCursorBar - Notifies listeners that the Cursorbar - % has been updated. - % - % Inherited handle Events: - % ObjectBeingDestroyed - Notifies listeners that a particular - % object has been destroyed. - % Web: - % Undocumented Matlab: Undocumented cursorbar object. - % - % See also: graphics.Cursorbar.Cursorbar, - % graphics.Cursorbar.getCursorInfo, - % graphics.Cursorbar.getMarkerLocations, - % graphics.Cursorbar.isTargetAxes, - % graphics.Cursorbar.isTargetChart, - % . - % cursorbar, crossbar, - % graphics.Cursorbar.duplicate, - % graphics.Cursorbar.drawCrossbar, - % . - % hobjects, - % graphics.Graphics, - % graphics.GraphicsPlaceholder, - % graphics.internal.CursorbarContainer. - % - % Thanks to Yaroslav Don for his assistance in updating cursorbar for - % MATLAB Graphics and for his contribution of new functionality. - - % Copyright 2003-2016 The MathWorks, Inc. - - % Change Log: - % 13 Feb 2015: First version posted on the MathWorks file exchange: - % Cursorbar - File Exchange - MATLAB Central. - % 14 May 2015: Added a custom display; added logarithmic scale support; minor bug fixes. - % XX Jan 2016: Added saving and loading functionality; added empty allocation; - % improved construction time; improved stability; minor bug fixes. - - %% Public Properties - - properties - Parent@handle % Parent of Cursorbar - - % Line objects used to create larger drag surfaces for cursor line - - DisplayHandle@handle % Display text handle - TopHandle@handle % Handle to the top (right) edge marker - BottomHandle@handle % Handle to the bottom (left) edge marker - - % Callbacks - - CreateFcn = '' % Creation callback - DeleteFcn = '' % Deletion callback - UpdateFcn = '' % Update callback - FigureCallbacks % Additional Figure callbacks - - % Identifiers - - Tag@char = '' % Tag to associate with the Cursorbar - UserData % Data to associate with the Cursorbar object - end - % -------------------------------------- - properties (Dependent) - - % Identifiers - - Annotation % Legend icon display style - DisplayName % Text used by the legend - - % Parent/Child - - Children % Children of Cursorbar - HandleVisibility % Visibility of object handle - - % Interactive Control - - Selected % Selection state - SelectionHighlight % Display of selection handles when selected - - % Callback Execution Control - - PickableParts % Children that can capture mouse clicks - HitTest % Response to mouse clicks captured by children - Interruptible % Callback interruption - BusyAction % Callback queuing - - % Creation and Deletion Control - - BeingDeleted % Deletion status of group - end - % -------------------------------------- - properties (SetObservable) - Location@double = 0 % Location is a single value which is used to set the Position, based on the Orientation - Position@double % Position is used to set the location of main marker for the intersection - - % Handles - - Target@handle % Handle to the Target - DataCursorHandle@handle % Handle to the Data Cursor - UIContextMenu@handle % Context menu - - % Cursor styles - - CursorLineColor@double = [0 0 0] % Cursor line's color - CursorLineStyle@char = '-' % Cursor line's style - CursorLineWidth@double = 2 % Cursor line's width - - % Marker styles - - TopMarker@char = 'v' % Top (right) edge marker shape - BottomMarker@char = '^' % Top (right) edge marker shape - TargetMarkerStyle@char = 'square' % Target's marker style - TargetMarkerSize@double= 8 % Target's marker size - TargetMarkerEdgeColor = [0 0 0] % Target's marker outline color - TargetMarkerFaceColor = 'none' % Target's marker fill color - end - % -------------------------------------- - properties (SetAccess=protected, SetObservable) - ButtonDownFcn % Mouse-click callback - end - % -------------------------------------- - properties (SetAccess=immutable) - Type = 'Cursorbar' % Type of graphics object - end - - % ============================================================= % - - %% Enumeration Properties - - properties (Constant) - CursorbarShowText = {'on','off'} % Permitted Cursorbar ShowText options - CursorbarOrientation = {'vertical','horizontal'} % Permitted Cursorbar Orientation options - CursorbarTargetIntersections = {'multiple','single'} % Permitted Cursorbar TargetIntersections options - CursorbarTextDescription = {'short','long'} % Permitted Cursorbar TextDescription options - - % Classes of permitted targets - PermittedChartTargets = { - 'matlab.graphics.chart.primitive.Line' - 'matlab.graphics.chart.primitive.Surface' - 'matlab.graphics.chart.primitive.Area' - 'matlab.graphics.chart.primitive.Bar' - 'matlab.graphics.chart.primitive.Contour' - 'matlab.graphics.chart.primitive.Histogram' - 'matlab.graphics.chart.primitive.Scatter' - 'matlab.graphics.chart.primitive.Stair' - 'matlab.graphics.chart.primitive.Stem' - 'matlab.graphics.primitive.Image' - 'matlab.graphics.primitive.Line' - 'matlab.graphics.primitive.Patch' - 'matlab.graphics.primitive.Surface' - } - end - % -------------------------------------- - properties (SetObservable) - ShowText@char = 'on' % Showing the Cursorbar Text {'on','off'} - Orientation@char = 'vertical' % Orientation of Cursorbar {'vertical','horizontal'} - Visible@char = 'on' % Visibility of Cursorbar {'on','off'} - TargetIntersections@char = 'multiple' % How many intersections are plotted {'multiple','single'} - TextDescription@char = 'short' % Type of text description {'short','long'} - end - - % ============================================================= % - - %% Hidden Properties - - properties (Hidden) - TargetXData@double % XData of Target - TargetYData@double % YData of Target - TargetZData@double % ZData of Target - TargetNData@double % NData of Target (which Target number out of several) - - CursorLineHandle@handle % Line object used to represent the cursor bar - TargetMarkerHandle@handle % Line objects used to represent the intersection points with the Target - end - % -------------------------------------- - properties (Hidden, SetAccess=protected) - DataCursorDummyTargetHandle@handle % Line object placeholder for the DataCursor - - SelfListenerHandles@handle % Self listeners - TargetListenerHandles@handle % Store listeners for Target - ExternalListenerHandles@handle % Store other external listeners - - PeerHandle@graphics.Cursorbar % Handle to another Cursorbar object - Container@graphics.internal.CursorbarContainer % Handle to the Cursorbar's container - end - % -------------------------------------- - properties (Hidden, GetAccess=public, SetAccess=immutable) - GroupHandle % The group containing all the objects - end - properties (GetAccess=protected, SetAccess=immutable) - ObjectBeingCreated = true % is the object being created - end - % -------------------------------------- - properties (Hidden, Constant) - % Classes of permitted 2D targets - Permitted2DTargets = { - 'matlab.graphics.chart.primitive.Surface' - 'matlab.graphics.chart.primitive.Area' - 'matlab.graphics.chart.primitive.Contour' - 'matlab.graphics.primitive.Image' - 'matlab.graphics.primitive.Patch' - 'matlab.graphics.primitive.Surface' - } - end - - % ============================================================= % - - %% Events - - events - BeginDrag % Notifies listeners that the dragging of the Cursorbar has begun. - EndDrag % Notifies listeners that the dragging of the Cursorbar has ended. - UpdateCursorBar % Notifies listeners that the Cursorbar has been updated. - end - - % ============================================================= % - - %% Public methods - - % constructor - methods - function hThis = Cursorbar(hTarget,varargin) - % CURSORBAR A Cursorbar constructor - % - % See also: Cursorbar. - - % Check MATLAB Graphics system version - if verLessThan('matlab','8.4.0') - error('graphics:Cursorbar:Cursorbar:oldVersion', ... - 'Cursorbar requires the new MATLAB graphics system that was introduced in R2014b.'); - end - - % input error check - narginchk(1,Inf); - - % validate correct property-value pair arguments - assert( mod(length(varargin),2)==0 && iscellstr(varargin(1:2:end)), ... - 'graphics:Cursorbar:BadParamValuePairs', ... - 'Invalid parameter/value pair arguments.') - - % Initialize Cursorbar - - % force hTarget to column vector of handles - hTarget = handle(hTarget(:)); - - % get Cursorbar Parent and Target - % don't set yet: this is to prevent creation of an empty - % figure when its HandleVisibility is off. - if numel(hTarget) == 1 - if all(hThis.isTargetChart(hTarget)) - hParent = handle(ancestor(hTarget,'axes')); - elseif isa(hTarget,'matlab.graphics.axis.Axes') - hParent = handle(hTarget); - else - % delete Cursorbar and error if Target isn't a line or axes - delete(hThis); - error(message('MATLAB:cursorbar:InvalidTarget')); - end - else - if all(hThis.isTargetChart(hTarget)) - hParent = handle(ancestor(hTarget(1),'axes')); - else - % delete Cursorbar if Target isn't a line or axes - delete(hThis); - error(message('MATLAB:cursorbar:InvalidTarget')); - end - end - - % set the parent & target property - set(hThis,'Parent',hParent,'Target',hTarget); - - % set GroupHandle - hThis.GroupHandle = hggroup('Parent',hParent); - hThis.GroupHandle.Visible = 'on'; - hThis.GroupHandle.PickableParts = 'visible'; - hThis.GroupHandle.HandleVisibility = 'off'; - hThis.GroupHandle.Tag = 'CursorbarGroup'; - hThis.GroupHandle.DisplayName = 'Cursorbar'; - hThis.GroupHandle.DeleteFcn = @(~,~)delete(hThis); - hThis.GroupHandle.Serializable = 'off'; % assert that none of the child handles is saved - - % add self property listeners - localAddSelfListeners(hThis); - - % add listeners for the target and its ancestors - localAddTargetListeners(hThis); - - % create Cursorbar and marker line - hLines = localCreateNewCursorBarLines(hThis); - set(hLines,'Parent',hThis.GroupHandle) - - % create context menu - hCtxtMenu = localCreateUIContextMenu(hThis); - set(hThis, 'UIContextMenu',hCtxtMenu) - set(hLines,'UIContextMenu',hCtxtMenu) - - % set up the cursorbar containers for saving and loading - localAddContainer(hThis,hParent); - - % set Position and Visible later, if they are specified as inputs - visiblepropval = ''; - positionpropval = []; - locationpropval = []; - orientationpropval = ''; - - % Loop through and set specified properties - if nargin>1 - for n = 1:2:length(varargin) - propname = varargin{n}; - propval = varargin{n+1}; - % - switch lower(propname) - case 'visible' - % Set the visible property at the end of this constructor - % since the visible listener requires the DataTip to - % be fully initialized. - visiblepropval = validatestring(propval,{'on','off'}); - case 'position' - % set the Position property just before setting Visible property - % force to a row vector - if numel(propval) > 3 || numel(propval) < 2 || ~isnumeric(propval) || ~all(isfinite(propval)) - error(message('MATLAB:graphics:cursorbar:invalidPosition')) - end - positionpropval = propval(:).'; - case 'location' - locationpropval = propval; - case 'orientation' - orientationpropval = validatestring(propval,hThis.CursorbarOrientation); - otherwise - set(hThis,propname,propval); - end - end - end - - % store vectors of Targets' XData and YData, sorted by Orientation - [x,y,z,n] = getTargetXYData(hThis,orientationpropval); - hThis.TargetXData = x; - hThis.TargetYData = y; - hThis.TargetZData = z; - hThis.TargetNData = n; - - % create new DataCursor - createNewDataCursor(hThis); - - % set Position - if ~isempty(positionpropval) - pos = positionpropval; - pos(3) = 0; % ensure 1-by-3 vector - % check Location - if ~isempty(locationpropval) - switch lower(orientationpropval) - case 'vertical', pos(1) = locationpropval; - case 'horizontal', pos(2) = locationpropval; - otherwise, pos(1) = locationpropval; % default vertical - end - end - % set Position for DataCursor from input - if isTargetAxes(hThis) - % if the Target is an axes, use the Position directly - set(hThis.DataCursorHandle.DataSource,'XData',pos(1),'YData',pos(2)); - else - % not an axes - [x,y] = closestvertex(hThis,pos,orientationpropval); - pos = [x y 0]; - hThis.DataCursorHandle.Position = pos; - end - else % Position not set - % set default Position - hAxes = get(hThis,'Parent'); - xLim = get(hAxes,'XLim'); - yLim = get(hAxes,'YLim'); - pos = [mean(xLim) mean(yLim) 0]; - % check Location - if ~isempty(locationpropval) - switch lower(orientationpropval) - case 'vertical', pos(1) = locationpropval; - case 'horizontal', pos(2) = locationpropval; - otherwise, pos(1) = locationpropval; % default vertical - end - end - % set Position for DataCursor - if isTargetAxes(hThis) - % set the DataCursor's Position to the middle of the axes - % use the 'pos' we already have - set(hThis.DataCursorHandle.DataSource,'XData',pos(1),'YData',pos(2)); - else - % choose the closest vertex to 'pos' - [x,y] = closestvertex(hThis,pos,orientationpropval); - pos = [x y 0]; - hThis.DataCursorHandle.Position = pos; - end - end - - % set Orientation - if ~isempty(orientationpropval) - set(hThis,'Orientation',orientationpropval); - end - - % set Position and Location - % set.Position silently sets Location - hThis.Position = pos; - - % Set the visible property if it was passed into the constructor - if ~isempty(visiblepropval) - set(hThis,'Visible',visiblepropval) - end - - % update - % release ObjectBeingCreated constraint - hThis.ObjectBeingCreated = false; - % update Cursorbar - update(hThis,[],[],'-nomove'); - - % apply user's CreateFcn - hThis.localApplyCreateFcn(hThis,[]); - end - end - - % ---------------------------------------- - - %% Set Methods - - methods - function s = setdisp(hThis) - % SETDISP Customize set method display. - s = set(hThis); - - % update - s.ShowText = graphics.Cursorbar.CursorbarShowText; - s.Orientation = graphics.Cursorbar.CursorbarOrientation; - s.TargetIntersections = graphics.Cursorbar.CursorbarTargetIntersections; - s.TextDescription = graphics.Cursorbar.CursorbarTextDescription; - % - s.CursorLineStyle = {'-','--',':','-.','none'}; - s.TopMarker = {'+','o','*','.','x','square','diamond','v','^','>','<','pentagram','hexagram','none'}; - s.BottomMarker = {'+','o','*','.','x','square','diamond','v','^','>','<','pentagram','hexagram','none'}; - s.TargetMarkerStyle = {'+','o','*','.','x','square','diamond','v','^','>','<','pentagram','hexagram','none'}; - s.TargetMarkerEdgeColor = {'none','flat','auto'}; - s.TargetMarkerFaceColor = {'none','flat','auto'}; - % - s.BusyAction = {'queue','cancel'}; - s.HandleVisibility = {'on','callback','off'}; - s.Interruptible = {'on','off'}; - s.HitTest = {'on','off'}; - s.Visible = {'on','off'}; - s.Selected = {'on','off'}; - s.SelectionHighlight = {'on','off'}; - s.PickableParts = {'visible','none','all'}; - - % show - if nargout==0 - disp(s); - end - end - % ---------------------------------------- - function set.ShowText(hThis,show) - try - show = validatestring(show,hThis.CursorbarShowText); - catch ME, - throwAsCaller(setmessage(ME,'ShowText')); - end - hThis.ShowText = show; - end - % ---------------------------------------- - function set.Orientation(hThis,orient) - try - orient = validatestring(orient,hThis.CursorbarOrientation); - catch ME, - throwAsCaller(setmessage(ME,'Orientation')); - end - hThis.Orientation = orient; - end - % ---------------------------------------- - function set.Visible(hThis,vis) - try - vis = validatestring(vis,{'on','off'}); - catch ME, - throwAsCaller(setmessage(ME,'Visible')); - end - hThis.Visible = vis; - end - % ---------------------------------------- - function set.TargetIntersections(hThis,tin) - try - tin = validatestring(tin,hThis.CursorbarTargetIntersections); - catch ME, - throwAsCaller(setmessage(ME,'TargetIntersections')); - end - hThis.TargetIntersections = tin; - end - % ---------------------------------------- - function set.TextDescription(hThis,des) - try - des = validatestring(des,hThis.CursorbarTextDescription); - catch ME, - throwAsCaller(setmessage(ME,'TextDescription')); - end - hThis.TextDescription = des; - end - % ---------------------------------------- - function set.Location(hThis,loc) - try - validateattributes(loc,{'double'},{'scalar','real','finite'}); - catch ME, - throwAsCaller(setmessage(ME,'Location')); - end - hThis.Location = loc; - end - % ---------------------------------------- - function set.Position(hThis,pos) - try - if numel(pos)==2, pos(3)=0; end % ensure a 3 element vector - validateattributes(pos,{'double'},{'row','numel',3,'real','finite'}); - catch ME, - throwAsCaller(setmessage(ME,'Position')); - end - hThis.Position = pos; - end - % ---------------------------------------- - function set.CreateFcn(hThis,fcn) - if ischar(fcn) || isa(fcn,'function_handle') ... - || ( iscell(fcn) && (ischar(fcn{1}) || isa(fcn{1},'function_handle')) ) - hThis.CreateFcn = fcn; - else - ME = MException('MATLAB:datatypes:callback:CreateCallback', ... - 'Callback value must be a string, a function handle, or a cell array containing string or function handle'); - throwAsCaller(setmessage(ME,'CreateFcn')); - end - end - % ---------------------------------------- - function set.DeleteFcn(hThis,fcn) - if ischar(fcn) || isa(fcn,'function_handle') ... - || ( iscell(fcn) && (ischar(fcn{1}) || isa(fcn{1},'function_handle')) ) - hThis.DeleteFcn = fcn; - else - ME = MException('MATLAB:datatypes:callback:DeleteCallback', ... - 'Callback value must be a string, a function handle, or a cell array containing string or function handle'); - throwAsCaller(setmessage(ME,'DeleteFcn')); - end - end - % ---------------------------------------- - function set.UpdateFcn(hThis,fcn) - if ischar(fcn) || isa(fcn,'function_handle') ... - || ( iscell(fcn) && (ischar(fcn{1}) || isa(fcn{1},'function_handle')) ) - hThis.UpdateFcn = fcn; - else - ME = MException('MATLAB:datatypes:callback:UpdateCallback', ... - 'Callback value must be a string, a function handle, or a cell array containing string or function handle'); - throwAsCaller(setmessage(ME,'UpdateFcn')); - end - end - end - - % ============================================================= % - - %% Dependent Methods - - methods - function set.Annotation(hThis,val) - try - hThis.GroupHandle.Annotation = val; - catch ME, - throwAsCaller(setmessage(ME,'Annotation')); - end - end - function val = get.Annotation(hThis) - val = hThis.GroupHandle.Annotation; - end - % ---------------------------------------- - function set.DisplayName(hThis,val) - try - hThis.GroupHandle.DisplayName = val; - catch ME, - throwAsCaller(setmessage(ME,'DisplayName')); - end - end - function val = get.DisplayName(hThis) - val = hThis.GroupHandle.DisplayName; - end - % ---------------------------------------- - function set.Children(~,~) - ME = MException('MATLAB:class:SetProhibited','You cannot set the read-only property ''Children'' of Class.'); - throwAsCaller(setmessage(ME,'Children')); - end - function val = get.Children(hThis) - val = hThis.GroupHandle.Children; - end - % ---------------------------------------- - function set.HandleVisibility(hThis,val) - try - hThis.GroupHandle.HandleVisibility = val; - catch ME, - throwAsCaller(setmessage(ME,'HandleVisibility')); - end - end - function val = get.HandleVisibility(hThis) - val = hThis.GroupHandle.HandleVisibility; - end - % ---------------------------------------- - function set.Selected(hThis,val) - try - hThis.GroupHandle.Selected = val; - catch ME, - throwAsCaller(setmessage(ME,'Selected')); - end - end - function val = get.Selected(hThis) - val = hThis.GroupHandle.Selected; - end - % ---------------------------------------- - function set.SelectionHighlight(hThis,val) - try - hThis.GroupHandle.SelectionHighlight = val; - catch ME, - throwAsCaller(setmessage(ME,'SelectionHighlight')); - end - end - function val = get.SelectionHighlight(hThis) - val = hThis.GroupHandle.SelectionHighlight; - end - % ---------------------------------------- - function set.PickableParts(~,~) - ME = MException('MATLAB:class:SetProhibited','You cannot set the read-only property ''PickableParts'' of Class.'); - throwAsCaller(setmessage(ME,'PickableParts')); - end - function val = get.PickableParts(hThis) - val = hThis.GroupHandle.PickableParts; - end - % ---------------------------------------- - function set.HitTest(hThis,val) - try - hThis.GroupHandle.HitTest = val; - catch ME, - throwAsCaller(setmessage(ME,'HitTest')); - end - end - function val = get.HitTest(hThis) - val = hThis.GroupHandle.HitTest; - end - % ---------------------------------------- - function set.Interruptible(hThis,val) - try - hThis.GroupHandle.Interruptible = val; - catch ME, - throwAsCaller(setmessage(ME,'Interruptible')); - end - end - function val = get.Interruptible(hThis) - val = hThis.GroupHandle.Interruptible; - end - % ---------------------------------------- - function set.BusyAction(hThis,val) - try - hThis.GroupHandle.BusyAction = val; - catch ME, - throwAsCaller(setmessage(ME,'BusyAction')); - end - end - function val = get.BusyAction(hThis) - val = hThis.GroupHandle.BusyAction; - end - % ---------------------------------------- - function set.BeingDeleted(hThis,val) - try - hThis.GroupHandle.BeingDeleted = val; - catch ME, - throwAsCaller(setmessage(ME,'BeingDeleted')); - end - end - function val = get.BeingDeleted(hThis) - val = hThis.GroupHandle.BeingDeleted; - end - end - - % ============================================================= % - - %% Sealed Methods - - methods (Sealed) - function tf = ishandle(hThis) - % ISHANDLE Checks on self if valid handle - % - % See also ishandle, graphics.Cursorbar. - tf = isvalid(hThis); - end - end - - % ============================================================= % - - %% Local (Protected Hidden) Methods - - methods (Access=protected, Hidden) - - function localApplyCreateFcn(hThis,obj,evd) - % LOCALAPPLYCREATEFCN Apply the create function. - localApplyCallbackFcn(hThis,obj,evd,'CreateFcn'); - end - - % -------------------------------------- - function localApplyDeleteFcn(hThis,obj,evd) - % LOCALAPPLYDELETEFCN Apply the delete function. - localApplyCallbackFcn(hThis,obj,evd,'DeleteFcn'); - end - - % -------------------------------------- - function localApplyUpdateFcn(hThis,obj,evd) - % LOCALAPPLYDELETEFCN Apply the delete function. - localApplyCallbackFcn(hThis,obj,evd,'UpdateFcn'); - end - - % -------------------------------------- - function localApplyCallbackFcn(hThis,obj,evd,callbackname) - % LOCALAPPLYCALLBACKFCN Apply some callback function. - func = hThis.(callbackname); - try % to apply delete function - switch class(func) - case 'char', eval(func); - case 'function_handle', feval(func,obj,evd); - case 'cell', feval(hThis.func{1},obj,evd,hThis.func{2:end}); - end - catch ME, % warn quietly - wME = setmessage(ME,callbackname); - warning(wME.identifier, wME.message); - end - end - - % -------------------------------------- - function hLines = localCreateNewCursorBarLines(hThis,~,~) - % LOCALCREATENEWCURSORBARLINES create lines for Cursorbar, and line for markers - - % Get axes and figure - hAxes = get(hThis,'Parent'); - hGroup = hThis.GroupHandle; - - % white line on dark axes, black line on light axes - AXCOLOR = get(hAxes,'Color'); - - % --------- cursor line --------- - lineprops = struct; - lineprops.Tag = 'DataCursorLine'; - lineprops.Parent = hGroup; - lineprops.XData = [NaN NaN]; - lineprops.YData = [NaN NaN]; - lineprops.Color = hThis.CursorLineColor; - % - % light colored axes - if sum(AXCOLOR) < 1.5 - lineprops.Color = [1 1 1]; - end - % - lineprops.Marker = 'none'; - lineprops.LineStyle = '-'; - lineprops.LineWidth = 2; - % - lineprops.Clipping = 'on'; - lineprops.XLimInclude = 'off'; % don't interfere with axes zooming - lineprops.YLimInclude = 'off'; % don't interfere with axes zooming - % - lineprops.HandleVisibility = 'off'; - lineprops.Visible = hThis.Visible; - lineprops.ButtonDownFcn = @hThis.localCursorButtonDownFcn; - lineprops.Serializable = 'off'; % don't save to file - - cursorline = line(lineprops); - hThis.CursorLineHandle = handle(cursorline); - hThis.ButtonDownFcn = lineprops.ButtonDownFcn; - - % --------- top,bottom affordances --------- - lineprops.XData = NaN; - lineprops.YData = NaN; - lineprops.MarkerFaceColor = hThis.CursorLineColor; - lineprops.LineStyle = 'none'; - % - % top - lineprops.Tag = 'DataCursorLineTop'; - lineprops.Marker = hThis.TopMarker; - % - topdragline = line(lineprops); - hThis.TopHandle = handle(topdragline); - % - % bottom - lineprops.Tag = 'DataCursorLineBottom'; - lineprops.Marker = hThis.BottomMarker; - % - bottomdragline = line(lineprops); - hThis.BottomHandle = handle(bottomdragline); - - % --------- marker line --------- - lineprops.Tag = 'DataCursorTargetMarker'; - lineprops.Marker = hThis.TargetMarkerStyle; - lineprops.MarkerSize = hThis.TargetMarkerSize; - lineprops.MarkerEdgeColor = hThis.TargetMarkerEdgeColor; - lineprops.MarkerFaceColor = hThis.TargetMarkerFaceColor; - lineprops.LineStyle = 'none'; - % - markerline = line(lineprops); - hThis.TargetMarkerHandle = handle(markerline); - - % combine handles - hLines = handle([ ... - cursorline; - topdragline; - bottomdragline; - markerline ]); - end - - % -------------------------------------- - function hCtxtMenu = localCreateUIContextMenu(hThis,~,~) - % LOCALCREATEUICONTEXTMENU - - if ismethod(hThis,'createUIContextMenu') - hCtxtMenu = hThis.createUIContextMenu(); - else - hCtxtMenu = hThis.defaultUIContextMenu(); - end - end - - % -------------------------------------- - function localAddTargetListeners(hThis,~,~) - % LOCALADDTARGETLISTENERS add listeners for Target and its parent axes - - % check Target - hTarget = hThis.Target; - if ~ishandle(hTarget) - return; - end - - % get handles for axes - hAxes = handle(get(hThis,'Parent')); - - % listen for changes to axes' Limits - axesLimProps = [ ... - findprop(hAxes,'XLim'); - findprop(hAxes,'YLim'); - findprop(hAxes,'ZLim')]; - l = event.proplistener(hAxes,axesLimProps,'PostSet',@hThis.localAxesLimUpdate); - - % Update if Target is line(s) and any target ...Data property changes - if ~isTargetAxes(hThis) - for n = 1:length(hTarget) - target_prop = [ ... - findprop(hTarget(n),'XData'); - findprop(hTarget(n),'YData'); - findprop(hTarget(n),'ZData')]; - l(end+1) = event.proplistener(hTarget(n),target_prop,'PostSet',... - @hThis.localTargetDataUpdate); %#ok - end - end - - % Listen to axes pixel bound resize events - axes_prop = findprop(hAxes,'PixelBound'); - l(end+1) = event.proplistener(hAxes,axes_prop, 'PostSet',... - @hThis.localAxesPixelBoundUpdate); - - % Clean up if Cursorbar or its Target is deleted - l(end+1) = event.listener(hThis.Target,'ObjectBeingDestroyed',... - @hThis.localTargetDestroy); - - % Store listeners - hThis.TargetListenerHandles = l; - end - - % -------------------------------------- - function localAddSelfListeners(hThis,~,~) - % LOCALADDSELFLISTENERS add listeners to Cursorbar's properties - - % Visible - l( 1 ) = event.proplistener(hThis,findprop(hThis,'Visible'),... - 'PostSet',@hThis.localSetVisible); - - % ShowText - l(end+1) = event.proplistener(hThis,findprop(hThis,'ShowText'),... - 'PostSet',@hThis.localSetShowText); - - % TargetIntersections - l(end+1) = event.proplistener(hThis,findprop(hThis,'TargetIntersections'),... - 'PostSet',@hThis.localSetTargetIntersections); - - % TextDescription - l(end+1) = event.proplistener(hThis,findprop(hThis,'TextDescription'),... - 'PostSet',@hThis.localSetTextDescription); - - % Orientation - l(end+1) = event.proplistener(hThis,findprop(hThis,'Orientation'),... - 'PostSet',@hThis.localSetOrientation); - - % Location - l(end+1) = event.proplistener(hThis,findprop(hThis,'Location'),... - 'PostSet',@hThis.localSetLocation); - - % Position - l(end+1) = event.proplistener(hThis,findprop(hThis,'Position'),... - 'PostSet',@hThis.localSetPosition); - - % UIContextMenu - l(end+1) = event.proplistener(hThis,findprop(hThis,'UIContextMenu'),... - 'PostSet',@hThis.localSetUIContextMenu); - - % ButtonDownFcn - l(end+1) = event.proplistener(hThis,findprop(hThis,'ButtonDownFcn'),... - 'PostSet', @hThis.localSetButtonDownFcn); - - % Target - l(end+1) = event.proplistener(hThis,findprop(hThis,'Target'),... - 'PreSet', @hThis.localPreSetTarget); - l(end+1) = event.proplistener(hThis,findprop(hThis,'Target'),... - 'PostSet', @hThis.localPostSetTarget); - l(end+1) = event.proplistener(hThis,findprop(hThis,'Target'),... - 'PostSet', @hThis.localAddTargetListeners); - - % Cursorbar appearance properties - p = [ ... - findprop(hThis,'CursorLineColor'); - findprop(hThis,'CursorLineStyle'); - findprop(hThis,'CursorLineWidth'); - findprop(hThis,'TopMarker'); - findprop(hThis,'BottomMarker')]; - l(end+1) = event.proplistener(hThis,p,'PostSet', @hThis.localSetCursorProps); - - % Marker properties - p = [ ... - findprop(hThis,'TargetMarkerStyle');... - findprop(hThis,'TargetMarkerSize'); ... - findprop(hThis,'TargetMarkerEdgeColor'); ... % YD - findprop(hThis,'TargetMarkerFaceColor'); ... % YD - ]; - l(end+1) = event.proplistener(hThis,p,'PostSet',@hThis.localSetMarkerProps); - - % Listen for update event - l(end+1) = event.listener(hThis,'UpdateCursorBar',@hThis.updateDisplay); - - % Clean up if Cursorbar is deleted - l(end+1) = event.listener(hThis,'ObjectBeingDestroyed',... - @hThis.localCursorBarDestroy); - - % Store listeners - hThis.SelfListenerHandles = l; - - end - - % -------------------------------------- - function localAxesPixelBoundUpdate(hThis,~,~) - % LOCALAXESPIXELBOUNDUPDATE - - update(hThis); - end - - % -------------------------------------- - function localSetOrientation(hThis,~,evd) - % LOCALSETORIENTATION - - % get new Orientation value - newval = evd.AffectedObject.Orientation; - - % get DataCursor's Position - pos = hThis.DataCursorHandle.Position; - x = pos(1); - y = pos(2); - - hAxes = get(hThis,'Parent'); - - % get axes' limits - xlimits = get(hAxes,'XLim'); - ylimits = get(hAxes,'YLim'); - - % get axes' directions - xdir = get(hAxes,'XDir'); - ydir = get(hAxes,'YDir'); - - % setting Marker for 'affordances' at ends of Cursorbar - switch newval - case 'vertical' - set(hThis.CursorLineHandle,'XData',[x x],'YData',ylimits); - switch ydir - case 'normal' - set(hThis.BottomHandle,'Marker','^') - set(hThis.TopHandle,'Marker','v') - case 'reverse' - set(hThis.BottomHandle,'Marker','v') - set(hThis.TopHandle,'Marker','^') - end - case 'horizontal' - set(hThis.CursorLineHandle,'XData',xlimits,'YData',[y y]); - switch xdir - case 'normal' - set(hThis.BottomHandle,'Marker','<') - set(hThis.TopHandle,'Marker','>') - case 'reverse' - set(hThis.BottomHandle,'Marker','>') - set(hThis.TopHandle,'Marker','<') - end - otherwise - error(message('MATLAB:graphics:cursorbar:invalidOrientation')) - end - - % update Cursorbar - if ~isempty(hThis.Position) - hThis.Position = hThis.Position; % silently update Location and Position - end - % update(hThis) - - end - - % -------------------------------------- - function localSetLocation(hThis,~,evd) - % LOCALSETLOCATION - - loc = evd.AffectedObject.Location; - - pos = get(hThis,'Position'); - - % during initialization (duplication) the position may not be set yet; - % assert proper length (=3) - if length(pos)<3 - pos(1,3) = 0; - end - - switch get(hThis,'Orientation') - case 'vertical' - pos(1) = loc; - case 'horizontal' - pos(2) = loc; - otherwise % default vertical - pos(1) = loc; - end - - % set(hThis.DataCursorHandle,'Position',pos) - set(hThis,'Position',pos) - - % update(hThis); % set.Position already updates Cursorbar - end - - % -------------------------------------- - function localSetPosition(hThis,~,evd) - % LOCALSETPOSITION - - % return early if not a handle - if ~ishandle(hThis) - return; - end - - % get new Position - pos = evd.AffectedObject.Position; - - % Position should be [X Y] or [X Y Z] - if numel(pos) ~= 2 && numel(pos) ~= 3 - return - end - - x = pos(1); - y = pos(2); - - hCursorLine = hThis.CursorLineHandle; - hTopLine = hThis.TopHandle; - hBottomLine = hThis.BottomHandle; - hAxes = get(hThis,'Parent'); - - switch get(hThis,'Orientation') - case 'vertical' - if isempty(hThis.Location) || hThis.Location ~= x - set(hThis,'Location',x); - end - - yLim = get(hAxes,'YLim'); - set(hCursorLine,'XData',[x x],'YData',yLim); - set(hBottomLine,'XData',x, 'YData',yLim(1)); - set(hTopLine, 'XData',x, 'YData',yLim(2)); - case 'horizontal' - if isempty(hThis.Location) || hThis.Location ~= y - set(hThis,'Location',y); - end - xLim = get(hAxes,'XLim'); - set(hCursorLine,'XData',xLim, 'YData',[y y]); - set(hBottomLine,'XData',xLim(2),'YData',y); - set(hTopLine, 'XData',xLim(1),'YData',y); - end - - % silently update - updateMarkers(hThis); - updateDisplay(hThis); - % defaultUpdateFcn(hThis,obj,evd); - - end - - % -------------------------------------- - function localSetShowText(hThis,obj,evd) - % LOCALSETSHOWTEXT - - validhandles = ishandle(hThis.DisplayHandle); - visibility = evd.AffectedObject.ShowText; - set(hThis.DisplayHandle(validhandles),'Visible',visibility); - % - hThis.updateDisplay(obj,evd); % update display - end - - % -------------------------------------- - function localSetTargetIntersections(hThis,obj,evd) - % LOCALSETTARGETINTERSECTION - - hThis.updateMarkers(); % update markers - hThis.updateDisplay(obj,evd); % update display - end - - % -------------------------------------- - function localSetTextDescription(hThis,obj,evd) - % LOCALSETTEXTDESCRIPTION - - hThis.updateMarkers(); % update markers - hThis.updateDisplay(obj,evd); % update display - end - - % -------------------------------------- - function localSetUIContextMenu(hThis,~,evd) - % LOCALSETUICONTEXTMENU - - contextmenu = evd.AffectedObject.UIContextMenu; - - hndls = [ - hThis.CursorLineHandle; - hThis.TargetMarkerHandle; - hThis.TopHandle; - hThis.BottomHandle; - hThis.GroupHandle]; - - set(hndls,'UIContextMenu',contextmenu); - end - - % -------------------------------------- - function localSetButtonDownFcn(hThis,~,evd) - % LOCALSETBUTTONDOWNFCN - - newVal = evd.AffectedObject.ButtonDownFcn; - - hLines = [ - hThis.CursorLineHandle; - hThis.TargetMarkerHandle; - hThis.TopHandle - hThis.BottomHandle - hThis.GroupHandle]; - - set(hLines,'ButtonDownFcn',newVal); - end - - % -------------------------------------- - function localSetCursorProps(hThis,~,evd) - % LOCALSETCURSORPROPS - - propname = evd.Source.Name; - propval = evd.AffectedObject.(propname); - - switch propname - case 'CursorLineColor' - newpropname = 'Color'; - hLine = [hThis.CursorLineHandle; hThis.TopHandle; hThis.BottomHandle]; - case 'CursorLineStyle' - newpropname = 'LineStyle'; - hLine = hThis.CursorLineHandle; - case 'CursorLineWidth' - newpropname = 'LineWidth'; - hLine = hThis.CursorLineHandle; - case 'TopMarker' - newpropname = 'Marker'; - hLine = hThis.TopHandle; - case 'BottomMarker' - newpropname = 'Marker'; - hLine = hThis.BottomHandle; - end - - set(hLine,newpropname,propval); - end - - % -------------------------------------- - function localSetMarkerProps(hThis,~,evd) - % LOCALSETMARKERPROPS - - propname = evd.Source.Name; - propval = evd.AffectedObject.(propname); - - switch propname - case 'TargetMarkerStyle' - newpropname = 'Marker'; - case 'TargetMarkerSize' - newpropname = 'MarkerSize'; - case 'TargetMarkerEdgeColor' % YD - newpropname = 'MarkerEdgeColor'; - case 'TargetMarkerFaceColor' % YD - newpropname = 'MarkerFaceColor'; - end - - set(hThis.TargetMarkerHandle,newpropname,propval) - end - - % -------------------------------------- - function localPreSetTarget(hThis,~,evd) - % LOCALPRESETTARGET - - % check new Target value - newTarget = evd.AffectedObject.Target; - if ~all(isTargetChart(hThis)) && ~isa(newTarget,'matlab.graphics.axis.Axes') - error(message('MATLAB:cursorbar:InvalidTarget')); - end - - % remove the old container - localRemoveContainer(hThis); - end - - % -------------------------------------- - function localPostSetTarget(hThis,~,~) - % LOCALPOSTSETTARGET - - % set up the container - localAddContainer(hThis); - - % if it's a line, set it close to the current location of the Cursorbar - if isTargetAxes(hThis) - % do nothing for axes, no need to change Position - return - end - - % update the Target...Data - [x,y,z,n] = getTargetXYData(hThis); - hThis.TargetXData = x; - hThis.TargetYData = y; - hThis.TargetZData = z; - hThis.TargetNData = n; - - % update Cursorbar - hThis.update([],[],'-nomove'); - end - - % -------------------------------------- - function localSetVisible(hThis,obj,evd) - % LOCALSETVISIBLE - - % Return early if no DataCursor - if ~isvalid(hThis.DataCursorHandle) || isempty(hThis.DataCursorHandle.Position) - hThis.Visible = 'off'; - hThis.GroupHandle.Visible = 'off'; - hThis.updateDisplay(obj,evd); - return; - end - - newvalue = evd.AffectedObject.Visible; - hThis.GroupHandle.Visible = newvalue; - hThis.updateDisplay(obj,evd); - end - - % -------------------------------------- - function localAddContainer(hThis,hParent) - % LOCALADDCONTAINER Add a CursorbarContainer to the parent - - % inputs - if nargin<2 - hParent = hThis.Parent; % default parent - end - hFig = ancestor(hParent,'figure'); % figure handle - - % store the application data in the parent axes for serialization reasons - key = graphics.internal.CursorbarContainer.Key; % application data key - containers = getappdata(hFig,key); % retrieve Cursorbar containers - % - if isempty(containers) || ~any(containers.hasCursorbar(hThis)) - % create a new container and store - newContainer = graphics.internal.CursorbarContainer(hThis);% container of the current object - containers = [containers newContainer]; % add the current container - % - setappdata(hFig,key,containers); % store all the containers in the parent - hThis.Container = newContainer; % store the new container - end - end - - % -------------------------------------- - function localRemoveContainer(hThis,hParent) - % LOCALREMOVECONTAINER Remove the CursorbarContainer from the parent - - % inputs - if nargin<2 - hParent = hThis.Parent; % default parent - end - hFig = ancestor(hParent,'figure'); % figure handle - - % remove the application data in the parent axes - key = graphics.internal.CursorbarContainer.Key; % application data key - containers = getappdata(hFig,key); % retrieve Cursorbar containers - % - if ~isempty(containers) && any(containers.hasCursorbar(hThis)) - % remove containers from the application data - current = containers.hasCursorbar(hThis); % containers for the current object - setappdata(hFig,key,containers(~current)); % leave only the non-current containers - end - - % delete the old container handle - if isvalid(hThis.Container) - delete(hThis.Container); - end - end - - % -------------------------------------- - function localCursorBarDestroy(hThis,~,~,varargin) - % LOCALCURSORBARDESTROY called when the Cursorbar is destroyed - - % user defined delete function - hThis.localApplyDeleteFcn(hThis,[]); - - % remove cursorbar containers - if ishandle(hThis.Parent) - localRemoveContainer(hThis,hThis.Parent); - end - - % delete all child objects - if ishandle(hThis.CursorLineHandle) - delete(hThis.CursorLineHandle); - end - if ishandle(hThis.TargetMarkerHandle) - delete(hThis.TargetMarkerHandle); - end - if ishandle(hThis.TopHandle) - delete(hThis.TopHandle); - end - if ishandle(hThis.BottomHandle) - delete(hThis.BottomHandle); - end - if isvalid(hThis.DataCursorHandle) - delete(hThis.DataCursorHandle) - end - validhandles = ishandle(hThis.DisplayHandle); - if any(validhandles) && all(isa(hThis.DisplayHandle(validhandles),'matlab.graphics.primitive.Text')) - delete(hThis.DisplayHandle(validhandles)) - end - if ishandle(hThis.GroupHandle) - delete(hThis.GroupHandle) - end - end - - % -------------------------------------- - function localTargetDestroy(hThis,~,evd,varargin) - % LOCALTARGETDESTROY called when the any of the Cursorbar's Target objects are destroyed - - % if there is a single Target, then Cursorbar should be destroyed when it is - % destroyed - if length(hThis.Target) == 1 - delete(hThis); - return - else - % determine which Target was deleted - deletedTarget = evd.Source; - - % remove from Target list - hTargets = handle(hThis.Target); - hTargets(hTargets == deletedTarget) = []; - set(hThis,'Target',hTargets); - - % update the Target_Data - [x,y,z,n] = getTargetXYData(hThis); - hThis.TargetXData = x; - hThis.TargetYData = y; - hThis.TargetZData = z; - hThis.TargetNData = n; - end - - update(hThis,[],[],'-nomove'); - end - - % -------------------------------------- - function localAxesLimUpdate(hThis,~,~) - % LOCALAXESLIMUPDATE update cursor line after limits change - - % get the Cursorbar's orientation - orient = get(hThis,'Orientation'); - - hAxes = handle(get(hThis,'Parent')); - hCursorLine = get(hThis,'CursorLineHandle'); - - switch orient - case 'vertical' - ylim = get(hAxes,'YLim'); - set(hCursorLine,'YData',ylim) - case 'horizontal' - xlim = get(hAxes,'XLim'); - set(hCursorLine,'XData',xlim) - end - end - - % -------------------------------------- - function localTargetDataUpdate(hThis,~,~,varargin) - % LOCALTARGETDATAUPDATE - - hDataCursor = hThis.DataCursorHandle; - - oldpos = hDataCursor.Position; - - % use the old position to determine the new position - [x,y] = closestvertex(hThis,oldpos); - pos = [x y 0]; - hDataCursor.Position = pos; - update(hThis); - end - - % -------------------------------------- - function localCursorButtonDownFcn(hThis,~,~) - % LOCALCURSORBUTTONDOWNFCN click on Cursorbar - - hFig = ancestor(hThis,'Figure'); - - % swap out the WindowButton...Fcns - uistate = struct; - uistate.WindowButtonUpFcn = get(hFig,'WindowButtonUpFcn'); - uistate.WindowButtonMotionFcn = get(hFig,'WindowButtonMotionFcn'); - uistate.Pointer = get(hFig,'Pointer'); - - % save figure's current state - setappdata(hFig,'CursorBarOriginalFigureCallbacks',uistate); - - % modify uistate - uistate.WindowButtonUpFcn = @hThis.localWindowButtonUpFcn; - uistate.WindowButtonMotionFcn = @hThis.localWindowButtonMotionFcn; - uistate.Pointer = 'fleur'; - - % set new state - set(hFig,uistate); - - % send BeginDrag event - notify(hThis,'BeginDrag'); - end - - % -------------------------------------- - function localWindowButtonMotionFcn(hThis,~,~) - % LOCALWINDOWBUTTONMOTIONFCN move Cursorbar - - % update the Cursorbar while moving - update(hThis); - end - - % -------------------------------------- - function localWindowButtonUpFcn(hThis,hFig,~) - % LOCALWINDOWBUTTONUPFCN restore original figure callbacks and pointer - - % get stored callbacks - uistate = getappdata(hFig,'CursorBarOriginalFigureCallbacks'); - - if ~isempty(uistate) - set(hFig,uistate); - end - - % send EndDrag event - notify(hThis,'EndDrag'); - end - - % -------------------------------------- - function localTestUpdate(~,~,evd) - % LOCALTESTUPDATE test for property listeners - - disp(get(evd)) - - end - end - - % ============================================================= % - - %% Folder (Protected Hidden) Methods - methods (Access=protected, Hidden) - % -------------------------------------- - uictxtmenu = defaultUIContextMenu(hThis); - % -------------------------------------- - defaultUpdateFcn(hThis,obj,evd); - % -------------------------------------- - [xIntersect,yIntersect,hIntersect] = getIntersections(hThis,hLines); - % -------------------------------------- - pixperdata = getPixelsPerData(hThis); - % -------------------------------------- - [x,y,z,n] = getTargetXYData(hThis,orient); - % -------------------------------------- - move(hThis,dir); - % -------------------------------------- - moveDataCursor(hThis,hDataCursor,direc); - % -------------------------------------- - update(hThis,obj,evd,varargin) - % -------------------------------------- - updateDataCursor(hThis,hNewDataCursor,target) - % -------------------------------------- - updateDisplay(hThis,obj,evd) - % -------------------------------------- - updateMarkers(hThis) - % -------------------------------------- - updatePosition(hThis,hNewDataCursor) - % -------------------------------------- - hNewDataCursor = createNewDataCursor(hThis,hTarget) - % -------------------------------------- - [x,y,n] = closestvertex(hThis,pos,orient) - end - - % ============================================================= % - - %% Custom Display Methods - - methods(Access = protected) - - % -------------------------------------- - function groups = getScalarPropertyGroups(hThis) %#ok - % GETSCALARPROPERTYGROUPS Construct array of property groups for display of scalar case. - - % Scalar case: change order - propList = { ... - 'Location', ... - 'Position', ... - 'Orientation', ... - ... - 'CursorLineColor', ... - 'CursorLineStyle', ... - 'CursorLineWidth', ... - 'TopMarker', ... - 'BottomMarker', ... - ... - 'TargetMarkerStyle', ... - 'TargetMarkerSize', ... - 'TargetMarkerEdgeColor', ... - 'TargetMarkerFaceColor', ... - ... - 'ShowText', ... - 'TargetIntersections', ... - 'TextDescription' ... - }; - groups = matlab.mixin.util.PropertyGroup(propList); - end - end - - % ============================================================= % - -end - -% ============================================================= % - -%% Subfunctions - -function newME = setmessage(ME,prop) -% SETMESSAGE Sets the error message for set.Property functions. -classLink = sprintf('Cursorbar',mfilename('class')); -% -firstLine = sprintf('While setting the ''%s'' property of %s:',prop,classLink); -messageStr = regexprep(ME.message,'Class',classLink); -identifier = regexprep(ME.identifier,{'\','\.'},{mfilename('class'),':'},'ignorecase'); -% -newME = MException(identifier,'%s\n%s',firstLine,messageStr); -end - -%% EOF diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/closestvertex.m b/Required packages/Cursorbar/+graphics/@Cursorbar/closestvertex.m deleted file mode 100644 index eab58e4..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/closestvertex.m +++ /dev/null @@ -1,99 +0,0 @@ -function [x,y,n] = closestvertex(hThis,pos,orient) -% CLOSESTVERTEX Return X,Y location of closest Target vertex -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2003-2016 The MathWorks, Inc. - -% Change Log: -% 13 Feb 2015: First version posted on the MathWorks file exchange. -% 14 May 2015: Added logarithmic scale support. - -% input check -if nargin<3 || isempty(orient) - orient = hThis.Orientation; -end - -% initialize -hTarget = hThis.Target; -hAxes = hThis.Parent; -% -x = []; -y = []; - -% don't need to find closest vertex if the Target is an axes -if isTargetAxes(hThis) - return -end - -% get XData and YData -x = hThis.TargetXData; -y = hThis.TargetYData; -n = hThis.TargetNData; - -% logarithmic scale requires a logarithmic distance -switch hAxes.XScale - case 'linear' - pos_dist(1) = pos(1); - x_dist = x; - case 'log' - pos_dist(1) = log10(pos(1)); - x_dist = log10(x); -end -% -switch hAxes.YScale - case 'linear' - pos_dist(2) = pos(2); - y_dist = y; - case 'log' - pos_dist(2) = log10(pos(2)); - y_dist = log10(y); -end - -% translate to pixels -pixperdata = getPixelsPerData(hThis); -% -pos_dist = pos_dist .* pixperdata; -x_dist = x_dist * pixperdata(1); -y_dist = y_dist * pixperdata(2); - -% determine distance -is_2D_target = any( strcmp(class(hTarget),graphics.Cursorbar.Permitted2DTargets) ) ... - || strcmp(hThis.TargetIntersections,'single'); -is_single_target = isscalar(hTarget); - -if is_2D_target - % determine distance to the closest target - dist = hypot(pos_dist(1) - x_dist, pos_dist(2) - y_dist); - -elseif is_single_target - % determine distance in a single dimension, dependent on Orientation - switch orient - case 'vertical' - dist = abs(pos_dist(1) - x_dist); - case 'horizontal' - dist = abs(pos_dist(2) - y_dist); - end -else - % determine distance to the closest target, dependent on Orientation - distX = abs(pos_dist(1) - x_dist); - distY = abs(pos_dist(2) - y_dist); - % punish the secondary distance if the primary distance is too large - pixoffset = 3; % the error range - switch orient - case 'vertical' - distY(distX-min(distX)>pixoffset) = Inf; - case 'horizontal' - distX(distY-min(distY)>pixoffset) = Inf; - end - dist = hypot(distX, distY); -end - -% get index for minimum distance -[~,ind] = min(dist); - -% set output variables -x = x(ind); -y = y(ind); -n = n(ind); diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/createNewDataCursor.m b/Required packages/Cursorbar/+graphics/@Cursorbar/createNewDataCursor.m deleted file mode 100644 index 64c4de8..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/createNewDataCursor.m +++ /dev/null @@ -1,53 +0,0 @@ -function hNewDataCursor = createNewDataCursor(hThis,hTarget) -% CREATENEWDATACURSOR Creates a new data cursor -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2015-2016 The MathWorks, Inc. - -% set source -if nargin<2 - hTarget = hThis.Target(1); -end - -% create a Data Cursor -if isTargetAxes(hThis) - % target is an axes: create a dummy line handle - hDummyLineHandle = line(0,0, 'Parent', hThis.GroupHandle, ... - 'Visible','off', 'HandleVisibility','off', 'Clipping','off',... - 'PickableParts','none', 'HitTest','off', 'Interruptible','off'); - hThis.DataCursorDummyTargetHandle = matlab.graphics.chart.primitive.Line(hDummyLineHandle); - % - hNewDataCursor = matlab.graphics.shape.internal.PointDataCursor(hThis.DataCursorDummyTargetHandle); - -elseif isTargetChart(hThis) - % target is a chart: create a direct point data cursor - try % create directly - hNewDataCursor = matlab.graphics.shape.internal.PointDataCursor( hTarget ); - catch % probably, not a DataAnnotatable (matlab.graphics.chart.interaction.DataAnnotatable) - try % create via datacursormode - hDataCursorMode = datacursormode(); - hDataTip = hDataCursorMode.createDatatip( hTarget ); - % - hNewDataCursor = hDataTip.Cursor; - % - delete(hDataTip); % it's services are no longer required - hDataCursorMode.Enable = 'off'; % disable data cursor mode - catch % throw error - error(message('MATLAB:cursorbar:InvalidTarget')); - end - end - -else - % throw error - error(message('MATLAB:cursorbar:InvalidTarget')); -end - -% delete old handle -hOldDataCursor = hThis.DataCursorHandle; -delete(hOldDataCursor); - -% update old handle -hThis.DataCursorHandle = hNewDataCursor; - diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/defaultUIContextMenu.m b/Required packages/Cursorbar/+graphics/@Cursorbar/defaultUIContextMenu.m deleted file mode 100644 index 24141b8..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/defaultUIContextMenu.m +++ /dev/null @@ -1,122 +0,0 @@ -function uictxtmenu = defaultUIContextMenu(hThis) -% DEFAULTUICONTEXTMENU Default Cursorbar UIContextMenu. -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2003-2016 The MathWorks, Inc. - -% check the figure -hFig = ancestor(hThis,'figure'); -% -if isempty(hFig) || ~ishghandle(hFig) - % probably, happens during loading process - % exit to prevent MATLAB from crushing - uictxtmenu = gobjects(0); - return -end - -% set context menu -uictxtmenu = uicontextmenu('Parent',hFig); -uictxtmenu.Serializable = 'off'; % don't save to file - -% define menu properties -menuprops = struct; -menuprops.Parent = uictxtmenu; -menuprops.Serializable = 'off'; % don't save to file - -% first menu -menuprops.Label = 'Show Text'; -menuprops.Checked = hThis.ShowText; -menuprops.Callback = {@localSetShowText,hThis}; -% -u(1) = uimenu(menuprops); -l(1) = event.proplistener(hThis,findprop(hThis,'ShowText'), ... - 'PostSet',@(obj,evd)localGetShowText(u(1),evd,hThis)); - -% second menu -menuprops.Label = 'Multiple Intersections'; -switch hThis.TargetIntersections - case 'multiple', menuprops.Checked = 'on'; - case 'single', menuprops.Checked = 'off'; -end -menuprops.Callback = {@localSetTargetIntersections,hThis}; -% -u(2) = uimenu(menuprops); -l(2) = event.proplistener(hThis,findprop(hThis,'TargetIntersections'), ... - 'PostSet',@(obj,evd)localGetTargetIntersections(u(2),evd,hThis)); - -% third menu -menuprops.Label = 'Short Description'; -switch hThis.TextDescription - case 'short', menuprops.Checked = 'on'; - case 'long', menuprops.Checked = 'off'; -end -menuprops.Callback = {@localSetTextDescription,hThis}; -% -u(3) = uimenu(menuprops); -l(3) = event.proplistener(hThis,findprop(hThis,'TextDescription'), ... - 'PostSet',@(obj,evd)localGetTextDescription(u(3),evd,hThis)); - -% store listeners -hThis.SelfListenerHandles = [hThis.SelfListenerHandles, l]; - -%% Subfunctions - -function localGetShowText(hMenu,~,hThis) -% LOCALGETSHOWTEXT Get ShowText property. -switch hThis.ShowText - case 'on', hMenu.Checked = 'on'; - case 'off', hMenu.Checked = 'off'; -end - -function localSetShowText(hMenu,~,hThis) -% LOCALSETSHOWTEXT SetShowText Property -switch get(hMenu,'Checked') - case 'on' - set(hMenu,'Checked','off') - set(hThis,'ShowText','off') - case 'off' - set(hMenu,'Checked','on') - set(hThis,'ShowText','on') -end - -% -------------------------------------- - -function localGetTargetIntersections(hMenu,~,hThis) -% LOCALGETTARGETINTERSECTIONS Get TargetIntersections Property -switch hThis.TargetIntersections - case 'multiple', hMenu.Checked = 'on'; - case 'single', hMenu.Checked = 'off'; -end - -function localSetTargetIntersections(hMenu,~,hThis) -% LOCALSETTARGETINTERSECTIONS Set TargetIntersections Property -switch get(hMenu,'Checked') - case 'on' - set(hMenu,'Checked','off') - set(hThis,'TargetIntersections','single') - case 'off' - set(hMenu,'Checked','on') - set(hThis,'TargetIntersections','multiple') -end - -% -------------------------------------- - -function localGetTextDescription(hMenu,~,hThis) -% LOCALGETTEXTDESCRIPTION Get TextDescription Property -switch hThis.TextDescription - case 'short', hMenu.Checked = 'on'; - case 'long', hMenu.Checked = 'off'; -end - -function localSetTextDescription(hMenu,~,hThis) -% LOCALSETTEXTDESCRIPTION Set TextDescription Property -switch get(hMenu,'Checked') - case 'on' - set(hMenu,'Checked','off') - set(hThis,'TextDescription','long') - case 'off' - set(hMenu,'Checked','on') - set(hThis,'TextDescription','short') -end diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/defaultUpdateFcn.m b/Required packages/Cursorbar/+graphics/@Cursorbar/defaultUpdateFcn.m deleted file mode 100644 index 2e2973a..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/defaultUpdateFcn.m +++ /dev/null @@ -1,193 +0,0 @@ -function defaultUpdateFcn(hThis,~,~) -% DEFAULTUPDATEFCN Default cursorbar UpdateFcn. -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2003-2016 The MathWorks, Inc. - -% Change Log: -% 13 Feb 2015: First version posted on the MathWorks file exchange. -% 14 May 2015: Added logarithmic scale support. - -hText = get(hThis,'DisplayHandle'); - -if strcmp(hThis.ShowText,'off') || strcmp(hThis.Visible,'off') - if ~isempty(hText) - delete(hText); - hThis.DisplayHandle = gobjects(0); - return - end - return -end - -% get the locations of the markers -if ~all(isvalid(hThis.TargetMarkerHandle)) - return; -end -xData = get(hThis.TargetMarkerHandle,'XData'); -yData = get(hThis.TargetMarkerHandle,'YData'); - -numIntersections = length(xData); - -% get the handles for the text objects, corresponding to each intersection -hAxes = get(hThis,'Parent'); - -%%%%%%%%%%%%% - -AXCOLOR = get(hAxes,'Color'); - -if ischar(AXCOLOR), AXCOLOR = get(hAxes,'Color'); end - -% light colored axes -if sum(AXCOLOR)>1.5 - TEXTCOLOR = [0,0,0]; FACECOLOR = [1 1 238/255]; EDGECOLOR = [.8 .8 .8]; - % dark colored axes (i.e. simulink scopes) -else - TEXTCOLOR = [.8 .8 .6]; FACECOLOR = 48/255*[1 1 1]; EDGECOLOR = [.8 .8 .6]; -end - -%%%%%%%%%%%%% - -% create text objects if necessary -if isempty(hText) || any(~ishandle(hText)) - hText = gobjects(numIntersections,1); - for n = 1:numIntersections - hText(n) = text(xData(n),yData(n),'',... - 'Parent',hThis.GroupHandle, ... % 'Parent',hAxes,... - 'Color',TEXTCOLOR,... - 'EdgeColor',EDGECOLOR,... - 'BackgroundColor',FACECOLOR,... - 'Visible','off'); - end - numText = numIntersections; -else - % if the number of intersections isn't equal to the number of text objects, - % add/delete them as necessary - - set(hText,'Visible','off'); - - numText = length(hText); - - if numText ~= numIntersections - % unequal number of text objects and intersections - delete(hText) - - hText = gobjects(numIntersections,1); - - for n = numIntersections: -1 : 1 - hText(n) = text(xData(n),yData(n),'',... - 'Parent',hThis.GroupHandle, ... % 'Parent',hAxes,... - 'Color',TEXTCOLOR,... - 'EdgeColor',EDGECOLOR,... - 'BackgroundColor',FACECOLOR,... - 'Visible','off'); - end - numText = numIntersections; - end - -end - -% now update the text objects - -set(hText,'Visible','off','Units','data') - -xl = get(hAxes,'XLim'); -yl = get(hAxes,'YLim'); - -xdir = get(hAxes,'XDir'); -ydir = get(hAxes,'YDir'); - -pixperdata = getPixelsPerData(hThis); -pixoffset = 12; - -for n = 1:numText - x = xData(n); - y = yData(n); - - if x >= mean(xl) - if strcmp(xdir,'normal') - halign = 'right'; - xoffset = -pixoffset * 1/pixperdata(1); - else - halign = 'left'; - xoffset = pixoffset * 1/pixperdata(1); - end - else - if strcmp(xdir,'normal') - halign = 'left'; - xoffset = pixoffset * 1/pixperdata(1); - else - halign = 'right'; - xoffset = -pixoffset * 1/pixperdata(1); - end - end - - if y >= mean(yl) - if strcmp(ydir,'normal') - valign = 'top'; - yoffset = -pixoffset * 1/pixperdata(2); - else - valign = 'bottom'; - yoffset = pixoffset * 1/pixperdata(2); - end - else - if strcmp(ydir,'normal') - valign = 'bottom'; - yoffset = pixoffset * 1/pixperdata(2); - else - valign = 'top'; - yoffset = -pixoffset * 1/pixperdata(2); - end - end - - if ~isempty( hThis.TargetZData ) - [~,ind] = min( hypot(hThis.TargetXData-x, hThis.TargetYData-y) ); - ind = ind(1); - z = hThis.TargetZData(ind); - else - z = []; - end - - % assert proper scale - switch [hAxes.XScale '-' hAxes.YScale] - case 'linear-linear' - posoffset = [x+xoffset, y+yoffset, 0]; - case 'log-linear' - posoffset = [x*10^xoffset, y+yoffset, 0]; - case 'linear-log' - posoffset = [x+xoffset, y*10^yoffset, 0]; - case 'log-log' - posoffset = [x*10^xoffset, y*10^yoffset, 0]; - end - - set(hText(n),'Position',posoffset,... - 'String',makeString(x,y,z,hThis.Orientation,hThis.TextDescription),... - 'HorizontalAlignment',halign,... - 'VerticalAlignment',valign); -end - - -set(hThis,'DisplayHandle',hText); - -set(hText,'Visible','on'); - -% -------------------------------------- -function str = makeString(x,y,z,orient,desc) -% MAKESTRING Make the text description string -frmt = '%.3g'; -switch desc - case 'short' - switch orient - case 'vertical' - str = ['Y: ' sprintf(frmt,y)]; - case 'horizontal' - str = ['X: ' sprintf(frmt,x)]; - end - case 'long' - if isempty(z) - str = { ['X: ' sprintf(frmt,x)] , ['Y: ' sprintf(frmt,y)]}; - else - str = { ['X: ' sprintf(frmt,x)] , ['Y: ' sprintf(frmt,y)] , ['Z: ' sprintf(frmt,z)]}; - end -end diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/drawCrossbar.m b/Required packages/Cursorbar/+graphics/@Cursorbar/drawCrossbar.m deleted file mode 100644 index 0ac66d5..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/drawCrossbar.m +++ /dev/null @@ -1,109 +0,0 @@ -function hThat = drawCrossbar(hThis,varargin) -% DRAWCROSSBAR Draws a linked perpendicular bar. -% -% See also: graphics.Cursorbar.duplicate, graphics.Cursorbar. -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2015-2016 The MathWorks, Inc. - -% draw crossbar depending on situation: -if isempty(hThis) - % empty Cursorbar: return empty array - hThat = graphics.Cursorbar.empty(size(hThis)); - return - -elseif ~isscalar(hThis) - % an array of Cursorbars: recursively draw crossbar to each - hThat = arrayfun(@(h)drawCrossbar(h,varargin{:}),hThis,'UniformOutput',0); - hThat = reshape([hThat{:}],size(hThis)); - return - -elseif ~isvalid(hThis) - % deleted Cursorbar: create default deleted object - hThat = graphics.GraphicsPlaceholder; - delete(hThat); - return -end - -% error check -assert( isempty(hThis.PeerHandle) || ~isvalid(hThis.PeerHandle), ... - 'graphics:Cursorbar:drawCrossbar:existingCrossbar', ... - 'A crossbar to this cursorbar already exists!'); - -% duplicate -newOrient = theOtherOrientation(hThis); -hThat = duplicate(hThis,varargin{:},'Orientation',newOrient); - -% set peers -hThat.PeerHandle = hThis; -hThis.PeerHandle = hThat; - -% reset position -hThis.Position = theOtherPosition(hThis); -hThat.Position = theOtherPosition(hThis); - -% set container peers -thisContainer = hThis.Container; -thatContainer = hThat.Container; -% -setPeer( thisContainer, thatContainer ); - -% link -localLinkCrossbarProps(hThis); -localLinkCrossbarProps(hThat); - -end - -% -------------------------------------- -function l = localLinkCrossbarProps(hCB) -% LOCALLINKCROSSBARPROPS Set the property linking - -% set listeners -l( 1 ) = addlistener(hCB,'Position', 'PostSet', ... - @(~,~)set(hCB.PeerHandle, 'Position', hCB.Position)); - -l(end+1) = addlistener(hCB,'Orientation','PostSet', ... - @(~,~)set(hCB.PeerHandle, 'Orientation',theOtherOrientation(hCB))); - -l(end+1) = addlistener(hCB,'ObjectBeingDestroyed', ... - @(~,~)localRemoveContainerPeers(hCB)); - -l(end+1) = addlistener(hCB,'ObjectBeingDestroyed', ... - @(~,~)delete(hCB.PeerHandle.ExternalListenerHandles)); - -% store listeners -hCB.ExternalListenerHandles = l; -end - -% -------------------------------------- -function localRemoveContainerPeers(hThis) -% LOCALREMOVECONTAINERPEERS Removes the peers from the containers -key = graphics.internal.CursorbarContainer.Key; -% -hFig = ancestor(hThis.Parent,'figure'); -thisContainers = getappdata(hFig,key); -thisCurrent = thisContainers( thisContainers.hasCursorbar(hThis) ); -% -removePeer(thisCurrent); -end - -% -------------------------------------- -function orient = theOtherOrientation(hCB) -% THEOTHERORIENTATION Returns the other orientation -switch hCB.Orientation - case 'horizontal', orient='vertical'; - case 'vertical', orient='horizontal'; -end -end - -% -------------------------------------- -function pos = theOtherPosition(hCB) -% THEOTHERPOSITION Returns the other position -pos = hCB.Position; -switch hCB.Orientation - case 'horizontal', pos(1)=hCB.PeerHandle.Location; - case 'vertical', pos(2)=hCB.PeerHandle.Location; -end -end diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/duplicate.m b/Required packages/Cursorbar/+graphics/@Cursorbar/duplicate.m deleted file mode 100644 index c2425ce..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/duplicate.m +++ /dev/null @@ -1,64 +0,0 @@ -function hThat = duplicate(hThis,varargin) -% DUPLICATE Duplicate the cursorbar to an identical one. -% -% Duplicate created an identical Cursorbar to the desired by directly -% calling the Cursorbar constructor. Cursorbar has no copy method and does -% not subclass matlab.mixin.Copyable, since the copying procedure in handle -% graphics may result in unexpected behavior. Instead, it uses duplicate as -% a hard-copy method. -% -% See also: graphics.Cursorbar.drawCrossbar, graphics.Cursorbar. -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2015-2016 The MathWorks, Inc. - -% duplicate depending on situation: -if isempty(hThis) - % empty Cursorbar: return empty array - hThat = graphics.Cursorbar.empty(size(hThis)); - -elseif ~isscalar(hThis) - % an array of Cursorbars: recursively duplicate each - hThat = arrayfun(@(h)duplicate(h,varargin{:}),hThis,'UniformOutput',0); - hThat = reshape([hThat{:}],size(hThis)); - -elseif ~isvalid(hThis) - % deleted Cursorbar: create default deleted object - hThat = graphics.GraphicsPlaceholder; - delete(hThat); - -else - % everything is okay: duplicate the Cursorbar - hThat = graphics.Cursorbar( hThis.Target, ... - 'BottomMarker', hThis.BottomMarker, ... - 'CreateFcn', hThis.CreateFcn, ... - 'CursorLineColor', hThis.CursorLineColor, ... - 'CursorLineStyle', hThis.CursorLineStyle, ... - 'CursorLineWidth', hThis.CursorLineWidth, ... - 'DeleteFcn', hThis.DeleteFcn, ... - 'DisplayName', hThis.DisplayName, ... - 'FigureCallbacks', hThis.FigureCallbacks, ... - 'HitTest', hThis.HitTest, ... - 'Interruptible', hThis.Interruptible, ... - 'Location', hThis.Location, ... - 'Orientation', hThis.Orientation, ... - 'Parent', hThis.Parent, ... - 'Position', hThis.Position, ... - 'SelectionHighlight', hThis.SelectionHighlight, ... - 'ShowText', hThis.ShowText, ... - 'Tag', hThis.Tag, ... - 'TargetIntersections', hThis.TargetIntersections, ... - 'TargetMarkerEdgeColor', hThis.TargetMarkerEdgeColor, ... - 'TargetMarkerFaceColor', hThis.TargetMarkerFaceColor, ... - 'TargetMarkerSize', hThis.TargetMarkerSize, ... - 'TargetMarkerStyle', hThis.TargetMarkerStyle, ... - 'TextDescription', hThis.TextDescription, ... - 'TopMarker', hThis.TopMarker, ... - 'UserData', hThis.UserData, ... - 'Visible', hThis.Visible, ... - varargin{:}); -end - -end diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/getCursorInfo.m b/Required packages/Cursorbar/+graphics/@Cursorbar/getCursorInfo.m deleted file mode 100644 index 7269e5f..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/getCursorInfo.m +++ /dev/null @@ -1,15 +0,0 @@ -function info = getCursorInfo(hThis) -% GETCURSORINFO Get datacursor info from cursorbar -% -% See also: graphics.Cursorbar.getMarkerLocations, graphics.Cursorbar. -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2003-2016 The MathWorks, Inc. - -info = struct; - -hDC = hThis.DataCursorHandle; - -info.Position = hDC.Position; diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/getIntersections.m b/Required packages/Cursorbar/+graphics/@Cursorbar/getIntersections.m deleted file mode 100644 index 533ad7d..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/getIntersections.m +++ /dev/null @@ -1,108 +0,0 @@ -function [xIntersect,yIntersect,hIntersect] = getIntersections(hThis,hLines) -% GETINTERSECTIONS Return intersection information -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2003-2016 The MathWorks, Inc. - -% check inputs -if nargin > 1 - hLines = handle(hLines); - if ~all(isTargetChart(hThis)) - error(message('MATLAB:graphics:cursorbar:invalidInput')); - end -else - hLines = hThis.Target; -end - -xIntersect = []; -yIntersect = []; -hIntersect = []; - -if isTargetAxes(hThis) && nargin < 2 - % Target is an axes, no additional line handles as input, return early - return -end - -% get lines' XData and YData -xData = get(hLines,'XData'); -yData = get(hLines,'YData'); - -numLines = numel(hLines); - -if numLines == 1 - xData = xData(:); - yData = yData(:); - hData = repmat(hLines,size(xData)); -else - xSizes = cellfun('prodofsize',xData); - - % determine new length for NaN separated data vectors - newLength = sum(xSizes) + numLines - 1; - - new_xData = zeros(newLength,1); - new_yData = zeros(newLength,1); - hData = zeros(newLength,1); - - startIndex = 1; - for n = 1:numLines - finishIndex = startIndex + xSizes(n) - 1; - new_xData(startIndex:finishIndex) = xData{n}; - new_yData(startIndex:finishIndex) = yData{n}; - hData(startIndex:finishIndex) = hLines(n); - - if n < numLines - new_xData(finishIndex + 1) = NaN; - new_yData(finishIndex + 1) = NaN; - hData(finishIndex + 1) = NaN; - end - startIndex = finishIndex + 2; - end - xData = new_xData; - yData = new_yData; -end - -xSegs = zeros(length(xData)-1,2); -ySegs = zeros(length(yData)-1,2); - -xSegs(:,1) = xData(1:end-1); -xSegs(:,2) = xData(2:end); - -ySegs(:,1) = yData(1:end-1); -ySegs(:,2) = yData(2:end); - -loc = hThis.Location; - -switch hThis.Orientation - case 'vertical' - btwn = (xSegs(:,1) < loc & loc <= xSegs(:,2)) | (xSegs(:,2) < loc & loc <= xSegs(:,1)); - case 'horizontal' - btwn = (ySegs(:,1) < loc & loc <= ySegs(:,2)) | (ySegs(:,2) < loc & loc <= ySegs(:,1)); -end - -new_xSegs = xSegs(btwn,:); -new_ySegs = ySegs(btwn,:); - -numIntersect = length(find(btwn)); -xIntersect = zeros(numIntersect,1); -yIntersect = zeros(numIntersect,1); - -if numIntersect == 0 - return -end - -switch hThis.Orientation - case 'vertical' - for n = 1:numIntersect - xIntersect(n) = loc; - yIntersect(n) = interp1(new_xSegs(n,:),new_ySegs(n,:),loc); - end - case 'horizontal' - for n = 1:numIntersect - yIntersect(n) = loc; - xIntersect(n) = interp1(new_ySegs(n,:),new_xSegs(n,:),loc); - end -end - -hIntersect = hData(btwn); diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/getMarkerLocations.m b/Required packages/Cursorbar/+graphics/@Cursorbar/getMarkerLocations.m deleted file mode 100644 index e53c7b7..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/getMarkerLocations.m +++ /dev/null @@ -1,14 +0,0 @@ -function [x,y] = getMarkerLocations(hThis) -% GETMARKERLOCATIONS Return x,y position of the Cursorbar's intersection markers -% -% See also: graphics.Cursorbar.getCursorInfo, graphics.Cursorbar. -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2003-2016 The MathWorks, Inc. - -hMarker = hThis.TargetMarkerHandle; - -x = get(hMarker,'XData'); -y = get(hMarker,'YData'); diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/getPixelsPerData.m b/Required packages/Cursorbar/+graphics/@Cursorbar/getPixelsPerData.m deleted file mode 100644 index 729f26f..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/getPixelsPerData.m +++ /dev/null @@ -1,32 +0,0 @@ -function pixperdata = getPixelsPerData(hThis) -% GETPIXELSPERDATA Return pixel-per-data ratio -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2003-2016 The MathWorks, Inc. - -% Change Log: -% 13 Feb 2015: First version posted on the MathWorks file exchange. -% 14 May 2015: Added logarithmic scale support. - -hAxes = get(hThis,'Parent'); - -% get axes' limits -xl = get(hAxes,'XLim'); -yl = get(hAxes,'YLim'); - -% get Axes' pixel position -pixpos = getpixelposition(hAxes); - -% assert proper scale -switch [hAxes.XScale '-' hAxes.YScale] - case 'linear-linear' - pixperdata = [ pixpos(3) / (xl(2)-xl(1)), pixpos(4) / (yl(2)-yl(1))]; - case 'log-linear' - pixperdata = [ pixpos(3) / log10(xl(2)/xl(1)), pixpos(4) / (yl(2)-yl(1))]; - case 'linear-log' - pixperdata = [ pixpos(3) / (xl(2)-xl(1)), pixpos(4) / log10(yl(2)/yl(1))]; - case 'log-log' - pixperdata = [ pixpos(3) / log10(xl(2)/xl(1)), pixpos(4) / log10(yl(2)/yl(1))]; -end diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/getTargetXYData.m b/Required packages/Cursorbar/+graphics/@Cursorbar/getTargetXYData.m deleted file mode 100644 index bd6581c..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/getTargetXYData.m +++ /dev/null @@ -1,103 +0,0 @@ -function [x,y,z,n] = getTargetXYData(hThis,orient) -% GETTARGETXYDATA Create vectors of Target XData and YData -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2003-2016 The MathWorks, Inc. - -% set inputs -if nargin<2 || isempty(orient) - orient = hThis.Orientation; -end -hTarget = hThis.Target; - -x = []; -y = []; -z = []; -n = []; - -% set data -if isTargetAxes(hThis) - return -else - if isa(hTarget,'matlab.graphics.chart.primitive.Histogram') - xDataName = 'BinEdges'; - yDataName = 'Values'; - else - xDataName = 'XData'; - yDataName = 'YData'; - end - % - if numel(hTarget) == 1 - xData = {get(hTarget,xDataName)}; - yData = {get(hTarget,yDataName)}; - else % should all be lines - xData = get(hTarget, xDataName); - yData = get(hTarget, yDataName); - end - % - if isa(hTarget,'matlab.graphics.chart.primitive.Histogram') - xData = cellfun(@(x)x(2:end)-diff(x)/2, xData, 'UniformOutput', 0); % transform to BinCenters - end -end - -% check if CData exists -try - try - zData = {hTarget.ZData}; - catch - zData = {hTarget.CData}; - end - isZData = sum(cellfun('prodofsize',zData))>0; - % - if isZData && ~isequal( cellfun(@numel,xData), cellfun(@numel,zData) ) - [xData, yData] = cellfun( @meshgrid, xData, yData, 'UniformOutput', 0); - xData = cellfun( @(a)a(:).', xData, 'UniformOutput', 0); - yData = cellfun( @(a)a(:).', yData, 'UniformOutput', 0); - end -catch % never mind ... - zData = {}; - isZData = false; -end - -% determine how many vertices each line has -numLineVertices = cellfun('prodofsize',xData); - -% determine the total number of vertices -numAllVertices = sum(numLineVertices); - -% create vectors to hold locations for all vertices -xVertices = zeros(1,numAllVertices); -yVertices = zeros(1,numAllVertices); -zVertices = zeros(1,numAllVertices); -nVertices = zeros(1,numAllVertices); - -% initialize variable to hold the last entered data position -lastDataPos = 0; -for n = 1:length(hTarget) - lenData = length(xData{n}); - xVertices(lastDataPos+1:lastDataPos+lenData) = xData{n}; - yVertices(lastDataPos+1:lastDataPos+lenData) = yData{n}; - if isZData - zVertices(lastDataPos+1:lastDataPos+lenData) = zData{n}; - end - nVertices(lastDataPos+1:lastDataPos+lenData) = n; - lastDataPos = lastDataPos + lenData; -end - -% sort the Target's XData and YData based on the Orientation -switch orient - case 'vertical' - [x,ind] = sort(xVertices); - y = yVertices(ind); - case 'horizontal' - [y,ind] = sort(yVertices); - x = xVertices(ind); -end - -if isZData - z = zVertices(ind); -end - -n = nVertices(ind); diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/isTargetAxes.m b/Required packages/Cursorbar/+graphics/@Cursorbar/isTargetAxes.m deleted file mode 100644 index a5be47f..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/isTargetAxes.m +++ /dev/null @@ -1,21 +0,0 @@ -function tf = isTargetAxes(hThis) -% ISTARGETAXES Is the Target an axes -% -% See also: graphics.Cursorbar.isTargetChart, graphics.Cursorbar. -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2003-2016 The MathWorks, Inc. - -tf = false; - -% return empty if there is no Target -if isempty(hThis.Target) - tf = []; - return -end - -if (length(hThis.Target) == 1) && ishandle(hThis.Target) && isa(hThis.Target,'matlab.graphics.axis.Axes') - tf = true; -end diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/isTargetChart.m b/Required packages/Cursorbar/+graphics/@Cursorbar/isTargetChart.m deleted file mode 100644 index c212f29..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/isTargetChart.m +++ /dev/null @@ -1,36 +0,0 @@ -function tf = isTargetChart(hThis,hTarget) -% ISTARGETCHART Is the Target a permitted chart object -% -% See also: graphics.Cursorbar.isTargetAxes, graphics.Cursorbar. -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2015-2016 The MathWorks, Inc. - -% set the target -if nargin<2 - hTarget = hThis.Target; -end - -% return empty if there is no Target -if isempty(hTarget) - tf = []; - return -end -% -if ~all(ishandle(hTarget)) - tf = false(size(hTarget)); - return -end - -% check -tf = arrayfun(@isChart,hTarget); - -function tf = isChart(hTarget) -switch class(hTarget) % fastest comparison with switch - case graphics.Cursorbar.PermittedChartTargets - tf = true; - otherwise - tf = false; -end diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/move.m b/Required packages/Cursorbar/+graphics/@Cursorbar/move.m deleted file mode 100644 index 504173a..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/move.m +++ /dev/null @@ -1,16 +0,0 @@ -function move(hThis,dir) -% MOVE Move the data cursor and update cursorbar -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2003-2016 The MathWorks, Inc. - -% Update cursor based on direction -moveDataCursor(hThis.DataCursorHandle,hThis,hThis.DataCursorHandle,dir); - -pos = get(hThis.DataCursorHandle,'Position'); -set(hThis,'Position',pos); - -% throw event indicating that the cursorbar was updated -notify(hThis,'UpdateCursorBar'); diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/moveDataCursor.m b/Required packages/Cursorbar/+graphics/@Cursorbar/moveDataCursor.m deleted file mode 100644 index 5afab4e..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/moveDataCursor.m +++ /dev/null @@ -1,187 +0,0 @@ -function moveDataCursor(hThis,hDataCursor,direc) -% MOVEDATACURSOR Move the data cursor -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2003-2016 The MathWorks, Inc. - -% Change Log: -% 13 Feb 2015: First version posted on the MathWorks file exchange. -% 14 May 2015: Added logarithmic scale support. - -pos = hDataCursor.Position; - -hTarget = hThis.Target; -hAxes = get(hThis,'Parent'); - -xdir = get(hAxes,'XDir'); -ydir = get(hAxes,'YDir'); - -if all(isTargetChart(hThis)) - % determine next vertex - x = pos(1); - y = pos(2); - - XData = hThis.TargetXData; - YData = hThis.TargetYData; - - switch hThis.Orientation - case 'vertical' - % determine what the next possible X value is - switch xdir - case 'normal' - switch direc - case 'right' - % find next largest x value - ind = localNextIndex(x,XData,'greater'); - pos(1) = XData(ind); - pos(2) = YData(ind); - case 'left' - % find next smallest x value - ind = localNextIndex(x,XData,'less'); - pos(1) = XData(ind); - pos(2) = YData(ind); - otherwise - % do nothing - end - case 'reverse' - switch direc - case 'right' - % find next smallest x value - ind = localNextIndex(x,XData,'less'); - pos(1) = XData(ind); - pos(2) = YData(ind); - case 'left' - % find next largest x value - ind = localNextIndex(x,XData,'greater'); - pos(1) = XData(ind); - pos(2) = YData(ind); - otherwise - % do nothing - end - end - case 'horizontal' - % determine what the next possible Y value is - switch ydir - case 'normal' - switch direc - case 'up' - % find next largest x value - ind = localNextIndex(y,YData,'greater'); - pos(1) = XData(ind); - pos(2) = YData(ind); - case 'down' - % find next smallest x value - ind = localNextIndex(y,YData,'less'); - pos(1) = XData(ind); - pos(2) = YData(ind); - otherwise - % do nothing - end - case 'reverse' - switch direc - case 'up' - % find next smallest x value - ind = localNextIndex(y,YData,'less'); - pos(1) = XData(ind); - pos(2) = YData(ind); - case 'down' - % find next largest x value - ind = localNextIndex(y,YData,'greater'); - pos(1) = XData(ind); - pos(2) = YData(ind); - otherwise - % do nothing - end - end - end -elseif numel(hTarget) == 1 && isa(hTarget,'matlab.graphics.axis.Axes') - pixperdata = getPixelsPerData(hThis); - switch hThis.Orientation - case 'vertical' - switch xdir - case 'normal' - switch direc - case 'right' - xoffset = + 1/pixperdata(1); - case 'left' - xoffset = - 1/pixperdata(1); - otherwise - % do nothing - end - case 'reverse' - switch direc - case 'right' - xoffset = - 1/pixperdata(1); - case 'left' - xoffset = + 1/pixperdata(1); - otherwise - % do nothing - end - end - case 'horizontal' - switch ydir - case 'normal' - switch direc - case 'up' - yoffset = + 1/pixperdata(2); - case 'down' - yoffset = - 1/pixperdata(2); - otherwise - % do nothing - end - case 'reverse' - switch direc - case 'up' - yoffset = - 1/pixperdata(2); - case 'down' - yoffset = + 1/pixperdata(2); - otherwise - % do nothing - end - end - otherwise - % not vertical or horizontal - end - - % assert proper scale - x = pos(1); - y = pos(2); - % - switch [hAxes.XScale '-' hAxes.YScale] - case 'linear-linear' - pos = [x+xoffset, y+yoffset, 0]; - case 'log-linear' - pos = [x*10^xoffset, y+yoffset, 0]; - case 'linear-log' - pos = [x+xoffset, y*10^yoffset, 0]; - case 'log-log' - pos = [x*10^xoffset, y*10^yoffset, 0]; - end - -else - % not lines or an axes -end - - -hDataCursor.Position = pos; - -function ind = localNextIndex(d,Data,cmp) - -switch cmp - case 'greater' - ind = find(Data > d); - if isempty(ind) - ind = length(Data); - return - end - ind = min(ind); - case 'less' - ind = find(Data < d); - if isempty(ind) - ind = 1; - return - end - ind = max(ind); -end diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/update.m b/Required packages/Cursorbar/+graphics/@Cursorbar/update.m deleted file mode 100644 index 1a79b84..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/update.m +++ /dev/null @@ -1,41 +0,0 @@ -function update(hThis,~,~,varargin) -% UPDATE Update cursorbar position and string -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2003-2016 The MathWorks, Inc. - - -% Exit during construction -if hThis.ObjectBeingCreated - return -end - -% Check input -movecb = true; -if nargin >= 4 && ischar(varargin{1}) && strcmp(varargin{1},'-nomove') - movecb = false; -end - -% Create new data cursor -if isvalid(hThis.DataCursorHandle) - hNewDataCursor = hThis.DataCursorHandle; -else - hNewDataCursor = createNewDataCursor(hThis); -end - -% Update cursor based on target -if movecb - updateDataCursor(hThis,hNewDataCursor); - hNewDataCursor = hThis.DataCursorHandle; % in the case it has changed -end - -% Update cursorbar based on cursor -updatePosition(hThis,hNewDataCursor); - -% Update markers -updateMarkers(hThis); - -% Send event indicating that the cursorbar was updated -notify(hThis,'UpdateCursorBar'); diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/updateDataCursor.m b/Required packages/Cursorbar/+graphics/@Cursorbar/updateDataCursor.m deleted file mode 100644 index f80e5e1..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/updateDataCursor.m +++ /dev/null @@ -1,43 +0,0 @@ -function updateDataCursor(hThis,hNewDataCursor,~) -%UPDATEDATACURSOR Updates DataCursor's position. -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2003-2016 The MathWorks, Inc. - -hAxes = get(hThis,'Parent'); -cp = get(hAxes,'CurrentPoint'); -pos = [cp(1,1) cp(1,2) 0]; -hTarget = hThis.Target; - -if isTargetAxes(hThis) - % axes: ignore interpolation, just use the axes' CurrentPoint - hNewDataCursor.DataSource.XData = pos(1); - hNewDataCursor.DataSource.YData = pos(2); - -else - % put the DataCursor in a correct place - [x,y,n] = closestvertex(hThis,pos); - if ~isscalar(hTarget) - if isa(hTarget,'matlab.graphics.chart.interaction.DataAnnotatable') - hNewDataCursor.DataSource = hTarget(n); - else - hNewDataCursor = createNewDataCursor(hThis,hTarget(n)); - end - end - - % update the DataCursor - if strcmp(hAxes.Parent.Type,'figure') - % update directly - hNewDataCursor.Position = [x y 0]; - else - % if the parent is not a figure (e.g., panel), the position of the - % DataCursor is not rendered correctly; thus, a change of parents - % is mandatory - axesPar = hAxes.Parent; - hAxes.Parent = ancestor(hAxes,'figure'); - hNewDataCursor.Position = [x y 0]; - hAxes.Parent = axesPar; - end -end diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/updateDisplay.m b/Required packages/Cursorbar/+graphics/@Cursorbar/updateDisplay.m deleted file mode 100644 index 9e4cbe7..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/updateDisplay.m +++ /dev/null @@ -1,33 +0,0 @@ -function updateDisplay(hThis,~,~) -% UPDATEDISPLAY Updates DisplayHandle. -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2003-2016 The MathWorks, Inc. - -% exit during construction -if hThis.ObjectBeingCreated - return -end - -% update text handles -hText = get(hThis,'DisplayHandle'); -% -if strcmp(hThis.ShowText,'off') || strcmp(hThis.Visible,'off') - if ~isempty(hText) - delete(hText); - hThis.DisplayHandle = gobjects(0); - return - end - return -end - -% update -defaultUpdateFcn(hThis); - -if ~isempty(hThis.UpdateFcn) - hThis.localApplyUpdateFcn(hThis,[]); -end - - diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/updateMarkers.m b/Required packages/Cursorbar/+graphics/@Cursorbar/updateMarkers.m deleted file mode 100644 index e2f127f..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/updateMarkers.m +++ /dev/null @@ -1,55 +0,0 @@ -function updateMarkers(hThis) -% UPDATEMARKERS Updates data markers. -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2003-2016 The MathWorks, Inc. - -% exit during construction -if hThis.ObjectBeingCreated - return -end - -% get line handles -hTarget = hThis.Target; - -% get current position -pos = get(hThis,'Position'); -if isempty(pos), return; end % probably, at startup - -% determine which vertices will be intersected -switch hThis.TargetIntersections - case 'multiple' % find all intersection based on the Orientation - switch hThis.Orientation - case 'vertical' - ind = find(hThis.TargetXData == pos(1)); % find only the identical positions - if isempty(ind) - [~,ind] = min( abs(hThis.TargetXData - pos(1)) ); % find the closest ones - end - case 'horizontal' - ind = find(hThis.TargetYData == pos(2)); % find only the identical positions - if isempty(ind) - [~,ind] = min( abs(hThis.TargetYData - pos(2)) ); % find the closest ones - end - end - case 'single' % just the closest ones - [~,ind] = min( hypot(hThis.TargetXData-pos(1), hThis.TargetYData-pos(2)) ); - if ~isempty(ind) - ind = ind(1); - end -end - -% set the target markers -if all(isvalid(hThis.TargetMarkerHandle)) - if all(isvalid(hTarget)) && ~isa(hTarget,'matlab.graphics.axis.Axes') - set(hThis.TargetMarkerHandle,'Visible','on',... - 'XData',hThis.TargetXData(ind),... - 'YData',hThis.TargetYData(ind)); - else - % - set(hThis.TargetMarkerHandle,'Visible','off',... - 'XData',[],... - 'YData',[]); - end -end diff --git a/Required packages/Cursorbar/+graphics/@Cursorbar/updatePosition.m b/Required packages/Cursorbar/+graphics/@Cursorbar/updatePosition.m deleted file mode 100644 index b8cfe0c..0000000 --- a/Required packages/Cursorbar/+graphics/@Cursorbar/updatePosition.m +++ /dev/null @@ -1,36 +0,0 @@ -function updatePosition(hThis,hNewDataCursor) -% UPDATEPOSITION Update cursorbar position based on data cursor -% -% Thanks to Yaroslav Don for his assistance in updating cursorbar for -% MATLAB Graphics and for his contribution of new functionality. - -% Copyright 2003-2016 The MathWorks, Inc. - -% Set parameters -pos = hNewDataCursor.Position; -hAxes = hThis.Parent; -ok = false; - -% See if the cursor position is empty or outside the axis limits -xlm = get(hAxes,'XLim'); -ylm = get(hAxes,'YLim'); -zlm = get(hAxes,'ZLim'); -% -if ~isempty(pos) && ... - (pos(1) >= min([xlm Inf])) && (pos(1) <= max([xlm -Inf])) && ... - (pos(2) >= min([ylm Inf])) && (pos(2) <= max([ylm -Inf])) - if length(pos) > 2 - if pos(3) >= min([zlm Inf]) && pos(3) <= max([zlm -Inf]) - ok =true; - end - else - pos(3) = 0; - ok = true; - end -end - -% Update DataCursorHandle and Position -if ok - hThis.DataCursorHandle = hNewDataCursor; - hThis.Position = pos; -end diff --git a/Required packages/Cursorbar/+graphics/Graphics.m b/Required packages/Cursorbar/+graphics/Graphics.m deleted file mode 100644 index a653740..0000000 --- a/Required packages/Cursorbar/+graphics/Graphics.m +++ /dev/null @@ -1,187 +0,0 @@ -classdef (Abstract) Graphics < handle & matlab.mixin.Heterogeneous & matlab.mixin.CustomDisplay - % Graphics Common base class for graphics objects - % - % The graphics.Graphics class is the base class of all graphics objects. - % Because graphics objects are part of a heterogeneous hierarchy, you - % can create arrays of mixed classes (for example, an array can contain - % lines, surfaces, axes, and other graphics objects). - % - % The class of an array of mixed objects is graphics.Graphics because - % this class is common to all graphics objects. - % - % Graphics requires the new MATLAB graphics system that - % was introduced in R2014b - % - % Inherited matlab.mixin.CustomDisplay Methods: - % details - Fully detailed formal object display. - % disp - Simple informal object display. - % display - Print variable name and display object. - % - % Inherited matlab.mixin.Heterogeneous Methods: - % cat - Concatenation for heterogeneous arrays. - % horzcat - Horizontal concatenation for - % heterogeneous arrays. - % vertcat - Vertical concatenation for - % heterogeneous arrays. - % - % Protected Methods: - % getDefaultScalarElement - Define default element for array - % operations. - % - % Inherited handle Methods: - % addlistener - Add listener for event. - % delete - Delete a handle object. - % eq - Test handle equality. - % findobj - Find objects with specified property - % values. - % findprop - Find property of MATLAB handle object. - % ge - Greater than or equal relation. - % gt - Greater than relation. - % isvalid - Test handle validity. - % le - Less than or equal relation for handles. - % lt - Less than relation for handles. - % ne - Not equal relation for handles. - % notify - Notify listeners of event. - % - % Inherited handle Events: - % ObjectBeingDestroyed - Notifies listeners that a particular - % object has been destroyed. - % Web: - % Undocumented Matlab: Undocumented cursorbar object. - % - % See also: graphics.GraphicsPlaceholder, graphics.Cursorbar. - % - % Thanks to Yaroslav Don for his assistance in updating cursorbar for - % MATLAB Graphics and for his contribution of new functionality. - - % Copyright 2016 The MathWorks, Inc. - - %% Main methods - - methods - function hThis = Graphics() - % Graphics A Graphics constructor. - % - % See also: Graphics. - - % Check MATLAB Graphics system version - if verLessThan('matlab','8.4.0') - error('graphics:Graphics:Graphics:oldVersion', ... - 'Graphics requires the new MATLAB graphics system that was introduced in R2014b.'); - end - end - end - - %% Heterogeneous methods - - methods (Static, Sealed, Access = protected) - function defaultObject = getDefaultScalarElement - defaultObject = graphics.GraphicsPlaceholder; - end - end - - %% Custom Display Methods - - methods (Access = protected, Sealed) - - % -------------------------------------- - function header = getHeader(hThis) - if ~isscalar(hThis) - % Non-scalar case: call superclass method - headerStr = getHeader@matlab.mixin.CustomDisplay(hThis); - if ismatrix(hThis) && ~isempty(hThis) - header = regexprep(headerStr,' with( no)? properties[\.:]',':'); - else - header = regexprep(headerStr,' with( no)? properties[\.:]','.'); - end - header = regexprep(header,'heterogeneous |heterogeneous ',''); - else - % Scalar case: check if a new header is required - if isprop(hThis,'Tag') - tagStr = hThis.Tag; - else - tagStr = ''; - end - % - if isempty(tagStr) - % No tag: call superclass method - header = getHeader@matlab.mixin.CustomDisplay(hThis); - else - % Use the tag - headerStr = matlab.mixin.CustomDisplay.getClassNameForHeader(hThis); - header = sprintf(' %s (%s) with properties:\n',headerStr,tagStr); - end - end - end - - % -------------------------------------- - function groups = getPropertyGroups(hThis) - % GETPROPERTYGROUPS Construct array of property groups. - if isscalar(hThis) - % Scalar case: call unsealed getScalarPropertyGroups - groups = getScalarPropertyGroups(hThis); - else - % Non-scalar case: empty list - groups = matlab.mixin.util.PropertyGroup({}); - end - end - - % -------------------------------------- - function footer = getFooter(hThis) - % GETFOOTER Build and return display footer text. - - if isscalar(hThis) - % Scalar case: prompt to show all properties - % similarly to graphics objects - if isempty(properties(hThis)) - % No properties: call superclass method - footer = getFooter@matlab.mixin.CustomDisplay(hThis); - return; - end - % - iname = inputname(1); - if isempty(iname) - iname = 'ans'; % ans is the default input name - end - % - footer = sprintf(... - [' Show all properties\n'], ... - iname,iname,iname,class(hThis),iname); - elseif ismatrix(hThis) && ~isempty(hThis) - % Non-scalar matrix case: show object's classes - % extract naked class name - txt = arrayfun(@class,hThis,'Uni',0); - txt = regexprep(txt,'^.*\.',''); - % add spaces and end-of-line - len = max(cellfun(@length,txt(:))); - txt = cellfun(@(s)sprintf(' %-*s',len,s),txt,'Uni',0); - txt(:,end) = strcat(txt(:,end),{sprintf('\n')}); - % finalize - footer = cell2mat(txt)'; - else - % Non-scalar case: call superclass method - footer = getFooter@matlab.mixin.CustomDisplay(hThis); - end - end - end - - % ============================================================= % - - methods (Access = protected) - function groups = getScalarPropertyGroups(hThis) %#ok - % GETSCALARPROPERTYGROUPS Construct array of property groups for display of scalar case. - - % default is empty - groups = matlab.mixin.util.PropertyGroup({}); - end - - end - -end - diff --git a/Required packages/Cursorbar/+graphics/GraphicsPlaceholder.m b/Required packages/Cursorbar/+graphics/GraphicsPlaceholder.m deleted file mode 100644 index f0c4608..0000000 --- a/Required packages/Cursorbar/+graphics/GraphicsPlaceholder.m +++ /dev/null @@ -1,85 +0,0 @@ -classdef (ConstructOnLoad=true) GraphicsPlaceholder < graphics.Graphics - % GraphicsPlaceholder Default graphics object. - % - % The graphics.GraphicsPlaceholder class defines the default graphics - % object. Instances of this class appear as: - % - % * Elements of pre-allocated arrays created with hobjects. - % * Unassigned array element placeholders - % * Graphics object properties that hold object handles, but are set - % to empty values - % * Empty values returned by functions that return object handles (for - % example, findobj). - % - % Usage: - % graphics.GraphicsPlaceholder() - Creates a GraphicsPlaceholder. - % - % Example: - % x = linspace(0,20,101); - % y = sin(x); - % % - % hPlot = plot(x,y); - % hCBar(3) = cursorbar(hPlot); - % hGPHolder = hCBar(1) - % - % GraphicsPlaceholder Constructor: - % GraphicsPlaceholder - GraphicsPlaceholder constructor. - % - % Inherited matlab.mixin.CustomDisplay Methods: - % details - Fully detailed formal object display. - % disp - Simple informal object display. - % display - Print variable name and display object. - % - % Inherited matlab.mixin.Heterogeneous Methods: - % cat - Concatenation for heterogeneous arrays. - % horzcat - Horizontal concatenation for - % heterogeneous arrays. - % vertcat - Vertical concatenation for - % heterogeneous arrays. - % - % Inherited handle Methods: - % addlistener - Add listener for event. - % delete - Delete a handle object. - % eq - Test handle equality. - % findobj - Find objects with specified property - % values. - % findprop - Find property of MATLAB handle object. - % ge - Greater than or equal relation. - % gt - Greater than relation. - % isvalid - Test handle validity. - % le - Less than or equal relation for handles. - % lt - Less than relation for handles. - % ne - Not equal relation for handles. - % notify - Notify listeners of event. - % - % Inherited handle Events: - % ObjectBeingDestroyed - Notifies listeners that a particular - % object has been destroyed. - % Web: - % Undocumented Matlab: Undocumented cursorbar object. - % - % See also: graphics.Graphics, hobjects. - % - % Thanks to Yaroslav Don for his assistance in updating cursorbar for - % MATLAB Graphics and for his contribution of new functionality. - - % Copyright 2016 The MathWorks, Inc. - - %% Main methods - - methods - function hThis = GraphicsPlaceholder() - % GRAPHICSPLACEHOLDER A GraphicsPlaceholder constructor. - % - % See also: GraphicsPlaceholder. - - % Check MATLAB Graphics system version - if verLessThan('matlab','8.4.0') - error('graphics:GraphicsPlaceholder:GraphicsPlaceholder:oldVersion', ... - 'GraphicsPlaceholder requires the new MATLAB graphics system that was introduced in R2014b.'); - end - end - end - -end - diff --git a/Required packages/Cursorbar/crossbar.m b/Required packages/Cursorbar/crossbar.m deleted file mode 100644 index 7e13f29..0000000 --- a/Required packages/Cursorbar/crossbar.m +++ /dev/null @@ -1,96 +0,0 @@ -function [hCursorbar, hCrossbar] = crossbar(hTarget,varargin) -% CROSSBAR Creates two perpendicular linked cursorbars. -% -% The cursorbar can be dragged interactively across the axes. If -% attached to a plot, the cursor points are updated as well. The -% cursorbar can be either horizontal or vertical. -% -% The two crossed cursorbars are linked in position. Dragging one will -% update the position of the other to the cursor's location as well. -% -% Cursorbar requires the new MATLAB graphics system that was -% introduced in R2014b -% -% Usage: -% crossbar(hTarget) - Creates two perpendicular linked -% cursorbars on a target Axes or Chart. -% crossbar(hTarget, ...) - Creates two perpendicular linked -% cursorbars on a target Axes or Chart -% with additional property-value pairs. -% hCursorbars = crossbar(...) - Returns the handles to the two -% cursorbars. -% [hCo,hCross]= crossbar(...) - Returns the handles to the main -% cursorbar and to the crossed one. -% -% See graphics.Cursorbar for the full list of Cursorbar's properties. -% -% Example 1: Simple Crossbars -% x = linspace(-10,10,101); -% y = erf(x/5); -% % -% h = plot(x,y); -% crossbar(h); -% -% Example 2: Update Properties -% x = linspace(-10,10,49); -% M = peaks(length(x)); -% % -% h = imagesc(x,x,M); -% [cb,cr] = crossbar(h,'ShowText','off','TargetMarkerStyle','none'); -% % -% set(cr,'ShowText','on','TargetIntersections','single', ... -% 'TextDescription','long'); -% set([cb; cr], {'CursorLineColor'},{[1.0 0.8 0.8];[1.0 1.0 0.8];}); -% % -% cb.Position = [-7 3 0]; -% -% Example 3: Link Cursorbars -% t = linspace(0,12*pi,10001)'; -% x = 2*cos(t) + 2*cos(t/6); -% y = 2*sin(t) - 2*sin(t/6); -% % -% h = plot(x,y,'LineWidth',2); -% axis([-4 4 -4 4]); -% cb1 = crossbar(h,'CursorLineColor',[1 0 0],'Position',[-1 -1 0], ... -% 'TargetIntersections','single','CursorLineStyle','--'); -% cb2 = crossbar(h,'CursorLineColor',[1 0 1],'Position',[ 1 1 0], ... -% 'TargetIntersections','single','CursorLineStyle','--'); -% % -% l(1)=addlistener( cb1(1),'Location','PostSet', ... -% @(~,~) set ( cb2(1),'Location',cb1(1).Location+2)); -% l(2)=addlistener( cb1(2),'Location','PostSet', ... -% @(~,~) set ( cb2(2),'Location',cb1(2).Location+2)); -% l(3)=addlistener( cb2(1),'Location','PostSet', ... -% @(~,~) set ( cb1(1),'Location',cb2(1).Location-2)); -% l(4)=addlistener( cb2(2),'Location','PostSet', ... -% @(~,~) set ( cb1(2),'Location',cb2(2).Location-2)); -% -% See also: cursorbar, hobjects, graphics.Cursorbar. - -% Copyright 2016 The MathWorks, Inc. - -% Check MATLAB Graphics system version -if verLessThan('matlab','8.4.0') - error('crossbar:oldVersion', ... - 'Crossbar requires the new MATLAB graphics system that was introduced in R2014b.'); -end - -% error check -narginchk (1,Inf); -nargoutchk(0,2); - -% draw cursorbar -hTemp(1) = graphics.Cursorbar(hTarget,varargin{:}); -hTemp(2) = drawCrossbar(hTemp(1), varargin{:}); - -% output -switch nargout - case 0 - % never mind ... - case 1 - hCursorbar = hTemp; - case 2 - hCursorbar = hTemp(1); - hCrossbar = hTemp(2); -end - diff --git a/Required packages/Cursorbar/cursorbar.m b/Required packages/Cursorbar/cursorbar.m deleted file mode 100644 index e2bee3c..0000000 --- a/Required packages/Cursorbar/cursorbar.m +++ /dev/null @@ -1,174 +0,0 @@ -function hCursorbar = cursorbar(hTarget,varargin) -% CURSORBAR Creates a cursor line attached to an axes or lines. -% -% The cursorbar can be dragged interactively across the axes. If -% attached to a plot, the cursor points are updated as well. The -% cursorbar can be either horizontal or vertical. -% -% Cursorbar requires the new MATLAB graphics system that was -% introduced in R2014b -% -% Usage: -% cursorbar(hTarget) - Creates a cursorbar on a target Axes -% or Chart. -% cursorbar(hTarget, ...) - Creates a cursorbar on a target Axes -% or Chart with additional property- -% value pairs. -% hCursorbar = cursorbar(...) - Returns the handle to the Cursorbar. -% -% See graphics.Cursorbar for the full list of Cursorbar's properties. -% -% Example 1: Simple Cursorbar -% x = linspace(0,20,101); -% y = sin(x); -% % -% h = plot(x,y); -% cursorbar(h); -% -% Example 2: Target Axes -% x = linspace(-2,2,101)'; -% Y = [sin(x), 1-x.^2/2, erf(x), ones(size(x))]; -% % -% area(x,abs(Y)); -% cursorbar(gca,'CursorLineColor',[0.02 0.75 0.27]); -% set(gca,'Color','r') -% -% Example 3: Stem Plot -% x = 1:19; -% Y = interp1(1:5,magic(5),linspace(1,5,19),'pchip'); -% % -% h = stem(x,Y); -% cb = cursorbar(h); -% cb.TargetMarkerStyle = 'x'; -% -% Example 4: Logarithmic YScale -% x = linspace(0,4,41)'; -% Y = [exp(2*x)/4, exp(x/10)+1/160*exp(3*x), x.^2+1]; -% % -% h = stairs(x,Y,'LineWidth',2); -% % -% ax = gca; -% ax.YScale = 'log'; -% ax.YLim(2) = 1000; -% grid on; -% % -% cursorbar(h,'Location',2.7) -% -% Example 5: Crossbar -% x = linspace(-10,10,49); -% M = peaks(length(x)); -% % -% h = imagesc(x,x,M); -% cb = cursorbar(h,'ShowText','off','TargetMarkerStyle','none'); -% % -% cr = drawCrossbar(cb); -% set(cr,'ShowText','on','TargetIntersections','single', ... -% 'TextDescription','long'); -% set([cb; cr], {'CursorLineColor'},{[1.0 0.8 0.8];[1.0 1.0 0.8];}); -% % -% cb.Position = [-7 3 0]; -% -% Example 6: Preallocation -% x = linspace(-3,3,101); -% y = exp(-x.^2/2); -% % -% h = plot(x,y); -% for i=5:-1:1, -% cb(i) = cursorbar( h, 'Location',i-3, ... -% 'CursorLineColor',[(1-i/5) 0 i/5]); -% end -% -% Example 7: Listeners -% t = linspace(0,32*pi,10001)'; -% x = 2*cos(t) + 2*cos(t/16); -% y = 2*sin(t) - 2*sin(t/16); -% % -% ax= axes('XLim',[-4 4],'YLim',[-4 4],'NextPlot','add'); -% h = plot(x,y,'Color',[0.929 0.694 0.125]); -% % -% for i=5:-1:1, -% cb(i) = cursorbar( h, 'Location',i-3, 'ShowText','off', ... -% 'CursorLineColor',[(1-i/5) i/5 0]); -% end -% % -% % add listeners -% for i=1:5, -% j = mod(i,5)+1; -% addlistener ( cb(i),'Location','PostSet', ... -% @(~,~)set(cb(j),'Location',cb(i).Location+j-i)); -% end -% % -% addlistener(cb,'BeginDrag',@(~,~)set(ax,'Color',[.9 .8 .9])); -% addlistener(cb,'EndDrag' ,@(~,~)set(ax,'Color','w')); -% addlistener(cb,'UpdateCursorBar', ... -% @(~,~)set(h,'LineWidth',abs(cb(3).Location)+1)); -% -% Example 8: Save and Load -% % draw Cursorbars -% x = linspace(-4,4,101); -% y = cos(x); -% h = plot(x,y); -% % -% cb(1) = cursorbar(h,'Location',2.50,'CursorLineColor',[1 0 0]); -% cb(2) = cursorbar(h,'Location',-.25,'CursorLineColor',[0 1 0],... -% 'Orientation','horizontal'); -% cb(3) = drawCrossbar(cb(2)); -% % -% % save and load -% tempname = 'temp_cb.fig'; -% savefig(gcf,tempname); -% % -% open(tempname); -% -% Example 9: Marker Styles -% % create line plot -% x = linspace(0,14,201); -% y = sin(2*pi*x/3); -% % -% h = plot(x,y,':k','LineWidth',2); -% ylim([-1.2 1.2]); -% % -% % define colors -% topMarkers = 'x+*o.sdv^>0 - % create a full array - hCBar(nel) = graphics.GraphicsPlaceholder; - hCBar = reshape(hCBar,siz); -else - % create an empty array - hCBar = graphics.GraphicsPlaceholder.empty(siz); -end - -end diff --git a/Resources/trace_disp_icon.png b/Resources/trace_disp_icon.png new file mode 100644 index 0000000..7a9ed40 Binary files /dev/null and b/Resources/trace_disp_icon.png differ diff --git a/Resources/trace_not_disp_icon.png b/Resources/trace_not_disp_icon.png new file mode 100644 index 0000000..ed53387 Binary files /dev/null and b/Resources/trace_not_disp_icon.png differ diff --git a/Test functions/DataAcquisitionMenu test/testdaqmenu.m b/Test functions/DataAcquisitionMenu test/testdaqmenu.m deleted file mode 100644 index c794d8a..0000000 --- a/Test functions/DataAcquisitionMenu test/testdaqmenu.m +++ /dev/null @@ -1,6 +0,0 @@ - runDPO4034_1; - runRSA5106; - - test_menu=DataAcquisitionMenu('InstrHandles',{GuiScopeDPO4034_1.Instr,GuiRsaRSA5106.Instr}); - - test_daq=MyDaq('daq_menu_handle',test_menu); \ No newline at end of file diff --git a/Test tools/Dummy objects/@MyDummyInstrument/MyDummyInstrument.m b/Test tools/Dummy objects/@MyDummyInstrument/MyDummyInstrument.m new file mode 100644 index 0000000..8515195 --- /dev/null +++ b/Test tools/Dummy objects/@MyDummyInstrument/MyDummyInstrument.m @@ -0,0 +1,106 @@ +% Object for testing data acquisition and header collection functionality + +classdef MyDummyInstrument < MyInstrument & MyDataSource & MyGuiCont + + properties (Access = public, SetObservable = true) + point_no = 1000 + + trace_type = 'zero' + end + + methods (Access = public) + function this = MyDummyInstrument(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + + this.gui_name = 'GuiDummyInstrument'; + + createCommandList(this); + end + + function readTrace(this) + + % Generate a random trace with the length equal to point_no + this.Trace.x = (0:this.point_no-1)/(this.point_no-1); + this.Trace.y = rand(1, this.point_no); + + switch this.trace_type + case 'zero' + + % Do nothing + case 'exp' + + % Add exponential "signal" + a = 5+rand(); + b = 10*rand(); + + sig = a*exp(-b*this.Trace.x); + + this.Trace.y = this.Trace.y + sig; + case 'lorentz' + + % Add lorentzian "signal" + a = 10+rand(); + b = 10*rand(); + x0 = rand(); + dx = 0.05*rand(); + + sig = a-b*dx^2./((this.Trace.x-x0).^2+dx^2); + + this.Trace.y = this.Trace.y + sig(:); + otherwise + error(['Unsupported trace type ' this.trace_type]) + end + + triggerNewData(this); + end + + function Lg = createLogger(this, varargin) + function x = getRandomMeasurement() + sync(this); + x = this.cmd3; + end + + Lg = MyLogger(varargin{:}, 'MeasFcn', @getRandomMeasurement); + + Lg.Record.data_headers = {'random 1', 'random 2', ... + 'random 3', 'random 4', 'random 5'}; + end + + function idn(this) + this.idn_str = 'dummy'; + end + end + + methods (Access = protected) + function createCommandList(this) + + % cmd1 is read and write accessible, it represents a paramter + % which we can set and which does not change unless we set it + addCommand(this, 'cmd1', ... + 'readFcn', @()this.cmd1, ... + 'writeFcn', @(x)fprintf('cmd 1 write %e\n', x), ... + 'default', rand()); + + addCommand(this, 'cmd2', ... + 'readFcn', @()rand(), ... + 'info', 'read only scalar'); + + % cmd3 is a read only vector + addCommand(this, 'cmd3', ... + 'readFcn', @()rand(1,5), ... + 'info', 'read only vector'); + end + end + + methods + function set.trace_type(this, val) + assert(strcmpi(val, 'zero')|| ... + strcmpi(val, 'exp')|| ... + strcmpi(val, 'lorentz'), ... + 'Trace type must be ''zero'', ''exp'' or ''lorentz''.') + this.trace_type = lower(val); + end + end +end + diff --git a/Test tools/Dummy objects/@MyDummyScpiInstrument/MyDummyScpiInstrument.m b/Test tools/Dummy objects/@MyDummyScpiInstrument/MyDummyScpiInstrument.m new file mode 100644 index 0000000..765dfe1 --- /dev/null +++ b/Test tools/Dummy objects/@MyDummyScpiInstrument/MyDummyScpiInstrument.m @@ -0,0 +1,95 @@ +% Object for testing data acquisition and header collection functionality + +classdef MyDummyScpiInstrument < MyScpiInstrument & MyDataSource & MyGuiCont + + properties (Access = public) + point_no = 1000 + end + + methods (Access = public) + function this = MyDummyScpiInstrument(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + + this.gui_name = 'GuiDummyInstrument'; + + createCommandList(this); + end + + function readTrace(this) + + % Generate a random trace with the length equal to point_no + this.Trace.x = 1:this.point_no; + this.Trace.y = rand(1, this.point_no); + + triggerNewData(this); + end + + % Replacement method that emulates communication with physical + % device + function resp_str = queryString(this, query_str) + query_cmds = strsplit(query_str, ';'); + + resp_str = ''; + for i = 1:length(query_cmds) + cmd = query_cmds{i}; + if ~isempty(cmd) + switch cmd + case 'COMMAND1?' + tmp_resp = sprintf(this.CommandList.cmd1.format, ... + this.cmd1); + case 'COMMAND2?' + tmp_resp = sprintf(this.CommandList.cmd2.format, ... + this.cmd2); + case 'COMMAND3?' + tmp_resp = sprintf(this.CommandList.cmd3.format, ... + this.cmd3); + case 'COMMAND4?' + tmp_resp = sprintf(this.CommandList.cmd4.format, ... + this.cmd4); + otherwise + tmp_resp = ''; + end + + resp_str = [resp_str, tmp_resp, ';']; %#ok + end + end + + if ~isempty(resp_str) + resp_str = resp_str(1:end-1); + end + end + + % writeString does nothing + function writeString(this, str) %#ok + end + + function idn(this) + this.idn_str = 'dummy'; + end + end + + methods (Access = protected) + function createCommandList(this) + + addCommand(this, 'cmd1', 'COMMAND1', ... + 'format', '%e', ... + 'info', 'regular read/write numeric command'); + + addCommand(this, 'cmd2', 'COMMAND2', ... + 'format', '%e', ... + 'access', 'r', ... + 'info', 'read-only numeric command'); + + addCommand(this, 'cmd3', 'COMMAND3', ... + 'format', '%i,%i,%i,%i,%i', ... + 'info', 'read/write vector'); + + addCommand(this, 'cmd4', 'COMMAND4', ... + 'format', '%s', ... + 'info', 'regular read/write string command', ... + 'default', 'val'); + end + end +end + diff --git a/Test tools/Dummy objects/GuiDummyInstrument.mlapp b/Test tools/Dummy objects/GuiDummyInstrument.mlapp new file mode 100644 index 0000000..b998dd9 Binary files /dev/null and b/Test tools/Dummy objects/GuiDummyInstrument.mlapp differ diff --git a/Test functions/GorodetskyFit test/testgorodetsky.m b/Test tools/GorodetskyFit test/testgorodetsky.m similarity index 74% rename from Test functions/GorodetskyFit test/testgorodetsky.m rename to Test tools/GorodetskyFit test/testgorodetsky.m index d6bbf07..8e31c17 100644 --- a/Test functions/GorodetskyFit test/testgorodetsky.m +++ b/Test tools/GorodetskyFit test/testgorodetsky.m @@ -1,15 +1,15 @@ load('testmat') figure(124) ax=gca; -testfit=MyFit('fit_name','Gorodetsky2000','x',xf,'y',yf,'plot_handle',ax,... +testfit=MyFit('fit_name','Gorodetsky2000','x',xf,'y',yf,'Axes',ax,... 'enable_gui',true,'enable_plot',true); testfit.Data.plot(ax); hold on testfit.genInitParams; testfit.init_params(1) testfit.init_params(3:end) testfit.plotInitFun; testfit.fitTrace; testfit.plotFit('Color','r'); testfit.Gof testfit.FitInfo \ No newline at end of file diff --git a/Test functions/GorodetskyFit test/testmat.mat b/Test tools/GorodetskyFit test/testmat.mat similarity index 100% rename from Test functions/GorodetskyFit test/testmat.mat rename to Test tools/GorodetskyFit test/testmat.mat diff --git a/Test functions/MyBeta test/beta_cal_4.018MHz.txt b/Test tools/MyBeta test/beta_cal_4.018MHz.txt similarity index 100% rename from Test functions/MyBeta test/beta_cal_4.018MHz.txt rename to Test tools/MyBeta test/beta_cal_4.018MHz.txt diff --git a/Test functions/MyBeta test/testBeta.m b/Test tools/MyBeta test/testBeta.m similarity index 100% rename from Test functions/MyBeta test/testBeta.m rename to Test tools/MyBeta test/testBeta.m diff --git a/Test functions/MyG test/testg0.m b/Test tools/MyG test/testg0.m similarity index 100% rename from Test functions/MyG test/testg0.m rename to Test tools/MyG test/testg0.m diff --git a/Test tools/runRandLogger.m b/Test tools/runRandLogger.m new file mode 100644 index 0000000..4b22e0e --- /dev/null +++ b/Test tools/runRandLogger.m @@ -0,0 +1,17 @@ +% Start a logger n random channels + +function [Lg, GuiLg] = runRandLogger(varargin) + p = inputParser(); + addParameter(p, 'channel_no', 2); + parse(p, varargin{:}); + + data_headers = cell(1, p.Results.channel_no); + for i = 1:length(data_headers) + data_headers{i} = ['Measurement' num2str(i)]; + end + + Lg = MyLogger('measFcn', @()rand(1, p.Results.channel_no), ... + 'log_opts', {'data_headers', data_headers}); + GuiLg = GuiLogger(Lg); +end + diff --git a/Utility functions/App utilities/@MyAppColors/MyAppColors.m b/Utility functions/App utilities/@MyAppColors/MyAppColors.m deleted file mode 100644 index 722a09e..0000000 --- a/Utility functions/App utilities/@MyAppColors/MyAppColors.m +++ /dev/null @@ -1,32 +0,0 @@ -% Set of colors indended to introduce some uniformity in GUIs - -classdef MyAppColors - - % Colors are represented by rgb triplets returned by static methods - methods (Static) - - function rgb=warning() - % Orange - rgb=[0.93, 0.69, 0.13]; - end - - function rgb=error() - % Red - rgb=[1,0,0]; - end - - % Labview-style lamp indicator colors - function rgb=lampOn() - % Bright green - rgb=[0,1,0]; - end - - function rgb=lampOff() - % Dark green - rgb=[0,0.4,0]; - end - - end - -end - diff --git a/Utility functions/App utilities/deleteInstrGui.m b/Utility functions/App utilities/deleteInstrGui.m deleted file mode 100644 index 9f270fc..0000000 --- a/Utility functions/App utilities/deleteInstrGui.m +++ /dev/null @@ -1,38 +0,0 @@ -% Delete Instrument object, clearing the global variable corresponding to -% gui name and then delete gui itself -function deleteInstrGui(app) - %Deletes listeners - try - lnames=fieldnames(app.Listeners); - for i=1:length(lnames) - try - delete(app.Listeners.(lnames{i})); - catch - fprintf(['Could not delete the listener to ''%s'' ' ... - 'event.\n'], lnames{i}) - end - end - catch - end - - try - % Check if the instrument object has appropriate method. This - % is a safety measure to never delete a file by accident if - % app.Instr happens to be a valid file name. - if ismethod(app.Instr, 'delete') - delete(app.Instr); - else - fprintf(['app.Instr of class ''%s'' does not have ' ... - '''delete'' method.\n'], class(app.Instr)) - end - catch - fprintf('Could not delete the instrument object.\n') - end - - try - evalin('base', sprintf('clear(''%s'')', app.name)); - catch - end - delete(app) -end - diff --git a/Utility functions/App utilities/genericValueChanged.m b/Utility functions/App utilities/genericValueChanged.m deleted file mode 100644 index ee95ccb..0000000 --- a/Utility functions/App utilities/genericValueChanged.m +++ /dev/null @@ -1,16 +0,0 @@ -function genericValueChanged(app, event) - val = event.Value; - - % Apply input processing function or prescaler if exist - if isfield(event.Source.UserData, 'InputProcessingFcn') - val = event.Source.UserData.InputProcessingFcn(val); - elseif isfield(event.Source.UserData, 'InputPrescaler') - val = val/event.Source.UserData.InputPrescaler; - end - - assert(isfield(event.Source.UserData,'LinkSubs'),... - '''LinkSubs'' structure is missing from a linked element.') - - app=subsasgn(app, event.Source.UserData.LinkSubs, val); %#ok -end - diff --git a/Utility functions/App utilities/initInstrGui.m b/Utility functions/App utilities/initInstrGui.m deleted file mode 100644 index 57669b0..0000000 --- a/Utility functions/App utilities/initInstrGui.m +++ /dev/null @@ -1,12 +0,0 @@ -% Send identification request to instrument and create listeners for -% instrument events -function initInstrGui(app) - % Initiate gui update via listeners - app.Listeners.PropertyRead=... - addlistener(app.Instr,'PropertyRead',@(~,~)updateGui(app)); - if ismethod(app,'updatePlot') - app.Listeners.NewData=... - addlistener(app.Instr,'NewData',@(~,~)updatePlot(app)); - end -end - diff --git a/Utility functions/App utilities/linkGuiElement.m b/Utility functions/App utilities/linkGuiElement.m deleted file mode 100644 index 001c60a..0000000 --- a/Utility functions/App utilities/linkGuiElement.m +++ /dev/null @@ -1,238 +0,0 @@ -% Using app.linked_elem_list, create correspondence between app -% properties or sub-properties and Value fields of control elements. -% The elements added to linked_elem_list are updated when updateGui is -% called. -% This function is applicable to any app sub-properties, but also contains -% extended functionality in case the tag corresponds to a command of -% MyScpiInstrument. -% By default a callback to the ValueChanged event of the gui element is -% assigned, for which the app needs to have createGenericCallback method - -function linkGuiElement(app, elem, prop_tag, varargin) - p=inputParser(); - - % GUI control element - addRequired(p,'elem'); - - % Instrument command to be linked to the GUI element - addRequired(p,'prop_tag',@ischar); - - % A property of the GUI element that is updated according to the value - % under prop_tag can be other than 'Value' (e.g. 'Color' in the case of - % a lamp indicator) - addParameter(p,'elem_prop','Value',@ischar); - - % If input_presc is given, the value assigned to the instrument propery - % is related to the value x displayed in GUI as x/input_presc. - addParameter(p,'input_presc',1,@isnumeric); - - % Arbitrary processing functions can be specified for input and output. - % out_proc_fcn is applied to values before assigning them to gui - % elements and in_proc_fcn is applied before assigning - % to the linked properties - addParameter(p,'out_proc_fcn',@(x)x,@(f)isa(f,'function_handle')); - addParameter(p,'in_proc_fcn',@(x)x,@(f)isa(f,'function_handle')); - - addParameter(p,'create_callback',true,@islogical); - - % For drop-down menues initializes entries automatically based on the - % list of values. Ignored for all the other control elements. - addParameter(p,'init_val_list',false,@islogical); - - parse(p,elem,prop_tag,varargin{:}); - - create_callback = p.Results.create_callback; - - if isempty(prop_tag) - warning('''prop_tag'' is empty, element is not linked') - return - end - - % Make sure the property tag starts with a dot and convert to - % subreference structure - if prop_tag(1)~='.' - PropSubref=str2substruct(['.',prop_tag]); - else - PropSubref=str2substruct(prop_tag); - end - - % Check if the referenced property is accessible - try - target_val=subsref(app, PropSubref); - catch - disp(['Property corresponding to tag ',prop_tag,... - ' is not accessible, element is not linked and disabled.']) - elem.Enable='off'; - return - end - - % Check if the tag refers to a property of an object, which also helps - % to determine is callback is to be created - if (length(PropSubref)>1) && isequal(PropSubref(end).type,'.') - % Potential MyInstrument object - Obj=subsref(app, PropSubref(1:end-1)); - % Potential command name - tag=PropSubref(end).subs; - % Check if the property corresponds to an instrument command - try - is_cmd=ismember(tag, Obj.command_names); - catch - % If anything goes wrong in the previous block the prop is not - % a command - is_cmd=false; - end - if is_cmd - % Object is an instrument. - Instr=Obj; - % Never create callbacks for read-only properties. - if ~contains(Instr.CommandList.(tag).access,'w') - create_callback=false; - end - % Then check if the tag corresponds to a simple object - % property (and not to a structure field) - elseif isprop(Obj, tag) - try - % indprop may sometimes throw errors, especially on Matlab - % below 2018a, therefore use try-catch - mp = findprop(Obj, tag); - % Newer create callbacks for the properties with - % attributes listed below, as those cannot be set - if mp.Constant||mp.Abstract||~strcmpi(mp.SetAccess,'public') - create_callback=false; - end - catch - end - end - else - is_cmd=false; - end - - % Check if the gui element is editable - if it is not, then a callback - % is not assigned. This is only meaningful for uieditfieds. Drop-downs - % also have 'Editable' property, but it corresponds to the editability - % of elements and does not have an effect on assigning callback. - if (strcmpi(elem.Type, 'uinumericeditfield') || ... - strcmpi(elem.Type, 'uieditfield')) ... - && strcmpi(elem.Editable, 'off') - create_callback=false; - end - - % If the gui element is disabled callback is not assigned - if isprop(elem, 'Enable') && strcmpi(elem.Enable, 'off') - create_callback=false; - end - - % If create_callback is true and the element does not already have - % a callback, assign genericValueChanged as ValueChangedFcn - if create_callback && isprop(elem, 'ValueChangedFcn') && ... - isempty(elem.ValueChangedFcn) - % A public createGenericCallback method needs to intorduced in the - % app, as officially Matlab apps do not support an automatic - % callback assignment (as of the version of Matlab 2018a) - assert(ismethod(app,'createGenericCallback'), ['App needs to ',... - 'contain public createGenericCallback method to automatically'... - 'assign callbacks. Use ''create_callback'',false in order to '... - 'disable automatic callback']); - elem.ValueChangedFcn = createGenericCallback(app); - % Make callbacks non-interruptible for other callbacks - % (but are still interruptible for timers) - try - elem.Interruptible = 'off'; - elem.BusyAction = 'cancel'; - catch - warning('Could not make callback for %s non-interruptible',... - prop_tag); - end - end - - % A step relevant for lamp indicators. It is often convenient to have a - % lamp as an indicator of on/off state. If a lamp is being linked to a - % logical-type variable we therefore assign OutputProcessingFcn puts - % logical values in corresponcence with colors - if strcmpi(elem.Type, 'uilamp') && ~iscolor(target_val) - % The property of lamp that is to be updated by updateGui is not - % Value but Color - elem.UserData.elem_prop='Color'; - % Select between the default on and off colors. Different colors - % can be indicated by explicitly setting OutputProcessingFcn that - % will overwrite the one assigned here. - elem.UserData.OutputProcessingFcn = ... - @(x)select(x, MyAppColors.lampOn(), MyAppColors.lampOff()); - end - - % If a prescaler, input processing function or output processing - % function is specified, store it in UserData of the element - if p.Results.input_presc ~= 1 - elem.UserData.InputPrescaler = p.Results.input_presc; - end - if ~ismember('in_proc_fcn',p.UsingDefaults) - elem.UserData.InputProcessingFcn = p.Results.in_proc_fcn; - end - if ~ismember('out_proc_fcn',p.UsingDefaults) - elem.UserData.OutputProcessingFcn = p.Results.out_proc_fcn; - end - - if ~ismember('elem_prop',p.UsingDefaults) - elem.UserData.elem_prop = p.Results.out_proc_fcn; - end - - %% Linking - - % The link is established by storing the subreference structure - % in UserData and adding elem to the list of linked elements - elem.UserData.LinkSubs = PropSubref; - app.linked_elem_list = [app.linked_elem_list, elem]; - - %% MyScpiInstrument-specific - % The remaining code provides extended functionality for linking to - % commands of MyScpiInstrument - - if is_cmd - % If supplied command does not have read permission, issue warning. - if ~contains(Instr.CommandList.(tag).access,'r') - fprintf(['Instrument command ''%s'' does not have read permission,\n',... - 'corresponding gui element will not be automatically ',... - 'syncronized\n'],tag); - % Try switching color of the gui element to orange - try - elem.BackgroundColor = MyAppColors.warning; - catch - try - elem.FontColor = MyAppColors.warning; - catch - end - end - end - - % Auto initialization of entries, for dropdown menus only - if p.Results.init_val_list && isequal(elem.Type, 'uidropdown') - try - cmd_val_list = Instr.CommandList.(tag).val_list; - if all(cellfun(@ischar, cmd_val_list)) - % If the command has only string values, get the list of - % values ignoring abbreviations - cmd_val_list = stdValueList(Instr, tag); - % Capitalized the displayed values - elem.Items = cellfun(@(x)[upper(x(1)),lower(x(2:end))],... - cmd_val_list,'UniformOutput',false); - else - % Items in a dropdown should be strings, so convert if - % necessary - str_list=cell(length(cmd_val_list), 1); - for i=1:length(cmd_val_list) - if ~ischar(cmd_val_list{i}) - str_list{i}=num2str(cmd_val_list{i}); - end - end - elem.Items = str_list; - end - % Assign raw values list as ItemsData - elem.ItemsData = cmd_val_list; - catch - warning(['Could not automatically assign values',... - ' when linking ',tag,' property']); - end - end - end -end - diff --git a/Utility functions/App utilities/relinkGuiElement.m b/Utility functions/App utilities/relinkGuiElement.m deleted file mode 100644 index 491697e..0000000 --- a/Utility functions/App utilities/relinkGuiElement.m +++ /dev/null @@ -1,16 +0,0 @@ -% This function replaces the reference path for a linked gui element. -% It mainly surves the purpose of decoupling the high-level operation -% of reassignment from the impementation of linking mechanism. - -function relinkGuiElement(elem, new_prop_tag) - % Make sure the property tag starts with a dot and convert to - % subreference structure - if new_prop_tag(1)~='.' - PropSubref=str2substruct(['.',new_prop_tag]); - else - PropSubref=str2substruct(new_prop_tag); - end - % Assign new subreference structure - elem.UserData.LinkSubs = PropSubref; -end - diff --git a/Utility functions/App utilities/updateGui.m b/Utility functions/App utilities/updateGui.m deleted file mode 100644 index 0589f8d..0000000 --- a/Utility functions/App utilities/updateGui.m +++ /dev/null @@ -1,6 +0,0 @@ -% This function is typically overloaded in a particular gui to implement -% display functionality in addition to updateLinkedGuiElements -function updateGui(app) - updateLinkedGuiElements(app) -end - diff --git a/Utility functions/App utilities/updateLinkedElement.m b/Utility functions/App utilities/updateLinkedElement.m deleted file mode 100644 index 5812a08..0000000 --- a/Utility functions/App utilities/updateLinkedElement.m +++ /dev/null @@ -1,35 +0,0 @@ - -function updateLinkedElement(app, elem) - try - % get value using the subreference structure - val = subsref(app, elem.UserData.LinkSubs); - % Apply the output processing function or input prescaler - if isfield(elem.UserData, 'OutputProcessingFcn') - val = elem.UserData.OutputProcessingFcn(val); - elseif isfield(elem.UserData, 'InputPrescaler') - val = val*elem.UserData.InputPrescaler; - end - % Get the gui property to be updated. The default is Value. - if isfield(elem.UserData, 'elem_prop') - elem_prop=elem.UserData.elem_prop; - else - elem_prop='Value'; - end - % Setting value of a matlab app elemen is time consuming, so do - % this only if the value has actually changed - if ~isequal(elem.(elem_prop),val) - elem.(elem_prop) = val; - end - catch ME - % Try converting the subreference structure to a readable - % format and throw a warning - try - tag=substruct2str(elem.UserData.LinkSubs); - catch - tag=''; - end - warning(['Could not update the value of element with tag ''%s'' ' ... - 'and value ''%s''. Error: ' ME.message], tag, var2str(val)); - end -end - diff --git a/Utility functions/App utilities/updateLinkedGuiElements.m b/Utility functions/App utilities/updateLinkedGuiElements.m deleted file mode 100644 index 60d48b6..0000000 --- a/Utility functions/App utilities/updateLinkedGuiElements.m +++ /dev/null @@ -1,13 +0,0 @@ -% Set values for all the gui elements listed in app.linked_elem_list -% according to the properties they are linked to. -% The linked property is specified for each element via a subreference -% structure array stored in elem.UserData.LinkSubs. -% If specified within the control element OutputProcessingFcn or -% InputPrescaler is applied to the property value first -function updateLinkedGuiElements(app) - for i=1:length(app.linked_elem_list) - tmpelem = app.linked_elem_list(i); - updateLinkedElement(app, tmpelem); - end -end - diff --git a/Utility functions/DataAcquisitionMenu.mlapp b/Utility functions/DataAcquisitionMenu.mlapp index f4bcf2a..34eff30 100644 Binary files a/Utility functions/DataAcquisitionMenu.mlapp and b/Utility functions/DataAcquisitionMenu.mlapp differ diff --git a/Utility functions/Dialogs/tabledlg.m b/Utility functions/Dialogs/tabledlg.m index 411dee9..d6bbc1c 100644 --- a/Utility functions/Dialogs/tabledlg.m +++ b/Utility functions/Dialogs/tabledlg.m @@ -1,116 +1,156 @@ % A table-based interface for editing structure field. The input structure % is displayed as fieldname-value pairs in an editable table and the % modified values are returned as an output structure. -function OutS = tabledlg(InS, varargin) - p=inputParser(); - addRequired(p,'InS', @(x)assert(isstruct(x), ... +function OutS = tabledlg(varargin) + p = inputParser(); + + % Input structure which fields are displayed by default + addOptional(p, 'InS', struct(), @(x)assert(isstruct(x), ... 'Input argument must be structure')); - addParameter(p, 'Name', '', @ischar); % Figure name to display - parse(p, InS, varargin{:}); + + % Figure name to display + addParameter(p, 'Name', '', @ischar); + + % Structure fields to be kept unchanged + addParameter(p, 'hidden_fields', {}, @iscellstr) + + parse(p, varargin{:}); + + InS = p.Results.InS; + + % Remove hidden fields from the display and store them in a separate + % structure + hidden_fn = p.Results.hidden_fields; + UnchangedS = struct(); + for i = 1:length(hidden_fn) + if isfield(InS, hidden_fn{i}) + UnchangedS.(hidden_fn{i}) = InS.(hidden_fn{i}); + InS = rmfield(InS, hidden_fn{i}); + end + end % Selected table row number is updated by callbacks - sel_row=[]; + sel_row = []; % Output structure is only assigned when 'Continue' button is pushed - OutS=[]; + OutS = []; % Define geometry - tbl_w=262; - tbl_h=204; + tbl_w = 262; + tbl_h = 204; - btn_w=56; - btn_h=22; + btn_w = 56; + btn_h = 22; - sps=10; % space between elements + sps = 10; % space between elements + + fig_w = tbl_w+btn_w+2*sps; + fig_h = tbl_h; + R = groot(); - fig_w=tbl_w+btn_w+2*sps; - fig_h=tbl_h; - R=groot(); % Display the figure window in the center of the screen - fig_x=R.ScreenSize(3)/2-fig_w/2; - fig_y=R.ScreenSize(4)/2-fig_h/2; - - F = figure('MenuBar','none', ... - 'Name',p.Results.Name, ... - 'NumberTitle','off', ... - 'Resize','off', ... - 'Position',[fig_x,fig_y,fig_w,fig_h]); - - uicontrol(F, 'Style', 'pushbutton', 'String', 'Add',... - 'Position', [tbl_w+sps, 2*sps+btn_h, btn_w, btn_h], ... - 'Callback', @addButtonCallback); - uicontrol(F, 'Style', 'pushbutton', 'String', 'Delete',... - 'Position', [tbl_w+sps, sps+(sps+btn_h)*2, btn_w, btn_h], ... - 'Callback', @deleteButtonCallback); - uicontrol(F, 'Style', 'pushbutton', 'String', 'Continue',... - 'Position', [tbl_w+sps, sps, btn_w, btn_h], ... - 'Callback', @continueButtonCallback); - - dat=[fieldnames(InS), cellfun(@(x)var2str(x), ... - struct2cell(InS),'UniformOutput', false)]; - T = uitable(F, 'Data', dat, ... - 'ColumnWidth',{tbl_w/2-15, tbl_w/2-15}, ... - 'ColumnName',{'parameter', 'value'}, ... - 'Position',[0 0 tbl_w tbl_h], 'ColumnEditable', true, ... - 'CellSelectionCallback', @selectionCallback, ... - 'CellEditCallback', @editCallback); + fig_x = R.ScreenSize(3)/2-fig_w/2; + fig_y = R.ScreenSize(4)/2-fig_h/2; + + F = figure('MenuBar', 'none', ... + 'Name', p.Results.Name, ... + 'NumberTitle', 'off', ... + 'Resize', 'off', ... + 'Position', [fig_x,fig_y,fig_w,fig_h]); + + % Define buttons + uicontrol(F, ... + 'Style', 'pushbutton', ... + 'String', 'Add',... + 'Position', [tbl_w+sps, 2*sps+btn_h, btn_w, btn_h], ... + 'Callback', @addButtonCallback); + + uicontrol(F, ... + 'Style', 'pushbutton', ... + 'String', 'Delete',... + 'Position', [tbl_w+sps, sps+(sps+btn_h)*2, btn_w, btn_h], ... + 'Callback', @deleteButtonCallback); + + uicontrol(F, ... + 'Style', 'pushbutton', ... + 'String', 'Continue',... + 'Position', [tbl_w+sps, sps, btn_w, btn_h], ... + 'Callback', @continueButtonCallback); + + % Define the table to dispaly + data = [fieldnames(InS), cellfun(@(x)var2str(x), ... + struct2cell(InS), 'UniformOutput', false)]; + + T = uitable(F, ... + 'Data', data, ... + 'ColumnWidth', {tbl_w/2, tbl_w/2-1}, ... + 'ColumnName', {'parameter', 'value'}, ... + 'RowName', {}, ... + 'Position', [0 0 tbl_w tbl_h], ... + 'ColumnEditable', true, ... + 'CellSelectionCallback', @selectionCallback, ... + 'CellEditCallback', @editCallback); % Wait until the figure f is deleted uiwait(F); %% Callbacks function selectionCallback(~, EventData) if ~isempty(EventData.Indices) sel_row = EventData.Indices(1); end end % Table edited, makes sure that the first column only contains valid % and unique Matlab variable names, which is necessary to convert the % table content to structure function editCallback(~, EventData) - row=EventData.Indices(1); - col=EventData.Indices(2); - if col==1 - [newval,mod]=matlab.lang.makeValidName(T.Data{row,1}); + row = EventData.Indices(1); + col = EventData.Indices(2); + if col == 1 + [newval, mod] = matlab.lang.makeValidName(T.Data{row, 1}); if mod warning(['Parameter name must be a valid Matlab ' ... 'variable name']); end - [newval,mod]=matlab.lang.makeUniqueStrings(newval, ... - [T.Data(1:row-1,1);T.Data(row+1:end,1)]); + [newval, mod] = matlab.lang.makeUniqueStrings(newval, ... + [T.Data(1:row-1, 1); T.Data(row+1:end, 1)]); if mod warning('Parameter name must be unique'); end - T.Data{row,1}=newval; + T.Data{row, 1} = newval; end end function continueButtonCallback(~,~) - [nrows, ~]=size(T.Data); - for i=1:nrows + [nrows, ~] = size(T.Data); + + OutS = UnchangedS; + for j = 1:nrows + % Try converting values to numbers - OutS.(T.Data{i,1})=str2doubleHedged(T.Data{i,2}); + OutS.(T.Data{j,1}) = str2doubleHedged(T.Data{j,2}); end delete(F); end % Add new entry function addButtonCallback(~, ~) + % Generate dummy parameter name - parname=matlab.lang.makeUniqueStrings('par', T.Data(:,1)); - T.Data=[T.Data;{parname,''}]; + parname = matlab.lang.makeUniqueStrings('par', T.Data(:,1)); + T.Data = [T.Data;{parname, ''}]; end % Delete selected entry function deleteButtonCallback(~, ~) if ~isempty(sel_row) - T.Data(sel_row,:)=[]; - sel_row=[]; + T.Data(sel_row,:) = []; + sel_row = []; end end end diff --git a/Utility functions/Initial parameter estimation/initParamDblLorentzian.m b/Utility functions/Initial parameter estimation/initParamDblLorentzian.m deleted file mode 100644 index 33fa0ce..0000000 --- a/Utility functions/Initial parameter estimation/initParamDblLorentzian.m +++ /dev/null @@ -1,60 +0,0 @@ -function [p_in,lim_lower,lim_upper]=initParamDblLorentzian(x,y) -%Assumes form a/pi*b/2/((x-c)^2+(b/2)^2)+d/pi*e/2/((x-f)^2+(e/2)^2))+g - -lim_upper=[Inf,Inf,Inf,Inf,Inf,Inf,Inf]; -lim_lower=[-Inf,0,-Inf,-Inf,0,-Inf,-Inf]; - -%Finds peaks on the positive signal (max 2 peaks) -[~,locs{1},widths{1},proms{1}]=findpeaks(y,x,... - 'MinPeakDistance',0.01*range(x),'SortStr','descend','NPeaks',2); - -%Finds peaks on the negative signal (max 2 peaks) -[~,locs{2},widths{2},proms{2}]=findpeaks(-y,x,... - 'MinPeakDistance',0.001*range(x),'SortStr','descend','NPeaks',2); - -%If the prominence of the peak in the positive signal is greater, we adapt -%our limits and parameters accordingly, if negative signal has a greater -%prominence, we use this for fitting. -if isempty(proms{2}) || proms{1}(1)>proms{2}(1) - ind=1; - lim_lower(1)=0; - lim_lower(4)=0; - p_in(7)=min(y); -else - lim_upper(1)=0; - lim_upper(4)=0; - ind=2; - p_in(7)=max(y); - proms{2}=-proms{2}; -end - -p_in(2)=widths{ind}(1); -%Calculates the amplitude, as when x=c, the amplitude is 2a/(pi*b) -p_in(1)=proms{ind}(1)*pi*p_in(2)/2; -p_in(3)=locs{ind}(1); -if length(locs{ind})==2 - p_in(5)=widths{ind}(2); - p_in(4)=proms{ind}(2)*pi*p_in(5)/2; - p_in(6)=locs{ind}(2); -else - p_in(5)=widths{ind}(1); - p_in(4)=proms{ind}(1)*pi*p_in(5)/2; - p_in(6)=locs{ind}(1); -end - -%If one of the lorentzians is found to be much smaller than the other, we -%instead fit using only the greater lorentzian's parameters. This is an -%adaption for very closely spaced lorentzians. -if abs(p_in(1))>abs(10*p_in(4)) - p_in(1)=p_in(1)/2; - p_in(5)=p_in(2); - p_in(6)=p_in(3); - p_in(4)=p_in(1); -end - -lim_lower(2)=0.01*p_in(2); -lim_upper(2)=100*p_in(2); - -lim_lower(5)=0.01*p_in(5); -lim_upper(5)=100*p_in(5); -end \ No newline at end of file diff --git a/Utility functions/Initial parameter estimation/initParamExponential.m b/Utility functions/Initial parameter estimation/initParamExponential.m deleted file mode 100644 index 2dc9d41..0000000 --- a/Utility functions/Initial parameter estimation/initParamExponential.m +++ /dev/null @@ -1,62 +0,0 @@ -function [p_in,lim_lower,lim_upper]=initParamExponential(x,y) -%Assumes form a*exp(-bx)+c - -%Setting upper and lower limits -[amp_max,ind_max]=max(y); -[amp_min,ind_min]=min(y); - -lim_upper=[Inf,Inf,Inf]; -lim_lower=-lim_upper; - -%Fix to avoid unphysical offsets on data where all y values exceed 0. -if all(y>0) - lim_lower(3)=0; -end - -if abs(amp_max)>abs(amp_min) - lim_upper(1)=Inf; - lim_lower(1)=0; -else - lim_upper(1)=0; - lim_lower(1)=-Inf; -end - -if (ind_max>ind_min && abs(amp_max)>abs(amp_min))... - || (ind_max0) - y=y-amp_min+eps; - n=length(x); - y2=log(y); - j=sum(x); - k=sum(y2); - l=sum(x.^2); - r2=sum(x .* y2); - p_in(2)=(n * r2 - k * j)/(n * l - j^2); - p_in(1)=exp((k-p_in(2)*j)/n); - y=y+amp_min-eps; -elseif abs(amp_max)>abs(amp_min) && amp_max>0 - p_in(1)=amp_max; - p_in(2)=-1; -else - p_in(1)=amp_min; - p_in(2)=-1; -end - -if abs(amp_max)>abs(amp_min) - p_in(3)=amp_min; -else - p_in(3)=amp_max; -end - -end \ No newline at end of file diff --git a/Utility functions/Initial parameter estimation/initParamGaussian.m b/Utility functions/Initial parameter estimation/initParamGaussian.m deleted file mode 100644 index 30e2ff9..0000000 --- a/Utility functions/Initial parameter estimation/initParamGaussian.m +++ /dev/null @@ -1,44 +0,0 @@ -function [p_in,lim_lower,lim_upper]=initParamGaussian(x,y) -%Assumes a*exp(-((x-c)/b)^2/2)+d - remember matlab orders the fit -%parameters alphabetically - -bg=median(y); -y=y-bg; - -[amp_max,ind_max]=max(y); -[amp_min,ind_min]=min(y); - -lim_upper=[Inf,Inf,Inf,Inf]; -lim_lower=-lim_upper; -y_sum=sum(y); - -if abs(amp_max)>abs(amp_min) - amp=amp_max; - center=x(ind_max); - lim_upper(1)=Inf; - lim_lower(1)=0; -else - amp=amp_min; - center=x(ind_min); - lim_upper(1)=0; - lim_lower(1)=-Inf; -end - -ind1=find(y>amp/2,1,'first'); -ind2=find(y>amp/2,1,'last'); -fwhm=x(ind2)-x(ind1); -width=fwhm/2.35482; - -%Sets the lower limit on width to zero -lim_lower(2)=0; - -%Sets the upper limit on width to 100 times the range of the data -lim_upper(2)=100*range(x); - -%Sets upper and lower limit on the center -lim_lower(3)=min(x)/2; -lim_upper(3)=max(x)*2; - -p_in=[amp,width,center, bg]; - -end \ No newline at end of file diff --git a/Utility functions/Initial parameter estimation/initParamGorodetsky2000.m b/Utility functions/Initial parameter estimation/initParamGorodetsky2000.m deleted file mode 100644 index c3eff63..0000000 --- a/Utility functions/Initial parameter estimation/initParamGorodetsky2000.m +++ /dev/null @@ -1,29 +0,0 @@ -function [p_in,lim_lower,lim_upper]=initParamGorodetsky2000(x,y) -% { 'a','b','c', 'gamma','k0', 'kex'},... -lim_upper=[Inf,Inf,Inf,Inf,Inf,Inf]; -lim_lower=[-Inf,-Inf,-Inf,0,0,0]; - - -%Finds peaks on the negative signal (max 2 peaks) -[~,locs,widths,proms]=findpeaks(-y,x,... - 'MinPeakDistance',0.001*range(x),'SortStr','descend','NPeaks',2); - - -p_in(1)=max(y); - -%position -p_in(2)=mean(locs); - -p_in(3)=(y(end)-y(1))/(x(end)-x(1)); - -if length(locs)==2 - p_in(4)=abs(diff(locs))/2; -else - p_in(4)=0; -end - -p_in(5)=mean(widths)/2; -%Assume critical coupling -p_in(6)=p_in(4); - -end \ No newline at end of file diff --git a/Utility functions/Initial parameter estimation/initParamGorodetsky2000Plus.m b/Utility functions/Initial parameter estimation/initParamGorodetsky2000Plus.m deleted file mode 100644 index f68550e..0000000 --- a/Utility functions/Initial parameter estimation/initParamGorodetsky2000Plus.m +++ /dev/null @@ -1,42 +0,0 @@ -function [p_in,lim_lower,lim_upper]=initParamGorodetsky2000Plus(x,y) -% { 'a','b','c', 'gamma','k0', 'kex'},... -lim_upper=[Inf,Inf,Inf,Inf,Inf,Inf,Inf,Inf,Inf]; -lim_lower=[0,-Inf,-Inf,-Inf,-Inf,-Inf,0,0,0]; - - -%Finds peaks on the negative signal (max 2 peaks) -[~,locs,widths,proms]=findpeaks(-y,x,... - 'MinPeakDistance',0.001*range(x),'SortStr','descend','NPeaks',2); - - - - -%position -p_in(2)=mean(locs); - -% %Extract data outside peak -ind=false(length(x),1); -ind(x<(locs(1)-widths(1)) | x>(locs(1)+widths(1)))=true; -poly_coeffs=polyfit(x(ind),y(ind),3); - - -p_in(1)=poly_coeffs(4); -p_in(3)=poly_coeffs(3); -p_in(4)=poly_coeffs(2); -p_in(5)=poly_coeffs(1); - -%background -p_in(6)=0; - -if length(locs)==2 - p_in(7)=abs(diff(locs))/2; -else - p_in(7)=0; -end - -transmission=min(y)/max(y); -p_in(9) = (widths(1)/2)*(1-sqrt(transmission)); -p_in(8) = widths(1)-p_in(9); - - -end \ No newline at end of file diff --git a/Utility functions/Initial parameter estimation/initParamLorentzian.m b/Utility functions/Initial parameter estimation/initParamLorentzian.m deleted file mode 100644 index 6fb3180..0000000 --- a/Utility functions/Initial parameter estimation/initParamLorentzian.m +++ /dev/null @@ -1,54 +0,0 @@ -function [p_in,lim_lower,lim_upper]=initParamLorentzian(x,y) -%Assumes form a/pi*b/2/((x-c)^2+(b/2)^2)+d - -lim_upper=[Inf,Inf,Inf,Inf]; -lim_lower=[-Inf,0,-Inf,-Inf]; - -%Finds peaks on the positive signal (max 1 peak) -try - [~,locs(1),widths(1),proms(1)]=findpeaks(y,x,... - 'MinPeakDistance',range(x)/2,'SortStr','descend',... - 'NPeaks',1); -catch - proms(1)=0; -end - -%Finds peaks on the negative signal (max 1 peak) -try - [~,locs(2),widths(2),proms(2)]=findpeaks(-y,x,... - 'MinPeakDistance',range(x)/2,'SortStr','descend',... - 'NPeaks',1); -catch - proms(2)=0; -end - -if proms(1)==0 && proms(2)==0 - warning('No peaks were found in the data, giving default initial parameters to fit function') - p_in=[1,1,1,1]; - lim_lower=-[Inf,0,Inf,Inf]; - lim_upper=[Inf,Inf,Inf,Inf]; - return -end - -%If the prominence of the peak in the positive signal is greater, we adapt -%our limits and parameters accordingly, if negative signal has a greater -%prominence, we use this for fitting. -if proms(1)>proms(2) - ind=1; - p_in(4)=min(y); -else - ind=2; - p_in(4)=max(y); - proms(2)=-proms(2); -end - -p_in(2)=widths(ind); -%Calculates the amplitude, as when x=c, the amplitude is 2a/(pi*b) -p_in(1)=proms(ind)*pi*p_in(2)/2; -p_in(3)=locs(ind); - - -lim_lower(2)=0.01*p_in(2); -lim_upper(2)=100*p_in(2); - -end \ No newline at end of file diff --git a/Utility functions/InstrumentManager.mlapp b/Utility functions/InstrumentManager.mlapp index 1f493e5..ebb8cf2 100644 Binary files a/Utility functions/InstrumentManager.mlapp and b/Utility functions/InstrumentManager.mlapp differ diff --git a/Utility functions/Local settings utilities/@MyInstrumentDescriptor/MyInstrumentDescriptor.m b/Utility functions/Local settings utilities/@MyInstrumentDescriptor/MyInstrumentDescriptor.m new file mode 100644 index 0000000..8e2d51e --- /dev/null +++ b/Utility functions/Local settings utilities/@MyInstrumentDescriptor/MyInstrumentDescriptor.m @@ -0,0 +1,68 @@ +% Class for storing information about local instruments + +classdef MyInstrumentDescriptor < handle + properties (Access = public) + name = '' % Identifier that is a MATLAB variable name + title = '' % Title displayed in menus + control_class = '' % Instrument control class + enabled = true + StartupOpts = struct() % Options passed to the class constructor on startup + LoggerOpts = struct() % Options for starting a logger with this instrument + end + + methods (Access = public) + function this = MyInstrumentDescriptor(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + end + end + + methods + % If title is not specified, return name + function val = get.title(this) + if isempty(this.title) + val = this.name; + else + val = this.title; + end + end + + function set.name(this, val) + assert(isvarname(val), ['Value assigned to ''name'' must ' ... + 'be a valid MATLAB variable name.']) + this.name = val; + end + + function set.enabled(this, val) + + % Attempt convertion to logical + val = logical(val); + + assert(islogical(val) && isscalar(val), ['Value assigned ' ... + 'to ''enabled'' must be a logical scalar.']); + this.enabled = val; + end + + function set.StartupOpts(this, val) + assert(isstruct(val), ['Value assigned to ''StartupOpts'''... + ' must be a structure.']) + this.StartupOpts = val; + end + + function Val = get.LoggerOpts(this) + Val = this.LoggerOpts; + + if ~isfield(Val, 'enabled') + Val.enabled = ismethod(this.control_class, ... + 'createLogger') && this.enabled; + end + end + + function set.LoggerOpts(this, val) + assert(isstruct(val), ['Value assigned to ''LoggerOpts''' ... + ' must be a structure.']) + this.LoggerOpts = val; + end + end +end + diff --git a/Utility functions/Local settings utilities/@MyProgramDescriptor/MyProgramDescriptor.m b/Utility functions/Local settings utilities/@MyProgramDescriptor/MyProgramDescriptor.m new file mode 100644 index 0000000..b7dd72b --- /dev/null +++ b/Utility functions/Local settings utilities/@MyProgramDescriptor/MyProgramDescriptor.m @@ -0,0 +1,58 @@ +% Descriptor for local data acquisition programs + +classdef MyProgramDescriptor < handle + properties (Access = public) + name = '' % Identifier that is a MATLAB variable name + title = '' % Title displayed in menus + info = '' % Optional detailed description + type = '' % runfile/instrument/logger + enabled = true + data_source = false % If program produces traces + run_expr = '' % Expression to run the program + run_bg_expr = '' % Expression to run the program without GUI + end + + methods (Access = public) + function this = MyProgramDescriptor(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + end + end + + methods + function set.name(this, val) + assert(isvarname(val), '''name'' must be valid variable name') + this.name = val; + end + + % If title is not specified, return name + function val = get.title(this) + if isempty(this.title) + val = this.name; + else + val = this.title; + end + end + + function set.enabled(this, val) + + % Attempt convertion to logical + val = logical(val); + + assert(islogical(val) && isscalar(val), ['Value assigned ' ... + 'to ''enabled'' must be a logical scalar.']); + this.enabled = val; + end + + function set.data_source(this, val) + + % Attempt convertion to logical + val = logical(val); + + assert(islogical(val) && isscalar(val), ['Value assigned ' ... + 'to ''data_source'' must be a logical scalar.']); + this.data_source = val; + end + end +end + diff --git a/Utility functions/Local settings utilities/applyLocalColorScheme.m b/Utility functions/Local settings utilities/applyLocalColorScheme.m new file mode 100644 index 0000000..8d35bbf --- /dev/null +++ b/Utility functions/Local settings utilities/applyLocalColorScheme.m @@ -0,0 +1,20 @@ +% Load the color scheme from local settings and apply it to Obj + +function applyLocalColorScheme(Obj) + + % Having right color scheme is usually not crucial for the + % functionality of code, therefore we never throw errors during the + % recoloring operation + try + + % Load the color scheme from local settings + S = getLocalSettings('ColorScheme'); + + if ~strcmpi(S.name, 'default') + S.colorSchemeFcn(findFigure(Obj)); + end + catch ME + warning(['Could not apply color scheme. Error: ' ME.message]) + end +end + diff --git a/Utility functions/Local settings utilities/convertInstrumentListToDescriptor.m b/Utility functions/Local settings utilities/convertInstrumentListToDescriptor.m new file mode 100644 index 0000000..04fc8cb --- /dev/null +++ b/Utility functions/Local settings utilities/convertInstrumentListToDescriptor.m @@ -0,0 +1,52 @@ +% Convert structure-based instrument list to a descriptor-based one + +function NewInstrList = convertInstrumentListToDescriptor(OldInstrList) + NewInstrList = MyInstrumentDescriptor.empty(); + + names = fieldnames(OldInstrList); + for i = 1:length(names) + EntrStruct = OldInstrList.(names{i}); + + NewInstrList(i).name = names{i}; + + try + NewInstrList(i).control_class = EntrStruct.control_class; + catch ME + warning(ME.message) + end + + try + NewInstrList(i).title = EntrStruct.menu_title; + catch ME + warning(ME.message) + end + + try + if ~isempty(EntrStruct.interface) + NewInstrList(i).StartupOpts.interface = EntrStruct.interface; + end + catch ME + warning(ME.message) + end + + try + if ~isempty(EntrStruct.address) + NewInstrList(i).StartupOpts.address = EntrStruct.address; + end + catch ME + warning(ME.message) + end + + try + if ~isempty(EntrStruct.StartupOpts) + opt_names = fieldnames(EntrStruct.StartupOpts); + for j=1:lengh(opt_names) + NewInstrList(i).StartupOpts.(opt_names{j}) = ... + EntrStruct.StartupOpts.(opt_names{j}); + end + end + catch + end + end +end + diff --git a/Utility functions/Local settings utilities/getIcPrograms.m b/Utility functions/Local settings utilities/getIcPrograms.m new file mode 100644 index 0000000..29b19f5 --- /dev/null +++ b/Utility functions/Local settings utilities/getIcPrograms.m @@ -0,0 +1,134 @@ +function ProgList = getIcPrograms() + + % [run files, instruments, loggers] + ProgList = MyProgramDescriptor.empty(); + + RunFiles = readRunFiles(); + + % Add entries, automatically generated from the InstrumentList + InstrumentList = getLocalSettings('InstrumentList'); + + j = 1; % Counter for the program list + + for i = 1:length(InstrumentList) + + % If a run file for instrument was not specified explicitly and if + % all the required fields in InstrList are filled, generate a new + % entry. + nm = InstrumentList(i).name; + + if ~ismember(nm, {RunFiles.name}) && ... + ~isempty(InstrumentList(i).control_class) + + ctrl_class = InstrumentList(i).control_class; + + ProgList(j).name = nm; + ProgList(j).title = InstrumentList(i).title; + ProgList(j).type = 'instrument'; + ProgList(j).info = ['% This entry is automatically ' ... + 'generated from InstrumentList']; + + ProgList(j).data_source = ismember('NewData', ... + events(ctrl_class)); + + try + ProgList(j).enabled = InstrumentList(i).enabled; + catch + ProgList(j).enabled = true; + end + + % Command for running the instrument without GUI + ProgList(j).run_bg_expr = sprintf( ... + 'runInstrument(''%s'', ''enable_gui'', false);', nm); + + % Command for running the instrument with GUI + ProgList(j).run_expr = sprintf( ... + 'runInstrument(''%s'', ''enable_gui'', true);', nm); + + j = j+1; + end + + % Make default logger name + nm_logger = [nm 'Logger']; + + if ~ismember(nm_logger, {RunFiles.name}) && ... + ismethod(ctrl_class, 'createLogger') + + % Add an entry for starting a logger with this instrument + ProgList(j).name = nm_logger; + ProgList(j).type = 'logger'; + ProgList(j).info = ['% This entry is automatically ' ... + 'generated from InstrumentList']; + ProgList(j).data_source = true; + + try + + % Assign logger options found in InstrumentList + for opt_nm = fieldnames(InstrumentList(i).LoggerOpts)' + if isprop(ProgList, opt_nm{1}) + ProgList(j).(opt_nm{1}) = ... + InstrumentList(i).LoggerOpts.(opt_nm{1}); + end + end + catch + end + + ProgList(j).run_expr = ['runLogger(''' nm ''');']; + + j = j+1; + end + end + + ProgList = [RunFiles, ProgList]; +end + +%% Subroutines + +% Read all the files which names start from 'run' from the local base +% directory and add entries, automatically generated from InstrumentList +function RunFiles = readRunFiles(dir) + if ~exist('dir', 'var') + + % Search in the local base directory if directory name is not + % supplied explicitly + dir = getLocalBaseDir(); + end + + % Find all the names of .m files that start with 'run' + all_names = what(dir); + is_run = startsWith(all_names.m, 'run', 'IgnoreCase', false); + run_names = all_names.m(is_run); + + RunFiles = MyProgramDescriptor.empty(); + + % Read headers of all the run*.m files + for i = 1:length(run_names) + RunFiles(i).type = 'runfile'; + + name_tok = regexp(run_names{i}, 'run(.*)\.m', 'tokens'); + + % Add information about the file name + RunFiles(i).name = name_tok{1}{1}; + + % By default title is the same as name + RunFiles(i).title = RunFiles(i).name; + + % Make an expression that needs to be evaluated to run the program. + % (just the name of file by default) + [~, run_name, ~] = fileparts(run_names{i}); + RunFiles(i).run_expr = run_name; + + % Read the run file comment header and assign the parameters found + Content = readCommentHeader(fullfile(dir, run_names{i})); + + RunFiles(i).info = Content.comment_header; + + par_names = fieldnames(Content.ParamList); + for j = 1:length(par_names) + fn = par_names{j}; + if isprop(RunFiles, fn) + RunFiles(i).(fn) = Content.ParamList.(fn); + end + end + end +end diff --git a/Utility functions/Local settings utilities/getLocalSettings.m b/Utility functions/Local settings utilities/getLocalSettings.m index 4388f83..2e06ebb 100644 --- a/Utility functions/Local settings utilities/getLocalSettings.m +++ b/Utility functions/Local settings utilities/getLocalSettings.m @@ -1,20 +1,26 @@ % Load local settings function Settings = getLocalSettings(varargin) try - Settings = load('LocalInstrumentControlSettings.mat'); + AllSettings = load('LocalInstrumentControlSettings.mat'); catch error(['The local settings file is not found. Add the folder, '... 'containing an existing file to the Matlab path or create '... 'a new one by runnign Setup.']); end + % If a property is specified as varargin{1}, return it directly if ~isempty(varargin) try - Settings = Settings.(varargin{1}); + Settings = AllSettings.(varargin{1}); catch - error('No such parameter as %s among the loaded settings',... - varargin{1}); + error(['No local setting with name ''%s'' found. Existing ' ... + 'settings are the following:\n%s'], varargin{1}, ... + var2str(fieldnames(AllSettings))); end + else + + % Return all settings as a structure + Settings = AllSettings; end end diff --git a/Utility functions/Local settings utilities/readCommentHeader.m b/Utility functions/Local settings utilities/readCommentHeader.m new file mode 100644 index 0000000..d471104 --- /dev/null +++ b/Utility functions/Local settings utilities/readCommentHeader.m @@ -0,0 +1,61 @@ +% Read the header and first code line of a Matlab file, interpreting +% 'property'='value' pairs indicated in comments + +function Info = readCommentHeader(file_name) + + % Parameter-value pairs found in the comment header are added as extra + % fields to this structure + Info = struct( ... + 'comment_header', '', ... + 'first_code_line', '', ... + 'ParamList', struct() ... + ); + + fid = fopen(file_name,'r'); + + while ~feof(fid) + str = fgetl(fid); + + trimstr = strtrim(str); + if isempty(trimstr) + + % Do nothing for empty strings + elseif trimstr(1) == '%' + + % A comment string, try to extract the 'property'='value' + % pair + match = regexp(trimstr, '[%\s]*(\S*)\s*=(.*)', 'tokens'); + if ~isempty(match) + tag = lower(match{1}{1}); + + if ~ismember(tag, fieldnames(Info)) + + % Remove leading and trailing whitespaces + val = strtrim(match{1}{2}); + + % Try converting to logical or double value + if strcmpi(val, 'true') + val = true; + elseif strcmpi(val, 'false') + val = false; + else + val = str2doubleHedged(val); + end + + Info.ParamList.(tag) = val; + end + end + else + + % Stop when the code begins + Info.first_code_line = [str, newline]; + break + end + + % Store the entire header + Info.comment_header = [Info.comment_header, str, newline]; + end + + fclose(fid); +end + diff --git a/Utility functions/Local settings utilities/setLocalSettings.m b/Utility functions/Local settings utilities/setLocalSettings.m index e19b378..963a3ec 100644 --- a/Utility functions/Local settings utilities/setLocalSettings.m +++ b/Utility functions/Local settings utilities/setLocalSettings.m @@ -1,19 +1,21 @@ -% Save local settings +% Save local settings provided as name-value pairs + function setLocalSettings(varargin) + + % Use an input parser with no parameters to ensure the proper + % formatting of name-value pairs + p = inputParser(); + p.KeepUnmatched = true; + parse(p, varargin{:}); + + SaveList = p.Unmatched; + + % Get the full name of file containing local settings dir_name = getLocalBaseDir(); - % Settings to be saved need to be specified as 'parameter','value' - % pairs in varargin file_name = fullfile(dir_name, 'LocalInstrumentControlSettings.mat'); - SaveList=struct(); - for i=1:floor(length(varargin)/2) - if isvarname(varargin{2*i-1}) - SaveList.(varargin{2*i-1})=varargin{2*i}; - else - warning('Setting is not saved as it is not a valid variable name:'); - disp(varargin{2*i-1}); - end - end - % Append parameters to the settings file - save(file_name,'-struct','SaveList','-append'); + + % Save the fields of structure SaveList as individual variables and + % overwrite if a variable already exists. + save(file_name, '-struct', 'SaveList', '-append'); end diff --git a/Utility functions/Setup.mlapp b/Utility functions/Setup.mlapp index 32ccd94..db1a345 100644 Binary files a/Utility functions/Setup.mlapp and b/Utility functions/Setup.mlapp differ diff --git a/Utility functions/Unused/FindInstrumentDlg.mlapp b/Utility functions/Unused/FindInstrumentDlg.mlapp deleted file mode 100644 index 0c2cafe..0000000 Binary files a/Utility functions/Unused/FindInstrumentDlg.mlapp and /dev/null differ diff --git a/Utility functions/Unused/GenRunFileDlg.mlapp b/Utility functions/Unused/GenRunFileDlg.mlapp deleted file mode 100644 index 1480df1..0000000 Binary files a/Utility functions/Unused/GenRunFileDlg.mlapp and /dev/null differ diff --git a/Utility functions/Unused/findInstrumentCallback.m b/Utility functions/Unused/findInstrumentCallback.m deleted file mode 100644 index 90cc0ce..0000000 --- a/Utility functions/Unused/findInstrumentCallback.m +++ /dev/null @@ -1,13 +0,0 @@ -function findInstrumentCallback(app) - RefVar = MyRefVar(''); - % call the FindInstrumentDlg and wait for it to be closed - waitfor(InstrumentManager(RefVar)); - if ~isequal(RefVar.value, '') - % if a new address was chosen, restart the control panel - app.Instr.Interface = 'constructor'; - app.Instr.address = RefVar.value; - readPropertyHedged(app.Instr,'all'); - updateGui(app); - end -end - diff --git a/Utility functions/Unused/findMyInstrument.m b/Utility functions/Unused/findMyInstrument.m deleted file mode 100644 index a26ff72..0000000 --- a/Utility functions/Unused/findMyInstrument.m +++ /dev/null @@ -1,20 +0,0 @@ -%Finds if the obj or struct contains a MyInstrument class and returns the handle -function instr_handle=findMyInstrument(obj) - props=properties(obj); - - if isempty(props) && isstruct(obj) - props=fieldnames(obj); - elseif isempty(props) - warning('Invalid data type. Could not search for MyInstrument') - instr_handle=[]; - return - end - - ind=cellfun(@(x) isa(obj.(x),'MyInstrument'), props); - if any(ind) - instr_handle=obj.(props{ind}); - else - warning('No MyInstrument found'); - instr_handle=[]; - end -end \ No newline at end of file diff --git a/Utility functions/Unused/findTcpipInstruments.m b/Utility functions/Unused/findTcpipInstruments.m deleted file mode 100644 index 1a6b7f6..0000000 --- a/Utility functions/Unused/findTcpipInstruments.m +++ /dev/null @@ -1,34 +0,0 @@ -% Pings all the IP addresses assuming the subnet mask 255.255.255.000 -% Local IP is a string of dot-separated decimals like '192.168.1.8' -% Caution: very slow, use the 'ind' option for partial search -function rsc_list = findTcpipInstruments(local_ip, varargin) - p=inputParser(); - addParameter(p,'ind',[1,254], @(x) validateattributes(x, {'numeric'},... - {'integer','nondecreasing','>',1,'<',254})); - addParameter(p,'visa_adaptor','ni',@ischar); - parse(p,varargin{:}); - v_ad = p.Results.visa_adaptor; - - a = sscanf(local_ip,'%i.%i.%i.%i'); - rsc_list = {}; - % Do full search. 0 and 255 are not valid as host names - disp('Found TCPIP-VISA instruments:'); - for i=p.Results.ind(1):p.Results.ind(2) - ip = sprintf('%i.%i.%i.%i',a(1),a(2),a(3),i); - rsc_name = sprintf('TCPIP0::%s::inst0::INSTR',ip); - % Try to connect to the device and open it - tmp_dev = visa(v_ad,rsc_name); %#ok - try - fopen(tmp_dev); - fclose(tmp_dev); - % If the fopen operation was successful, add the IP to ip_list - rsc_list = [rsc_list,sprintf('visa(''%s'',''%s'');',... - v_ad,rsc_name)]; %#ok - disp(rsc_name); - catch - % If error - do nothing - end - delete(tmp_dev); - end -end - diff --git a/Utility functions/Unused/genUid.m b/Utility functions/Unused/genUid.m deleted file mode 100644 index 5de8810..0000000 --- a/Utility functions/Unused/genUid.m +++ /dev/null @@ -1,3 +0,0 @@ -function uid=genUid() - uid=datestr(now,'yyyymmdd-HHMMSSFFF'); -end \ No newline at end of file diff --git a/Utility functions/Unused/generateRunFile.m b/Utility functions/Unused/generateRunFile.m deleted file mode 100644 index 925dea7..0000000 --- a/Utility functions/Unused/generateRunFile.m +++ /dev/null @@ -1,73 +0,0 @@ -% Automatically generates a run file for an entry from the InstrumentsList -function generateRunFile(inst_entry, varargin) - p=inputParser(); - % Ignore unmatched parameters - p.KeepUnmatched = true; - addParameter(p,'out_dir','',@ischar); - addParameter(p,'menu_title','',@ischar); - addParameter(p,'show_in_daq',false,@islogical); - parse(p,varargin{:}); - - if ~isempty(p.Results.out_dir) - dir = p.Results.out_dir; - else - %By default, create files in the same directory with InstrumentList - %or in the base directory if it does not exist - dir=getLocalBaseDir(); - end - - % Create run file if there is a default_gui indicated for the - % instrument and no such file already exists - file_name = fullfile(dir, ['run',inst_entry.name,'.m']); - if ~logical(exist(inst_entry.default_gui,'file')) - warning(['No valid Gui specified for %s, a run file cannot ',... - 'be created'],inst_entry.name) - return - end - if ~logical(exist(inst_entry.control_class,'class')) - warning(['No valid control class specified for %s, a run ',... - 'file cannot be created'], inst_entry.name) - return - end - if exist(file_name,'file') - warning(['The run file %s already exists, a new file has not ',... - 'been created'], file_name) - return - end - - header_str=''; - if ~isempty(p.Results.menu_title) - header_str = [header_str, sprintf('%% menu_title=%s\n',... - p.Results.menu_title)]; - end - if p.Results.show_in_daq - header_str = [header_str, sprintf('%% show_in_daq=true\n')]; - end - - % Code, defining the run function - code_str = [... - 'function instance_name = run%s()\n',... - ' instance_name = ''%s%s'';\n',... - ' if ~isValidBaseVar(instance_name)\n',... - ' gui = %s(''instr_list'', ''%s'', ''instance_name'', instance_name);\n',... - ' assignin(''base'', instance_name, gui);\n',... - ' if ~isValidBaseVar(''Collector''); runCollector; end \n',... - ' evalin(''base'', ... \n',... - ' sprintf(''addInstrument(Collector,%%s)'',instance_name)); \n',... - ' else\n',... - ' warning(''%%s is already running'', instance_name);\n',... - ' end\n',... - 'end\n']; - - try - fid = fopen(file_name,'w'); - fprintf(fid, '%s', header_str); - fprintf(fid, code_str,... - inst_entry.name, inst_entry.default_gui, inst_entry.name,... - inst_entry.default_gui, inst_entry.name); - fclose(fid); - catch - warning('Failed to create the run file') - end -end - diff --git a/Utility functions/Unused/genericInitGui.m b/Utility functions/Unused/genericInitGui.m deleted file mode 100644 index 4113ecd..0000000 --- a/Utility functions/Unused/genericInitGui.m +++ /dev/null @@ -1,50 +0,0 @@ -% Start the Gui with specified instrument class -% Assign the instance name to the class variable and the instrument name to -% the window -function genericInitGui(app, default_instr_class, interface, address, varargin) - p=inputParser(); - % Ignore unmatched parameters - p.KeepUnmatched = true; - addParameter(p,'instance_name','',@ischar); - addParameter(p,'instr_class','',@ischar); - parse(p,varargin{:}); - - % Assign the instance name (instance is gui+instrument) - if isprop(app, 'instance_name') - app.instance_name = p.Results.instance_name; - else - warning(['''instance_name'' property is absent in the ',... - 'gui class, the instance global variable will not ',... - 'be cleared on exit']) - end - - % Connect to the instrument by using either instr_class or - % default_instr_class - if ~ismember(p.UsingDefaults, 'instr_class') - class = p.Results.instr_class; - elseif isequal(interface,'instr_list') - % load the InstrumentList structure - InstrumentList = getLocalSettings('InstrumentList'); - % In this case 'address' is the instrument name in the list - instr_name = address; - if ~isfield(InstrumentList, instr_name) - error('%s is not a field of InstrumentList', instr_name) - end - class = InstrumentList.(instr_name).control_class; - else - % The default class is Gui-specific, so need to set it manually - class = default_instr_class; - end - app.Instr = feval(class, interface, address, varargin{:}); - readPropertyHedged(app.Instr,'all'); - - % Display instrument's name if given - if ~isempty(app.Instr.name) - fig_handle=findfigure(app); - if ~isempty(fig_handle) - fig_handle.Name=char(app.Instr.name); - else - warning('No UIFigure found to assign the name') - end - end -end diff --git a/Utility functions/Unused/getLocalIP.m b/Utility functions/Unused/getLocalIP.m deleted file mode 100644 index 4faa7b6..0000000 --- a/Utility functions/Unused/getLocalIP.m +++ /dev/null @@ -1,31 +0,0 @@ -% get the IP addresses of this computer on local networks -function ip_list = getLocalIP() - % Regex to match IP address, e.g 192.168.1.8 - % Each of the 4 blocks match dot-separated numbers and read as - % ('25x', where x=0 to 5) or ('2xy' where x=0 to 4 and y=0 to 9) or - % (zyx where z=0,1 or nothing; y=0 to 9; x=0 to 9 or nothing) - % The 4 blocks are peceeded and followed by non-numeric characters - ip_regex = ['(\D)',... - '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.',... - '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.',... - '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.',... - '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\D)']; - instr_info = instrhwinfo('tcpip'); - ip_list = cell(1,length(instr_info.LocalHost)); - for i=1:length(instr_info.LocalHost) - tmp_ip=regexp([' ',instr_info.LocalHost{i},' '],ip_regex,'match'); - if length(tmp_ip)>=1 - % if a match is found, assign it to the output disregarding the - % first and the last separating characters - ip_list{i}=tmp_ip{1}(2:end-1); - if length(tmp_ip)>1 - warning('Multiple IP''s are matched by regex') - end - else - ip_list{i}=''; - end - end - % Discard all the empty cells - ip_list = ip_list(~cellfun(@isempty,ip_list)); -end - diff --git a/Utility functions/Unused/printArraySubs.m b/Utility functions/Unused/printArraySubs.m deleted file mode 100644 index 5bfbdb3..0000000 --- a/Utility functions/Unused/printArraySubs.m +++ /dev/null @@ -1,52 +0,0 @@ -% Print subscripts of array - -function sn = printArraySubs(arr, varargin) - p=inputParser(); - % own_name is optionally prependend to the names of subs to create - % full expressions that can be used to access parts of var - addParameter(p, 'own_name', '', @(x) assert(... - isvarname(x), '''own_name'' must be a valid variable name.')); - addParameter(p, 'contract_dims', 0, @(x) assert(... - isnumeric(x) && mod(x,1)==0 && x>0,... - '''contract_dims'' must be a positive integer.')); - parse(p, varargin{:}); - - ncdim=p.Results.contract_dims; - own_name=p.Results.own_name; - - % Create string for printing formatted indices - sz = size(arr); - - % Number of expanded dimensions - nedim=length(sz)-ncdim; - - if nedim<1 || all(sz(1:nedim)==1) - % Do not expand if all dimensions are contracted or have only the - % size of 1 - sn={own_name}; - return - end - - ind_fmt='('; - for i=1:nedim - ind_fmt=[ind_fmt,'%i,']; %#ok - end - for i=1:ncdim - ind_fmt=[ind_fmt,':,']; %#ok - end - ind_fmt(end)=')'; - - % Number of non-contracted indices - nind=prod(sz(1:nedim)); - sn=cell(nind,1); - % Iterate over array elements - ind=cell(1, length(sz)); - for i=1:prod(sz(1:nedim)) - % Convert 1D to multi-dimensional index - [ind{:}]=ind2sub(sz,i); - ind_str=sprintf(ind_fmt, ind{1:nedim}); - sn{i}=[own_name,ind_str]; - end - -end - diff --git a/Utility functions/Unused/updateElement.m b/Utility functions/Unused/updateElement.m deleted file mode 100644 index ec33a1d..0000000 --- a/Utility functions/Unused/updateElement.m +++ /dev/null @@ -1,24 +0,0 @@ -% If specified within the control element OutputProcessingFcn or -% InputPrescaler is applied to the property value first -function updateElement(elem, EventData) - try - % Get value of the property that generated event - tmpval = EventData.AffectedObject.(EventData.Source.Name); - % scale the value if the control element has a prescaler - if isprop(elem, 'OutputProcessingFcn') - tmpval = elem.OutputProcessingFcn(tmpval); - elseif isprop(elem, 'InputPrescaler') - tmpval = tmpval*elem.InputPrescaler; - end - % Setting value of a matlab app elemen is time consuming, so do - % this only if the value has actually changed - if ~isequal(elem.Value,tmpval) - elem.Value = tmpval; - end - catch - warning(['Could not update the value of element with tag ''%s'' ',... - 'and value ''%s''. The element will be disabled.'],... - elem.Tag,var2str(tmpval)); - elem.Enable='off'; - end -end \ No newline at end of file diff --git a/Utility functions/axesToClipboard.m b/Utility functions/axesToClipboard.m new file mode 100644 index 0000000..5ba8e49 --- /dev/null +++ b/Utility functions/axesToClipboard.m @@ -0,0 +1,80 @@ +function axesToClipboard(Axes) + pos = [0, 0, Axes.OuterPosition(3:4)]; + + %Creates a new invisible figure with new axes + NewFig = figure( ... + 'Visible', 'off', ... + 'Units', Axes.Units, ... + 'Position', pos); + + NewAxes = axes(NewFig, ... + 'Units', Axes.Units, ... + 'OuterPosition', pos); + + if isa(Axes.XAxis, 'matlab.graphics.axis.decorator.DatetimeRuler') + + % Convert the x axis to datetime format + L = plot(NaT,NaN); + delete(L); + end + + % Properties, always set to default + NewAxes.Color = 'none'; + NewAxes.XColor = [0, 0, 0]; + NewAxes.YColor = [0, 0, 0]; + + % Properties, copied from the source plot + copy_prop_list = {'Box', 'BoxStyle', 'Clipping', 'ColorOrder', ... + 'FontAngle', 'FontName', 'FontSize', 'FontSmoothing', ... + 'FontUnits', 'FontWeight', 'GridAlpha', 'GridColor', ... + 'GridLineStyle', 'LineStyleOrder', 'LineWidth', ... + 'MinorGridAlpha', 'MinorGridColor', 'MinorGridLineStyle', ... + 'TickDir', 'TickLabelInterpreter', 'TickLength', 'TickDir', ... + 'XDir', 'XGrid', 'XLim', 'XMinorGrid', 'XMinorTick', ... + 'XScale', 'XTick', ... + 'YDir', 'YGrid', 'YLim', 'YMinorGrid', 'YMinorTick', ... + 'YScale', 'YTick'}; + + for i = 1:length(copy_prop_list) + if isprop(Axes, copy_prop_list{i}) + NewAxes.(copy_prop_list{i}) = Axes.(copy_prop_list{i}); + end + end + + % Handling font sizes is different in axes and uiaxes, so copy it + % specifically + NewAxes.XAxis.FontSize = Axes.XAxis.FontSize; + NewAxes.YAxis.FontSize = Axes.YAxis.FontSize; + + % Copy axes labels and legend as these are handle objects + NewAxes.XLabel = copy(Axes.XLabel); + NewAxes.YLabel = copy(Axes.YLabel); + + if ~isempty(Axes.Legend) + legend(NewAxes, ... + 'String', Axes.Legend.String, ... + 'Location', Axes.Legend.Location,... + 'Orientation', Axes.Legend.Orientation, ... + 'FontSize', Axes.Legend.FontSize) + end + + try + + % Loose inset is an undocumented feature so we do not complain if + % the code below does not work + NewAxes.LooseInset = [0, 0, 0, 0]; + catch + end + + % Copy the axes content + for i = 1:length(Axes.Children) + copyobj(Axes.Children(i), NewAxes); + end + + %Prints the figure to the clipboard + print(NewFig, '-clipboard', '-dbitmap'); + + %Deletes the figure + delete(NewFig); +end + diff --git a/Utility functions/centerFigure.m b/Utility functions/centerFigure.m new file mode 100644 index 0000000..65e4539 --- /dev/null +++ b/Utility functions/centerFigure.m @@ -0,0 +1,29 @@ +% Find figure within the object and move it to the center of the screen + +function centerFigure(Obj) + + % Find figure within the object if the object is not a figure itself + Fig = findFigure(Obj); + + if isempty(Fig) + return + end + + R = groot(); + + fig_units = Fig.Units; + Fig.Units = R.Units; + + fig_w = Fig.Position(3); + fig_h = Fig.Position(4); + + % Display the figure window in the center of the screen + fig_x = R.ScreenSize(3)/2-fig_w/2; + fig_y = R.ScreenSize(4)/2-fig_h/2; + + Fig.Position(1:2) = [fig_x, fig_y]; + + % Restore the orignal value of figure units + Fig.Units = fig_units; +end + diff --git a/Utility functions/closeApp.m b/Utility functions/closeApp.m new file mode 100644 index 0000000..eb5e69a --- /dev/null +++ b/Utility functions/closeApp.m @@ -0,0 +1,17 @@ +% Delete App by closing its figure window + +function closeApp(App) + Fig = findFigure(App); + + if ~isempty(Fig) + + % Close the figure so that the cleanup procedure possibly defined + % by the user are executed + close(Fig); + else + + % Simply delete the object + delete(App) + end +end + diff --git a/Utility functions/createFitParser.m b/Utility functions/createFitParser.m deleted file mode 100644 index ffc8a9f..0000000 --- a/Utility functions/createFitParser.m +++ /dev/null @@ -1,19 +0,0 @@ -function p=createFitParser(n_arg) -%Creates an input parser for a fit function with n_arg arguments. Default -%values are ones for initial parameters and plus and minus inf for upper -%and lower limits respectively. Ensures that the length is equal to the -%number of arguments. - -p=inputParser; -validateStart=@(x) assert(isnumeric(x) && isvector(x) && length(x)==n_arg,... - 'Starting points must be given as a vector of size %d',n_arg); -validateLower=@(x) assert(isnumeric(x) && isvector(x) && length(x)==n_arg,... - 'Lower limits must be given as a vector of size %d',n_arg); -validateUpper=@(x) assert(isnumeric(x) && isvector(x) && length(x)==n_arg,... - 'Upper limits must be given as a vector of size %d',n_arg); - -addOptional(p,'init_params',ones(1,n_arg),validateStart) -addOptional(p,'lower',-Inf*ones(1,n_arg),validateLower) -addOptional(p,'upper',Inf*ones(1,n_arg),validateUpper) - -end \ No newline at end of file diff --git a/Utility functions/createSessionPath.m b/Utility functions/createSessionPath.m index 71aae85..85108c9 100644 --- a/Utility functions/createSessionPath.m +++ b/Utility functions/createSessionPath.m @@ -1,11 +1,12 @@ function path = createSessionPath(base_dir, session_name) % The function creates a path of the format %'base_dir\yyyy-mm-dd session_name' path = fullfile(base_dir,[datestr(now,'yyyy-mm-dd '), session_name]); - %Adds the \ at the end if it was not added by the user. + + % Adds the \ at the end if it was not added by the user. if ~strcmp(path(end),filesep) path(end+1) = filesep; end end diff --git a/Utility functions/createUniqueFileName.m b/Utility functions/createUniqueFileName.m new file mode 100644 index 0000000..9226103 --- /dev/null +++ b/Utility functions/createUniqueFileName.m @@ -0,0 +1,35 @@ +% Generate a unique file name based on the full file name supplied as the +% input by appending _n with sufficiently large n. +% +% This function does not make sure that the filename is valid - i.e. that +% it does not contain symbols forbidden by the file system. +% +% The second output argument is true if the file name was modified + +function [file_name, is_mod] = createUniqueFileName(file_name) + [path, name, ext] = fileparts(file_name); + + if isempty(name) + name = 'placeholder'; + end + + if isempty(path) + path = pwd(); + end + + % List all the existing files in the measurement directory + % that have the same extension as our input file name + DirCont = dir(fullfile(path, ['*', ext])); + file_ind = ~[DirCont.isdir]; + existing_fns = {DirCont(file_ind).name}; + + % Remove extensions + [~, existing_fns, ~] = cellfun(@fileparts, existing_fns, ... + 'UniformOutput', false); + + % Generate a new file name + [name, is_mod] = matlab.lang.makeUniqueStrings(name, existing_fns); + + file_name = fullfile(path, [name, ext]); +end + diff --git a/Utility functions/findFigure.m b/Utility functions/findFigure.m new file mode 100644 index 0000000..81809de --- /dev/null +++ b/Utility functions/findFigure.m @@ -0,0 +1,50 @@ +% Find matlab.ui.Figure handle in structure or class instance + +function Fig = findFigure(Obj) + + % First check if the object is empty + if isempty(Obj) + Fig = []; + return + end + + % Check if the object itself is a figure + if isa(Obj, 'matlab.ui.Figure') + Fig = Obj; + return + end + + % If the object contains 'Gui' property, search over it instead of the + % object itself. + gui_prop_name_list = {'gui', 'Gui', 'GUI'}; + ind = ismember(gui_prop_name_list, properties(Obj)); + if nnz(ind) == 1 + tag = gui_prop_name_list{ind}; + Fig = findFigure(Obj.(tag)); + return + end + + % Find figure among the object properties + if isstruct(Obj) + prop_names = fieldnames(Obj); + else + if isvalid(Obj) + prop_names = properties(Obj); + else + Fig = []; + return + end + end + + % Try to find the figure among the properties + figure_ind = cellfun(@(x) isa(Obj.(x),'matlab.ui.Figure'), prop_names); + if any(figure_ind) + + % Returns the 1-st figure among those found + fig_names = prop_names(figure_ind); + Fig = Obj.(fig_names{1}); + else + Fig = []; + end +end + diff --git a/Utility functions/findfigure.m b/Utility functions/findfigure.m deleted file mode 100644 index f1eecef..0000000 --- a/Utility functions/findfigure.m +++ /dev/null @@ -1,18 +0,0 @@ -% Find matlab.ui.Figure handle in structure or class instance -function fig_handle = findfigure(obj) - if isstruct(obj) - prop_names = fieldnames(obj); - else - prop_names = properties(obj); - end - % Try to find the figure among the properties - figure_ind=cellfun(@(x) isa(obj.(x),'matlab.ui.Figure'), prop_names); - if any(figure_ind) - % Returns the 1-st figure among the found - fig_names = prop_names(figure_ind); - fig_handle=obj.(fig_names{1}); - else - fig_handle=[]; - end -end - diff --git a/Utility functions/initParamDblLorentzianGrad.m b/Utility functions/initParamDblLorentzianGrad.m deleted file mode 100644 index 4507077..0000000 --- a/Utility functions/initParamDblLorentzianGrad.m +++ /dev/null @@ -1,63 +0,0 @@ -function [p_in,lim_lower,lim_upper]=initParamDblLorentzianGrad(x,y) -%Assumes form a/pi*b/2/((x-c)^2+(b/2)^2)+d/pi*e/2/((x-f)^2+(e/2)^2))+g*x+h - -lim_upper=[Inf,Inf,Inf,Inf,Inf,Inf,Inf,Inf]; -lim_lower=[-Inf,0,-Inf,-Inf,0,-Inf,-Inf,-Inf]; - -%Finds peaks on the positive signal (max 2 peaks) -[~,locs{1},widths{1},proms{1}]=findpeaks(y,x,... - 'MinPeakDistance',0.001*range(x),'SortStr','descend','NPeaks',2); - -%Finds peaks on the negative signal (max 2 peaks) -[~,locs{2},widths{2},proms{2}]=findpeaks(-y,x,... - 'MinPeakDistance',0.001*range(x),'SortStr','descend','NPeaks',2); - -%If the prominence of the peak in the positive signal is greater, we adapt -%our limits and parameters accordingly, if negative signal has a greater -%prominence, we use this for fitting. -if isempty(proms{2}) || proms{1}(1)>proms{2}(1) - ind=1; - lim_lower(1)=0; - lim_lower(4)=0; - p_in(8)=min(y); -else - lim_upper(1)=0; - lim_upper(4)=0; - ind=2; - p_in(8)=max(y); - proms{2}=-proms{2}; -end - -p_in(2)=widths{ind}(1); -%Calculates the amplitude, as when x=c, the amplitude is 2a/(pi*b) -p_in(1)=proms{ind}(1)*pi*p_in(2)/2; -p_in(3)=locs{ind}(1); -if length(locs{ind})==2 - p_in(5)=widths{ind}(2); - p_in(4)=proms{ind}(2)*pi*p_in(5)/2; - p_in(6)=locs{ind}(2); -else - p_in(5)=widths{ind}(1); - p_in(4)=proms{ind}(1)*pi*p_in(5)/2; - p_in(6)=locs{ind}(1); -end - -%If one of the lorentzians is found to be much smaller than the other, we -%instead fit using only the greater lorentzian's parameters. This is an -%adaption for very closely spaced lorentzians. -if abs(p_in(1))>abs(10*p_in(4)) - p_in(1)=p_in(1)/2; - p_in(5)=p_in(2); - p_in(6)=p_in(3); - p_in(4)=p_in(1); -end - -%Find gradient offset -p_in(7)=(y(end)-y(1))/(x(end)-x(1)); - -lim_lower(2)=0.01*p_in(2); -lim_upper(2)=100*p_in(2); - -lim_lower(5)=0.01*p_in(5); -lim_upper(5)=100*p_in(5); -end \ No newline at end of file diff --git a/Utility functions/initParamLorentzianGrad.m b/Utility functions/initParamLorentzianGrad.m deleted file mode 100644 index afe67da..0000000 --- a/Utility functions/initParamLorentzianGrad.m +++ /dev/null @@ -1,55 +0,0 @@ -function [p_in,lim_lower,lim_upper]=initParamLorentzianGrad(x,y) -%Assumes form a/pi*b/2/((x-c)^2+(b/2)^2)+d - -lim_upper=[Inf,Inf,Inf,Inf,Inf]; -lim_lower=[-Inf,0,-Inf,-Inf,-Inf]; - -%Finds peaks on the positive signal (max 1 peak) -try - [~,locs(1),widths(1),proms(1)]=findpeaks(y,x,... - 'MinPeakDistance',range(x)/2,'SortStr','descend',... - 'NPeaks',1); -catch - proms(1)=0; -end - -%Finds peaks on the negative signal (max 1 peak) -try - [~,locs(2),widths(2),proms(2)]=findpeaks(-y,x,... - 'MinPeakDistance',range(x)/2,'SortStr','descend',... - 'NPeaks',1); -catch - proms(2)=0; -end - -if proms(1)==0 && proms(2)==0 - warning('No peaks were found in the data, giving default initial parameters to fit function') - p_in=[1,1,1,1,1]; - lim_lower=-[Inf,0,Inf,Inf]; - lim_upper=[Inf,Inf,Inf,Inf]; - return -end - -%If the prominence of the peak in the positive signal is greater, we adapt -%our limits and parameters accordingly, if negative signal has a greater -%prominence, we use this for fitting. -if proms(1)>proms(2) - ind=1; - p_in(5)=min(y); -else - ind=2; - p_in(5)=max(y); - proms(2)=-proms(2); -end - -p_in(2)=widths(ind); -%Calculates the amplitude, as when x=c, the amplitude is 2a/(pi*b) -p_in(1)=proms(ind)*pi*p_in(2)/2; -p_in(3)=locs(ind); - -p_in(4)=(y(end)-y(1))/(x(end)-x(1)); - -lim_lower(2)=0.01*p_in(2); -lim_upper(2)=100*p_in(2); - -end \ No newline at end of file diff --git a/Utility functions/isValidBaseVar.m b/Utility functions/isValidBaseVar.m index ec20ca8..324f807 100644 --- a/Utility functions/isValidBaseVar.m +++ b/Utility functions/isValidBaseVar.m @@ -1,15 +1,9 @@ -function bool = isValidBaseVar(name) - % Check if the name exists and valid in the global numspace - name_exist = evalin('base',['exist(''',name,''', ''var'')']); - if name_exist - % Check if the variable is a valid object - try - bool = evalin('base',sprintf('isvalid(%s)',name)); - catch - bool = false; - end - else - bool = false; - end +% Check if the name belongs to a valid object in the global workspace + +function bool = isValidBaseVar(name) + cmd = sprintf(['exist(''%s'', ''var'') && ' ... + '((ismethod(%s, ''isvalid'') && isvalid(%s)) || ' ... + '~ismethod(%s, ''isvalid''))'], name, name, name, name); + bool = evalin('base', cmd); end diff --git a/Utility functions/isaxes.m b/Utility functions/isaxes.m new file mode 100644 index 0000000..e2bbc37 --- /dev/null +++ b/Utility functions/isaxes.m @@ -0,0 +1,7 @@ +% Check if the object is Axes or UIAxes + +function bool = isaxes(Obj) + bool = isa(Obj, 'matlab.graphics.axis.Axes') ... + || isa(Obj, 'matlab.ui.control.UIAxes'); +end + diff --git a/Utility functions/isfieldval.m b/Utility functions/isfieldval.m deleted file mode 100644 index 229bcf2..0000000 --- a/Utility functions/isfieldval.m +++ /dev/null @@ -1,14 +0,0 @@ -% Checks if the structure (or class) has fields with particular values -% usage: isfieldval(structure, 'field1', value1, 'field2', value2, ...) -function bool = isfieldval(x, varargin) - bool = true; - for i=1:floor(length(varargin)/2) - try - tmp_bool = isequal(x.(varargin{2*i-1}), varargin{2*i}); - catch - tmp_bool = false; - end - bool = bool & tmp_bool; - end -end - diff --git a/Utility functions/isline.m b/Utility functions/isline.m deleted file mode 100644 index b2bc601..0000000 --- a/Utility functions/isline.m +++ /dev/null @@ -1,3 +0,0 @@ -function bool=isline(linestyle) -bool=any(strcmpi({'-','--',':','-.','none'},linestyle)); -end \ No newline at end of file diff --git a/Utility functions/ismarker.m b/Utility functions/ismarker.m deleted file mode 100644 index 497b096..0000000 --- a/Utility functions/ismarker.m +++ /dev/null @@ -1,6 +0,0 @@ -function bool=ismarker(marker) - -bool=any(strcmpi({'.','o','x','+','*','s','d','v','^','<','>','p','h',... - 'square','diamond','pentagram','hexagram','none'}, marker)); - -end \ No newline at end of file diff --git a/Utility functions/menuFromRunFiles.m b/Utility functions/menuFromRunFiles.m deleted file mode 100644 index 2eb28dd..0000000 --- a/Utility functions/menuFromRunFiles.m +++ /dev/null @@ -1,34 +0,0 @@ -% Give menu content based on the structure, returned by readRunFiles -function content = menuFromRunFiles(RunFiles, varargin) - % varargin is made of tag-value pairs, so only those elements that have - % the property tag for which .(tag)=value are selected for the output - run_file_list = struct2cell(RunFiles); - is_sel = cellfun(@(x) isfieldval(x, varargin{:}), run_file_list); - - run_file_list = run_file_list(is_sel); - tag_list = cell(length(run_file_list),1); - title_list = cell(length(run_file_list),1); - - for i=1:length(run_file_list) - TmpRunFile = run_file_list{i}; - if isfield(TmpRunFile, 'name') - tag_list{i} = TmpRunFile.name; - if isfield(TmpRunFile, 'menu_title') &&... - ~isempty(TmpRunFile.menu_title) - title_list{i} = TmpRunFile.menu_title; - else - title_list{i} = TmpRunFile.name; - end - else - title_list{i} = 'no_name'; - tag_list{i} = 'no_name'; - warning('The following entry has no name field') - disp(run_file_list{i}) - end - end - - content = struct(); - content.titles = title_list; - content.tags = tag_list; -end - diff --git a/Utility functions/readCommentHeader.m b/Utility functions/readCommentHeader.m deleted file mode 100644 index 1b8769e..0000000 --- a/Utility functions/readCommentHeader.m +++ /dev/null @@ -1,45 +0,0 @@ -% Read the header and first code line of a Matlab file, interpreting -% 'property'='value' pairs indicated in comments -function Info = readCommentHeader(file_name) - Info = struct(); - Info.header = ''; - Info.first_code_line =''; - try - fid = fopen(file_name,'r'); - % Read the file line by line, with a paranoid limitation on - % the number of cycles - j = 1; - while j<100000 - j=j+1; - str = fgetl(fid); - trimstr = strtrim(str); - if isempty(trimstr) - % An empty string - elseif trimstr(1)=='%' - % A comment string, try to extract the 'property'='value' - % pair - match = regexp(trimstr,'[%\s]*(\S*)\s*=(.*)','tokens'); - try - tag = lower(match{1}{1}); - % Tags can be anything, except for the protected values - if ~isequal(tag, 'header') &&... - ~isequal(tag, 'first_code_line') - % Remove leading and trailing whitespaces with strtrim - Info.(tag) = strtrim(match{1}{2}); - end - catch - end - else - % Stop when the code begins - Info.first_code_line = [str, newline]; - break - end - % Also store the header in its entirety - Info.header = [Info.header, str, newline]; - end - fclose(fid); - catch - warning('Could not read the file header of %s', file_name) - end -end - diff --git a/Utility functions/readRunFiles.m b/Utility functions/readRunFiles.m deleted file mode 100644 index debf8e5..0000000 --- a/Utility functions/readRunFiles.m +++ /dev/null @@ -1,69 +0,0 @@ -% Read all the files which names start from 'run' from the local base -% directory and add entries, autometically generated from the -% InstrumentList -function RunFiles = readRunFiles(varargin) - if ~isempty(varargin) && ischar(varargin{1}) - % The directory to search in can be supplied as varargin{1} - dir = varargin{1}; - else - % Otherwise use the local base directory - dir = getLocalBaseDir(); - end - % Find all the names of .m files that start with 'run' - all_names = what(dir); - is_run = startsWith(all_names.m,'run','IgnoreCase',false); - run_names = all_names.m(is_run); - RunFiles = struct(); - - % Read headers of all the run*.m files - for i=1:length(run_names) - name_match = regexp(run_names{i},'run(.*)\.m','tokens'); - nm = name_match{1}{1}; - fname = fullfile(dir, run_names{i}); - % Read the run file comment header - RunFiles.(nm) = readCommentHeader(fname); - if isfield(RunFiles.(nm),'show_in_daq') - RunFiles.(nm).show_in_daq = eval(... - lower(RunFiles.(nm).show_in_daq)); - end - % Add information about file name - RunFiles.(nm).name = nm; - % Expression that needs to be evaluated to run the program. In this - % case full name of the file - RunFiles.(nm).fullname = fname; - [~, run_name, ~] = fileparts(fname); - RunFiles.(nm).run_expr = run_name; - end - - % Add entries, automatically generated from the InstrumentList - InstrumentList = getLocalSettings('InstrumentList'); - instr_names = fieldnames(InstrumentList); - for i=1:length(instr_names) - nm = instr_names{i}; - % If run file for instrument was not specified explicitly and if - % all the required fields in InstrList are filled, add an entry to - % RunFiles - try - add_entry = ~isfield(RunFiles, nm) &&... - ~isempty(InstrumentList.(nm).control_class); - catch - end - if add_entry - RunFiles.(nm) = InstrumentList.(nm); - - % Command for running an instrument without gui - RunFiles.(nm).run_bg_expr = ... - sprintf('runInstrument(''%s'');',nm); - - % Command for running an instrument with gui, added only if gui - % is specified - if ~isempty(InstrumentList.(nm).gui) - RunFiles.(nm).run_expr = ... - sprintf('runInstrumentWithGui(''%s'');',nm); - end - RunFiles.(nm).header = ['% This entry is automatically ',... - 'generated from InstrumentList']; - end - end -end - diff --git a/Utility functions/runInstrument.m b/Utility functions/runInstrument.m index 05e9dd7..dd41a1a 100644 --- a/Utility functions/runInstrument.m +++ b/Utility functions/runInstrument.m @@ -1,91 +1,140 @@ % Create instrument instance and add it to the collector -function Instr = runInstrument(name, instr_class, interface, address) +function Instr = runInstrument(name, varargin) + + % Process inputs + p = inputParser(); + p.KeepUnmatched = true; + addParameter(p, 'instr_class', '', @ischar); + addParameter(p, 'enable_gui', false, @islogical); + parse(p, varargin{:}); + + instr_class = p.Results.instr_class; + enable_gui = p.Results.enable_gui; + un_varargin = struct2namevalue(p.Unmatched); + % Get the unique instance of Collector - Collector=MyCollector.instance(); + Collector = MyCollector.instance(); - if ~ismember(name, Collector.running_instruments) - if nargin()==1 - % load instr_class, interface and address parameters - % from InstrumentList - InstrumentList = getLocalSettings('InstrumentList'); - if ~isfield(InstrumentList, name) - error('%s is not a field of InstrumentList',... - name); - end - if ~isfield(InstrumentList.(name), 'interface') - error(['InstrumentList entry ', name,... - ' has no ''interface'' field']); - else - interface = InstrumentList.(name).interface; - end - if ~isfield(InstrumentList.(name), 'address') - error(['InstrumentList entry ', name,... - ' has no ''address'' field']); - else - address = InstrumentList.(name).address; - end - if ~isfield(InstrumentList.(name), 'control_class') - error(['InstrumentList entry ', name,... - ' has no ''control_class'' field']); - else - instr_class = InstrumentList.(name).control_class; - end + % Check if the instrument is already running + if ismember(name, Collector.running_instruments) + + % If instrument is already present in the Collector, do not create + % a new object, but return the existing one. + disp([name, ' is already running. Assigning the existing ', ... + 'object instead of creating a new one.']); + + Instr = getInstrument(Collector, name); + + Fig = findFigure(Instr); + + if isempty(Fig) + if enable_gui && ismethod(Instr, 'createGui') - % Make a list of optional name-value pairs - opt_args={}; - if isfield(InstrumentList.(name), 'StartupOpts') - opt_names=fieldnames(InstrumentList.(name).StartupOpts); - for i=1:length(opt_names) - opt_args=[opt_args, {opt_names{i}, ... - InstrumentList.(name).StartupOpts.(opt_names{i})}]; %#ok - end + % Ensure the instrument has GUI + createGui(Instr); + + Fig = findFigure(Instr); + setupFigure(Fig, name); end - elseif nargin()==4 - % Case when all the arguments are supplied explicitly, do - % nothing - opt_args={}; else - error(['Wrong number of input arguments. ',... - 'Function can be called as f(name) or ',... - 'f(name, instr_class, interface, address).']) + + % Bring the window of existing GUI to the front + setFocus(Fig); end - % Skip the interface and address arguments if they are empty - req_args={}; - if ~isempty(interface) - req_args=[req_args,{interface}]; - end - if ~isempty(address) - req_args=[req_args,{address}]; + return + end + + % Create a new instrument object + if isempty(instr_class) + + % Load instr_class, interface, address and other startup arguments + % from InstrumentList + InstrumentList = getLocalSettings('InstrumentList'); + + ind = cellfun(@(x)isequal(x, name), {InstrumentList.name}); + + assert(any(ind), [name ' must correspond to an entry in ' ... + 'InstrumentList.']) + + InstrEntry = InstrumentList(ind); + + if length(InstrEntry) > 1 + + % Multiple entries found + warning(['Multiple InstrumentList entries found with ' ... + 'name ' name]); + InstrEntry = InstrEntry(1); end - Instr = feval(instr_class, req_args{:}, opt_args{:}, 'name', name); - addInstrument(Collector, Instr); + instr_class = InstrEntry.control_class; + + assert(~isempty(instr_class), ['Control class is not specified '... + 'for ' name]); + + instr_args = [struct2namevalue(InstrEntry.StartupOpts), ... + un_varargin]; else - % If instrument is already present in the Collector, do not create - % a new object, but try taking the existing one. - disp([name,' is already running. Assign existing instrument ',... - 'instead of running a new one.']); - try - Instr = Collector.InstrList.(name); - catch - % Return with empty results in the case of falure - warning('Could not assign instrument %s from Collector',name); - Instr = []; - return + + % Case in which all the arguments are supplied explicitly + instr_args = un_varargin; + end + + % Create an instrument instance and store it in Collector + Instr = feval(instr_class, instr_args{:}); + addInstrument(Collector, name, Instr); + + try + + % Open communication. Typically instrument commands will re-open + % the communication object if it is closed, but keepeing it open + % speeds communication up. + if ismethod(Instr, 'openComm') + openComm(Instr); + end + + % Send identification request to the instrument + if ismethod(Instr, 'idn') + idn(Instr); end + + % Read out the state of the physical device + if ismethod(Instr, 'sync') + sync(Instr); + end + catch ME + warning(['Could not start communication with ' name ... + '. Error: ' ME.message]); end - % Open device. Communication commands will re-open the device if is - % closed, but keepeing it always open speeds communication up. - if ismethod(Instr,'openDevice') - openDevice(Instr); + if enable_gui && ismethod(Instr, 'createGui') + + % Ensure the instrument has GUI + createGui(Instr); end - % Send identification request to the instrument - if ismethod(Instr,'idn') - idn(Instr); + + Fig = findFigure(Instr); + + if ~isempty(Fig) + setupFigure(Fig, name); end +end + +function setupFigure(Fig, name) + try + + % Display the instrument's name + Fig.Name = char(name); + % Apply color scheme + applyLocalColorScheme(Fig); + + % Move the app figure to the center of the screen + centerFigure(Fig); + catch ME + warning(['Error while setting up the GUI of ' name ':' ... + ME.message]); + end end diff --git a/Utility functions/runInstrumentWithGui.m b/Utility functions/runInstrumentWithGui.m deleted file mode 100644 index 199fd3c..0000000 --- a/Utility functions/runInstrumentWithGui.m +++ /dev/null @@ -1,58 +0,0 @@ -% Create instrument instance with gui and add it to the collector - -function [Instr, GuiInstr] = runInstrumentWithGui(name, instr_class, interface, address, gui) - % Run instrument - if nargin==1 - % load parameters from InstrumentList - InstrumentList = getLocalSettings('InstrumentList'); - if ~isfield(InstrumentList, name) - error('%s is not a field of InstrumentList',... - name); - end - if ~isfield(InstrumentList.(name), 'gui') - error(['InstrumentList entry ', name,... - ' has no ''gui'' field']); - else - gui = InstrumentList.(name).gui; - end - - Instr = runInstrument(name); - elseif nargin==5 - % Case when all the arguments are supplied explicitly - Instr = runInstrument(name, instr_class, interface, address); - else - error(['Wrong number of input arguments. ',... - 'Function can be called as f(name) or ',... - 'f(name, instr_class, interface, address, gui)']) - end - - % Run gui and assign handles to variable in global workspace - gui_name = ['Gui',name]; - if ~isValidBaseVar(gui_name) - % If gui is not present in the base workspace, create it - GuiInstr = feval(gui, Instr); - if isprop(GuiInstr,'name') - GuiInstr.name = gui_name; - end - % Store gui handle in a global variable - assignin('base', GuiInstr.name, GuiInstr); - % Display instrument's name if given - fig_handle=findfigure(GuiInstr); - if ~isempty(fig_handle) - fig_handle.Name=char(name); - else - warning('No UIFigure found to assign the name') - end - else - % Otherwise return gui from base workspace - GuiInstr = evalin('base',['Gui',name]); - try - % bring app figure on top of other windows - Fig = findfigure(GuiInstr); - Fig.Visible = 'off'; - Fig.Visible = 'on'; - catch - end - end -end - diff --git a/Utility functions/runLogger.m b/Utility functions/runLogger.m new file mode 100644 index 0000000..75ac3f7 --- /dev/null +++ b/Utility functions/runLogger.m @@ -0,0 +1,98 @@ +% Create and add to Collector an instrument logger using buil-in method +% of the instrument class. +% +% This function is called with two syntaxes: +% +% runLogger(instr_name) where instr_name corresponds to an entry in +% the local InstrumentList +% +% runLogger(Instrument) where Instrument is an object that is +% already present in the collector + +function Lg = runLogger(arg) + + % Get the instance of collector + C = MyCollector.instance(); + + if ischar(arg) + + % The function is called with an instrument name + instr_name = arg; + Instr = runInstrument(instr_name); + else + + % The function is called with an instrument object + Instr = arg; + + % Find the instrument name from the collector + ri = C.running_instruments; + ind = cellfun(@(x)isequal(Instr, getInstrument(C, x)), ri); + + assert(nnz(ind) == 1, 'Instrument must be present in Collector'); + + instr_name = ri{ind}; + end + + % Make a logger name + name = [instr_name 'Logger']; + + % Add logger to the collector so that it can transfer data to Daq + if ~isrunning(C, name) + assert(ismethod(Instr, 'createLogger'), ['A logger is not ' ... + 'created as instrument class ' class(Instr) ... + ' does not define ''createLogger'' method.']) + + % Create and set up a new logger + try + dir = getLocalSettings('default_log_dir'); + catch + try + dir = getLocalSettings('measurement_base_dir'); + dir = createSessionPath(dir, [instr_name ' log']); + catch + dir = ''; + end + end + + Lg = createLogger(Instr); + + createLogFileName(Lg, dir, instr_name); + + % Add logger to Collector + addInstrument(C, name, Lg); + else + disp(['Logger for ' instr_name ' is already running. ' ... + 'Returning existing.']) + + Lg = getInstrument(C, name); + end + + % Check if the logger already has a GUI + if isempty(Lg.Gui) || ~isvalid(Lg.Gui) + + % Run a new GUI and store it in the collector + createGui(Lg); + + % Display the instrument's name + Fig = findFigure(Lg.Gui); + if ~isempty(Fig) + Fig.Name = char(name); + else + warning('No UIFigure found to assign the name') + end + + % Apply color scheme + applyLocalColorScheme(Fig); + + % Move the app figure to the center of the screen + centerFigure(Fig); + else + + % Bring the window of existing GUI to the front + try + setFocus(Lg.Gui); + catch + end + end +end + diff --git a/Utility functions/runSession.m b/Utility functions/runSession.m new file mode 100644 index 0000000..0c24aef --- /dev/null +++ b/Utility functions/runSession.m @@ -0,0 +1,154 @@ +% Load metadata specified in filename, run all the instruments indicated in +% it and configure the settings of those instruments from metadata +% parameters + +function runSession(filename) + Mdt = MyMetadata.load(filename); + + assert(~isempty(Mdt), ['Metadata is not found in the file ''' ... + filename '''.']); + + disp(['Loading session info from file ' filename '...']) + + % SessionInfo contains information about the state of Collector + CollMdt = titleref(Mdt, 'SessionInfo'); + + if length(CollMdt)>1 + warning(['Multiple SessionInfo fields are found in the file ' ... + 'metadata.']); + CollMdt = CollMdt(1); + end + + ProgList = getIcPrograms(); + prog_names = {ProgList.name}; + + if ~isempty(CollMdt) + + % Get the list of instruments active during the session from the + % collector metadata + ind = cellfun(@(x)ismember(x, CollMdt.ParamList.instruments), ... + prog_names); + else + + % Get the list of instruments as the titles of those metadata + % entries that have a corresponding local measurement routine + ind = cellfun(@(x)ismember(x, {Mdt.title}), prog_names); + end + + ActiveProgList = ProgList(ind); + + % Delete all the instruments present in the collector + C = MyCollector.instance(); + + disp('Closing the current session...') + + flush(C); + + % Run new instruments and configure their settings + for i = 1:length(ActiveProgList) + nm = ActiveProgList(i).name; + + disp(['Starting ' nm '...']) + + % Extract instument options from the collector metadata or assign + % default values + try + collect_header = CollMdt.ParamList.Props.(nm).collect_header; + catch + collect_header = true; + end + + try + has_gui = CollMdt.ParamList.Props.(nm).has_gui; + catch + has_gui = true; + end + + try + gui_position = CollMdt.ParamList.Props.(nm).gui_position; + catch + gui_position = ''; + end + + % We hedge the operation of running a new instrument so that the + % falure of one would not prevent starting the others + try + if has_gui + eval(ActiveProgList(i).run_expr); + + if ~isempty(gui_position) + Instr = getInstrument(C, nm); + Fig = findFigure(Instr); + + original_units = Fig.Units; + Fig.Units = 'pixels'; + + % Set x and y position of GUI figure + Fig.Position(1) = gui_position(1); + Fig.Position(2) = gui_position(2); + + % Restore the figure settings + Fig.Units = original_units; + end + else + eval(ActiveProgList(i).run_bg_expr); + end + + setInstrumentProp(C, nm, 'collect_header', collect_header); + + % Configure the settings of instrument object + InstrMdt = titleref(Mdt, nm); + Instr = getInstrument(C, nm); + + if ~isempty(InstrMdt) && ismethod(Instr, 'writeSettings') + if length(InstrMdt) > 1 + warning(['Duplicated entries are found for the ' ... + 'instrument with name ''' nm '''.']); + InstrMdt = InstrMdt(1); + end + + try + writeSettings(Instr, InstrMdt); + catch ME + warning(['Error while attempting to write serrings '... + 'to ''' nm ''': ' ME.message]) + end + end + catch ME + warning(['Could not start instrument with name ''' nm ... + '''. Error: ' ME.message]) + end + end + + % Run apps + for i = 1:length(CollMdt.ParamList.apps) + try + nm = CollMdt.ParamList.apps{i}; + + % The convention is such that the apps can be instantiated as + % classname(), i.e. that their constructor does not have + % required input arguments. + App = eval(CollMdt.ParamList.AppProps.(nm).class); + + pos = CollMdt.ParamList.AppProps.(nm).position; + if ~isempty(pos) + Fig = findFigure(App); + + original_units = Fig.Units; + Fig.Units = 'pixels'; + + % Set x and y position of figure + Fig.Position(1) = pos(1); + Fig.Position(2) = pos(2); + + % Restore the figure settings + Fig.Units = original_units; + end + catch ME + warning(['Error while attempting to run an app: ' ME.message]) + end + end + + disp('Finished loading session.') +end + diff --git a/Utility functions/runSingletonApp.m b/Utility functions/runSingletonApp.m new file mode 100644 index 0000000..90a5ca2 --- /dev/null +++ b/Utility functions/runSingletonApp.m @@ -0,0 +1,29 @@ +function runSingletonApp(App, varargin) + + C = MyCollector.instance(); + + % Singleton apps can be uniquely identified by the name of their class + name = class(App); + + if ismember(name, C.running_apps) + Fig = findFigure(App); + close(Fig); + + % Make sure the new instance is deleted + delete(App); + App = getApp(C, name); + + % Bring to the focus the figure of existing app + setFocus(App); + + error([name ' already exists']); + else + addApp(C, App, name); + + % Recolor app according to the present color scheme + applyLocalColorScheme(App); + + % Move the app figure to the center of the screen + centerFigure(App); + end +end \ No newline at end of file diff --git a/Utility functions/setCursorColor.m b/Utility functions/setCursorColor.m deleted file mode 100644 index 5bb058a..0000000 --- a/Utility functions/setCursorColor.m +++ /dev/null @@ -1,5 +0,0 @@ -function setCursorColor(crsobj,color) -set(crsobj.TopHandle,'MarkerFaceColor',color); -set(crsobj.BottomHandle,'MarkerFaceColor',color); -set(crsobj,'CursorLineColor',color); -end \ No newline at end of file diff --git a/Utility functions/setFocus.m b/Utility functions/setFocus.m new file mode 100644 index 0000000..bc1278b --- /dev/null +++ b/Utility functions/setFocus.m @@ -0,0 +1,9 @@ +% Bring the figure contained in Obj on top of other windows and set focus +% to it + +function setFocus(Obj) + Fig = findFigure(Obj); + Fig.Visible = 'off'; + Fig.Visible = 'on'; +end + diff --git a/Utility functions/setIfChanged.m b/Utility functions/setIfChanged.m new file mode 100644 index 0000000..df54ba0 --- /dev/null +++ b/Utility functions/setIfChanged.m @@ -0,0 +1,17 @@ +% Shorthand for an assignment with AbortSet=true. + +function is_changed = setIfChanged(Obj, prop, val) + if length(Obj) > 1 + + % Span over the array of objects + is_changed = arrayfun(@(x)setIfChanged(x, prop, val), Obj); + return + end + + is_changed = ~isequal(Obj.(prop), val); + + if is_changed + Obj.(prop) = val; + end +end + diff --git a/Utility functions/splitFilename.m b/Utility functions/splitFilename.m new file mode 100644 index 0000000..51e1d62 --- /dev/null +++ b/Utility functions/splitFilename.m @@ -0,0 +1,33 @@ +% Split filename into name and extension (if present), applying some more +% elaborate procedure to determine the real extension than that used +% in fileparts() + +function [name, ext] = splitFilename(filename) + filename_split = regexp(filename, '\.', 'split'); + + if length(filename_split) == 1 + + % No extension found + name = filename; + ext = ''; + return + end + + % A candidate for the extension + ext = filename_split{end}; + + if ~isempty(ext) && ~any(isspace(ext)) && length(ext)<4 && ... + ~all(ismember(ext(2:end), '0123456789')) + + % ext is actual extension + % Add a point to conform with the convention of fileparts() + ext = ['.' ext]; + name = strjoin(filename_split(1:end-1), '.'); + else + + % ext is not an actual extension + name = filename; + ext = ''; + end +end + diff --git a/Utility functions/splitSessionPath.m b/Utility functions/splitSessionPath.m new file mode 100644 index 0000000..8886dae --- /dev/null +++ b/Utility functions/splitSessionPath.m @@ -0,0 +1,23 @@ +% Inverse function to createSessionPath - it splits path into base +% directory and session name + +function [base_dir, session_name] = splitSessionPath(path) + path_split = split(path, filesep()); + + if length(path_split) > 1 + base_dir = fullfile(path_split{1:end-1}); + else + base_dir = ''; + end + + % Remove date from the session name + session_name_tok = regexp(path_split{end}, ... + '\d\d\d\d-\d\d-\d\d (.*)', 'tokens'); + + if ~isempty(session_name_tok) + session_name = session_name_tok{1}{1}; + else + session_name = path_split{end}; + end +end + diff --git a/Utility functions/str2doubleHedged.m b/Utility functions/str2doubleHedged.m index 99b2fbe..52893f1 100644 --- a/Utility functions/str2doubleHedged.m +++ b/Utility functions/str2doubleHedged.m @@ -1,10 +1,44 @@ -function [val,str_spec]=str2doubleHedged(str) - conv_str=str2double(str); +function [val, format] = str2doubleHedged(str) + + % Span over multiple inputs given as cell + if iscell(str) + val = cellfun(@str2doubleHedged, str, 'UniformOutput', false); + return + end + + conv_str = str2double(str); + if ~isnan(conv_str) - val=conv_str; - str_spec='%e'; + val = conv_str; + + % Determine the printed format type - float or integer and + % fixed point or exponential + is_float = contains(str, '.'); + is_exp = contains(str, {'e', 'E'}); + + % Determine the precision: separate mantissa and exponent and count + % the digits of mantissa + num_str = regexp(strtrim(str), 'e|E', 'split'); + prec = length(regexp(num_str{1}, '\d'))-1; + + if prec >= 16 + warning(['Standard double type precision limit is reached ' ... + 'while converting the string ''%s''. ' ... + 'The converted numeric value may have lower precision ' ... + 'compare to that of the original string.'], str) + end + + if is_float + if is_exp + format = sprintf('%%.%ie', prec); + else + format = sprintf('%%.%if', prec); + end + else + format = '%i'; + end else - val=str; - str_spec='%s'; + val = str; + format = '%s'; end end \ No newline at end of file diff --git a/Utility functions/str2substruct.m b/Utility functions/str2substruct.m index dfd0496..04de409 100644 --- a/Utility functions/str2substruct.m +++ b/Utility functions/str2substruct.m @@ -1,58 +1,58 @@ % Convert textual representation of a Matlab expression to structure that % can be used by subsref and subsasgn functions function [S, varname] = str2substruct(str) % Define patterns to match the variable name and subscript references, % i.e. structure fields, array indices and cell indices vn = '^(?[a-zA-Z]\w*)?'; % pattern for variable name % Expresion always returns one and only one match, which might be empty [re_tokens, re_rem]=regexp(str,vn,'tokens','split','once','emptymatch'); varname=re_tokens{1}; str=re_rem{2}; % Pattern to find comma-separated integers, possibly % surrounded by white spaces, which represent array indices csint = '(( *[:0-9]+ *,)*( *[:0-9]+ *))'; % Define patterns to match subscript references, i.e. structure fields, % array indices and cell indices - aind = ['\((?',csint,')\)']; % regular array index pattern cind = ['{(?',csint,')}']; % cell array index pattern fn = '\.(?\w+)'; % field name pattern [re_tokens, re_rem] = regexp(str, ... [fn,'|',aind,'|',cind],'names','split','emptymatch'); % Check that the unmatched remainder of the expression is empty, % i.e. that the expression has a proper format assert(all(cellfun(@(x)isempty(x),re_rem)), ['Expression ''',str,... ''' is not a valid subscript reference.']); type_cell=cell(1,length(re_tokens)-1); subs_cell=cell(1,length(re_tokens)-1); for i=1:length(re_tokens) if ~isempty(re_tokens(i).arrind) type_cell{i}='()'; + % Split and convert indices to numbers. char_ind=regexp(re_tokens(i).arrind,',','split'); - subs_cell{i}=cellfun(@str2doubleHedged, char_ind, ... - 'UniformOutput', false); + subs_cell{i}=str2doubleHedged(char_ind); elseif ~isempty(re_tokens(i).cellind) type_cell{i}='{}'; + % Split and convert indices to numbers. char_ind=regexp(re_tokens(i).cellind,',','split'); - subs_cell{i}=cellfun(@str2doubleHedged, char_ind, ... - 'UniformOutput', false); + subs_cell{i}=str2doubleHedged(char_ind); elseif ~isempty(re_tokens(i).fieldname) type_cell{i}='.'; subs_cell{i}=re_tokens(i).fieldname; end end + S=struct('type', type_cell, 'subs', subs_cell); end diff --git a/Utility functions/struct2namevalue.m b/Utility functions/struct2namevalue.m new file mode 100644 index 0000000..5b79da7 --- /dev/null +++ b/Utility functions/struct2namevalue.m @@ -0,0 +1,12 @@ +% Convert structure S to the list of fieldname-value pairs + +function nv = struct2namevalue(S) + fns=fieldnames(S); + vals=struct2cell(S); + nv=cell(1,2*length(fns)); + for i=1:length(fns) + nv{2*i-1}=fns{i}; + nv{2*i}=vals{i}; + end +end + diff --git a/Utility functions/testfit.m b/Utility functions/testfit.m deleted file mode 100644 index 8719a73..0000000 --- a/Utility functions/testfit.m +++ /dev/null @@ -1,37 +0,0 @@ -%Testing tool for MyFit -clear -x_vec=linspace(0,200,1000); - -testFit=MyFit('fit_name','Lorentzian','enable_gui',1); -params=cell(1,testFit.n_params); -switch testFit.fit_name - case 'Lorentzian' - for i=1:testFit.n_params - params{i}=5*rand; - params{3}=200*rand; - end - case 'Exponential' - params{1}=-5*rand; - params{2}=-0.1*rand; - params{3}=0.01*rand; - otherwise - for i=1:testFit.n_params - params{i}=5*rand; - end -end - -params -y_vec=testFit.FitStruct.(testFit.fit_name).anon_fit_fun(x_vec,params{:}).*normrnd(1,0.04,size(x_vec)); -figure(1) -ax=gca; -plot(x_vec,y_vec,'x'); -axis([min(x_vec),max(x_vec),0.5*min(y_vec),1.5*max(y_vec)]); -hold on -testFit.plot_handle=ax; -testFit.enable_plot=1; -testFit.Data.x=x_vec; -testFit.Data.y=y_vec; -% testFit.genInitParams; -% testFit.init_params -% testFit.fitTrace; -% testFit.init_params \ No newline at end of file diff --git a/Utility functions/toSingleLine.m b/Utility functions/toSingleLine.m new file mode 100644 index 0000000..ac77130 --- /dev/null +++ b/Utility functions/toSingleLine.m @@ -0,0 +1,15 @@ +% Remove carriage return and new line symbols from the string + +function [new_str, is_modified] = toSingleLine(str, repl) + if nargin() == 1 + repl = ' '; + end + + newline_smb = {sprintf('\n'),sprintf('\r')}; %#ok + new_str = replace(str, newline_smb, repl); + + if nargout() > 1 + is_modified = ~strcmp(new_str, str); + end +end + diff --git a/Utility functions/var2str.m b/Utility functions/var2str.m index f949217..f407d8c 100644 --- a/Utility functions/var2str.m +++ b/Utility functions/var2str.m @@ -1,22 +1,28 @@ % Convert variable of arbitrary type to char string function str_out = var2str(var) switch lower(class(var)) case {'single','double'} + % Default display of numbers is with the precision of % up to 8 decimals and trailing zeros removed - str_out=sprintf('%.8g',var); + str_out = sprintf('%.8g',var); case {'uint8','int8','uint16','int16','uint32','int32',... 'uint64','int64','logical'} - str_out=sprintf('%i',var); + str_out = sprintf('%i',var); case {'char','string'} - str_out=sprintf('%s',var); + str_out = sprintf('%s',var); + case 'cell' + + % Concatenate individual strings for the elements of cell + str_cell = cellfun(@var2str, var, 'UniformOutput', false); + str_out = ['{', strjoin(str_cell, ', '), '}']; case 'datetime' - str_out=datestr(var); + str_out = datestr(var); otherwise warning(['Method for conversion of variable of class ',... '''%s'' to string is not specified explicitly. ',... 'Using disp() by default.'], class(var)); - str_out=evalc('disp(var)'); + str_out = evalc('disp(var)'); end end