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/@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/@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