0

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?

  • I would attach a boolean flag (`dirty`) for each setting, and keep them all in an array (or a structure if you prefer named/string parameters. As soon as the user change a setting in the app, the relevant setting flag is marked `dirty`. When the times come to produce the plot, you can quickly iterate through the flag array to know which stage has to be recalculated, if any. Changing some specific settings may have to mark several other settings flags `dirty` (I guess changing the base filename would have to mark everything else `dirty` for example). – Hoki Nov 16 '20 at 11:38
  • The dirty flags are calculated when as are needed, by comparing settings current component values stored in structure Settings against saved values in SavedSettings. That's the job of GetComponentSettings. This has to be edited whenever components are changed/added to ensure all settings are accounted for, but at least this 'manual coding' (with the extra work that entails and opportunities for error) is localized in one place and not repeated. The alternative of creating a callback function for every relevant change in component state would be very long-winded and entail more manual effort. – Francis Burton Nov 17 '20 at 09:30
  • How the dirty flags are acted on is a separate issue. I think what I should be doing is creating a dependency graph in one place, rather than what I am doing at the moment which is specifying the dependencies multiple times in the calls to ExecuteProcessingStages. I'd like to know a) whether that is a useful goal and, if so, b) how to implement it cleanly. – Francis Burton Nov 17 '20 at 09:31
  • All the settings don't need to have each a separate callback. They could all call the same dispatcher `SettingsChangedCallback`, which will take actions depending on which setting is modified (and do the `dirty` flagging). – Hoki Nov 17 '20 at 10:34
  • Point taken, thanks. – Francis Burton Nov 17 '20 at 10:36

0 Answers0