Problem: I have a MATLAB App Designer app that processes a single input file (containing recorded signals in my case) to produce various output plots. The data processing pipeline comprises several stages, each of which can produce intermediate data, an indication of success and a plot; each can take significant time to execute. Each stage is controlled by a number of settings that are specified by GUI controls. When an output plot is requested, typically by the push of a button, all the stages needed to produce the data to make that plot are executed. However, if stages have already been executed when a plot is requested, they do not need to be executed again - unless any settings relevant to that stage have changed since last execution.
So, for example, the first stage is normally reading the file from disk. If the file has already been read, it would waste time to read it again - unless the name of the file had changed. Here, file name can be considered a 'setting'. The next stage could be some fancy kind of filtering controlled by some more settings/controls. Its input would be the data in the file read by the previous stage and its output would be filtered data. And so on down the chain or, more generally, directed graph.
Question: What is a correct, elegant, readable and maintainable (assuming it can be all of these!) way to implement this design paradigm in MATLAB?
The way I have done this is by associating strings that name execution stages with strings that name structure fields containing values of settings. The relationship between settings (GUI control values) and execution stages is specified in function CheckUIChanges.
function PlotValuesButtonPushed(app, event)
% The following 4 stages need to be executed in the order specified to produce
% the output to be plotted
app.ExecuteProcessingStages('ExtractData DetectMarkers BlankStimuli FindPeaks');
if ~app.ErrorOccurred
msg = PlotValues(app.PeakTimes, app.Settings);
app.LogText(msg);
end
end
function ExecuteProcessingStages(app, stages)
app.CheckUIChanges;
app.ErrorOccurred = 0;
stages = split(stages, ' ');
for i = 1:numel(stages)
stage = stages{i};
switch stage
case 'DetectMarkers' % One stage of several
if app.ProcessingState.DetectMarkers ~= app.STATUS_SUCCESS
app.DetectMarkersLamp.Color = app.COLOR_BUSY;
% DetectMarkers function processes data
% 'Markers' and a message 'msg' from input 'Traces' using 'Settings'
[app.Markers, msg] = DetectMarkers(app.Traces, app.Settings);
app.ProcessingState.DetectMarkers = app.CheckData(app.Markers);
app.LogText(msg);
if app.ProcessingState.DetectMarkers == app.STATUS_ERROR
app.ErrorOccurred = 1;
break
end
end
% ... as above for each execution stage
end
app.UpdateProcessingState;
end
function CheckUIChanges(app)
app.GetComponentSettings;
% ...
settings = 'minstiminterval';
if app.FieldsChanged(settings)
% These 3 stages depend on 'minstiminterval'
app.ResetStateFields('DetectMarkers BlankStimuli FindPeaks');
end
%
settings = 'channelthreshold thresholdamppc markeroffset';
% Stages 'DetectMarkers' and 'FindPeaks' depend on the 3 settings above
if app.FieldsChanged(settings)
app.ResetStateFields('DetectMarkers FindPeaks');
end
% ... and so on for all settings associated with execution stages
% Update GUI indicators (Lamp controls) with status
app.UpdateProcessingState;
end
function GetComponentSettings(app)
% ...
app.Settings.minstiminterval = double(app.MinStimIntervalEditField.Value);
app.Settings.channelthreshold = app.ChThresholdPeakDropDown.Value;
% ...
end
function ResetStateFields(app, names)
names = split(names, ' ');
for i = 1:numel(names)
name = names{i};
app.ProcessingState.(name) = app.STATUS_EMPTY;
end
end
function changed = FieldsChanged(app, names)
changed = 0;
names = split(names, ' ');
for i = 1:numel(names)
name = names{i};
if ~strcmp(num2str(app.Settings.(name)), num2str(app.SavedSettings.(name)))
changed = 1;
break
end
end
end
function status = CheckData(app, data)
if ~isempty(data)
status = app.STATUS_SUCCESS;
else
status = app.STATUS_ERROR;
end
end
function UpdateProcessingState(app)
% Lamps show state of stage, one of:
% STATUS_EMPTY / idle - grey
% STATUS_BUSY / executing - yellow
% STATUS_SUCCESS - green
% STATUS_ERROR - red
app.UpdateProcessingStateLamps;
app.GetComponentSettings;
app.SavedSettings = app.Settings;
end
There is unwelcome redundancy in my scheme, because I have to list the stages affected by a change in control setting value (in CheckUIChanges) in addition to specifying which execution stages depend on which other execution stages (in the calls to ExecuteProcessingStages). It works, but is there a better way to achieve the same result?