diff --git a/@MyCollector/MyCollector.m b/@MyCollector/MyCollector.m index 683c315..f8ed559 100644 --- a/@MyCollector/MyCollector.m +++ b/@MyCollector/MyCollector.m @@ -1,192 +1,198 @@ classdef MyCollector < MySingleton & matlab.mixin.Copyable properties (Access=public, SetObservable=true) InstrList % Structure accomodating instruments InstrProps % Properties of instruments MeasHeaders collect_flag end properties (Access=private) Listeners end properties (Dependent=true) running_instruments end events NewDataWithHeaders end methods (Access=private) % Constructor of a singleton class must be private function this=MyCollector(varargin) p=inputParser; addParameter(p,'InstrHandles',{}); parse(p,varargin{:}); this.collect_flag=true; if ~isempty(p.Results.InstrHandles) cellfun(@(x) addInstrument(this,x),p.Results.InstrHandles); end this.MeasHeaders=MyMetadata(); this.InstrList=struct(); this.InstrProps=struct(); this.Listeners=struct(); end end methods (Access=public) function delete(this) cellfun(@(x) deleteListeners(this,x), this.running_instruments); end function addInstrument(this,instr_handle,varargin) p=inputParser; addParameter(p,'name','UnknownDevice',@ischar) parse(p,varargin{:}); %Find a name for the instrument if ~ismember('name',p.UsingDefaults) name=p.Results.name; elseif isprop(instr_handle,'name') && ~isempty(instr_handle.name) name=genvarname(instr_handle.name, this.running_instruments); else name=genvarname(p.Results.name, this.running_instruments); end if ismethod(instr_handle, 'readHeader') %Defaults to read header this.InstrProps.(name).header_flag=true; else % If class does not have readHeader function, it can still % be added to the collector to transfer trace to Daq this.InstrProps.(name).header_flag=false; warning(['%s does not have a readHeader function, ',... 'measurement headers will not be collected from ',... 'this instrument.'],name) end this.InstrList.(name)=instr_handle; %If the added instrument has a newdata event, we add a listener for it. if contains('NewData',events(this.InstrList.(name))) this.Listeners.(name).NewData=... addlistener(this.InstrList.(name),'NewData',... @(~,InstrEventData) acquireData(this, InstrEventData)); end %Cleans up if the instrument is closed this.Listeners.(name).Deletion=... addlistener(this.InstrList.(name),'ObjectBeingDestroyed',... @(~,~) deleteInstrument(this,name)); end - function acquireData(this,InstrEventData) + function acquireData(this, InstrEventData) src=InstrEventData.Source; % Check that event data object is MyNewDataEvent, % and fix otherwise if ~isa(InstrEventData,'MyNewDataEvent') InstrEventData=MyNewDataEvent(); InstrEventData.new_header=true; - InstrEventData.Instr=src; + InstrEventData.Trace=copy(src.Trace); + try + InstrEventData.src_name=src.name; + catch + InstrEventData.src_name='UnknownDevice'; + end end % Collect the headers if the flag is on and if the triggering % instrument does not request suppression of header collection if this.collect_flag && InstrEventData.new_header this.MeasHeaders=MyMetadata(); %Add field indicating the time when the trace was acquired addTimeField(this.MeasHeaders, 'AcquisitionTime') addField(this.MeasHeaders,'AcquiringInstrument') - if isprop(src,'name') - name=src.name; - else - name='Not Accessible'; - end + %src_name is a valid matlab variable name as ensured by + %its set method addParam(this.MeasHeaders,'AcquiringInstrument',... - 'Name',name); + 'Name', InstrEventData.src_name); acquireHeaders(this); - %We copy the MeasHeaders to the trace. + + %We copy the MeasHeaders to both copies of the trace - the + %one that is with the source and the one that is forwarded + %to Daq. + InstrEventData.Trace.MeasHeaders=copy(this.MeasHeaders); src.Trace.MeasHeaders=copy(this.MeasHeaders); end triggerNewDataWithHeaders(this,InstrEventData); end %Collects headers for open instruments with the header flag on function acquireHeaders(this) for i=1:length(this.running_instruments) name=this.running_instruments{i}; if this.InstrProps.(name).header_flag try TmpMetadata=readHeader(this.InstrList.(name)); addMetadata(this.MeasHeaders, TmpMetadata); catch warning(['Error while reading metadata from %s.',... 'Measurement header collection is switched ',... 'off for this instrument.'],name) this.InstrProps.(name).header_flag=false; end end end end function clearHeaders(this) this.MeasHeaders=MyMetadata(); end function bool=isrunning(this,name) assert(~isempty(name),'Instrument name must be specified') assert(ischar(name)&&isvector(name),... 'Instrument name must be a character vector, not %s',... class(name)); bool=ismember(name,this.running_instruments); end function deleteInstrument(this,name) if isrunning(this,name) %We remove the instrument this.InstrList=rmfield(this.InstrList,name); this.InstrProps=rmfield(this.InstrProps,name); deleteListeners(this,name); end end end methods(Static) % Concrete implementation of the singletone constructor. function this = instance() persistent UniqueInstance if isempty(UniqueInstance)||(~isvalid(UniqueInstance)) disp('Creating new instance of MyCollector') this = MyCollector(); UniqueInstance = this; else this = UniqueInstance; end end end methods (Access=private) function triggerNewDataWithHeaders(this,InstrEventData) notify(this,'NewDataWithHeaders',InstrEventData); end %deleteListeners is in a separate file deleteListeners(this, obj_name); end methods function running_instruments=get.running_instruments(this) running_instruments=fieldnames(this.InstrList); end end end diff --git a/@MyDaq/MyDaq.m b/@MyDaq/MyDaq.m index 449b681..fcbf6d9 100644 --- a/@MyDaq/MyDaq.m +++ b/@MyDaq/MyDaq.m @@ -1,1028 +1,1025 @@ % Acquisition and analysis program that receives data from Collector. Can % also be used for analysis of previously acquired traces. classdef MyDaq < handle properties %Global variable with Daq name is cleared on exit. global_name %Contains GUI handles Gui %Contains Reference trace (MyTrace object) Ref %Contains Data trace (MyTrace object) Data %Contains Background trace (MyTrace object) Background %List of all the programs with run files ProgramList %Struct containing Cursor objects Cursors %Struct containing Cursor labels CrsLabels %Struct containing MyFit objects Fits %Input parser for class constructor ConstructionParser %Struct for listeners Listeners %Sets the colors of fits, data and reference fit_color='k'; data_color='b'; ref_color='r'; bg_color='c'; end properties (Dependent=true) save_dir main_plot open_fits open_crs end properties (Dependent=true, SetAccess=private, GetAccess=public) %Properties for saving files base_dir session_name filename % File name is always returned with extension end methods (Access=public) %% Class functions %Constructor function function this=MyDaq(varargin) % Initialize variables % Traces this.Ref=MyTrace(); this.Data=MyTrace(); this.Background=MyTrace(); % Lists this.ProgramList=struct(); this.Cursors=struct(); this.CrsLabels=struct(); this.Fits=struct(); this.ConstructionParser; this.Listeners=struct(); % Parse inputs p=inputParser; addParameter(p,'global_name','',@ischar); addParameter(p,'collector_handle',[]); this.ConstructionParser=p; parse(p, varargin{:}); this.global_name = p.Results.global_name; %Sets a listener to the collector if ~isempty(p.Results.collector_handle) this.Listeners.Collector.NewDataWithHeaders=... addlistener(p.Results.collector_handle,... 'NewDataWithHeaders',... @(~,eventdata) acquireNewData(this,eventdata)); else errordlg(['No collector handle was supplied. ',... 'DAQ will be unable to acquire data'],... 'Error: No collector'); end %The list of instruments is automatically populated from the %run files this.ProgramList = readRunFiles(); %We grab the guihandles from a GUI made in Guide. this.Gui=guihandles(eval('GuiDaq')); %This function sets all the callbacks for the GUI. If a new %button is made, the associated callback must be put in the %initGui function initGui(this); % Initialize the menu based on the available run files content = menuFromRunFiles(this.ProgramList,... 'show_in_daq',true); set(this.Gui.InstrMenu,'String',[{'Select the application'};... content.titles]); % Add a property to the menu for storing the program file % names if ~isprop(this.Gui.InstrMenu, 'ItemsData') addprop(this.Gui.InstrMenu, 'ItemsData'); end set(this.Gui.InstrMenu,'ItemsData',[{''};... content.tags]); hold(this.main_plot,'on'); %Initializes saving locations this.base_dir=getLocalSettings('measurement_base_dir'); this.session_name='placeholder'; this.filename='placeholder'; end function delete(this) %Deletes the MyFit objects and their listeners cellfun(@(x) deleteListeners(this,x), this.open_fits); structfun(@(x) delete(x), this.Fits); %Deletes other listeners if ~isempty(fieldnames(this.Listeners)) cellfun(@(x) deleteListeners(this, x),... fieldnames(this.Listeners)); end % clear global variable, to which Daq handle is assigned evalin('base', sprintf('clear(''%s'')', this.global_name)); %A class destructor should never through errors, so enclose the %attempt to close figure into try-catch structure try this.Gui.figure1.CloseRequestFcn=''; %Deletes the figure delete(this.Gui.figure1); %Removes the figure handle to prevent memory leaks this.Gui=[]; catch end end end methods (Access=private) %Sets callback functions for the GUI initGui(this) %Executes when the GUI is closed function closeFigure(this,~,~) delete(this); end %Updates fits function updateFits(this) %Pushes data into fits in the form of MyTrace objects, so that %units etc follow. Also updates user supplied parameters. for i=1:length(this.open_fits) switch this.open_fits{i} case {'Linear','Quadratic','Gaussian',... 'Exponential','Beta'} this.Fits.(this.open_fits{i}).Data=... getFitData(this,'VertData'); case {'Lorentzian','DoubleLorentzian'} this.Fits.(this.open_fits{i}).Data=... getFitData(this,'VertData'); %Here we push the information about line spacing %into the fit object if the reference cursors are %open. Only for Lorentzian fits. if isfield(this.Cursors,'VertRef') ind=findCursorData(this,'Data','VertRef'); this.Fits.(this.open_fits{i}).CalVals.line_spacing=... range(this.Data.x(ind)); end case {'G0'} this.Fits.G0.MechTrace=getFitData(this,'VertData'); this.Fits.G0.CalTrace=getFitData(this,'VertRef'); end end end % If vertical cursors are on, takes only data within cursors. If %the cursor is not open, it takes all the data from the selected %trace in the analysis trace selection dropdown function Trace=getFitData(this,varargin) %Parses varargin input p=inputParser; addOptional(p,'name','',@ischar); parse(p,varargin{:}) name=p.Results.name; %Finds out which trace the user wants to fit. trc_opts=this.Gui.SelTrace.String; trc_str=trc_opts{this.Gui.SelTrace.Value}; % Note the use of copy here! This is a handle %class, so if normal assignment is used, this.Fits.Data and %this.(trace_str) will refer to the same object, causing roblems. %Name input is the name of the cursor to be used to extract data. Trace=copy(this.(trc_str)); %If the cursor is open for the trace we are analyzing, we take %only the data enclosed by the cursor. if isfield(this.Cursors,name) ind=findCursorData(this, trc_str, name); Trace.x=this.(trc_str).x(ind); Trace.y=this.(trc_str).y(ind); end end %Finds data between named cursors in the given trace function ind=findCursorData(this, trc_str, name) crs_pos=sort([this.Cursors.(name){1}.Location,... this.Cursors.(name){2}.Location]); ind=(this.(trc_str).x>crs_pos(1) & this.(trc_str).x %Prints the figure to the clipboard print(newFig,'-clipboard','-dbitmap'); %Deletes the figure delete(newFig); end %Resets the axis to be tight around the plots. function updateAxis(this) axis(this.main_plot,'tight'); end end methods (Access=public) %% Callbacks %Callback for copying the plot to clipboard function copyPlotCallback(this,~,~) copyPlot(this); end %Callback for centering cursors function centerCursorsCallback(this, ~, ~) if ~this.Gui.LogX.Value x_pos=mean(this.main_plot.XLim); else x_pos=10^(mean(log10(this.main_plot.XLim))); end if ~this.Gui.LogY.Value y_pos=mean(this.main_plot.YLim); else y_pos=10^(mean(log10(this.main_plot.YLim))); end for i=1:length(this.open_crs) switch this.Cursors.(this.open_crs{i}){1}.Orientation case 'horizontal' pos=y_pos; case 'vertical' pos=x_pos; end %Centers the position cellfun(@(x) set(x,'Location',pos), ... this.Cursors.(this.open_crs{i})); %Triggers the UpdateCursorBar event, which triggers %listener callback to reposition text cellfun(@(x) notify(x,'UpdateCursorBar'),... this.Cursors.(this.open_crs{i})); %Triggers the EndDrag event, updating the data in the fit %objects. cellfun(@(x) notify(x,'EndDrag'),... this.Cursors.(this.open_crs{i})); end end %Callback for creating vertical data cursors function cursorButtonCallback(this, hObject, ~) name=erase(hObject.Tag,'Button'); %Gets the first four characters of the tag (Vert or Horz) type=name(1:4); %Changes the color of the button and appropriately creates or %deletes the cursors. if hObject.Value hObject.BackgroundColor=[0,1,0.2]; createCursors(this,name,type); else hObject.BackgroundColor=[0.941,0.941,0.941]; deleteCursors(this,name); end end %Callback for the instrument menu function instrMenuCallback(this,hObject,~) val=hObject.Value; if val==1 %Returns if we are on the dummy option ('Select instrument') return else tag = hObject.ItemsData{val}; end try eval(this.ProgramList.(tag).run_expr); catch errordlg(sprintf('An error occured while running %s',... this.ProgramList.(tag).name)) end end %Select trace callback. If we change the trace being analyzed, the %fit objects are updated. function selTraceCallback(this, ~, ~) updateFits(this) end %Saves the data if the save data button is pressed. function saveCallback(this, src, ~) switch src.Tag case 'SaveData' saveTrace(this,'Data'); case 'SaveRef' saveTrace(this,'Ref'); end end function saveTrace(this, trace_tag, varargin) p=inputParser(); % If file already exists, generate a uinique name rather than % show user the overwrite dialog. addParameter(p, 'make_unique_name', false, @islogical); parse(p,varargin{:}); %Check if the trace is valid (i.e. x and y are equal length) %before saving if ~this.(trace_tag).validatePlot errordlg(sprintf('%s trace was empty, could not save',... trace_tag)); return end fullfilename=fullfile(this.save_dir,this.filename); if p.Results.make_unique_name && exist(fullfilename, 'file')~=0 fullfilename=makeUniqueFileName(this); end %Save in readable format using the method of MyTrace save(this.(trace_tag), fullfilename) end % Make the filename unique within the measurement folder % by appending _n. This function does not make sure that the % filename is valid - i.e. does not contain symbols forbidden by % the file system. function fullfilename=makeUniqueFileName(this) [~, fn, ext]=fileparts(this.filename); % List all the existing files in the measurement directory % that have the same extension as our filename DirCont=dir(fullfile(this.save_dir,['*', ext])); file_ind=~[DirCont.isdir]; existing_fns={DirCont(file_ind).name}; % Remove extensions [~,existing_fns,~]=cellfun(@fileparts, existing_fns, ... 'UniformOutput',false); % Generate a new file name if ~isempty(fn) fn=matlab.lang.makeUniqueStrings(fn, existing_fns); else fn=matlab.lang.makeUniqueStrings('placeholder', ... existing_fns); end fullfilename=fullfile(this.save_dir,[fn, ext]); end %Toggle button callback for showing the data trace. function showDataCallback(this, hObject, ~) if hObject.Value hObject.BackgroundColor=[0,1,0.2]; setVisible(this.Data,this.main_plot,1); updateAxis(this); else hObject.BackgroundColor=[0.941,0.941,0.941]; setVisible(this.Data,this.main_plot,0); updateAxis(this); end end %Toggle button callback for showing the ref trace function showRefCallback(this, hObject, ~) if hObject.Value hObject.BackgroundColor=[0,1,0.2]; setVisible(this.Ref,this.main_plot,1); updateAxis(this); else hObject.BackgroundColor=[0.941,0.941,0.941]; setVisible(this.Ref,this.main_plot,0); updateAxis(this); end end %Callback for moving the data to reference. function dataToRefCallback(this, ~, ~) if this.Data.validatePlot set(this.Ref,... 'x',this.Data.x,... 'y',this.Data.y,... 'name_x',this.Data.name_x,... 'name_y',this.Data.name_y,... 'unit_x',this.Data.unit_x,... 'unit_y',this.Data.unit_y); %Since UID is automatically reset when y is changed, we now %change it back to be the same as the Data. this.Ref.uid=this.Data.uid; this.Ref.MeasHeaders=copy(this.Data.MeasHeaders); %Plot the reference trace and make it visible plot(this.Ref, this.main_plot, 'Color',this.ref_color,... 'make_labels',true); setVisible(this.Ref, this.main_plot,1); %Update the fit objects updateFits(this); %Change button color this.Gui.ShowRef.Value=1; this.Gui.ShowRef.BackgroundColor=[0,1,0.2]; else warning('Data trace was empty, could not move to reference') end end %Callback for ref to bg button. Sends the reference to background function refToBgCallback(this, ~, ~) if this.Ref.validatePlot this.Background.x=this.Ref.x; this.Background.y=this.Ref.y; this.Background.plot(this.main_plot,... 'Color',this.bg_color,'make_labels',true); this.Background.setVisible(this.main_plot,1); else warning('Reference trace was empty, could not move to background') end end %Callback for data to bg button. Sends the data to background function dataToBgCallback(this, ~, ~) if this.Data.validatePlot this.Background.x=this.Data.x; this.Background.y=this.Data.y; this.Background.plot(this.main_plot,... 'Color',this.bg_color,'make_labels',true); this.Background.setVisible(this.main_plot,1); else warning('Data trace was empty, could not move to background') end end %Callback for clear background button. Clears the background function clearBgCallback(this, ~, ~) this.Background.x=[]; this.Background.y=[]; this.Background.setVisible(this.main_plot,0); end %Callback for LogY button. Sets the YScale to log/lin function logYCallback(this, hObject, ~) if hObject.Value this.main_plot.YScale='Log'; hObject.BackgroundColor=[0,1,0.2]; else this.main_plot.YScale='Linear'; hObject.BackgroundColor=[0.941,0.941,0.941]; end updateAxis(this); updateCursors(this); end %Callback for LogX button. Sets the XScale to log/lin. Updates the %axis and cursors afterwards. function logXCallback(this, hObject, ~) if get(hObject,'Value') set(this.main_plot,'XScale','Log'); set(hObject, 'BackgroundColor',[0,1,0.2]); else set(this.main_plot,'XScale','Linear'); set(hObject, 'BackgroundColor',[0.941,0.941,0.941]); end updateAxis(this); updateCursors(this); end %Base directory callback. Sets the base directory. Also %updates fit objects with the new save directory. function baseDirCallback(this, ~, ~) for i=1:length(this.open_fits) this.Fits.(this.open_fits{i}).base_dir=this.base_dir; end end %Callback for session name edit box. Sets the session name. Also %updates fit objects with the new save directory. function sessionNameCallback(this, ~, ~) for i=1:length(this.open_fits) this.Fits.(this.open_fits{i}).session_name=this.session_name; end end %Callback for filename edit box. Sets the file name. Also %updates fit objects with the new file name. function fileNameCallback(this, ~,~) for i=1:length(this.open_fits) this.Fits.(this.open_fits{i}).filename=this.filename; end end %Callback for the analyze menu (popup menu for selecting fits). %Opens the correct MyFit object. function analyzeMenuCallback(this, hObject, ~) analyze_ind=hObject.Value; %Finds the correct fit name by erasing spaces and other %superfluous strings analyze_name=hObject.String{analyze_ind}; analyze_name=erase(analyze_name,'Fit'); analyze_name=erase(analyze_name,'Calibration'); analyze_name=erase(analyze_name,' '); %Sets the correct tooltip hObject.TooltipString=sprintf(this.Gui.AnalyzeTip{analyze_ind}) ; %Opens the correct analysis tool switch analyze_name case {'Linear','Quadratic','Exponential',... 'Lorentzian','Gaussian',... 'DoubleLorentzian'} openMyFit(this,analyze_name); case 'g0' openMyG(this); case 'Beta' openMyBeta(this); end end function openMyFit(this,fit_name) %Sees if the MyFit object is already open, if it is, changes the %focus to it, if not, opens it. if ismember(fit_name,fieldnames(this.Fits)) %Changes focus to the relevant fit window figure(this.Fits.(fit_name).Gui.Window); else %Gets the data for the fit using the getFitData function %with the vertical cursors DataTrace=getFitData(this,'VertData'); %Makes an instance of MyFit with correct parameters. this.Fits.(fit_name)=MyFit(... 'fit_name',fit_name,... 'enable_plot',1,... 'plot_handle',this.main_plot,... 'Data',DataTrace,... 'base_dir',this.base_dir,... 'session_name',this.session_name,... 'filename',this.filename); updateFits(this); %Sets up a listener for the Deletion event, which %removes the MyFit object from the Fits structure if it is %deleted. this.Listeners.(fit_name).Deletion=... addlistener(this.Fits.(fit_name),'ObjectBeingDestroyed',... @(src, eventdata) deleteFit(this, src, eventdata)); %Sets up a listener for the NewFit. Callback plots the fit %on the main plot. this.Listeners.(fit_name).NewFit=... addlistener(this.Fits.(fit_name),'NewFit',... @(src, eventdata) plotNewFit(this, src, eventdata)); %Sets up a listener for NewInitVal this.Listeners.(fit_name).NewInitVal=... addlistener(this.Fits.(fit_name),'NewInitVal',... @(~,~) updateCursors(this)); end end %Opens MyG class if it is not open. function openMyG(this) if ismember('G0',this.open_fits) figure(this.Fits.G0.Gui.figure1); else %Populate the MyG class with the right data. We assume the %mechanics is in the Data trace. MechTrace=getFitData(this,'VertData'); CalTrace=getFitData(this,'VertRef'); this.Fits.G0=MyG('MechTrace',MechTrace,'CalTrace',CalTrace,... 'name','G0'); %Adds listener for object being destroyed this.Listeners.G0.Deletion=addlistener(this.Fits.G0,... 'ObjectBeingDestroyed',... @(~,~) deleteObj(this,'G0')); end end %Opens MyBeta class if it is not open. function openMyBeta(this) if ismember('Beta', this.open_fits) figure(this.Fits.Beta.Gui.figure1); else DataTrace=getFitData(this); this.Fits.Beta=MyBeta('Data',DataTrace); %Adds listener for object being destroyed, to perform cleanup this.Listeners.Beta.Deletion=addlistener(this.Fits.Beta,... 'ObjectBeingDestroyed',... @(~,~) deleteObj(this,'Beta')); end end %Callback for load data button function loadDataCallback(this, ~, ~) if isempty(this.base_dir) warning('Please input a valid folder name for loading a trace'); this.base_dir=pwd; end [load_name,path_name]=uigetfile('.txt','Select the trace',... this.base_dir); if load_name==0 warning('No file was selected'); return end load_path=[path_name,load_name]; %Finds the destination trace from the GUI dest_trc=this.Gui.DestTrc.String{this.Gui.DestTrc.Value}; %Call the load trace function on the right trace load(this.(dest_trc), load_path); %Color and plot the right trace. plot(this.(dest_trc), this.main_plot,... 'Color',this.(sprintf('%s_color',lower(dest_trc))),... 'make_labels',true); %Update axis and cursors updateAxis(this); updateCursors(this); end % Callback for open folder button function openFolderCallback(this, hObject, eventdata) dir=uigetdir(this.Gui.BaseDir.String); if ~isempty(dir) this.Gui.BaseDir.String=dir; end % Execute the same callback as if the base directory edit % field was manually updated baseDirCallback(this, hObject, eventdata); end end methods (Access=public) %% Listener functions %Callback function for NewFit listener. Plots the fit in the %window using the plotFit function of the MyFit object function plotNewFit(this, src, ~) src.plotFit('Color',this.fit_color); updateAxis(this); updateCursors(this); end %Callback function for the NewData listener function acquireNewData(this, EventData) %Get the currently selected instrument val=this.Gui.InstrMenu.Value; curr_instr_name=this.Gui.InstrMenu.ItemsData{val}; - %Get the name of instrument that generated new data - SourceInstr = EventData.Instr; - source_name = SourceInstr.name; %Check if the data originates from the currently selected %instrument - if strcmp(source_name, curr_instr_name) + if strcmp(EventData.src_name, curr_instr_name) hline=getLineHandle(this.Data,this.main_plot); %Copy the data from the source instrument - this.Data=copy(SourceInstr.Trace); + this.Data=copy(EventData.Trace); %We give the new trace object the right line handle to plot in if ~isempty(hline) this.Data.hlines{1}=hline; end plot(this.Data, this.main_plot,... 'Color',this.data_color,... 'make_labels',true) updateAxis(this); updateCursors(this); updateFits(this); % If the save flag is on in EventData, save the new trace if isprop(EventData, 'save') && EventData.save if isprop(EventData, 'filename') && ... ~isempty(EventData.filename) % If present, use the file name supplied externally this.filename=EventData.filename; saveTrace(this, 'Data'); else % Generate a new unique filename saveTrace(this, 'Data', 'make_unique_name', true); end end end end %Callback function for MyFit ObjectBeingDestroyed listener. %Removes the relevant field from the Fits struct and deletes the %listeners from the object. function deleteFit(this, src, ~) %Deletes the object from the Fits struct and deletes listeners deleteObj(this,src.fit_name); %Clears the fits src.clearFit; %Updates cursors since the fits are removed from the plot updateCursors(this); end %Callback function for other analysis method deletion listeners. %Does the same as above. function deleteObj(this,name) if ismember(name,this.open_fits) this.Fits=rmfield(this.Fits,name); end deleteListeners(this, name); end %Listener update function for vertical cursor function vertCursorUpdate(this, src) %Finds the index of the cursor. All cursors are tagged %(name)1, (name)2, e.g. VertData2, ind is the number. ind=str2double(src.Tag(end)); tag=src.Tag(1:(end-1)); %Moves the cursor labels set(this.CrsLabels.(tag){ind},'Position',[src.Location,... this.CrsLabels.(tag){ind}.Position(2),0]); if strcmp(tag,'VertData') %Sets the edit box displaying the location of the cursor this.Gui.(sprintf('EditV%d',ind)).String=... num2str(src.Location); %Sets the edit box displaying the difference in locations this.Gui.EditV2V1.String=... num2str(this.Cursors.VertData{2}.Location-... this.Cursors.VertData{1}.Location); end end %Listener update function for horizontal cursor function horzCursorUpdate(this, src) %Finds the index of the cursor. All cursors are tagged %(name)1, (name)2, e.g. VertData2, ind is the number. ind=str2double(src.Tag(end)); tag=src.Tag(1:(end-1)); %Moves the cursor labels set(this.CrsLabels.(tag){ind},'Position',... [this.CrsLabels.(tag){ind}.Position(1),... src.Location,0]); if strcmp(tag,'HorzData') %Sets the edit box displaying the location of the cursor this.Gui.(sprintf('EditH%d',ind)).String=... num2str(src.Location); %Sets the edit box displaying the difference in locations this.Gui.EditH2H1.String=... num2str(this.Cursors.HorzData{2}.Location-... this.Cursors.HorzData{1}.Location); end end %Function that deletes listeners from the listeners struct, %corresponding to an object of name obj_name deleteListeners(this, obj_name); end %Get functions for dependent variables without set functions methods %Get function from save directory function save_dir=get.save_dir(this) save_dir=createSessionPath(this.base_dir,this.session_name); end %Get function for the plot handles function main_plot=get.main_plot(this) main_plot=this.Gui.figure1.CurrentAxes; end %Get function for open fits function open_fits=get.open_fits(this) open_fits=fieldnames(this.Fits); end %Get function that displays names of open cursors function open_crs=get.open_crs(this) open_crs=fieldnames(this.Cursors); end end %Get and set functions for dependent properties with SetAccess methods function base_dir=get.base_dir(this) try base_dir=this.Gui.BaseDir.String; catch base_dir=pwd; end end function set.base_dir(this,base_dir) this.Gui.BaseDir.String=base_dir; end function session_name=get.session_name(this) try session_name=this.Gui.SessionName.String; catch session_name=''; end end function set.session_name(this,session_name) this.Gui.SessionName.String=session_name; end % Always return filename with extension function filename=get.filename(this) try filename=this.Gui.FileName.String; [~,~,ext]=fileparts(filename); if isempty(ext) % Add default file extension filename=[filename,'.txt']; end catch filename='placeholder.txt'; end end function set.filename(this, str) this.Gui.FileName.String=str; end end end \ No newline at end of file diff --git a/@MyDataSource/MyDataSource.m b/@MyDataSource/MyDataSource.m new file mode 100644 index 0000000..4be0da9 --- /dev/null +++ b/@MyDataSource/MyDataSource.m @@ -0,0 +1,69 @@ +% Class that contains functionality of transferring trace to Collector and +% then to Daq + +classdef MyDataSource < handle + + properties (GetAccess=public, SetAccess={?MyClassParser}) + % name is sometimes used as identifier in listeners callbacks, so + % it better not to be changed after the object is created. + % Granting MyClassParser access to this variable allows to + % conveniently assign it in a subclass constructor from name-value + % pairs. + name='MyDataSource' + end + + % There does not seem to be a way to have a read-only protected access + % for a handle variable, so keep it public + properties (Access=public) + Trace + end + + events + NewData + end + + methods (Access=public) + + function this=MyDataSource() + this.Trace=MyTrace(); % Create an empty trace object + 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{:}); + EventData.src_name=this.name; + % Pass trace by value to make sure that it is not modified + % before being transferred + EventData.Trace=copy(this.Trace); + notify(this,'NewData',EventData); + end + + end + + %% Set and get methods + + methods + + % Ensures that the instrument name is a valid Matlab variable + function set.name(this, str) + assert(ischar(str), ['The value assigned to ''name'' ' ... + 'property must be char']) + if ~isempty(str) + str=matlab.lang.makeValidName(str); + else + str=class(this); + end + this.name=str; + end + + function set.Trace(this, Val) + assert(isa(Val, 'MyTrace'), ['The value of Trace must be ' ... + 'of MyTrace class or its subcleass']) + this.Trace=Val; + end + end +end + diff --git a/@MyInstrument/MyInstrument.m b/@MyInstrument/MyInstrument.m index dcd559e..465895b 100644 --- a/@MyInstrument/MyInstrument.m +++ b/@MyInstrument/MyInstrument.m @@ -1,249 +1,225 @@ % Generic class to implement communication with instruments -classdef MyInstrument < dynamicprops +classdef MyInstrument < dynamicprops & MyDataSource % Access for these variables is 'protected' and in addition % granted to MyClassParser in order to use construction parser - properties (GetAccess=public, SetAccess={?MyClassParser,?MyInstrument}) - % name is sometimes used as identifier in listeners callbacks, so - % it better not to be changed after the instrument object is - % created - name=''; + properties (GetAccess=public, SetAccess={?MyClassParser}) interface=''; address=''; end properties (Access=public) Device %Device communication object - Trace %MyTrace object for storing data end properties (GetAccess=public, SetAccess=protected) idn_str=''; % identification string end properties (Constant=true) % Default parameters for device connection DEFAULT_INP_BUFF_SIZE = 1e7; % buffer size bytes DEFAULT_OUT_BUFF_SIZE = 1e7; % buffer size bytes DEFAULT_TIMEOUT = 10; % Timeout in s end events - NewData PropertyRead end methods (Access=public) function this=MyInstrument(interface, address, varargin) P=MyClassParser(); addRequired(P,'interface',@ischar); addRequired(P,'address',@ischar); addParameter(P,'name','',@ischar); processInputs(P, this, interface, address, varargin{:}); - % Create an empty trace - this.Trace=MyTrace(); - % Create dummy device object that supports properties this.Device=struct(); this.Device.Status='not connected'; % Interface and address can correspond to an entry in the list % of local instruments. Read this entry in such case. if strcmpi(interface, 'instr_list') % load the InstrumentList structure InstrumentList = getLocalSettings('InstrumentList'); % In this case 'address' is the instrument name in % the list instr_name = address; if ~isfield(InstrumentList, instr_name) error('%s is not a field of InstrumentList',... instr_name); end if ~isfield(InstrumentList.(instr_name), 'interface') error(['InstrumentList entry ', instr_name,... ' has no ''interface'' field']); else this.interface = InstrumentList.(instr_name).interface; end if ~isfield(InstrumentList.(instr_name), 'address') error(['InstrumentList entry ', instr_name,... ' has no ''address'' field']); else this.address = InstrumentList.(instr_name).address; end % Assign name automatically, but not overwrite if % already specified if isempty(this.name) this.name = instr_name; end end % Connecting device creates a communication object, % but does not attempt communication connectDevice(this); end function delete(this) %Closes the connection to the device closeDevice(this); %Deletes the device object try delete(this.Device); catch end end - %Trigger event signaling the acquisition of a new trace. - %Any properties of MyNewDataEvent can be set by indicating the - %corresponding name-value pars in varargin. For the list of options - %see the definition of MyNewDataEvent. - function triggerNewData(this, varargin) - EventData = MyNewDataEvent(varargin{:}); - EventData.Instr=this; - notify(this,'NewData',EventData); - end - %Triggers event for property read from device function triggerPropertyRead(this) notify(this,'PropertyRead') end % Read all the relevant instrument properties and return as a % MyMetadata object. % Dummy method that needs to be re-defined by a parent class function Hdr=readHeader(this) Hdr=MyMetadata(); - % Generate valid field name from instrument name if present and - % class name otherwise - if ~isempty(this.name) - field_name=genvarname(this.name); - else - field_name=class(this); - end + % Instrument name is a valid Matalb identifier as ensured by + % its set method (see the superclass) addField(Hdr, field_name); % Add identification string as parameter addParam(Hdr, field_name, 'idn', this.idn_str); end %% Connect, open, configure, identificate and close the device % Connects to the device, explicit indication of interface and % address is for ability to handle instr_list as interface function connectDevice(this) int_list={'constructor','visa','tcpip','serial'}; if ~ismember(lower(this.interface), int_list) warning(['Device is not connected, unknown interface ',... this.interface,'. Valid interfaces are ',... '''constructor'', ''visa'', ''tcpip'' and ''serial''']) return end try switch lower(this.interface) % Use 'constructor' interface to connect device with % more that one parameter, specifying its address case 'constructor' % in this case the 'address' is a command % (ObjectConstructorName), e.g. as returned by the % instrhwinfo, that creates communication object % when executed this.Device=eval(this.address); case 'visa' % visa brand is 'ni' by default this.Device=visa('ni', this.address); case 'tcpip' % Works only with default socket. Use 'constructor' % if socket or other options need to be specified this.Device=tcpip(this.address); case 'serial' this.Device=serial(this.address); otherwise error('Unknown interface'); end configureDeviceDefault(this); catch warning(['Device is not connected, ',... 'error while creating communication object.']); end end % Opens the device if it is not open. Does not throw error if % device is already open for communication with another object, but % tries to close existing connections instead. function openDevice(this) if ~isopen(this) try fopen(this.Device); catch % try to find and close all the devices with the same % VISA resource name try instr_list=instrfind('RsrcName',this.Device.RsrcName); fclose(instr_list); fopen(this.Device); warning('Multiple instrument objects of address %s exist',... this.address); catch error('Could not open device') end end end end % Closes the connection to the device function closeDevice(this) if isopen(this) fclose(this.Device); end end function configureDeviceDefault(this) dev_prop_list = properties(this.Device); if ismember('OutputBufferSize',dev_prop_list) this.Device.OutputBufferSize = this.DEFAULT_OUT_BUFF_SIZE; end if ismember('InputBufferSize',dev_prop_list) this.Device.InputBufferSize = this.DEFAULT_INP_BUFF_SIZE; end if ismember('Timeout',dev_prop_list) this.Device.Timeout = this.DEFAULT_TIMEOUT; end end % Checks if the connection to the device is open function bool=isopen(this) try bool=strcmp(this.Device.Status, 'open'); catch warning('Cannot access device Status property'); bool=false; end end %% Identification % Attempt communication and identification of the device function [str, msg]=idn(this) was_open=isopen(this); try openDevice(this); [str,~,msg]=query(this.Device,'*IDN?'); catch ErrorMessage str=''; msg=ErrorMessage.message; end % Remove carriage return and new line symbols from the string newline_smb={sprintf('\n'),sprintf('\r')}; %#ok str=replace(str, newline_smb,' '); this.idn_str=str; % Leave device in the state it was in the beginning if ~was_open try closeDevice(this); catch end end end end end \ No newline at end of file diff --git a/@MyNewDataEvent/MyNewDataEvent.m b/@MyNewDataEvent/MyNewDataEvent.m index 55d55e5..318c657 100644 --- a/@MyNewDataEvent/MyNewDataEvent.m +++ b/@MyNewDataEvent/MyNewDataEvent.m @@ -1,33 +1,58 @@ -%Class for NewData events that are generated by MyInstrument +%Class for NewData events that are generated by MyDataSource and its +%subclasses, including MyInstrument + classdef MyNewDataEvent < event.EventData properties - % Handle of the instrument that triggered the event. Usefult for + % Name of the instrument that triggered the event. Usefult for % passing the event data forward, e.g. by triggering % NewDataWithHeaders - Instr + src_name = 'UnknownInstrument' + + % New acquired trace. Should be passed by value in order to prevent + % race condition when multiple NewData events are triggered by + % the same instrument in a short period of time. Passing by value + % makes sure that the trace is not modified before it is received + % by Daq. + Trace % If false then MyCollector does not acquire new measurement % headers for this trace. Setting new_header = false allows % transferring an existing trace to Daq by triggering NewData. new_header = true % If the new data should be automatically saved by Daq. save = false % If 'save' is true and 'filename' is not empty, Daq uses the % supplied file name to save the trace. This file name is relative % to the measurement session folder. filename = '' end methods % Use parser to process properties supplied as name-value pairs via % varargin function this=MyNewDataEvent(varargin) P=MyClassParser(this); processInputs(P, this, varargin{:}); end end + + %% Set and get methods + + methods + % Ensures that the source name is a valid Matlab variable + function set.src_name(this, str) + assert(ischar(str), ['The value assigned to ''src_name'' ' ... + 'must be char']) + if ~isempty(str) + str=matlab.lang.makeValidName(str); + else + str='UnknownInstrument'; + end + this.name=str; + end + end end \ No newline at end of file diff --git a/@MyZiRingdown/MyZiRingdown.m b/@MyZiRingdown/MyZiRingdown.m index a39f5a1..8ef8aac 100644 --- a/@MyZiRingdown/MyZiRingdown.m +++ b/@MyZiRingdown/MyZiRingdown.m @@ -1,895 +1,879 @@ % Class for performing ringdown measurements of mechanical oscillators % using Zurich Instruments UHF or MF lock-in. % % Operation: sweep the driving tone (drive_osc) using the sweep module % in LabOne web user interface, when the magnitude of the demodulator % signal exceeds trig_threshold the driving tone is switched off and % the recording of demodulated signal is started, the signal is recorded % for the duration of record_time. % % Features: % Adaptive measurement oscillator frequency % Averaging % % Auto saving % % Auxiliary output signal: If enable_aux_out=true % then after a ringdown is started a sequence of pulses is applied % to the output consisting of itermittent on and off periods % starting from on. -classdef MyZiRingdown < handle +classdef MyZiRingdown < MyDataSource properties (Access=public) % Ringdown is recorded if the signal in the triggering demodulation % channel exceeds this value trig_threshold=1e-3 % V % Duration of the recorded ringdown record_time=1 % (s) % If enable_trig is true, then the drive is on and the acquisition % of record is triggered when signal exceeds trig_threshold enable_trig=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 n_avg=1 % number of ringdowns to be averaged auto_save=false % if all ringdowns should be automatically saved % In adaptive measurement oscillator mode the oscillator frequency % is continuously changed to follow the signal frequency during % ringdown acquisition. This helps against the oscillator frequency % drift. adaptive_meas_osc=false end % The properties which are read or set only once during the class % initialization - properties (GetAccess=public, SetAccess={?MyClassParser,?MyZiRingdown}) - name='ziRingdown' - + properties (GetAccess=public, SetAccess={?MyClassParser}) dev_serial='dev4090' % enumeration for demodulators, oscillators and output starts from 1 demod=1 % demodulator used for both triggering and measurement % Enumeration in the node structure starts from 0, so, for example, % the default path to the trigger demodulator refers to the % demodulator #1 demod_path='/dev4090/demods/0' drive_osc=1 meas_osc=2 % Signal input, integers above 1 correspond to main inputs, aux % input etc. (see the user interface for device-specific details) signal_in=1 drive_out=1 % signal output used for driving % Number of an auxiliary channel used for the output of triggering % signal, primarily intended to switch the measurement apparatus % off during a part of the ringdown and thus allow for free % evolution of the oscillator during that period. aux_out=1 % Device clock frequency, i.e. the number of timestamps per second clockbase % The string that specifies the device name as appears % in the server's node tree. Can be the same as dev_serial. dev_id % Device information string containing the data returned by % ziDAQ('discoveryGet', ... idn_str % Poll duration of 1 ms practically means that ziDAQ('poll', ... % returns immediately with the data accumulated since the % previous function call. poll_duration=0.001 % s poll_timeout=50 % ms % Margin for adaptive oscillator frequency adjustment - oscillator % follows the signal if the dispersion of frequency in the % demodulator band is below ad_osc_margin times the demodulation % bandwidth (under the condition that adaptive_meas_osc=true) ad_osc_margin=0.1 end % Internal variables properties (GetAccess=public, SetAccess=protected) recording=false % true if a ringdown is being recorded % true if adaptive measurement oscillator mode is on and if the % measurement oscillator is actually actively following. ad_osc_following=false % Reference timestamp at the beginning of measurement record. % Stored as uint64. t0 elapsed_t=0 % Time elapsed since the last recording was started - Trace % MyTrace object storing the ringdown + DemodSpectrum % MyTrace object to store FFT of the demodulator data end % Setting or reading the properties below automatically invokes % communication with the device properties (Dependent=true) drive_osc_freq meas_osc_freq drive_on % true when the dirive output is on % demodulator sampling rate (as transferred to the computer) demod_rate % The properties below are only used within the program to display % the information about device state. drive_amp % (V), peak-to-peak amplitude of the driving tone lowpass_order % low-pass filter order lowpass_bw % low-pass filter bandwidth end % Other dependent variables that are dont device properties properties (Dependent=true) % Downsample the measurement record to reduce the amount of data % while keeping the large demodulation bandwidth. % (samples/s), sampling rate of the trace after avraging downsampled_rate % number of the oscillator presently in use with the demodulator current_osc % true/false, true if the signal output from aux out is in on state aux_out_on % Provides public access to the average counter of private AvgTrace n_avg_completed fft_rbw % resolution bandwidth of fft end % Keeping handle objects fully private is the only way to restrict set % access to their properties properties (Access=private) PollTimer AuxOutOffTimer % Timer responsible for switching the aux out off AuxOutOnTimer % Timer responsible for switching the aux out on % Demodulator samples z(t) stored to continuously calculate % spectrum, values of z are complex here, z=x+iy. % osc_freq is the demodulation frequency DemodRecord=struct('t',[],'z',[],'osc_freq',[]) AvgTrace % MyAvgTrace object used for averaging ringdowns end events - % Event for communication with Daq that signals the acquisition of - % a new ringdown - NewData % New demodulator samples received NewDemodSample % Device settings changed, used mostly for syncronization with Gui NewSetting end methods (Access=public) %% Constructor and destructor function this = MyZiRingdown(dev_serial, varargin) P=MyClassParser(this); % Poll timer period addParameter(P,'poll_period',0.1,@isnumeric); - processInputs(P, varargin{:}); + processInputs(P, this, varargin{:}); % Create and configure trace objects - this.Trace=MyTrace(... - 'name_x','Time',... - 'unit_x','s',... - 'name_y','Magnitude r',... - 'unit_y','V'); + % Trace is inherited from the superclass + this.Trace.name_x='Time'; + this.Trace.unit_x='s'; + this.Trace.name_x='Magnitude r'; + this.Trace.unit_x='V'; + this.DemodSpectrum=MyTrace(... 'name_x','Frequency',... 'unit_x','Hz',... 'name_y','PSD',... 'unit_y','V^2/Hz'); + this.AvgTrace=MyAvgTrace(); % Set up the poll timer. Using a timer for anyncronous % data readout allows to use the wait time for execution % of other programs. % Fixed spacing is preferred as it is the most robust mode of % operation when keeping the intervals between callbacks % precisely defined is not the biggest concern. this.PollTimer=timer(... 'ExecutionMode','fixedSpacing',... 'Period',P.Results.poll_period,... 'TimerFcn',@(~,~)pollTimerCallback(this)); % Aux out timers use fixedRate mode for more precise timing. % The two timers are executed periodically with a time lag. % The first timer switches the auxiliary output off this.AuxOutOffTimer=timer(... 'ExecutionMode','fixedRate',... 'TimerFcn',@(~,~)auxOutOffTimerCallback(this)); % The second timer switches the auxiliary output on this.AuxOutOnTimer=timer(... 'ExecutionMode','fixedRate',... 'TimerFcn',@(~,~)auxOutOnTimerCallback(this)); % Check the ziDAQ MEX (DLL) and Utility functions can be found in Matlab's path. if ~(exist('ziDAQ', 'file') == 3) && ~(exist('ziCreateAPISession', 'file') == 2) fprintf('Failed to either find the ziDAQ mex file or ziDevices() utility.\n') fprintf('Please configure your path using the ziDAQ function ziAddPath().\n') fprintf('This can be found in the API subfolder of your LabOne installation.\n'); fprintf('On Windows this is typically:\n'); fprintf('C:\\Program Files\\Zurich Instruments\\LabOne\\API\\MATLAB2012\\\n'); return end % Do not throw errors in the constructor to allow creating an % instance when the physical device is disconnected try % Create an API session and connect to the correct Data % Server. This is a high level function that uses % ziDAQ('connect',.. and ziDAQ('connectDevice', ... when % necessary apilevel=6; [this.dev_id,~]=ziCreateAPISession(dev_serial, apilevel); % Read the divice clock frequency this.clockbase = ... double(ziDAQ('getInt',['/',this.dev_id,'/clockbase'])); catch ME warning(ME.message) end end function delete(this) % delete function should never throw errors, so protect % statements with try-catch try stopPoll(this) catch warning(['Could not usubscribe from the demodulator ', ... 'or stop the poll timer.']) end % Delete timers to prevent them from running indefinitely in % the case of program crash try delete(this.PollTimer) catch warning('Could not delete the poll timer.') end try stop(this.AuxOutOffTimer); delete(this.AuxOutOffTimer); catch warning('Could not stop and delete AuxOutOff timer.') end try stop(this.AuxOutOnTimer); delete(this.AuxOutOnTimer); catch warning('Could not stop and delete AuxOutOn timer.') end end %% Other methods function startPoll(this) % Configure the oscillators, demodulator and driving output % -1 accounts for the difference in enumeration conventions % in the software names (starting from 1) and node numbers % (starting from 0) 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); this.drive_on=true; % By convention, we start form 'enable_trig=false' state this.enable_trig=false; % Configure the auxiliary trigger output - put it in the manual % mode so it does not output demodulator readings path=sprintf('/%s/auxouts/%i/outputselect', ... this.dev_id, this.aux_out-1); ziDAQ('setInt', path, -1); % The convention is that aux out is on by default this.aux_out_on=true; % Subscribe to continuously receive samples from the % demodulator. Samples accumulated between timer callbacks % will be read out using ziDAQ('poll', ... ziDAQ('subscribe',[this.demod_path,'/sample']); % Start continuous polling start(this.PollTimer) end function stopPoll(this) stop(this.PollTimer) ziDAQ('unsubscribe',[this.demod_path,'/sample']); end % Main function that polls data from the device demodulator function pollTimerCallback(this) % ziDAQ('poll', ... with short poll_duration returns % immediately with the data accumulated since the last timer % callback Data = ziDAQ('poll', this.poll_duration, this.poll_timeout); if ziCheckPathInData(Data, [this.demod_path,'/sample']) % Demodulator returns data DemodSample= ... Data.(this.dev_id).demods(this.demod).sample; % Append new samples to the record and recalculate spectrum appendSamplesToBuff(this, DemodSample); calcfft(this); if this.recording % If recording is under way, append the new samples to % the trace rec_finished = appendSamplesToTrace(this, DemodSample); % Update elapsed time this.elapsed_t=this.Trace.x(end); % If the adaptive measurement frequency mode is on, % update the measurement oscillator frequency. % Make sure that the demodulator record actually % contains signal by comparing the dispersion of % frequency to demodulator bandwidth. if this.adaptive_meas_osc [df_avg, df_dev]=calcfreq(this); if df_dev < this.ad_osc_margin*this.lowpass_bw this.meas_osc_freq=df_avg; % Change indicator this.ad_osc_following=true; else this.ad_osc_following=false; end else this.ad_osc_following=false; end else r=sqrt(DemodSample.x.^2+DemodSample.y.^2); if this.enable_trig && max(r)>this.trig_threshold % Start acquisition of a new trace if the maximum % of the signal exceeds threshold this.recording=true; % Find index at which the threshold was % exceeded ind0=find(r>this.trig_threshold,1,'first'); this.t0=DemodSample.timestamp(ind0); this.elapsed_t=0; % Switch the drive off this.drive_on=false; % Set the measurement oscillator frequency to be % the frequency at which triggering occurred this.meas_osc_freq=this.drive_osc_freq; % Switch the oscillator this.current_osc=this.meas_osc; % Optionally start the auxiliary output timers if this.enable_aux_out % Configure measurement periods and delays T=this.aux_out_on_t+this.aux_out_off_t; this.AuxOutOffTimer.Period=T; this.AuxOutOnTimer.Period=T; this.AuxOutOffTimer.startDelay=... this.aux_out_on_t; this.AuxOutOnTimer.startDelay=T; % Start timers start(this.AuxOutOffTimer) start(this.AuxOutOnTimer) end % Clear trace and append new data starting from the % index, at which triggering occurred clearData(this.Trace); rec_finished = ... appendSamplesToTrace(this, DemodSample, ind0); else rec_finished=false; end % Indicator for adaptive measurement is off, since % recording is not under way this.ad_osc_following=false; end notify(this,'NewDemodSample'); % Stop recording if a record was completed if rec_finished % stop recording this.recording=false; this.ad_osc_following=false; % Stop auxiliary timers stop(this.AuxOutOffTimer); stop(this.AuxOutOnTimer); % Return the drive and aux out to the default state this.aux_out_on=true; this.current_osc=this.drive_osc; this.drive_on=true; % Downsample the trace to reduce the amount of data downsample(this.Trace, this.downsample_n, 'avg'); % Do trace averaging addAverage(this.AvgTrace, this.Trace); + triggerNewData(this, 'save', this.auto_save); + % If the ringdown averaging is complete, disable % further triggering to exclude data overwriting if this.AvgTrace.avg_count>=this.n_avg this.enable_trig=false; this.Trace.x=this.AvgTrace.x; this.Trace.y=this.AvgTrace.y; + % Trigger one more time to transfer the average + % trace. New measurement header is not necessary as + % the delay since the last triggering is minimum. + triggerNewData(this, 'save', this.auto_save, ... + 'new_header', false); end - - triggerNewData(this, 'save', this.auto_save); end end end % Append timestamps vs r=sqrt(x^2+y^2) to the measurement record. % Starting index can be supplied as varargin. % The output variable tells if the record is finished. function isfin = appendSamplesToTrace(this, DemodSample, varargin) if isempty(varargin) startind=1; else startind=varargin{1}; end r=sqrt(DemodSample.x(startind:end).^2 + ... DemodSample.y(startind:end).^2); % Subtract the reference time, convert timestamps to seconds ts=double(DemodSample.timestamp(startind:end) -... this.t0)/this.clockbase; % Check if recording should be stopped isfin=(ts(end)>=this.record_time); if isfin % Remove excess data points from the new data ind=(tsflen this.DemodRecord.t = this.DemodRecord.t(end-flen+1:end); this.DemodRecord.z = this.DemodRecord.z(end-flen+1:end); end end function calcfft(this) flen=min(this.fft_length, length(this.DemodRecord.t)); [freq, spectr]=xyFourier( ... this.DemodRecord.t(end-flen+1:end), ... this.DemodRecord.z(end-flen+1:end)); this.DemodSpectrum.x=freq; this.DemodSpectrum.y=abs(spectr).^2; end % Calculate the average frequency and dispersion of the demodulator % signal function [f_avg, f_dev]=calcfreq(this) if ~isempty(this.DemodSpectrum) norm=sum(this.DemodSpectrum.y); % Calculate the center frequency of the spectrum f_avg=dot(this.DemodSpectrum.x, this.DemodSpectrum.y)/norm; % Shift the FFT center by the demodulation frequency to % output absolute value f_avg=f_avg+mean(this.DemodRecord.osc_freq); f_dev=sqrt(dot(this.DemodSpectrum.x.^2, ... this.DemodSpectrum.y)/norm-f_avg^2); else f_avg=[]; f_dev=[]; end end % Provide restricted access to private AvgTrace function resetAveraging(this) resetCounter(this.AvgTrace); notify(this,'NewSetting'); end function str=idn(this) DevProp=ziDAQ('discoveryGet', this.dev_id); str=this.dev_id; if isfield(DevProp, 'devicetype') str=[str,'; device type: ', DevProp.devicetype]; end if isfield(DevProp, 'options') % Print options from the list as comma-separated values and % discard the last comma. opt_str=sprintf('%s,',DevProp.options{:}); str=[str,'; options: ', opt_str(1:end-1)]; end if isfield(DevProp, 'serverversion') str=[str,'; server version: ', DevProp.serverversion]; end this.idn_str=str; end - function triggerNewData(this, varargin) - EventData = MyNewDataEvent(varargin{:}); - EventData.Instr=this; - notify(this,'NewData',EventData); - end - function auxOutOffTimerCallback(this) this.aux_out_on=false; end function auxOutOnTimerCallback(this) this.aux_out_on=true; end %% measurement header functionality function Hdr=readHeader(this) Hdr=MyMetadata(); % name is always a valid variable as ensured by its set method addField(Hdr, this.name); % Instrument identification addParam(Hdr, this.name, 'idn', this.idn_str); addClassParam(this, Hdr, 'clockbase', 'comment', ... ['Device clock frequency, i.e. the number of ', ... 'timestamps per second']); % Demodulator parameters addClassParam(this, Hdr, 'demod', 'comment', ... 'Number of the demodulator in use (starting from 1)'); addClassParam(this, Hdr, 'demod_rate', 'comment', ... '(samples/s), demodulator data transfer rate'); addClassParam(this, Hdr, 'lowpass_order', 'comment', ... 'Order of the demodulator low-pass filter'); addClassParam(this, Hdr, 'lowpass_bw', 'comment', ... ['(Hz), 3 dB bandwidth of the demodulator low-pass ', ... 'filter']); addClassParam(this, Hdr, 'meas_osc', 'comment', ... 'Measurement oscillator number'); addClassParam(this, Hdr, 'meas_osc_freq', 'comment', '(Hz)'); % Signal input addClassParam(this, Hdr, 'signal_in', 'comment', ... 'Singnal input number'); % Drive parameters addClassParam(this, Hdr, 'drive_out', 'comment', ... 'Driving output number'); addClassParam(this, Hdr, 'drive_osc', 'comment', ... 'Swept oscillator number'); addClassParam(this, Hdr, 'drive_amp', 'comment', ... '(V) peak to peak'); % Parameters of the auxiliary output addClassParam(this, Hdr, 'aux_out', 'comment', ... 'Auxiliary output number'); addClassParam(this, Hdr, 'enable_aux_out', 'comment', ... 'Auxiliary output is applied during ringdown'); addClassParam(this, Hdr, 'aux_out_on_lev', 'comment', '(V)'); addClassParam(this, Hdr, 'aux_out_off_lev', 'comment', '(V)'); addClassParam(this, Hdr, 'aux_out_on_t', 'comment', '(s)'); addClassParam(this, Hdr, 'aux_out_off_t', 'comment', '(s)'); % Software parameters addClassParam(this, Hdr, 'trig_threshold', 'comment', ... '(V), threshold for starting a ringdown record'); addClassParam(this, Hdr, 'record_time', 'comment', '(s)'); addClassParam(this, Hdr, 'n_avg', 'comment', ... 'Number of ringdowns to be averaged'); addClassParam(this, Hdr, 'downsampled_rate', 'comment', ... ['(samples/s), rate to which a ringown trace is ', ... 'downsampled with averaging after acquisition']); addClassParam(this, Hdr, 'auto_save', 'comment', '(s)'); % Adaptive measurement oscillator addClassParam(this, Hdr, 'adaptive_meas_osc', 'comment', ... ['If true the measurement oscillator frequency is ', ... 'adjusted during ringdown']); addClassParam(this, Hdr, 'ad_osc_margin'); addClassParam(this, Hdr, 'fft_length', 'comment', '(points)'); % Timer poll parameters addParam(Hdr, this.name, 'PollTimer.Period', ... this.PollTimer.Period, 'comment', '(s)'); addClassParam(this, Hdr, 'poll_duration', 'comment', '(s)'); addClassParam(this, Hdr, 'poll_timeout', 'comment', '(ms)'); end % The function below ensures the correspondence between the header % parameter names and class property names. It spares quite a few % lines of code given the large size of readHeader function. function addClassParam(this, Hdr, tag, varargin) addParam(Hdr, this.name, tag, this.(tag), varargin{:}); end end %% Set and get methods. methods - % Ensures that the instrument name is a valid Matlab variable - function set.name(this, str) - assert(ischar(str), ['The value assigned to ''name'' ' ... - 'property must be char']) - if ~isempty(str) - str=matlab.lang.makeValidName(str); - else - str=class(this); - end - this.name=str; - end - function freq=get.drive_osc_freq(this) path=sprintf('/%s/oscs/%i/freq', this.dev_id, this.drive_osc-1); freq=ziDAQ('getDouble', path); end function set.drive_osc_freq(this, val) assert(isfloat(val), ... 'Oscillator frequency must be a floating point number') path=sprintf('/%s/oscs/%i/freq', this.dev_id, this.drive_osc-1); ziDAQ('setDouble', path, val); notify(this,'NewSetting'); end function freq=get.meas_osc_freq(this) path=sprintf('/%s/oscs/%i/freq', this.dev_id, this.meas_osc-1); freq=ziDAQ('getDouble', path); end function set.meas_osc_freq(this, val) assert(isfloat(val), ... 'Oscillator frequency must be a floating point number') path=sprintf('/%s/oscs/%i/freq', this.dev_id, this.meas_osc-1); ziDAQ('setDouble', path, val); notify(this,'NewSetting'); end function set.drive_on(this, val) path=sprintf('/%s/sigouts/%i/on',this.dev_id,this.drive_out-1); % Use double() to convert from logical type ziDAQ('setInt', path, double(val)); notify(this,'NewSetting'); end function bool=get.drive_on(this) path=sprintf('/%s/sigouts/%i/on',this.dev_id,this.drive_out-1); bool=logical(ziDAQ('getInt', path)); end function set.current_osc(this, val) assert((val==this.drive_osc) || (val==this.meas_osc), ... ['The number of current oscillator must be that of ', ... 'the drive or measurement oscillator, not ', num2str(val)]) ziDAQ('setInt', [this.demod_path,'/oscselect'], val-1); notify(this,'NewSetting') end function osc_num=get.current_osc(this) osc_num=double(ziDAQ('getInt', ... [this.demod_path,'/oscselect']))+1; end function amp=get.drive_amp(this) path=sprintf('/%s/sigouts/%i/amplitudes/%i', ... this.dev_id, this.drive_out-1, this.drive_osc-1); amp=ziDAQ('getDouble', path); end function set.drive_amp(this, val) path=sprintf('/%s/sigouts/%i/amplitudes/%i', ... this.dev_id, this.drive_out-1, this.drive_osc-1); ziDAQ('setDouble', path, val); notify(this,'NewSetting'); end function set.lowpass_order(this, val) assert(any(val==[1,2,3,4,5,6,7,8]), ['Low-pass filter ', ... 'order must be an integer between 1 and 8']) ziDAQ('setInt', [this.demod_path,'/order'], val); notify(this,'NewSetting'); end function n=get.lowpass_order(this) n=ziDAQ('getInt', [this.demod_path,'/order']); end function bw=get.lowpass_bw(this) tc=ziDAQ('getDouble', [this.demod_path,'/timeconstant']); bw=ziBW2TC(tc, this.lowpass_order); end function set.lowpass_bw(this, val) tc=ziBW2TC(val, this.lowpass_order); ziDAQ('setDouble', [this.demod_path,'/timeconstant'], tc); notify(this,'NewSetting'); end function rate=get.demod_rate(this) rate=ziDAQ('getDouble', [this.demod_path,'/rate']); end function set.demod_rate(this, val) ziDAQ('setDouble', [this.demod_path,'/rate'], val); notify(this,'NewSetting'); end function set.downsample_n(this, val) n=round(val); assert(n>=1, ['Number of points for trace averaging must ', ... 'be greater than 1']) this.downsample_n=n; notify(this,'NewSetting'); end function set.aux_out_on(this, bool) path=sprintf('/%s/auxouts/%i/offset', ... this.dev_id, this.aux_out-1); if bool out_offset=this.aux_out_on_lev; else out_offset=this.aux_out_off_lev; end ziDAQ('setDouble', path, out_offset); end function bool=get.aux_out_on(this) path=sprintf('/%s/auxouts/%i/offset', ... this.dev_id, this.aux_out-1); val=ziDAQ('getDouble', path); % Signal from the auxiliary output is continuous, we make the % binary decision about the output state depending on if % the signal is closer to the ON or OFF level bool=(abs(val-this.aux_out_on_lev) < ... abs(val-this.aux_out_off_lev)); end function set.downsampled_rate(this, val) dr=this.demod_rate; if val>dr % Downsampled rate should not exceed the data transfer rate val=dr; end % Round so that the averaging is done over an integer number of % points this.downsample_n=round(dr/val); notify(this,'NewSetting'); end function val=get.downsampled_rate(this) val=this.demod_rate/this.downsample_n; end function set.fft_length(this, val) if val<1 val=1; end % Round val to the nearest 2^n to make the calculation of % Fourier transform efficient n=round(log2(val)); this.fft_length=2^n; notify(this,'NewSetting'); end function val=get.fft_rbw(this) val=this.demod_rate/this.fft_length; end function set.fft_rbw(this, val) assert(val>0,'FFT resolution bandwidth must be greater than 0') % Rounding of fft_length to the nearest integer is handled by % its own set method this.fft_length=this.demod_rate/val; notify(this,'NewSetting'); end function set.n_avg(this, val) % Number of averages needs to be integer and greater than one if val<1 val=1; end this.n_avg=round(val); notify(this,'NewSetting'); end function val=get.n_avg_completed(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 end end