diff --git a/.gitignore b/.gitignore index a9143d2..04beb21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.asv *.asv *.asv *.DS_Store +MyDaq_*.mlapp \ No newline at end of file diff --git a/@MyDataSource/MyDataSource.m b/@MyDataSource/MyDataSource.m index 9109cd4..1b8eba6 100644 --- a/@MyDataSource/MyDataSource.m +++ b/@MyDataSource/MyDataSource.m @@ -1,50 +1,50 @@ -% Class that contains functionality of transferring trace to Collector and +% Class that contains functionality of transferring a trace to Collector and % then to Daq classdef MyDataSource < handle properties (Access = public) % An object derived from MyTrace Trace end events NewData end 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. function triggerNewData(this, varargin) EventData = MyNewDataEvent(varargin{:}); % Pass trace by value to make sure that it is not modified % before being transferred if isempty(EventData.traces) % EventData.Trace can be set either automaticallt here or % explicitly as a name-value pair supplied to the function. EventData.traces = {copy(this.Trace)}; end notify(this, 'NewData', EventData); end end methods function set.Trace(this, Val) assert(isa(Val, 'MyTrace'), ['Trace must be a derivative ' ... 'of MyTrace class.']) this.Trace = Val; end end end diff --git a/@MyFit/MyFit.m b/@MyFit/MyFit.m index 3f25bde..ef857d1 100644 --- a/@MyFit/MyFit.m +++ b/@MyFit/MyFit.m @@ -1,807 +1,813 @@ % Class that implements fitting routines with GUI capabilities. classdef MyFit < dynamicprops & MyAnalysisRoutine & ... matlab.mixin.CustomDisplay properties (Access = public) % MyTrace object contains the data to be fitted to Data MyTrace lim_lower % Lower limits for fit parameters lim_upper % Upper limits for fit parameters % If enabled, plots fit curve in the Axes every time the parameter % values are updated enable_plot fit_color = 'black' % Color of the fit line fit_length = 1e3 % Number of points in the fit trace end properties (GetAccess = public, SetAccess = protected) Axes % The handle which the fit is plotted in Fit % MyTrace object containing the fit Gui = struct() % Handles of GUI elements % Array of cursors with length=2 for the selection of fitting range RangeCursors MyCursor % Output structures from fit() function: FitResult cfit Gof struct FitInfo struct param_vals % Numeric values of fit parameters fit_name fit_tex % tex representation of the fit formula fit_function % fit formula as character string fit_params % character names of fit parameters in fit_function fit_param_names % long informative names of fit parameters anon_fit_fun % fit expression represented by anonimous function % Additional parameters that are calculated from the fit parameters % or inputed externally. Properties of user parameters including % long name and write attribute UserParamList = struct() end properties (Dependent = true, GetAccess = public) n_params % Indices of data points selected for fitting data_selection % Enable cursors for the selection of fit range enable_range_cursors end properties (Access = protected) enable_gui = 1 % Vectors for varying the range of the sliders in GUI slider_vecs end methods (Access = public) function this = MyFit(varargin) % Parse the arguments supplied to the constructor p = inputParser(); addParameter(p, 'fit_name', '') addParameter(p, 'fit_function', 'x') addParameter(p, 'fit_tex', '') addParameter(p, 'fit_params', {}) addParameter(p, 'fit_param_names', {}) addParameter(p, 'Data', MyTrace()); addParameter(p, 'x', []); addParameter(p, 'y', []); addParameter(p, 'Axes', [], @isaxes); addParameter(p, 'enable_gui', true); addParameter(p, 'enable_plot', true); addParameter(p, 'enable_range_cursors', false) % The parameters below are only active when GUI is enabled % If true, adds save trace panel to the fit gui addParameter(p,'save_panel',false,@islogical); addParameter(p,'base_dir', ''); addParameter(p,'session_name','placeholder'); addParameter(p,'file_name','placeholder'); parse(p, varargin{:}); for i=1:length(p.Parameters) % Takes the value from the inputParser to the appropriate % property. if isprop(this, p.Parameters{i}) this.(p.Parameters{i}) = p.Results.(p.Parameters{i}); end end this.Fit = MyTrace( ... 'name_x', this.Data.name_x, ... 'unit_x', this.Data.unit_x, ... 'name_y', this.Data.name_y, ... 'unit_y', this.Data.unit_y); %Generates the anonymous fit function from the input fit %function. This is used for fast plotting of the initial %values. args=['@(', strjoin([{'x'}, this.fit_params], ','),')']; this.anon_fit_fun=... str2func(vectorize([args,this.fit_function])); %Sets dummy values for the GUI this.param_vals=zeros(1,this.n_params); this.lim_lower=-Inf(1,this.n_params); this.lim_upper=Inf(1,this.n_params); %Allows us to load either x/y data or a MyTrace object directly if ismember('Data',p.UsingDefaults) &&... ~ismember('x',p.UsingDefaults) &&... ~ismember('y',p.UsingDefaults) this.Data.x=p.Results.x; this.Data.y=p.Results.y; end %Creates the structure that contains variables for calibration %of fit results createUserParamList(this); %Creates the gui if the flag is enabled. This function is in a %separate file. if this.enable_gui createGui(this, 'save_panel', p.Results.save_panel) %Generates the slider lookup table genSliderVecs(this); if isempty(p.Results.base_dir) try bd = getLocalSettings('measurement_base_dir'); catch ME warning(ME.message) bd = ''; end else bd = ''; end this.Gui.BaseDir.String = bd; this.Gui.SessionName.String = p.Results.session_name; this.Gui.FileName.String = p.Results.file_name; end if ~isempty(this.Axes) rng_color = [0.2510, 0.1961, 0.4118]; % Add two vertical cursors to the axes xlim = this.Axes.XLim; x1 = xlim(1)+0.1*(xlim(2)-xlim(1)); x2 = xlim(2)-0.1*(xlim(2)-xlim(1)); this.RangeCursors = ... [MyCursor(this.Axes, ... 'orientation', 'vertical', ... 'position', x1, ... 'Label','Fit range 1', 'Color', rng_color),... MyCursor(this.Axes, ... 'orientation', 'vertical', ... 'position', x2, ... 'Label','Fit range 2', 'Color', rng_color)]; % Enabling/disabling of the cursors by setting the class % property can be done only after the cursors are % initialized this.enable_range_cursors = p.Results.enable_range_cursors; end %If data was supplied, generates initial fit parameters if ~isDataEmpty(this.Data) genInitParams(this) end end %Deletion function of object function delete(this) if this.enable_gui %Avoids loops set(this.Gui.Window,'CloseRequestFcn',''); %Deletes the figure delete(this.Gui.Window); %Removes the figure handle to prevent memory leaks this.Gui=[]; end if ismethod(this.Fit, 'delete') % Delete the fit trace, in particular, in order to remove % the fit curve from the axes delete(this.Fit); end if ~isempty(this.RangeCursors) delete(this.RangeCursors); end end %This function is used to set the coefficients, to avoid setting it %to a number not equal to the number of parameters function setFitParams(this,param_vals) assert(length(param_vals)==this.n_params,... ['The length of the coefficient vector (currently %i) ',... 'must be equal to the number of parameters (%i)'],... length(this.param_vals),this.n_params) this.param_vals=param_vals; end %Fits the trace using currently set parameters, depending on the %model. function fitTrace(this) %Check the validity of data validateData(this); %Check for valid limits lim_check=this.lim_upper>this.lim_lower; assert(all(lim_check),... sprintf(['All upper limits must exceed lower limits. ',... 'Check limit %i, fit parameter %s'],find(~lim_check,1),... this.fit_params{find(~lim_check,1)})); %Check the consistency of initial parameters assert(isnumeric(this.param_vals) && isvector(this.param_vals) && ... length(this.param_vals)==this.n_params, ['Starting points must be given as ' ... 'a vector of size %d'],this.n_params); assert(isnumeric(this.lim_lower) && isvector(this.lim_lower) && ... length(this.lim_lower)==this.n_params, ['Lower limits must be given as ' ... 'a vector of size %d'], this.n_params); assert(isnumeric(this.lim_upper) && isvector(this.lim_upper) && ... length(this.lim_upper)==this.n_params, ['Upper limits must be given as ' ... 'a vector of size %d'], this.n_params); %Perform the fit with current parameters as a starting point ind = this.data_selection; this.param_vals = doFit(this, ... this.Data.x(ind), this.Data.y(ind), this.param_vals, ... this.lim_lower, this.lim_upper); %Calculate the fit curve calcFit(this); %Calculate user parameters that depend on the fit parameters calcUserParams(this); %Update fit metadata this.Fit.UserMetadata = createMetadata(this); %Updates the gui if it is enabled if this.enable_gui genSliderVecs(this); updateSliderPanel(this); end %Plots the fit if the flag is on if this.enable_plot plotFit(this); end end %Clears the plots function clearFit(this) clearData(this.Fit); deleteLine(this.Fit); end %Plots the trace contained in the Fit MyTrace object function plotFit(this, varargin) % Fit trace does not make its own labels in order to keep the % labels made by the data trace plot(this.Fit, this.Axes, 'Color', this.fit_color, ... 'make_labels', false, varargin{:}); end %Generates model-dependent initial parameters, lower and upper %boundaries. function genInitParams(this) validateData(this); % Delete existing fit line from the plot, the new fit line % will appear on top of the plot deleteLine(this.Fit); calcInitParams(this); calcFit(this); calcUserParams(this); %Plots the fit function with the new initial parameters if this.enable_plot plotFit(this, 'DisplayName', 'fit'); end %Updates the GUI and creates new lookup tables for the init %param sliders if this.enable_gui genSliderVecs(this); updateSliderPanel(this); end end % Bring the cursors within the axes limits function centerCursors(this) if ~isempty(this.Axes) && ~isempty(this.RangeCursors) ... && all(isvalid(this.RangeCursors)) xlim = this.Axes.XLim; x1 = xlim(1)+0.1*(xlim(2)-xlim(1)); x2 = xlim(2)-0.1*(xlim(2)-xlim(1)); this.RangeCursors(1).value = x1; this.RangeCursors(2).value = x2; end end % Create metadata with all the fitting and user-defined parameters function Mdt = createMetadata(this) % Field for the fit parameters InfoMdt = MyMetadata('title', 'FitInfo'); addObjProp(InfoMdt, this, 'fit_name'); addObjProp(InfoMdt, this, 'fit_function'); % Indicate if the parameter values were obtained manually or % from performing a fit if isempty(this.Gof) param_val_mode = 'manual'; else param_val_mode = 'fit'; end addParam(InfoMdt, 'param_val_mode', param_val_mode, ... 'comment', ['If the parameter values were set manually '... 'or obtained from fit']); % Field for the fit parameters ParValMdt = MyMetadata('title', 'FittingParameters'); if ~isempty(this.Gof) % Add fit parameters with confidence intervals - ci = confint(this.FitResult, 0.95); + ci = getConfInt(this, 0.95); for i=1:length(this.fit_params) str = sprintf('%8.4g (%.4g, %.4g)', ... this.param_vals(i), ci(1,i), ci(2,i)); addParam(ParValMdt, this.fit_params{i}, str, ... 'comment', [this.fit_param_names{i} ... ' (95% confidence interval)']); end else % Add only fit parameters for i=1:length(this.fit_params) addParam(ParValMdt, this.fit_params{i}, ... this.param_vals(i), 'comment', ... this.fit_param_names{i}); end end user_params = fieldnames(this.UserParamList); if ~isempty(user_params) % Add a field with the user parameters UserParMdt = MyMetadata('title', 'UserParameters'); for i=1:length(user_params) tag = user_params{i}; addParam(UserParMdt, tag, this.(tag), ... 'comment', this.UserParamList.(tag).title); end else UserParMdt = MyMetadata.empty(); end if ~isempty(this.Gof) % Field for the goodness of fit which copies the fields of % corresponding structure GofMdt = MyMetadata('title', 'GoodnessOfFit'); addParam(GofMdt, 'sse', this.Gof.sse, 'comment', ... 'Sum of squares due to error'); addParam(GofMdt, 'rsquare', this.Gof.rsquare, 'comment',... 'R-squared (coefficient of determination)'); addParam(GofMdt, 'dfe', this.Gof.dfe, 'comment', ... 'Degrees of freedom in the error'); addParam(GofMdt, 'adjrsquare', this.Gof.adjrsquare, ... 'comment', ['Degree-of-freedom adjusted ' ... 'coefficient of determination']); addParam(GofMdt, 'rmse', this.Gof.rmse, 'comment', ... 'Root mean squared error (standard error)'); else GofMdt = MyMetadata.empty(); end Mdt = [InfoMdt, ParValMdt, UserParMdt, GofMdt]; end end methods (Access = protected) %Creates the GUI of MyFit, in separate file. createGui(this, varargin); %Does the fit with the currently set parameters. This method is %often overloaded in subclasses to improve performance. function fitted_vals = doFit(this, x, y, init_vals, lim_lower, ... lim_upper) %Fits with the below properties. Chosen for maximum accuracy. Ft = fittype(this.fit_function,'coefficients',this.fit_params); Opts = fitoptions('Method','NonLinearLeastSquares',... 'Lower', lim_lower,... 'Upper', lim_upper,... 'StartPoint', init_vals,... 'MaxFunEvals', 2000,... 'MaxIter', 2000,... 'TolFun', 1e-10,... 'TolX', 1e-10); [this.FitResult, this.Gof, this.FitInfo] = fit(x, y, Ft, Opts); %Return the coefficients fitted_vals = coeffvalues(this.FitResult); end %Low level function that generates initial parameters. %The default version of this function is not meaningful, it %should be overloaded in subclasses. function calcInitParams(this) this.param_vals=ones(1,this.n_params); this.lim_lower=-Inf(1,this.n_params); this.lim_upper=Inf(1,this.n_params); end % Calculate user parameters from fit parameters. % Dummy method that needs to be overloaded in subclasses. function calcUserParams(this) %#ok end function addUserParam(this, name, varargin) % Process inputs p = inputParser(); addRequired(p, 'name', @ischar); addParameter(p, 'title', ''); addParameter(p, 'editable', @(x)assert(isequal(x, 'on') || ... isequal(x, 'off'), ['''editable'' property must be ' ... '''on'' or ''off'''])); addParameter(p, 'default', []); parse(p, name, varargin{:}); % Store the information about the user parameter this.UserParamList.(name).title = p.Results.title; this.UserParamList.(name).editable = p.Results.editable; % Create a dynamic property for easy access Mp = addprop(this, name); this.UserParamList.(name).Metaprop = Mp; Mp.GetAccess = 'public'; if ~isempty(p.Results.default) this.(name) = p.Results.default; end if this.UserParamList.(name).editable Mp.SetAccess = 'public'; else Mp.SetAccess = 'private'; end end % addUserParam statements must be contained in this function % overloaded in subclasses. function createUserParamList(this) %#ok end function genSliderVecs(this) %Return values of the slider slider_vals=1:101; %Default scaling vector def_vec=10.^((slider_vals-51)/50); %Sets the cell to the default value for i=1:this.n_params this.slider_vecs{i}=def_vec*this.param_vals(i); set(this.Gui.(sprintf('Slider_%s', this.fit_params{i})),... 'Value',50); end end %Checks if the class is ready to perform a fit function validateData(this) assert(~isempty(this.Data), 'Data is empty'); assert(~isempty(this.Data.x) && ~isempty(this.Data.y) && ... length(this.Data.x)==length(this.Data.y) && ... length(this.Data.x)>=this.n_params, ... ['The data must be vectors of equal length greater ' ... 'than the number of fit parameters.', ... ' Currently the number of fit parameters is %i, the', ... ' length of x is %i and the length of y is %i'], ... this.n_params, length(this.Data.x), length(this.Data.y)); end %Calculates the trace object that represents the fitted curve function calcFit(this) xmin = this.Data.x(1); xmax = this.Data.x(end); if this.enable_range_cursors % If range cursors are active, restrict to the selected % range xmin = max(xmin, min(this.RangeCursors.value)); xmax = min(xmax, max(this.RangeCursors.value)); end this.Fit.x=linspace(xmin, xmax, this.fit_length); input_coeffs=num2cell(this.param_vals); this.Fit.y=this.anon_fit_fun(this.Fit.x, input_coeffs{:}); end %Overload a method of matlab.mixin.CustomDisplay in order to %separate the display of user properties from the others. function PrGroups = getPropertyGroups(this) user_params = fieldnames(this.UserParamList); static_props = setdiff(properties(this), user_params); PrGroups = [matlab.mixin.util.PropertyGroup(static_props), ... matlab.mixin.util.PropertyGroup(user_params)]; end %Function to calculate data selection, introduced such that it can %be overloaded in subclasses. function ind=findDataSelection(this) if this.enable_range_cursors xmin = min(this.RangeCursors.value); xmax = max(this.RangeCursors.value); ind = (this.Data.x>xmin & this.Data.x<=xmax); else ind = true(1, length(this.Data.x)); end end + + %Function to calculate confidence intervals. Overloaded in fits + %with scaling + function ci=getConfInt(this, interval) + ci = confint(this.FitResult, interval); + end end %Callbacks methods (Access = protected) %Callback for saving the fit trace function saveFitCallback(this,~,~) base_dir=this.Gui.BaseDir.String; session_name=this.Gui.SessionName.String; file_name=this.Gui.FileName.String; % Add extension to the file name if missing [~,~,ext]=fileparts(file_name); if isempty(ext) || (length(ext) > 5) || any(isspace(ext)) file_name=[file_name, '.txt']; end assert(~isempty(base_dir),'Save directory is not specified'); save_path=createSessionPath(base_dir, session_name); save(this.Fit, fullfile(save_path, file_name)); end %Creates callback functions for sliders in GUI. Uses ind to find %out which slider the call is coming from. Note that this gets %triggered whenever the value of the slider is changed. function f = createSliderStateChangedCallback(this, ind) edit_field_name = sprintf('Edit_%s',this.fit_params{ind}); function sliderStateChangedCallback(hObject, ~) %Gets the value from the slider val=hObject.Value; %Find out if the current slider value is correct for the %current init param value. If so, do not change anything. %This is required as the callback also gets called when %the slider values are changed programmatically [~, slider_ind]=... min(abs(this.param_vals(ind)-this.slider_vecs{ind})); if slider_ind~=(val+1) %Updates the scale with a new value from the lookup %table this.param_vals(ind)=... this.slider_vecs{ind}(val+1); %Updates the edit box with the new value from the %slider set(this.Gui.(edit_field_name),... 'String', sprintf('%3.3e',this.param_vals(ind))); %Re-calculate the fit curve. calcFit(this); if this.enable_plot plotFit(this); end end end f = @sliderStateChangedCallback; end function f = createParamFieldEditedCallback(this, ind) function paramEditFieldCallback(hObject, ~) val=str2double(hObject.String); manSetParamVal(this, ind, val); end f = @paramEditFieldCallback; end function f = createSliderMouseReleasedCallback(this, ind) function sliderMouseReleasedCallback(hObject, ~) slider_ind=hObject.Value; val = this.slider_vecs{ind}(slider_ind+1); manSetParamVal(this, ind, val); end f = @sliderMouseReleasedCallback; end %Callback function for the manual update of the values of fit %parameters in GUI. Triggered when values in the boxes are editted %and when pulling a slider is over. function manSetParamVal(this, ind, new_val) %Updates the correct initial parameter this.param_vals(ind)=new_val; %Re-calculate the fit curve. calcFit(this); if this.enable_plot plotFit(this) end %Centers the slider set(this.Gui.(sprintf('Slider_%s',this.fit_params{ind})),... 'Value',50); %Generate the new slider vectors genSliderVecs(this); %Reset fit structures to indicate that the current parameters %were set manually this.FitResult=cfit.empty(); this.Gof=struct.empty(); this.FitInfo=struct.empty(); %Calculate user parameters calcUserParams(this); %Update fit metadata this.Fit.UserMetadata=createMetadata(this); end function f = createLowerLimEditCallback(this, ind) function lowerLimEditCallback(hObject, ~) this.lim_lower(ind)=str2double(hObject.String); end f = @lowerLimEditCallback; end function f = createUpperLimEditCallback(this, ind) function upperLimEditCallback(hObject, ~) this.lim_upper(ind)=str2double(hObject.String); end f = @upperLimEditCallback; end %Create a callback that is executed when an editable user parameter %is set in the GUI function f = createUserParamCallback(this, param_name) function userParamCallback(hObject, ~) this.(param_name) = str2double(hObject.String); calcUserParams(this); end f = @userParamCallback; end %Callback function for analyze button in GUI. Checks if the data is %ready for fitting. function analyzeCallback(this, ~, ~) fitTrace(this); end function acceptFitCallback(this, ~, ~) triggerNewProcessedData(this, 'traces', {copy(this.Fit)}, ... 'trace_tags', {'_fit'}); end function enableCursorsCallback(this, hObject, ~) this.enable_range_cursors = hObject.Value; if this.enable_gui if hObject.Value this.Gui.CenterCursorsButton.Enable = 'on'; else this.Gui.CenterCursorsButton.Enable = 'off'; end end end %Callback for clearing the fits on the axis. function clearFitCallback(this, ~, ~) clearFit(this); end %Callback function for the button that generates init parameters. function initParamCallback(this, ~, ~) genInitParams(this); end %Close figure callback simply calls delete function for class function closeFigureCallback(this,~,~) delete(this); end end %Private methods methods(Access = private) %Creates a panel for the GUI, in separate file createUserControls(this, varargin); %Updates the GUI if the edit or slider boxes are changed from %elsewhere. function updateSliderPanel(this) for i=1:this.n_params str=this.fit_params{i}; set(this.Gui.(sprintf('Edit_%s',str)),... 'String',sprintf('%3.3e',this.param_vals(i))); set(this.Gui.(sprintf('Lim_%s_upper',str)),... 'String',sprintf('%3.3e',this.lim_upper(i))); set(this.Gui.(sprintf('Lim_%s_lower',str)),... 'String',sprintf('%3.3e',this.lim_lower(i))); end end end methods % Can set enable_plot to true only if Axes are present function set.enable_plot(this, val) val = logical(val); this.enable_plot = val & ~isempty(this.Axes); %#ok end function set.enable_range_cursors(this, val) if ~isempty(this.RangeCursors) for i=1:length(this.RangeCursors) this.RangeCursors(i).Line.Visible = val; end end try if this.enable_gui && ... this.Gui.CursorsCheckbox.Value ~= val this.Gui.CursorsCheckbox.Value = val; end catch end end % Visibility of the range cursors is the reference if they are % enabled or not function val = get.enable_range_cursors(this) if ~isempty(this.RangeCursors) val = strcmpi(this.RangeCursors(1).Line.Visible, 'on'); else val = false; end end function ind = get.data_selection(this) ind=findDataSelection(this); 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/@MyGuiSync/MyGuiSync.m b/@MyGuiSync/MyGuiSync.m index 09094f3..6ed3668 100644 --- a/@MyGuiSync/MyGuiSync.m +++ b/@MyGuiSync/MyGuiSync.m @@ -1,799 +1,799 @@ % A mechanism to implement synchronization between parameters and GUI % elements in app-based GUIs classdef MyGuiSync < handle properties (GetAccess = public, SetAccess = protected) Listeners = struct( ... 'AppDeleted', [], ... 'KernelDeleted', [] ... ) % 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 + % Deletion of kernel object triggers the deletion 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 % Remove an object from the cleanup list. The main usage of this % function is to provide a way to close GUI without deleting % the kernel object function removeFromCleanup(this, Obj) ind = cellfun(@(x)isequal(x, Obj), this.cleanup_list); this.cleanup_list(ind) = []; 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.KernelDeleted = 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 app by closing its figure closeApp(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/@MyScpiInstrument/MyScpiInstrument.m b/@MyScpiInstrument/MyScpiInstrument.m index 5d8875c..8bb803c 100644 --- a/@MyScpiInstrument/MyScpiInstrument.m +++ b/@MyScpiInstrument/MyScpiInstrument.m @@ -1,372 +1,372 @@ % Class featuring a specialized framework for instruments supporting SCPI % % Undefined/dummy methods: % queryString(this, cmd) % writeString(this, cmd) classdef MyScpiInstrument < MyInstrument methods (Access = public) % 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 % Extract format symbol smb = parseFormat(this, format); end 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; % 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 = createReadFcn(this, read_command, format); sub_varargin = [sub_varargin, {'readFcn', readFcn}]; else read_command = ''; end 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 = createWriteFcn(this, write_command); sub_varargin = [sub_varargin, {'writeFcn', writeFcn}]; else write_command = ''; end this.CommandList.(tag).write_command = write_command; % 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 % Assign validation function based on the value format if isempty(validationFcn) validationFcn = createArrayValidationFcn(this, smb); end 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 % 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); read_cns = cns(ind_r); % List of names of readable commands read_commands = cellfun(... @(x) this.CommandList.(x).read_command, read_cns,... 'UniformOutput', false); res_list = queryStrings(this, read_commands{:}); if length(read_cns) == length(res_list) % Assign outputs to the class properties 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(['Could not read %i out of %i parameters, ',... 'no properties of %s object are updated.'], ... length(read_commands)-length(res_list), ... length(read_commands), class(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. % 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 % 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 % 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 % 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) % Standardization is applicable to char-valued properties % which have value list if isempty(value_list) || ~ischar(val) std_val = val; return end % 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 % 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 % Create a function that writes the value of a command % to the instrument function fcn = createWriteFcn(this, command) function writeFcn(x) - writeString(this, sprintf(command, x)) + writeString(this, sprintf(command, x)); end fcn = @writeFcn; end % Create a function that reads the value of a command and % interprets it according to the format function fcn = createReadFcn(this, command, format) function val = readFcn() val = sscanf(queryString(this, command), format); end fcn = @readFcn; end % 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 % 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 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 function validateLogical(val) assert((length(val) == length(smb)) && ... all(val==1 | val==0), ['Value must be a logical ' ... 'array of length ' length(smb) '.']) end function valudateCharacterString(val) assert(ischar(val), 'Value must be a character string.'); end % 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 end 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 end diff --git a/Fit classes/@MyFitParamScaling/MyFitParamScaling.m b/Fit classes/@MyFitParamScaling/MyFitParamScaling.m index ebab51d..a4f87f3 100644 --- a/Fit classes/@MyFitParamScaling/MyFitParamScaling.m +++ b/Fit classes/@MyFitParamScaling/MyFitParamScaling.m @@ -1,50 +1,66 @@ % 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 mean_x = mean(x); std_x = std(x); scaled_x = (x - mean_x)/std_x; mean_y = mean(y); std_y = std(y); scaled_y = (y - mean_y)/std_y; % Vector of 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 + + %Scales the confidence intervals before saving + function ci=getConfInt(this, interval) + ind=this.data_selection; + x=this.Data.x(ind); + y=this.Data.y(ind); + mean_y = mean(y); + std_y = std(y); + mean_x = mean(x); + std_x = std(x); + xy_zscore={mean_x,std_x,mean_y,std_y}; + sc_ci=confint(this.FitResult,interval); + ci=zeros(size(sc_ci)); + ci(1,:)=unscaleFitParams(this,sc_ci(1,:), xy_zscore); + ci(2,:)=unscaleFitParams(this,sc_ci(2,:), xy_zscore); + end end methods (Access = protected, Abstract) % Functions that define scaling and unscaling of the fit parameters % by zscore, the zscore is a vector {mean_x, std_x, mean_y, std_y} sc_vals = scaleFitParams(~, vals, xy_zscore) vals = unscaleFitParams(~, sc_vals, xy_zscore) end end diff --git a/Fit classes/@MyOpticalLorentzianFit/MyOpticalLorentzianFit.m b/Fit classes/@MyOpticalLorentzianFit/MyOpticalLorentzianFit.m index 3d37a1b..f6dca92 100644 --- a/Fit classes/@MyOpticalLorentzianFit/MyOpticalLorentzianFit.m +++ b/Fit classes/@MyOpticalLorentzianFit/MyOpticalLorentzianFit.m @@ -1,91 +1,135 @@ % 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'); + addUserParam(this, 'eta_oc', ... + 'title', '\eta overcoupled', ... + 'editable', 'off'); + addUserParam(this, 'eta_uc', ... + 'title', '\eta undercoupled', ... + '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; + a = this.param_vals(1); + d = this.param_vals(4); + R_min = 1 + 2*a/pi/raw_lw/d; + this.eta_oc = (1 + sqrt(R_min))/2; + this.eta_uc = (1 - sqrt(R_min))/2; + end + + function acceptFitCallback(this, ~, ~) + 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 + ScaledData = MyTrace; + ScaledData.x = (this.Data.x -this.param_vals(3)) * ... + this.line_spacing*this.line_no/ref_spacing/1e3; + ScaledData.y = this.Data.y; + ScaledData.name_x = 'Detuning'; + ScaledData.name_y = this.Data.name_y; + ScaledData.unit_x = 'GHz'; + ScaledData.unit_y = this.Data.unit_y; + + ScaledFit = MyTrace; + ScaledFit.x = (this.Fit.x - this.param_vals(3)) * ... + this.line_spacing*this.line_no/ref_spacing/1e3; + ScaledFit.y = this.Fit.y; + ScaledFit.name_x = 'Detuning'; + ScaledFit.name_y = this.Fit.name_y; + ScaledFit.unit_x = 'GHz'; + ScaledFit.unit_y = this.Fit.unit_y; + triggerNewProcessedData(this, 'traces', {copy(this.Fit),ScaledFit, ScaledData}, ... + 'trace_tags', {'_fit','_fit_scaled','_scaled'}); end end end diff --git a/Fit classes/@MySpringShiftFit/MySpringShiftFit.m b/Fit classes/@MySpringShiftFit/MySpringShiftFit.m new file mode 100644 index 0000000..7b56954 --- /dev/null +++ b/Fit classes/@MySpringShiftFit/MySpringShiftFit.m @@ -0,0 +1,321 @@ +classdef MySpringShiftFit < MyFitParamScaling + properties (GetAccess = public, SetAccess = protected) + RefCursors MyCursor + SpikeCursors MyCursor + + end + methods (Access = public) + function this = MySpringShiftFit(varargin) + this@MyFitParamScaling( ... + 'fit_name', 'Optomechanical spring shift', ... + 'fit_function', 'e*4*(x-c)*b/2/((x-c)^2+(b/2)^2)^2 + 1/pi*a*b/2/((x-c)^2+(b/2)^2)+d', ... + 'fit_tex', '$$e\frac{4(x-c)b/2}{((x-c)^2+(b/2)^2)^2} + \frac{a}{\pi}\frac{b/2}{(x-c)^2+(b/2)^2}+d$$', ... + 'fit_params', {'a','b','c','d','e'}, ... + 'fit_param_names', {'Absorption amplitude','Width','Center','Offset', 'OM shift amplitude'}, ... + 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])]; + + x1_spike = xlim(1)+(xlim(2)-xlim(1))/3; + x2_spike = xlim(2)-(xlim(2)-xlim(1))/3; + + this.SpikeCursors = ... + [MyCursor(this.Axes, ... + 'orientation', 'vertical', ... + 'position', x1_spike, ... + 'Label','Spike removal 1', 'Color', [0, 0, 0.6]), ... + MyCursor(this.Axes, 'orientation', 'vertical', ... + 'position', x2_spike, ... + 'Label','Spike removal 2', 'Color', [0, 0, 0.6])]; + end + end + function delete(this) + if ~isempty(this.RefCursors) + delete(this.RefCursors); + end + if ~isempty(this.SpikeCursors) + delete(this.SpikeCursors); + 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; + + x1_spike = xlim(1)+(xlim(2)-xlim(1))/3; + x2_spike = xlim(2)-(xlim(2)-xlim(1))/3; + + this.SpikeCursors(1).value = x1_spike; + this.SpikeCursors(2).value = x2_spike; + end + end + end + + methods (Access = protected) + function ind=findDataSelection(this) + if this.enable_range_cursors + xmin = min(this.RangeCursors.value); + xmax = max(this.RangeCursors.value); + ind = (this.Data.x>xmin & this.Data.x<=xmax); + else + ind = true(1, length(this.Data.x)); + end + % Removing the spike on the resonance + if ~isempty(this.SpikeCursors) + x = this.Data.x(ind); + y = this.Data.y(ind); + % Get the reference spacing from the position of cursors + ind_spike = (x > min(this.SpikeCursors.value)) & ... + (x < max(this.SpikeCursors.value)); + y_spike = y(ind_spike); + Spike = max(abs(y_spike)); + IndSpike = find(abs(this.Data.y) == Spike); + ind(IndSpike) = 0; + end + + end + + function calcInitParams(this) + ind = this.data_selection; + + x = this.Data.x(ind); + y = this.Data.y(ind); + + this.lim_upper=[0,Inf,Inf,Inf,Inf]; + this.lim_lower=[-Inf,0,-Inf,-Inf,0]; + + rng_x = max(x)-min(x); + try + [max_val, max_loc, max_width, max_prom] = findpeaks(y, x,... + 'MinPeakDistance', rng_x/2, 'SortStr', 'descend',... + 'NPeaks', 1); + catch ME + warning(ME.message) + end + + % Finds peaks on the negative signal (max 1 peak) + try + [min_val, min_loc, min_width, min_prom] = findpeaks(-y, x,... + 'MinPeakDistance', rng_x/2, 'SortStr', 'descend',... + 'NPeaks', 1); + catch ME + warning(ME.message) + end + + if min_prom==0 && max_prom==0 + warning(['No peaks were found in the data, giving ' ... + 'default initial parameters to fit function']) + return + end + % Width + p_in(2) = abs(min_loc-max_loc)*sqrt(3); + + % OM Amplitude + p_in(5) = abs(max_val + min_val)*p_in(2)^2/6/sqrt(3); + + % Center + p_in(3) = (min_loc+max_loc)/2; + + % Offset + p_in(4) = mean(y); + + % Absorption amplitude +% p_in(1) = -abs(abs(max_val - p_in(4)) - abs(min_val - p_in(4)))*pi*p_in(2)/2; + p_in(1) = -abs(max_val - min_val)*pi*p_in(2)/2/p_in(4); + + + 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 + + function acceptFitCallback(this, ~, ~) + 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 + ind = this.data_selection; + ScaledData = MyTrace; + ScaledData.x = (this.Data.x(ind) -this.param_vals(3)) * ... + this.line_spacing*this.line_no/ref_spacing/1e3; + ScaledData.y = this.Data.y(ind); + ScaledData.name_x = 'Detuning'; + ScaledData.name_y = '$\delta\Omega_m$'; + ScaledData.unit_x = 'GHz'; + ScaledData.unit_y = 'Hz'; + + ScaledFit = MyTrace; + ScaledFit.x = (this.Fit.x - this.param_vals(3)) * ... + this.line_spacing*this.line_no/ref_spacing/1e3; + ScaledFit.y = this.Fit.y; + ScaledFit.name_x = 'Detuning'; + ScaledFit.name_y = '$\delta\Omega_m$'; + ScaledFit.unit_x = 'GHz'; + ScaledFit.unit_y = 'Hz'; + triggerNewProcessedData(this, 'traces', {copy(this.Fit),ScaledFit, ScaledData}, ... + 'trace_tags', {'_fit','_fit_scaled','_scaled'}); + 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, 'eta_c', ... + 'title', '\eta_c', ... + 'editable', 'on', ... + 'default', 0.5); + addUserParam(this, 'eta_r', ... + 'title', '\eta_r (P_{ref} / P_{in})', ... + 'editable', 'on', ... + 'default', 0.5); + addUserParam(this, 'P_in', ... + 'title', 'Input power (uW)', ... + 'editable', 'on', ... + 'default', 1); + addUserParam(this, 'WL', ... + 'title', 'Wavelength (nm)', ... + 'editable', 'on', ... + 'default', 1550); + addUserParam(this, 'f_m', ... + 'title', 'f_m (MHz)', ... + 'editable', 'on', ... + 'default', 1); + addUserParam(this, 'lw_m', ... + 'title', '\Gamma_m/2\pi (Hz)', ... + 'editable', 'on', ... + 'default', 1); + addUserParam(this, 'T', ... + 'title', 'T (K)', ... + 'editable', 'on', ... + 'default', 300); + addUserParam(this, 'g0', ... + 'title', 'g_0 (kHz)', ... + 'editable', 'off'); + addUserParam(this, 'lw', ... + 'title', 'Linewidth (GHz)', ... + 'editable', 'off'); + addUserParam(this, 'C0', ... + 'title', 'C_0', ... + 'editable', 'off'); + addUserParam(this, 'Cq', ... + 'title', 'C_q', ... + 'editable', 'off'); + end + + function calcUserParams(this) + + 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 + % scaling from seconds to MHz + + scale_factor = this.line_spacing*this.line_no/ref_spacing; + raw_lw = this.param_vals(2); + this.lw = raw_lw*scale_factor/1e3; + + f0 = this.eta_c * (sqrt(this.eta_r)*this.P_in*1e-6) /... + (6.62607004e-34*physconst('LightSpeed') / (this.WL*1e-9))/... + (2*pi); + this.g0 = sqrt(this.param_vals(5) / f0) * scale_factor* 1e3; + this.C0 = 4*(this.g0*1e3)^2 / (this.lw*1e9) / this.lw_m; + + this.Cq = (4 * this.eta_c * sqrt(this.eta_r)*(this.P_in*1e-6)*(this.f_m*1e6))/... + (this.lw*2*pi*1e9 * 1.38e-23 * this.T * physconst('LightSpeed') / (this.WL*1e-9)); + + 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; + sc_vals(5)=vals(5) / std_y / std_x^2; + 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; + vals(5)=sc_vals(5) * std_y * std_x^2; + end + end + +end \ No newline at end of file diff --git a/GUIs/GuiNewpTlb.mlapp b/GUIs/GuiNewpTlb.mlapp index 6c09714..de2f29f 100644 Binary files a/GUIs/GuiNewpTlb.mlapp and b/GUIs/GuiNewpTlb.mlapp differ diff --git a/GUIs/GuiRohdeFsw.mlapp b/GUIs/GuiRohdeFsw.mlapp new file mode 100644 index 0000000..d90f0f0 Binary files /dev/null and b/GUIs/GuiRohdeFsw.mlapp differ diff --git a/GUIs/GuiTekMso.mlapp b/GUIs/GuiTekMso.mlapp new file mode 100644 index 0000000..af3f61f Binary files /dev/null and b/GUIs/GuiTekMso.mlapp differ diff --git a/GUIs/GuiTekMso46.mlapp b/GUIs/GuiTekMso46.mlapp new file mode 100644 index 0000000..0f605f8 Binary files /dev/null and b/GUIs/GuiTekMso46.mlapp differ diff --git a/GUIs/GuiTekMso_bu.mlapp b/GUIs/GuiTekMso_bu.mlapp new file mode 100644 index 0000000..98c2f41 Binary files /dev/null and b/GUIs/GuiTekMso_bu.mlapp differ diff --git a/GUIs/GuiTekRsaFvt.mlapp b/GUIs/GuiTekRsaFvt.mlapp new file mode 100644 index 0000000..fec7160 Binary files /dev/null and b/GUIs/GuiTekRsaFvt.mlapp differ diff --git a/Instrument classes/@MyNewpTlb6300/MyNewpTlb6300.m b/Instrument classes/@MyNewpTlb6300/MyNewpTlb6300.m index def90e9..fff08c2 100644 --- a/Instrument classes/@MyNewpTlb6300/MyNewpTlb6300.m +++ b/Instrument classes/@MyNewpTlb6300/MyNewpTlb6300.m @@ -1,102 +1,102 @@ % Class for communication with NewFocus TLB6300 tunable laser controllers classdef MyNewpTlb6300 < MyScpiInstrument & MyCommCont & MyGuiCont 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 % 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) % Output wavelength, nm addCommand(this, 'wavelength',':SENS:WAVE',... 'access','r','default',780,'format','%e',... 'info','Output wavelength (nm)'); % Diode current, mA addCommand(this, 'current',':SENS:CURR:DIOD',... '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,'format','%e',... 'info','Diode temperature (C)'); % Output power, mW addCommand(this, 'power',':SENS:POW:LEV:FRON',... 'access','r','default',1,'format','%e',... 'info','Output power (mW)'); % Wavelength setpoint, nm addCommand(this, 'wavelength_sp',':WAVE',... 'default',780,'format','%e',... 'info','Wavelength setpoint (nm)'); % Constant power mode on/off addCommand(this, 'const_power',':CPOW',... 'access','w','default',true,'format','%b',... 'info','Constant power mode on/off'); % Power setpoint, mW addCommand(this, 'power_sp',':POW',... 'access','w','default',10,'format','%e',... 'info','Power setpoint (mW)'); % Current setpoint, mW addCommand(this, 'current_sp',':CURR',... 'default',100,'format','%e',... 'info','Current setpoint (mA)'); % Control mode local/remote addCommand(this, 'control_mode',':SYST:MCON',... - 'access','w','val_list',{'EXT','INT'},... + 'access','w','value_list',{'EXT','INT','LOC','REM'},... 'default','LOC','format','%s',... 'info','Control local(EXT)/remote(INT)'); % Output on/off addCommand(this, 'enable_output',':OUTP',... 'default',false,'format','%b',... 'info','on/off'); % Wavelength track is not fully remotely controllable with % TLB6300 end end methods (Access = public) function setMaxOutPower(this) % Depending on if the laser in the constat power or current % mode, set value to max if this.const_power % Actual power is clipped to max practical value writeString(this, ':POW 99'); else % Maximum current according to specs is 152 mA writeString(this, ':CURR 150'); end end end end diff --git a/Instrument classes/@MyTekRsa/MyTekRsa.m b/Instrument classes/@MyRohdeFsw/MyRohdeFsw.m similarity index 63% copy from Instrument classes/@MyTekRsa/MyTekRsa.m copy to Instrument classes/@MyRohdeFsw/MyRohdeFsw.m index 6bca004..6ac0e9e 100644 --- a/Instrument classes/@MyTekRsa/MyTekRsa.m +++ b/Instrument classes/@MyRohdeFsw/MyRohdeFsw.m @@ -1,200 +1,206 @@ -% Class for controlling Tektronix RSA5103 and RSA5106 spectrum analyzers +% Class for controlling Rohde & Schwarcz FSW43 spectrum analyzers -classdef MyTekRsa < MyScpiInstrument & MyDataSource & MyCommCont ... +classdef MyRohdeFsw < 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) + function this = MyRohdeFsw(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); + this.Comm.Timeout = 20; %VISA timeout set to 20s, + % From experiences 20s should be + % the longest time needed to complete + % a single averaging measurement. + % Set up the list of communication commands createCommandList(this); end - - function str = idn(this) - str = idn@MyInstrument(this); - - % The instrument needs to be in DPX Spectrum mode - res = queryString(this, ':DISPlay:WINDow:ACTive:MEASurement?'); - assert(contains(lower(res), {'dpsa', 'dpx'}), ... - 'The spectrum analyzer must be in DPX Spectrum mode.'); - 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', ... + addCommand(this, 'rbw', 'SENSe:BANDwidth:RESolution', ... 'format', '%e', ... 'info', 'Nominal resolution bandwidth (Hz)'); - addCommand(this, 'rbw_act', ':DPX:BANDwidth:ACTual', ... + addCommand(this, 'bw_ratio', 'SENSe:BANDwidth:RESolution:RATio', ... 'format', '%e', ... - 'access', 'r', ... - 'info', 'Actual resolution bandwidth (Hz)'); + 'info', 'rbw / span'); - addCommand(this, 'auto_rbw', ':DPX:BAND:RES:AUTO', ... + addCommand(this, 'auto_rbw', 'SENSe:BANDwidth:RESolution:AUTO', ... 'format', '%b'); - addCommand(this, 'span', ':DPX:FREQ:SPAN', ... + addCommand(this, 'span', 'SENSe:FREQuency:SPAN', ... 'format', '%e', ... 'info', '(Hz)'); - addCommand(this, 'start_freq', ':DPX:FREQ:STAR',... + addCommand(this, 'start_freq', 'SENSe:FREQuency:STARt',... 'format', '%e', ... 'info', '(Hz)'); - addCommand(this, 'stop_freq', ':DPX:FREQ:STOP',... + addCommand(this, 'stop_freq', 'SENSe:FREQuency:STOP',... 'format', '%e', ... 'info', '(Hz)'); - addCommand(this, 'cent_freq', ':DPX:FREQ:CENT',... + addCommand(this, 'cent_freq', 'SENSe:FREQuency:CENTer',... 'format', '%e', ... 'info', '(Hz)'); % Continuous triggering - addCommand(this, 'init_cont', ':INIT:CONT', ... + 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}); + addCommand(this, 'point_no', 'SENSe:SWEep:POINts', ... + 'format', '%i'); + % Reference level (dB) - addCommand(this, 'ref_level',':INPut:RLEVel', ... + addCommand(this, 'ref_level','DISP:TRAC:Y:RLEV', ... 'format', '%e',... 'info', '(dB)'); % Display scale per division (dBm/div) - addCommand(this, 'disp_y_scale', ':DISPlay:DPX:Y:PDIVision',... + addCommand(this, 'disp_y_scale', 'DISP:TRAC:Y',... 'format', '%e', ... - 'info', '(dBm/div)'); + 'info', '(dB)'); % Display vertical offset (dBm) - addCommand(this, 'disp_y_offset', ':DISPLAY:DPX:Y:OFFSET', ... + addCommand(this, 'disp_y_offset', 'DISP:TRAC:Y:RLEV:OFFS', ... 'format', '%e', ... 'info', '(dBm)'); % Parametric commands - for i = 1:3 + for i = 1:6 i_str = num2str(i); % Display trace addCommand(this, ['disp_trace',i_str], ... - [':TRAC',i_str,':DPX'], ... + ['DISP:TRAC',i_str], ... 'format', '%b', ... 'info', 'on/off'); % Trace Detection addCommand(this, ['det_trace',i_str],... - [':TRAC',i_str,':DPX:DETection'],... + ['DET',i_str],... 'format', '%s', ... - 'value_list', {'AVERage', 'NEGative', 'POSitive'}); + 'value_list', {'APE','AVER', 'NEG', 'POS'}); % Trace Function addCommand(this, ['func_trace',i_str], ... - [':TRAC',i_str,':DPX:FUNCtion'], ... + ['DISP:TRAC',i_str,':MODE'], ... 'format', '%s', ... - 'value_list', {'AVERage', 'HOLD', 'NORMal'}); + 'value_list', {'AVER', 'VIEW', 'WRIT'}); % Number of averages addCommand(this, ['average_no',i_str], ... - [':TRAC',i_str,':DPX:AVER:COUN'], ... + ['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)); + writeString(this,'FORM:DATA REAL,32');%Set to binary data format + writeString(this, sprintf('TRAC:DATA? 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.Trace.y = (10.^(data/10))/this.rbw*50*0.001; this.acq_trace = n_trace; % Trigger acquired data event triggerNewData(this); end % Abort data acquisition function abortAcq(this) - writeString(this, ':ABORt'); + writeString(this, 'ABORt'); end % Initiate data acquisition function initAcq(this) - writeString(this, ':INIT'); + 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 + + % Appantly, this device sometimemes fails if it receives very long + % commands, so query them one by one + % 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 function set.acq_trace(this, val) - assert((val==1 || val==2 || val==3), ... - 'Acquisition trace number must be 1, 2 or 3.'); + assert((val==1 || val==2 || val==3 || val==4 || val==5 || val==6), ... + 'Acquisition trace number must be 1, 2, 3, 4, 5, or 6.'); this.acq_trace = val; end end end diff --git a/Instrument classes/@MyTekMso/MyTekMso.m b/Instrument classes/@MyTekMso/MyTekMso.m new file mode 100644 index 0000000..bb5d035 --- /dev/null +++ b/Instrument classes/@MyTekMso/MyTekMso.m @@ -0,0 +1,256 @@ +% Class for controlling 4-channel Tektronix MSO scopes. +classdef MyTekMso < MyTekScope + + methods (Access = public) + function this = MyTekMso(varargin) + this.gui_name = 'GuiTekMso'; + 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}); + + %%%% Not implemented in UI. What's selected on display + %%%% is not relevant. + addCommand(this, 'ctrl_channel', ':DISplay:SELect:SOUrce', ... + 'format', 'CH%i', ... + 'info', ['Channel currently selected in ' ... + 'the scope display'], ... + 'value_list', {1, 2, 3, 4}); + + %Changed to any number input + addCommand(this, 'point_no', ':HORizontal:RECOrdlength', ... + 'format', '%i', ... + 'info', 'Numbers of points in the scope trace (1k-62.5M)'); + + addCommand(this, 'time_scale', ':HORizontal:SCAle', ... + 'format', '%e', ... + 'info', 'Time scale (s/div)'); + + %Added EITHER option + addCommand(this, 'trig_slope', ':TRIGger:A:EDGE:SLOpe', ... + 'format', '%s', ... + 'value_list', {'RISe','FALL','EITHER'}); + + %Deleted EXT, AUX, does not seem to support on MSO + addCommand(this, 'trig_source', ':TRIGger:A:EDGE:SOUrce', ... + 'format', '%s', ... + 'value_list', {'CH1','CH2','CH3','CH4','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'}); + + addCommand(this, 'avg_point_no', ':ACQuire:NUMAVg', ... + 'format', '%i', ... + 'info', 'Numbers of averages (2-10240)'); + + % Spectrum view + addCommand(this, 'span', ':SV:SPAN', ... + 'format', '%e', ... + 'info', '(Hz)'); + + addCommand(this, 'rbw', ':SV:RBW', ... + 'format', '%e', ... + 'info', '(Hz))'); + + addCommand(this, 'rbwratio', ':SV:SPANRBWRatio', ... + 'format', '%e', ... + 'info', 'span / rbw'); + + addCommand(this, 'auto_rbw', ':SV:RBWMode', ... + 'format', '%s',... + 'info', 'Adjust Span|RBW based on Ratio',... + 'value_list', {'AUTOMATIC','MANUAL'}); + + % Parametric commands + for i = 1:this.channel_no + i_str = num2str(i); + + %%%% removed GND, not supported by MSO + addCommand(this,... + ['cpl',i_str],[':CH',i_str,':COUP'], ... + 'format', '%s', ... + 'info', 'Channel coupling: AC or DC', ... + 'value_list', {'DC','AC'}); + + % impedances, 1MOhm or 50 Ohm, changed string value list + addCommand(this,... + ['imp', i_str], [':CH', i_str, ':TERmination'],... + 'format', '%s', ... + 'info', 'Channel impedance: 1 MOhm or 50 Ohm', ... + 'value_list', {'1.0000E+6', '50.0000'}); + + % Offset Max/Min +-10V + addCommand(this, ... + ['offset',i_str], [':CH',i_str,':OFFSet'], ... + 'format', '%e', ... + 'info', '(V)'); + % Scale Max 10V/div, Min 0.0005V/div + 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'); + + %%%% MSO can specify the trigger level to any channel + addCommand(this,... + ['trig_lev',i_str], [':TRIGger:A:LEVel:CH',i_str],... + 'format', '%e', ... + 'info', '(V)'); + + addCommand(this,... + ['start_freq',i_str],[':CH',i_str,':SV:STARTFrequency'],... + 'format', '%e', ... + 'info', '(Hz)'); + + addCommand(this,... + ['stop_freq',i_str],[':CH',i_str,':SV:STOPFrequency'],... + 'format', '%e', ... + 'info', '(Hz)'); + + addCommand(this,... + ['center_freq',i_str],[':CH',i_str,':SV:CENTERFrequency'],... + 'format', '%e', ... + 'info', '(Hz)'); + + addCommand(this,... + ['sv_state',i_str],[':CH',i_str,':SV:STATE'],... + 'format', '%b', ... + 'info', 'Set SV on | off'); + + addCommand(this,... + ['sv_norm',i_str],[':SV:CH',i_str,':SELect:RF_NORMal'],... + 'format', '%b', ... + 'info', 'Set SV_normal on | off'); + + addCommand(this,... + ['sv_avg',i_str],[':SV:CH',i_str,':SELect:RF_AVErage'],... + 'format', '%b', ... + 'info', 'Set SV__average on | off'); + + addCommand(this,... + ['sv_avg_no',i_str],[':SV:CH',i_str,':RF_AVErage:NUMAVg'],... + 'format', '%i', ... + 'info', 'Number of average for SV (2~2^9)'); + end + end + end + + methods (Access = public) + %%%% spectrum data aquisition + function readTrace_SV(this,arg1,arg2) + % Read units, offsets and steps for the scales + % Moved the parm query before the data aquisition + % because it seems that MSO has a problem responding + % to query after data aquisition + + % set the aquisition source to spectrum trace + writeStrings(this,... + sprintf(':DATa:SOUrce CH%i_SV_%s',arg2,arg1)); + parms = queryStrings(this, ... + ':WFMOutpre:XUNit?', ... + ':WFMOutpre:YUNit?', ... + ':WFMOutpre:XINcr?', ... + ':WFMOutpre:YMUlt?', ... + ':WFMOutpre:XZEro?', ... + ':WFMOutpre:YZEro?', ... + ':WFMOutpre:YOFf?'); + % Read raw y data + + % Configure data transfer: binary format and two bytes per + % point. Then query the trace. + this.Comm.ByteOrder = 'bigEndian'; + SV_NP=str2num(cell2mat(queryStrings(this,... + 'WFMOutpre:NR_Pt?'))); + + % I don't know what's the correct setting + writeStrings(this, ... + ':DATA:ENCDG ASCIi', ... + ':DATA:WIDTH 2', ... + ':DATA:STARt 1', ... + sprintf(':DATA:STOP %i', SV_NP),... + ':CURVe?'); + + pause(0.01) + writeStrings(this, ':CURVe?'); + + y_data=str2num(fscanf(this.Comm)); + %%%% the binary data does not work for SV on MSO + %y_data = double(binblockread(this.Comm, 'int16')); + + + % read off the terminating character + % which can not be read by the binblockread + if this.Comm.BytesAvailable == 1 || this.Comm.BytesAvailable == 2 + fread(this.Comm,this.Comm.BytesAvailable,'uint8'); + end + + % For some reason MDO3000 scope needs to have an explicit pause + % between data reading and any other communication + %pause(0.01); + + 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); + writeStrings(this,... + sprintf(':DATa:SOUrce CH%i',arg2)); + this.Trace.name_x = 'Frequency'; + this.Trace.name_y = 'PSD'; + triggerNewData(this); + this.Trace.name_x = 'Time'; + this.Trace.name_y = 'Voltage'; + end + end +end \ No newline at end of file diff --git a/Instrument classes/@MyTekMso46/MyTekMso46.m b/Instrument classes/@MyTekMso46/MyTekMso46.m new file mode 100644 index 0000000..beef0a6 --- /dev/null +++ b/Instrument classes/@MyTekMso46/MyTekMso46.m @@ -0,0 +1,256 @@ +% Class for controlling 6-channel Tektronix MSO scopes. +classdef MyTekMso46 < MyTekScope6 + + methods (Access = public) + function this = MyTekMso46(varargin) + this.gui_name = 'GuiTekMso46'; + 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, 5, 6}); + + %%%% Not implemented in UI. What's selected on display + %%%% is not relevant. + addCommand(this, 'ctrl_channel', ':DISplay:SELect:SOUrce', ... + 'format', 'CH%i', ... + 'info', ['Channel currently selected in ' ... + 'the scope display'], ... + 'value_list', {1, 2, 3, 4, 5, 6}); + + %Changed to any number input + addCommand(this, 'point_no', ':HORizontal:RECOrdlength', ... + 'format', '%i', ... + 'info', 'Numbers of points in the scope trace (1k-62.5M)'); + + addCommand(this, 'time_scale', ':HORizontal:SCAle', ... + 'format', '%e', ... + 'info', 'Time scale (s/div)'); + + %Added EITHER option + addCommand(this, 'trig_slope', ':TRIGger:A:EDGE:SLOpe', ... + 'format', '%s', ... + 'value_list', {'RISe','FALL','EITHER'}); + + %Deleted EXT, AUX, does not seem to support on MSO + addCommand(this, 'trig_source', ':TRIGger:A:EDGE:SOUrce', ... + 'format', '%s', ... + 'value_list', {'CH1','CH2','CH3','CH4','CH5','CH6','LINE','AUXiliary'}); + + % + 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'}); + + addCommand(this, 'avg_point_no', ':ACQuire:NUMAVg', ... + 'format', '%i', ... + 'info', 'Numbers of averages (2-10240)'); + + % Spectrum view + addCommand(this, 'span', ':SV:SPAN', ... + 'format', '%e', ... + 'info', '(Hz)'); + + addCommand(this, 'rbw', ':SV:RBW', ... + 'format', '%e', ... + 'info', '(Hz))'); + + addCommand(this, 'rbwratio', ':SV:SPANRBWRatio', ... + 'format', '%e', ... + 'info', 'span / rbw'); + + addCommand(this, 'auto_rbw', ':SV:RBWMode', ... + 'format', '%s',... + 'info', 'Adjust Span|RBW based on Ratio',... + 'value_list', {'AUTOMATIC','MANUAL'}); + + % Parametric commands + for i = 1:this.channel_no + i_str = num2str(i); + + %%%% removed GND, not supported by MSO + addCommand(this,... + ['cpl',i_str],[':CH',i_str,':COUP'], ... + 'format', '%s', ... + 'info', 'Channel coupling: AC or DC', ... + 'value_list', {'DC','AC'}); + + % impedances, 1MOhm or 50 Ohm, changed string value list + addCommand(this,... + ['imp', i_str], [':CH', i_str, ':TERmination'],... + 'format', '%s', ... + 'info', 'Channel impedance: 1 MOhm or 50 Ohm', ... + 'value_list', {'1.0000E+6', '50.0000'}); + + % Offset Max/Min +-10V + addCommand(this, ... + ['offset',i_str], [':CH',i_str,':OFFSet'], ... + 'format', '%e', ... + 'info', '(V)'); + % Scale Max 10V/div, Min 0.0005V/div + 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'); + + %%%% MSO can specify the trigger level to any channel + addCommand(this,... + ['trig_lev',i_str], [':TRIGger:A:LEVel:CH',i_str],... + 'format', '%e', ... + 'info', '(V)'); + + addCommand(this,... + ['start_freq',i_str],[':CH',i_str,':SV:STARTFrequency'],... + 'format', '%e', ... + 'info', '(Hz)'); + + addCommand(this,... + ['stop_freq',i_str],[':CH',i_str,':SV:STOPFrequency'],... + 'format', '%e', ... + 'info', '(Hz)'); + + addCommand(this,... + ['center_freq',i_str],[':CH',i_str,':SV:CENTERFrequency'],... + 'format', '%e', ... + 'info', '(Hz)'); + + addCommand(this,... + ['sv_state',i_str],[':CH',i_str,':SV:STATE'],... + 'format', '%b', ... + 'info', 'Set SV on | off'); + + addCommand(this,... + ['sv_norm',i_str],[':SV:CH',i_str,':SELect:RF_NORMal'],... + 'format', '%b', ... + 'info', 'Set SV_normal on | off'); + + addCommand(this,... + ['sv_avg',i_str],[':SV:CH',i_str,':SELect:RF_AVErage'],... + 'format', '%b', ... + 'info', 'Set SV__average on | off'); + + addCommand(this,... + ['sv_avg_no',i_str],[':SV:CH',i_str,':RF_AVErage:NUMAVg'],... + 'format', '%i', ... + 'info', 'Number of average for SV (2~2^9)'); + end + end + end + + methods (Access = public) + %%%% spectrum data aquisition + function readTrace_SV(this,arg1,arg2) + % Read units, offsets and steps for the scales + % Moved the parm query before the data aquisition + % because it seems that MSO has a problem responding + % to query after data aquisition + + % set the aquisition source to spectrum trace + writeStrings(this,... + sprintf(':DATa:SOUrce CH%i_SV_%s',arg2,arg1)); + parms = queryStrings(this, ... + ':WFMOutpre:XUNit?', ... + ':WFMOutpre:YUNit?', ... + ':WFMOutpre:XINcr?', ... + ':WFMOutpre:YMUlt?', ... + ':WFMOutpre:XZEro?', ... + ':WFMOutpre:YZEro?', ... + ':WFMOutpre:YOFf?'); + % Read raw y data + + % Configure data transfer: binary format and two bytes per + % point. Then query the trace. + this.Comm.ByteOrder = 'bigEndian'; + SV_NP=str2num(cell2mat(queryStrings(this,... + 'WFMOutpre:NR_Pt?'))); + + % I don't know what's the correct setting + writeStrings(this, ... + ':DATA:ENCDG ASCIi', ... + ':DATA:WIDTH 2', ... + ':DATA:STARt 1', ... + sprintf(':DATA:STOP %i', SV_NP),... + ':CURVe?'); + + pause(0.01) + writeStrings(this, ':CURVe?'); + + y_data=str2num(fscanf(this.Comm)); + %%%% the binary data does not work for SV on MSO + %y_data = double(binblockread(this.Comm, 'int16')); + + + % read off the terminating character + % which can not be read by the binblockread + if this.Comm.BytesAvailable == 1 || this.Comm.BytesAvailable == 2 + fread(this.Comm,this.Comm.BytesAvailable,'uint8'); + end + + % For some reason MDO3000 scope needs to have an explicit pause + % between data reading and any other communication + %pause(0.01); + + 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); + writeStrings(this,... + sprintf(':DATa:SOUrce CH%i',arg2)); + this.Trace.name_x = 'Frequency'; + this.Trace.name_y = 'PSD'; + triggerNewData(this); + this.Trace.name_x = 'Time'; + this.Trace.name_y = 'Voltage'; + end + end +end \ No newline at end of file diff --git a/Instrument classes/@MyTekRsa/MyTekRsa.m b/Instrument classes/@MyTekRsa/MyTekRsa.m index 6bca004..72585d1 100644 --- a/Instrument classes/@MyTekRsa/MyTekRsa.m +++ b/Instrument classes/@MyTekRsa/MyTekRsa.m @@ -1,200 +1,200 @@ % 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 function str = idn(this) str = idn@MyInstrument(this); % The instrument needs to be in DPX Spectrum mode res = queryString(this, ':DISPlay:WINDow:ACTive:MEASurement?'); assert(contains(lower(res), {'dpsa', 'dpx'}), ... 'The spectrum analyzer must be in DPX Spectrum mode.'); 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/@MyTekRsaFvt/MyTekRsaFvt.m b/Instrument classes/@MyTekRsaFvt/MyTekRsaFvt.m new file mode 100644 index 0000000..4e8ea1d --- /dev/null +++ b/Instrument classes/@MyTekRsaFvt/MyTekRsaFvt.m @@ -0,0 +1,112 @@ +% Class for controlling Tektronix RSA5103 and RSA5106 spectrum analyzers + +classdef MyTekRsaFvt < MyScpiInstrument & MyDataSource & MyCommCont ... + & MyGuiCont + + methods (Access = public) + function this = MyTekRsaFvt(varargin) + P = MyClassParser(this); + processInputs(P, this, varargin{:}); + + this.Trace.unit_x = 's'; + this.Trace.unit_y = 'Hz'; + this.Trace.name_y = 'Frequency'; + this.Trace.name_x = 'Time'; + + % Create communication object + connect(this); + + % Set up the list of communication commands + createCommandList(this); + end + + function str = idn(this) + str = idn@MyInstrument(this); + + % The instrument needs to be in DPX Spectrum mode + res = queryString(this, ':DISPlay:WINDow:ACTive:MEASurement?'); + assert(contains(lower(res), {'dpsa', 'dpx'}), ... + 'The spectrum analyzer must be in DPX Spectrum mode.'); + end + end + + methods (Access = protected) + function createCommandList(this) + % Frequency vs time measurement frequency (Hz) + addCommand(this, 'measuremnt_freq', ':MEAS:FREQ',... + 'format', '%e', ... + 'info', '(Hz)'); + + % Frequency vs time measurement bandwidth (Hz) + addCommand(this, 'measurement_BW', ':FVT:FREQ:SPAN',... + 'format', '%e', ... + 'info', '(Hz)'); + + % Frequency vs time display x extent (s) + addCommand(this, 'disp_x_length', ':ANAL:LENG',... + 'format', '%e', ... + 'info', '(s)'); + + % Frequency vs time display x offset (s) + addCommand(this, 'disp_x_start', ':ANAL:STAR',... + 'format', '%e', ... + 'info', '(s)'); + + % Frequency vs time display offset (Hz) + addCommand(this, 'disp_y_offset', ':DISP:FVT:Y:OFFS',... + 'format', '%e', ... + 'info', '(Hz)'); + + % Frequency vs time display scale (extent) (Hz) + addCommand(this, 'disp_y_scale', ':DISP:FVT:Y',... + 'format', '%e', ... + 'info', '(Hz)'); + end + end + + + methods (Access = public) + function readTrace(this, varargin) + % 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, 'fetch:fvt?'); + data = binblockread(this.Comm, 'float'); + + % Calculate the time axis + this.Trace.x = linspace(this.disp_x_start, ... + this.disp_x_start + this.disp_x_length, length(data)); + + this.Trace.y = data; + + % 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); + + end + end + +end diff --git a/Instrument classes/@MyTekScope/MyTekScope.m b/Instrument classes/@MyTekScope/MyTekScope.m index 3a1a1b2..091421f 100644 --- a/Instrument classes/@MyTekScope/MyTekScope.m +++ b/Instrument classes/@MyTekScope/MyTekScope.m @@ -1,110 +1,116 @@ % 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 + % Moved the parm query before the data aquisition + % because it seems that MSO has a problem responding + % to query after data aquisition parms = queryStrings(this, ... ':WFMOutpre:XUNit?', ... ':WFMOutpre:YUNit?', ... ':WFMOutpre:XINcr?', ... ':WFMOutpre:YMUlt?', ... ':WFMOutpre:XZEro?', ... ':WFMOutpre:YZEro?', ... ':WFMOutpre:YOFf?'); - + % Read raw y data + y_data = readY(this); 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?'); + ':CURVe?'); y_data = double(binblockread(this.Comm, 'int16')); + % read off the terminating character + % which can not be read by the binblockread + if this.Comm.BytesAvailable == 1 || this.Comm.BytesAvailable == 2 + fread(this.Comm,this.Comm.BytesAvailable,'uint8'); + end + % 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/@MyTekScope/MyTekScope.m b/Instrument classes/@MyTekScope6/MyTekScope6.m similarity index 84% copy from Instrument classes/@MyTekScope/MyTekScope.m copy to Instrument classes/@MyTekScope6/MyTekScope6.m index 3a1a1b2..9182640 100644 --- a/Instrument classes/@MyTekScope/MyTekScope.m +++ b/Instrument classes/@MyTekScope6/MyTekScope6.m @@ -1,110 +1,116 @@ % Generic class for controlling Tektronix scopes -classdef MyTekScope < MyScpiInstrument & MyDataSource & MyCommCont ... +classdef MyTekScope6 < MyScpiInstrument & MyDataSource & MyCommCont ... & MyGuiCont - properties (GetAccess = public, SetAccess={?MyClassParser,?MyTekScope}) + properties (GetAccess = public, SetAccess={?MyClassParser,?MyTekScope6}) % number of channels - channel_no = 4 + channel_no = 6 % List of the physical knobs, which can be rotated programmatically knob_list = {} end methods (Access = public) - function this = MyTekScope(varargin) + function this = MyTekScope6(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 + % Moved the parm query before the data aquisition + % because it seems that MSO has a problem responding + % to query after data aquisition parms = queryStrings(this, ... ':WFMOutpre:XUNit?', ... ':WFMOutpre:YUNit?', ... ':WFMOutpre:XINcr?', ... ':WFMOutpre:YMUlt?', ... ':WFMOutpre:XZEro?', ... ':WFMOutpre:YZEro?', ... ':WFMOutpre:YOFf?'); - + % Read raw y data + y_data = readY(this); 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?'); + ':CURVe?'); y_data = double(binblockread(this.Comm, 'int16')); + % read off the terminating character + % which can not be read by the binblockread + if this.Comm.BytesAvailable == 1 || this.Comm.BytesAvailable == 2 + fread(this.Comm,this.Comm.BytesAvailable,'uint8'); + end + % 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/@MyZiRingdown/MyZiRingdown.m b/Instrument classes/@MyZiRingdown/MyZiRingdown.m index 7f96e95..4f48e7a 100644 --- a/Instrument classes/@MyZiRingdown/MyZiRingdown.m +++ b/Instrument classes/@MyZiRingdown/MyZiRingdown.m @@ -1,929 +1,952 @@ % 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 % 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 + %Pll channel + pll_ch = 1 + % 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; + % Disable PLL + this.pll_on = false; + % 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); % Create index tag if this.n_avg > 1 ind_str = sprintf('_%i', this.AvgTrace.avg_count); else ind_str = ''; end traces = {copy(this.Trace)}; trace_tags = {ind_str}; if avg_compl % If the ringdown averaging is complete, disable % further triggering to exclude data overwriting this.enable_acq = false; this.drive_on = false; if this.n_avg > 1 traces = [traces, {copy(this.AvgTrace)}]; trace_tags = [trace_tags, {'_avg'}]; end else % Continue the acquisition of new ringdowns this.enable_acq = true; this.drive_on = true; end % Trigger a new data event with the last ringdown % and possibly the completed average trace triggerNewData(this, 'traces', traces, ... 'trace_tags', trace_tags); 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, 'pll_on', ... + 'readFcn', @this.readPllOn, ... + 'writeFcn', @this.writePllOn); + 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 = readPllOn(this) + path = sprintf('/%s/pids/%i/enable', this.dev_id, ... + this.pll_ch-1); + val = logical(ziDAQ('getInt', path)); + end + + function writePllOn(this, val) + path = sprintf('/%s/pids/%i/enable', this.dev_id, ... + this.pll_ch-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']); % 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/Local/LocalInstrumentControlSettings.mat b/Local/LocalInstrumentControlSettings.mat new file mode 100644 index 0000000..3811d22 Binary files /dev/null and b/Local/LocalInstrumentControlSettings.mat differ diff --git a/Measurement and analysis routines/DrivenMechDispCal/GuiDrivenMechDispCal.mlapp b/Measurement and analysis routines/DrivenMechDispCal/GuiDrivenMechDispCal.mlapp new file mode 100644 index 0000000..aa81885 Binary files /dev/null and b/Measurement and analysis routines/DrivenMechDispCal/GuiDrivenMechDispCal.mlapp differ diff --git a/Measurement and analysis routines/PhaseModCal/MyPhaseModCal.m b/Measurement and analysis routines/DrivenMechDispCal/MyDrivenMechDispCal.m similarity index 55% copy from Measurement and analysis routines/PhaseModCal/MyPhaseModCal.m copy to Measurement and analysis routines/DrivenMechDispCal/MyDrivenMechDispCal.m index 81b8b44..3c1d8fa 100644 --- a/Measurement and analysis routines/PhaseModCal/MyPhaseModCal.m +++ b/Measurement and analysis routines/DrivenMechDispCal/MyDrivenMechDispCal.m @@ -1,257 +1,376 @@ -% 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)) +% Routine for the calibration of mechanical displacement in a homodyne +% interferometer, where the oscillations are comparable to the scale of +% interference fringes, resulting in multiple sidebands observable in the +% displacement spectrum. -classdef MyPhaseModCal < MyAnalysisRoutine +classdef MyDrivenMechDispCal < 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 + min_peak_height double + + lambda = 0 % optical wavelength (nm) 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 + + x0 = 0 % Mechanical oscillations amplitude + + theta = 0 % Quadrature angle + + disp_cal = 0 % Displacement calibration factor + + disp_cal_err = 0 %95% confidence interval for calibration factor + + sb_n = [] % Integrated sideband indices + + sb_pow = [] % Sideband powers + end properties (Access = protected) % Line that displays the positions of peaks found PlottedPeaks end methods (Access = public) - function this = MyPhaseModCal(varargin) + function this = MyDrivenMechDispCal(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)); + + if isempty(this.Data) + pos = min(ylim(1)+0.30*(ylim(2)-ylim(1)), 10*ylim(1)); + else + pos = min(this.Data.y)*exp(0.5*(log(max(this.Data.y))-log(min(this.Data.y)))); + end 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); + this.Gui = GuiDrivenMechDispCal(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 + + % Check if the peaks are roughly 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 + %Improve the estimate of mechanical frequency + + max_search_band = [0.9*mod_f, 1.1*mod_f]; + + [~, mech_peak_x] = max(this.Data.y((this.Data.x > min(max_search_band)) & ... + (this.Data.x < max(max_search_band)))); + + freq_search = this.Data.x(((this.Data.x > min(max_search_band)) & ... + (this.Data.x < max(max_search_band)))); + + mod_f = freq_search(mech_peak_x); + % 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) + scaled_peak_x = peak_x/mod_f; + sb_ind = []; + for i = 1:n_peaks % 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)); + [err, ind] = min(abs(scaled_peak_x - i)); - if (err_p/i < mod_peak_tol) && (err_m/i < mod_peak_tol) + if (err/i < mod_peak_tol) && (err/i < mod_peak_tol) % Add the found indices to the list of sideband % peak indices - sb_ind = [ind_m, sb_ind, ind_p]; %#ok + sb_ind = [sb_ind, ind]; else break end end - % Out of all peaks select sideband peaks that appear in pairs + % Select sideband peaks at the harmonics of the mechanical + % frequency peak_y = peak_y(sb_ind); peak_x = peak_x(sb_ind); + peak_w = peak_w(sb_ind); + + % Take the integration width to be a few times the width of the + % largest peak. + int_w = 15*max(peak_w); 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); + %Re-construct the sideband indices + + sb_ind = round(peak_x/mod_f); + % 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); + peak_norm = peak_int/max(peak_int); + + % Find beta value by fitting + + Ft = fittype(@(beta,a,b,n) bessel_full(beta,a,b,n), 'independent', 'n', ... + 'coefficients', {'beta', 'a', 'b'}); + + % Find initial guess for beta + + xb = linspace(0,15,5000); + nb = 1:10; + [XB,NB] = meshgrid(xb,nb); + YB = besselj(NB,XB).^2; + [~,imax] = max(peak_int); + ind_max = sb_ind(imax); + if ind_max <=10 % Initial guess is the argmax for the sideband with largest amplitude + [~,imax] = max(YB(ind_max,:)); + beta0 = xb(imax); + else + beta0 = 13; + end - % 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],... + 'StartPoint', [beta0, 1, 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); + FitResult = fit(sb_ind(:), peak_norm(:), Ft, Opts); + + %Get 95% confidence interval on beta + + if n_peaks >= 4 + FitConfint = confint(FitResult,.95); + dbeta = FitConfint(2,1) - FitConfint(1,1); + else + dbeta = 0; + warning('Confidence interval has not been calculated; too few sidebands included in the fit'); + end % Store the result in class variables this.beta = abs(FitResult.beta); this.mod_freq = mod_f; - end - + this.theta = 180*atan(sqrt(FitResult.b/FitResult.a))/pi; + this.sb_n = sb_ind; + this.sb_pow = peak_norm; + + if isempty(this.lambda) || this.lambda<=0 + + % Prompt user to specify approximate wavelength. + % Show warning dialog if running in a gui + % mode or output warning in the command line otherwise. + if ~isempty(this.Gui) + Wd = warndlg(['Please input ' ... + 'an appropriate value for the optical wavelength.'], 'Warning'); + centerFigure(Wd); + else + warning('An appropriate value for optical wavelength must be set.'); + end + end + + wavelength = this.lambda*1e-9; + + if isempty(this.lambda) || this.lambda<=0 + this.x0 = 0; + else + this.x0 = 1e9*this.beta*wavelength/4/pi; + end + + %Calculate spectrum calibration factor from sidebands with + %sufficient SNR + + if isempty(this.lambda) || isempty(this.mod_freq) || this.lambda<=0 + this.disp_cal = 0; + this.disp_cal_err = 0; + else + + C = 0; + m = 0; + for k = 1:numel(peak_int) + ph = peak_int(k); + if ph/max(peak_int) > 1e-2 % Discard low amplitude sidebands from the calculation + if mod(sb_ind(k),2) ~= 0 %Odd sideband + Cp = (wavelength*besselj(sb_ind(k),this.beta))^2/8/(pi^2)/ph; + C = C + Cp; + m = m + 1; + else %Even sideband + Cp = (wavelength*besselj(sb_ind(k),this.beta))^2/8/(pi^2)/ph/(tan(pi*this.theta/180)^-2); + C = C + Cp; + m = m +1; + end + end + end + this.disp_cal = C/m; + + %Confidence interval for calibration factor + if ~any(sb_ind == 1) || peak_norm(sb_ind == 1) < 1e-1 + warning('Confidence interval has not been calculated; first order sideband has low amplitude.'); + this.disp_cal_err = 0; + else + this.disp_cal_err = abs((besselj(1,this.beta))*(besselj(0,this.beta) - besselj(2,this.beta))*... + dbeta*(wavelength^2)/(8*pi^2*peak_int(sb_ind == 1))); + end + end + end + function clearPeakDisp(this) if ~isempty(this.PlottedPeaks) delete(this.PlottedPeaks) end - 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/DrivenMechDispCal/bessel_full.m b/Measurement and analysis routines/DrivenMechDispCal/bessel_full.m new file mode 100644 index 0000000..54a363d --- /dev/null +++ b/Measurement and analysis routines/DrivenMechDispCal/bessel_full.m @@ -0,0 +1,12 @@ +function y = bessel_full(beta,a,b,n) + y = zeros(size(n)); + for i = 1:size(n) + k = n(i); + if mod(k,2) == 1 + y(i) = a*(besselj(k,beta)^2); + else + y(i) = b*(besselj(k,beta)^2); + end + end +end + \ No newline at end of file diff --git a/Measurement and analysis routines/DrivenMechDispCal/sidebands_power_def.png b/Measurement and analysis routines/DrivenMechDispCal/sidebands_power_def.png new file mode 100644 index 0000000..91de97d Binary files /dev/null and b/Measurement and analysis routines/DrivenMechDispCal/sidebands_power_def.png differ diff --git a/Measurement and analysis routines/PhaseModCal/MyPhaseModCal.m b/Measurement and analysis routines/PhaseModCal/MyPhaseModCal.m index 81b8b44..f0ea62a 100644 --- a/Measurement and analysis routines/PhaseModCal/MyPhaseModCal.m +++ b/Measurement and analysis routines/PhaseModCal/MyPhaseModCal.m @@ -1,257 +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 + 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