Creating analysis functions for PipelineManager

Creating analysis functions for PipelineManager

A compatible analysis function declares what it accepts and produces, then follows that contract at run time. PipelineManager uses the declaration to discover the function, check graph connections, display editable options, plan RAM use, and save outputs.

Choose an authoritative template

TemplateUse it when
funcTemplate.mThe function receives and returns numeric data in RAM.
funcTemplateUMT.mThe function returns a validated UMT structure.
funcTemplateFileManifest.mThe function writes its own files and returns the files actually created.
funcTemplateAcquisitionInitializer.mAn acquisition importer owns AcqInfos.mat and may initialize an unprocessed Save Folder.
funcTemplateAcquisitionCompanion.mA root side-artifact importer accompanies an initializer without owning AcqInfos.mat.

Procedure

  1. Copy the closest template to Analysis/<Category>/YourFunction.m. PipelineManager scans public analysis folders and uses the containing folder as the category.
  2. Rename the MATLAB function, the name passed to createPipelineInfo, descriptions, and every default output filename.
  3. Keep the one-argument 'pipelineInfo' query before normal input parsing.
  4. Declare each positional data input and the supplied Save Folder in the same position as the function signature.
  5. Declare user options as name-value parameter inputs. Keep defaults, accepted values, data types, and runtime validators consistent.
  6. Declare every output with semantic type, output mode (data or file), position, and a unique managed filename where applicable. Decide whether the output is a .dat or a .umt file (see Choosing the output format) and give the filename the matching extension.
  7. Implement the scientific calculation. Resolve required metadata from the concrete file being processed; do not choose an arbitrary file from the Save Folder.
  8. If input dimensions change, explicitly update UMT dimNames, labels, event information, and entry metadata.
  9. Test the function directly, query pipelineInfo, and confirm it appears in Pipeline Manager under the intended category.

Annotated contract excerpts

% Discovery query: preserve this behavior.
if nargin == 1 && strcmpi(string(data),'pipelineInfo')
    outData = localGetPipelineInfo();
    return
end

% supportsFile=true is valid only after filename input is implemented and tested.
info = PipelineManager.addInput(info,'data','ImageTimeSeries', ...
    'Numeric YXT input.','position',1,'isData',true, ...
    'supportsFile',false,'dataMode','ram');

% outputMode='data' means return the value; PipelineManager manages persistence.
info = PipelineManager.addOutput(info,'outData','ImageTimeSeries','data', ...
    'Scaled output.','scaleImage.dat',1,'isData',true);

Choosing the output format

An analysis function's output is saved either as a .dat file or as a .umt file. This section tells you which formats are possible for your output, which one to prefer, and how your function produces each of them.

The format is your decision, and it is final for everyone who runs your function
The people who run a pipeline cannot choose or change the format in which a function saves its output. The format you choose here therefore decides how quickly, and whether, everyone using your function can open and process its results.

The two formats

.dat files. A .dat file is a binary format optimized for large imaging data. It allows data to be read in parts (frames, trials) rather than loaded into memory all at once. This makes it much faster and lighter than .umt for image stacks, and it keeps very large recordings usable even when they exceed the computer's memory.

An output can be saved as .dat only if both conditions are true:

  1. Every dimension of the data is one of the .dat dimensions: Y and X (image rows and columns), T (time), E (events or trials, or one slice per event condition after averaging), and F (frequency).
  2. The data comes from a single recording. A recording created by umIT's concatenation tool counts as a single recording. Data combined across recordings, such as averages across sessions, runs or animals, does not.

If either condition is false, the output must be saved as .umt.

.umt files. A .umt file can hold any of the dimensions listed in Organization of .umt files (for example ROI, Pixel, and Measure, besides Y, X, T, E and F), several named entries, labels, and event information. A .umt file is always read completely into memory.

Recommended use. Raw imaging data imported into umIT is stored as .dat. Functions whose output remains large image data, typically preprocessing steps like filtering or normalization that keep the full image stack, should also save as .dat. Saving such outputs as .umt is possible but can make them slow to open and, for long recordings, impossible to load. .umt remains appropriate for eligible outputs that are small, or that belong together with other results such as ROI labels or related measurements.

Examples

OutputDimensionsRecordingsFormat
Filtered or normalized image stackY, X, Tone.dat or .umt
Trial-sorted image stackY, X, T, Eone.dat or .umt
Trial-averaged responseY, X, Tone.dat or .umt
Pixel-wise power spectrumY, X, Fone.dat or .umt
Correlation map from a single seedY, Xone.dat or .umt
Correlation maps from several seedsY, X, Seedone.umt only
ROI time coursesROI, Tone.umt only
Average image stack across animalsY, X, Tseveral.umt only

A seed is not a .dat dimension, so maps from several seeds cannot be stored in one .dat file. The toolbox's own genCorrelationMatrix stores them in one .umt file with one Y-X entry per seed.

Technical rules for .dat output

  • The axes of a .dat file always start with Y and X, followed by those of T, E and F that the data have, always in the order T, E, F. Examples of valid layouts are Y-X, Y-X-T, Y-X-E, Y-X-F and Y-X-T-E.
  • Values are saved in single precision. saveData refuses any other numeric class, so convert the result with single before returning or saving it.
  • Data with a T axis need a frame rate, which the file stores in its header. Data without a T axis store none.
  • Not every tool opens every layout. DataViewer currently opens .dat files with the axes Y-X, Y-X-T, Y-X-E and Y-X-T-E; it does not open a Y-X-F .dat file. Check the layout against the tools your users will use before choosing .dat for such an output.

Event-split data in .dat files

An event-split .dat file stores no event labels. Its E axis is matched to the recording's events.mat by size: one slice per event instance, or one slice per condition after averaging. For that matching to work, a function that saves an average over events as .dat must:

  • keep one slice for every condition, using NaN when all of its events were excluded;
  • order the slices by each condition's first appearance in time.

Do not implement these rules yourself. The toolbox's aggregation helpers, EventsManager.conditionAggregationPlan and EventsManager.reduceByCondition, apply them; use them to reduce data over E (see their help text, help EventsManager.conditionAggregationPlan). Event information of .umt files is described in Organization of .umt files.

How a function saves each format

A function does not choose the format with a setting: the format follows from what the function produces. A function whose output has output mode data returns the value, and PipelineManager saves it with the toolbox function saveData, which writes a single-precision numeric array as .dat and a UMT structure as .umt. If the file extension in the declared default filename differs from the format actually written, PipelineManager keeps the real one, but declare the matching extension so that the pipeline description is accurate.

To save a ...Return ...Declare the default filename as ...
.dat filea single-precision numeric array (or, for a function that streams its output to disk, the name of the .dat file it wrote)'myOutput.dat'
.umt filea UMT structure made with genUMTStruct'myOutput.umt'
% .dat: return the array. See Analysis/funcTemplate.m.
outData = single(result);                       % PipelineManager saves it as myOutput.dat

% .umt: return a validated UMT structure. See Analysis/funcTemplateUMT.m.
outData = genUMTStruct(result, 'kind', 'image', 'entryName', 'MeanImage', ...
    'dimNames', {'Y','X'});                     % PipelineManager saves it as myOutput.umt

When PipelineManager saves a returned array as .dat, it takes the axis names from the metadata of the input data if the output has the same layout, and otherwise from the declared semantic type of the output: ImageTimeSeries gives Y-X-T for a three-dimensional array and Y-X-T-E for a four-dimensional one; Image gives Y-X for a two-dimensional array and Y-X-E for a three-dimensional one. Other layouts, such as Y-X-F, cannot be inferred. In that case write the file yourself with saveData and name the axes.

A function can also write files itself, for example in file mode or to save an additional result. Use the same saveData function:

% .dat: the axes are always named by you ('DimNames' is required).
saveData(fullfile(SaveFolder, 'myOutput.dat'), single(result), ...
    'DimNames', {'Y','X','T'}, 'FrameRateHz', frameRateHz);

% .umt: pass a UMT structure. It is validated before it is written.
saveData(fullfile(SaveFolder, 'myOutput.umt'), umtStruct);

Instead of 'FrameRateHz' you can pass 'Info', info, the metadata of the input file returned by loadMetaData, so that the output inherits its frame rate and exposure. Data with a T axis and neither of the two are refused (Umitoolbox:saveData:missingFrameRate).

A .dat output that is too large for the computer's memory cannot be built as one array. Write it in pieces instead, with spatialSlabIO('create'), 'write' and 'finalize', and return the name of the file. Analysis/Filters/normalizeZScore.m and Analysis/Imaging/split_data_by_event.m are working examples. The functions for describing, reading, and writing .dat files are explained in Reading and writing .dat files.

Initialize a fresh Save Folder

PipelineManager normally requires current acquisition metadata before executing a pipeline. A function can opt into the fresh-folder import gate by declaring one of two narrowly scoped roles. Ordinary analysis functions keep the default freshSaveFolderRole='none'.

RoleContract
acquisition-initializerA root acquisition importer that writes authoritative image data and a valid current-schema AcqInfos.mat. It must derive metadata from raw acquisition facts.
acquisition-companionA root importer for an acquisition side artifact, such as events. It may run on a fresh folder only with one successful initializer and must not create or repair AcqInfos.mat.
noneThe default for processing, export, report, and other pipeline functions. It does not make a fresh folder eligible.
% In the acquisition importer:
info = PipelineManager.createPipelineInfo('importMyAcquisition', ...
    'Import images and acquisition metadata.');
info.freshSaveFolderRole = 'acquisition-initializer';

% In a separate side-artifact function:
info = PipelineManager.createPipelineInfo('importMyEvents', ...
    'Import acquisition events.');
info.freshSaveFolderRole = 'acquisition-companion';

PipelineManager treats an existing Save Folder as fresh when it contains no AcqInfos.mat. Other files do not change this: raw acquisition files and subfolders are expected, so the same directory may be supplied as both Raw Folder and Save Folder, and leftovers of a failed import may be overwritten by the importer. The template's example importer is stricter than PipelineManager: it refuses a Save Folder that already holds .dat, .umt, .roi or .umitlink files, AcqInfos.mat, DataParams.mat, events.mat, legacy text events, processing history, pipeline logs, or a project ledger (UMITProjectLedger.json). The toolbox's own importers instead treat an import into a used Save Folder as a fresh import (see ImagesClassification).

The pipeline DAG must contain exactly one root initializer. Companion roots are permitted in that DAG, and every other stream node must descend from the initializer. PipelineManager runs the initializer first and checks that it created valid current metadata before running companions or downstream analysis. If the initializer fails or leaves missing or legacy metadata, the remaining nodes do not run.

A Save Folder whose AcqInfos.mat exists but is invalid or uses an older schema is not fresh: PipelineManager blocks it as a legacy-schema folder (PipelineManager:executePipeline:LegacySchema). PipelineManager never creates, migrates, or repairs acquisition metadata. Older saved pipelines that lack the role field use the callable function's current declaration, while an absent or unknown role remains ineligible.

Ownership checklist for fresh imports

  • The initializer alone creates AcqInfos.mat, after successfully importing enough data to describe it.
  • The initializer returns success only when AcqInfos.mat contains a scalar AcqInfoStream with the current ImportedChannels registry.
  • A companion creates only its declared side artifact. For example, EventsManager owns events.mat.
  • Do not use either role to bypass metadata validation for analysis, export, or reporting functions.

Complete worked example

function outData = scaleImage(data,SaveFolder,varargin)
if nargin == 1 && strcmpi(string(data),'pipelineInfo')
    info = PipelineManager.createPipelineInfo('scaleImage', ...
        'Scale a YXT image time series.');
    info = PipelineManager.addInput(info,'data','ImageTimeSeries', ...
        'Numeric YXT input.','position',1,'callType','positional', ...
        'isData',true,'supportsFile',false,'dataMode','ram');
    info = PipelineManager.addInput(info,'SaveFolder','SaveFolder', ...
        'Current dataset folder.','position',2,'callType','positional', ...
        'isData',false);
    info = PipelineManager.addInput(info,'Scale','parameter', ...
        'Nonnegative scale factor.','kind','parameter', ...
        'callType','namevalue','default',1,'allowed',[0 Inf], ...
        'dataType','double');
    info = PipelineManager.addOutput(info,'outData','ImageTimeSeries', ...
        'data','Scaled YXT output.','scaleImage.dat',1,'isData',true);
    outData = info;
    return
end
p = inputParser;
addRequired(p,'data',@(x)isnumeric(x) && ndims(x)==3);
addRequired(p,'SaveFolder',@isfolder);
addParameter(p,'Scale',1,@(x)isnumeric(x)&&isscalar(x)&&x>=0);
parse(p,data,SaveFolder,varargin{:});
outData = p.Results.data .* cast(p.Results.Scale,'like',p.Results.data);
end

Test before using real data

info = scaleImage('pipelineInfo');
data = single(reshape(1:24,[2 3 4]));
result = scaleImage(data,tempdir,'Scale',2);
assert(isequal(size(result),size(data)));
pm = PipelineManager(tempdir,'Missing');  % then inspect pm.funcList

Updating custom functions for PipelineManager

This section is for you if you wrote an analysis function for an earlier release and PipelineManager no longer lists it. When PipelineManager scans the Analysis folders, it now skips such a function and prints a warning (PipelineManager:createFcnList:NoPipelineInfo). The function itself is not damaged: it has to be rewritten to the current contract, which takes a few well-defined steps.

Why the old style no longer works

  • Functions are no longer recognized by their argument names. Earlier releases identified a function by names in its signature (data, metaData, SaveFolder, RawFolder, opts, outFile) and by the lines default_Output, default_opts and opts_values that PipelineManager read as text. A function is now listed only if it answers the query myFunction('pipelineInfo') with a declaration of its inputs, parameters, and outputs.
  • The metaData argument is gone. The frame rate, the axes, and the exposure describe the data, not the pipeline. A data file now carries them in its own header, and PipelineManager passes them to the functions that ask for them.
  • Metadata are no longer taken from AcqInfos.mat. That file describes the raw acquisition. Imported data can differ from it (for example, temporal binning during import changes the frame rate), so reading the frame rate or image size from it would be a guess.

Update steps

  1. Start from the template that fits your output (see the table above) and move your calculation into it. Use the argument order (data, SaveFolder, ...).
  2. Declare the function with pipelineInfo: PipelineManager.createPipelineInfo, then PipelineManager.addInput for each input and parameter and PipelineManager.addOutput for each output (examples above).
  3. Replace every use of metaData as described in the next two sections.
  4. Replace the opts structure by name-value parameters (default_opts and opts_values become the 'default' and 'allowed' values of addInput), and default_Output by the default filename of addOutput. A function that creates its own files and returned their names in outFile uses an output of mode file (template funcTemplateFileManifest.m). The arguments RawFolder and object have no counterpart in the templates.
  5. Read and write .dat files only with the functions listed under Reading and writing .dat files.
  6. Check the result (Checking the updated function).

Replacing metaData fields

The metadata of a .dat file, as returned by loadMetaData, has a new, fixed set of fields. The old field names no longer exist in it.

Old field of metaDataNowNotes
Freq, FrameRateHzinfo.frameRateHzNaN when the file has no T axis. Inside a function use a 'FrameRateHz' sourceInfo input (see below).
ExposureMsecinfo.exposureMsecNaN when unknown.
dim_namesinfo.dimNamesFor example {'Y','X','T'}. Inside a function use a 'DimNames' sourceInfo input.
HeightdatAxisSize(info, 'Y')Number of image rows.
WidthdatAxisSize(info, 'X')Number of image columns.
Length, datLengthdatAxisSize(info, 'T')Number of frames. The function returns 0 when the file has no such axis.
datSize[datAxisSize(info, 'Y'), datAxisSize(info, 'X')]The full shape of the file is info.dimSizes, in the order of info.dimNames.
Datatypeinfo.dataClassFor example 'single'.

The other fields of the new metadata are filePath, format, dataOffset, channelName, and writeComplete. Read only these fields. For .umt files loadMetaData still returns its own UMT field names; for them, read the entries of the UMT structure itself (see Organization of .umt files).

Receiving frame rate, axes, and exposure: sourceInfo inputs

A function that needs the frame rate, the axis names, or the exposure of its data declares them as sourceInfo inputs. PipelineManager then reads the value from the data that flows into the step and passes it to the function as a name-value pair. These inputs never appear among the options that users edit. Only three exist:

Name-value namesourceField to declareMeaning
'FrameRateHz''frameRateHz'Frame rate of the data in hertz (a positive number).
'DimNames''dimNames'Axis names of the data, such as {'Y','X','T'}.
'ExposureMsec''exposureMsec'Camera exposure in milliseconds.
% In the pipelineInfo block:
info = PipelineManager.addInput(info, 'FrameRateHz', 'sourceInfo', ...
    'Frame rate of the input data (Hz).', ...
    'kind', 'sourceInfo', 'sourceField', 'frameRateHz');

% In the function body (the parameter is declared with addParameter(p,'FrameRateHz',[])):
frameRateHz = resolveDataInfoValue('frameRateHz', p.Results.FrameRateHz, data, mfilename);

resolveDataInfoValue applies one rule everywhere: a value you pass explicitly is used first; otherwise, if data is the name of a .dat file, the value in the file's header is used; otherwise the function stops with an error (Umitoolbox:<function>:missingFrameRateHz for the frame rate) that tells the caller which name-value pair to pass. It never reads AcqInfos.mat. When you call your function directly from MATLAB with an array, pass the value yourself, for example myFunction(data, saveFolder, 'FrameRateHz', 10).

By default ('required' is true) PipelineManager reports an error before the pipeline runs if a sourceInfo value cannot be found in the data. Pass 'required', false for a value that your function can do without. By default the value comes from the first data input; 'sourceInput' selects another data input.

Reading and writing .dat files

A .dat file begins with a header that describes its contents, so it must not be opened with fopen, fread, fseek, or memmapfile. Use only the following functions:

TaskFunction
Describe a file (axes, sizes, class, frame rate, exposure)loadMetaData(file)
Load a whole file into memoryloadData(file)
Read or write a large file in piecesspatialSlabIO ('open', 'read', 'close'; 'create', 'write', 'finalize')
Write a whole array (or append along the last axis)saveData(file, array, 'DimNames', names, ...)
dataFile = fullfile(SaveFolder, dataFileName);        % the file being processed
info = loadMetaData(dataFile);
assertDatLayout(info, { {'Y','X','T'} }, mfilename);  % see "Checking the layout" below
nFrames = datAxisSize(info, 'T');

% Read columns xIdx of all frames (all Y) without loading the whole file ...
slabIn = spatialSlabIO('open', dataFile, 'Info', info);
cIn = onCleanup(@() spatialSlabIO('close', slabIn));
slab = single(spatialSlabIO('read', slabIn, xIdx));              % Y x numel(xIdx) x T
% ... or only some frames: spatialSlabIO('read', slabIn, xIdx, frameIdx)

% Write the result piece by piece. The output inherits axes, sizes, and rate from info.
slabOut = spatialSlabIO('create', outFile, datHeaderFromInfo(info, 'myOutput'));
cOut = onCleanup(@() spatialSlabIO('close', slabOut));
spatialSlabIO('write', slabOut, xIdx, slab);
spatialSlabIO('finalize', slabOut);
  • 'DimNames' is required whenever saveData writes an array. It never guesses the axes. Pass the names of the axes of the array, for example {'Y','X','T'} (see Choosing the output format).
  • The frame rate of an output with a T axis comes from 'FrameRateHz' or from 'Info', the loadMetaData result of the input file. It is never taken from AcqInfos.mat; without either, saveData stops with Umitoolbox:saveData:missingFrameRate.
  • A file may be written only once it is complete: always end a piece-by-piece write with 'finalize'. A file that was only closed is marked incomplete, and loading it prints a warning.
  • Describe every file with loadMetaData, and use the file that belongs to the data being processed, not the first file found in the Save Folder.

Checking the layout

A .dat file can have several axis layouts (Y-X, Y-X-T, Y-X-T-E, Y-X-E, Y-X-F). A function written for Y-X-T data would silently mis-shape the others. Call assertDatLayout on the metadata before reading or writing anything:

assertDatLayout(info, { {'Y','X','T'}, {'Y','X','T','E'} }, mfilename);

The second argument lists every layout your function supports. For any other layout, the function stops with the error Umitoolbox:<function>:unsupportedLayout, whose message names the file, its axes, and the layouts accepted. The check also works on a list of axis names alone: assertDatLayout(struct('dimNames', {dimNames}), layouts, mfilename).

Worked example: updating one function

The function below smooths an image time series with a moving average along time. The window is given in seconds, so the function needs the frame rate. First, the old style:

% BEFORE (earlier release; no longer listed by PipelineManager)
function [outData, metaData] = smoothInTime(data, metaData, varargin)
% SMOOTHINTIME Moving-average smoothing of an image time series along T.

% Defaults (PipelineManager read these lines as text):
default_Output = 'smoothed.dat';
default_opts = struct('WindowSec', 1);
opts_values = struct('WindowSec', [eps, Inf]);

p = inputParser;
addRequired(p, 'data', @(x) isnumeric(x) && ndims(x) == 3);
addRequired(p, 'metaData', @isstruct);
addOptional(p, 'opts', default_opts, @isstruct);
parse(p, data, metaData, varargin{:});
opts = p.Results.opts;

assert(isequal(metaData.dim_names, {'Y','X','T'}), 'Data must be Y-X-T.');
windowFrames = max(1, round(opts.WindowSec * metaData.Freq));
outData = movmean(data, windowFrames, 3);
end

The updated function. Every difference from the old one is explained below the code.

% AFTER (current contract)
function outData = smoothInTime(data, SaveFolder, varargin)
% SMOOTHINTIME Moving-average smoothing of an image time series along T.

if nargin == 1 && strcmpi(string(data), 'pipelineInfo')
    outData = localGetPipelineInfo();
    return
end

p = inputParser;
p.FunctionName = mfilename;
addRequired(p, 'data', @(x) isnumeric(x) && ~isempty(x));
addRequired(p, 'SaveFolder', @isfolder);
addParameter(p, 'WindowSec', 1, @(x) isnumeric(x) && isscalar(x) && isfinite(x) && x > 0);
addParameter(p, 'FrameRateHz', []);   % injected by PipelineManager
addParameter(p, 'DimNames', []);      % injected by PipelineManager
parse(p, data, SaveFolder, varargin{:});

% Axes and frame rate of the data: explicit value, else the data's own file.
dimNames = resolveDataInfoValue('dimNames', p.Results.DimNames, data, mfilename);
assertDatLayout(struct('dimNames', {dimNames}), { {'Y','X','T'} }, mfilename);
frameRateHz = resolveDataInfoValue('frameRateHz', p.Results.FrameRateHz, data, mfilename);

windowFrames = max(1, round(p.Results.WindowSec * frameRateHz));
outData = movmean(data, windowFrames, 3);
end

function info = localGetPipelineInfo()
info = PipelineManager.createPipelineInfo('smoothInTime', ...
    'Smooth an image time series with a moving average along T.');

info = PipelineManager.addInput(info, 'data', 'ImageTimeSeries', ...
    'Numeric Y-X-T image time series held in RAM.', ...
    'kind', 'input', 'position', 1, 'callType', 'positional', ...
    'isData', true, 'supportsFile', false, 'dataMode', 'ram');

info = PipelineManager.addInput(info, 'SaveFolder', 'SaveFolder', ...
    'Folder of the current dataset.', ...
    'kind', 'input', 'position', 2, 'callType', 'positional', 'isData', false);

info = PipelineManager.addInput(info, 'WindowSec', 'parameter', ...
    'Length of the moving-average window, in seconds.', ...
    'kind', 'parameter', 'callType', 'namevalue', ...
    'default', 1, 'allowed', [eps Inf], 'dataType', 'double');

% Per-data metadata: PipelineManager reads it from the data and passes it in.
info = PipelineManager.addInput(info, 'FrameRateHz', 'sourceInfo', ...
    'Frame rate of the input data (Hz).', ...
    'kind', 'sourceInfo', 'sourceField', 'frameRateHz');
info = PipelineManager.addInput(info, 'DimNames', 'sourceInfo', ...
    'Axis names of the input data.', ...
    'kind', 'sourceInfo', 'sourceField', 'dimNames');

info = PipelineManager.addOutput(info, 'outData', 'ImageTimeSeries', 'data', ...
    'Smoothed Y-X-T image time series.', 'smoothed.dat', 1, ...
    'isData', true, 'saveFileName', 'smoothed.dat');
end
BeforeAfter
[outData, metaData] = smoothInTime(data, metaData, varargin)outData = smoothInTime(data, SaveFolder, varargin): the data come first and the Save Folder second. The function no longer receives or returns metaData.
default_Output, default_opts, opts_valuesThe default filename of addOutput, and the 'default' and 'allowed' values of the WindowSec parameter. The one-argument 'pipelineInfo' query returns them.
opts.WindowSec in an opts structure'WindowSec', an ordinary name-value parameter.
metaData.dim_namesThe 'DimNames' sourceInfo input, resolved with resolveDataInfoValue and checked with assertDatLayout.
metaData.FreqThe 'FrameRateHz' sourceInfo input, resolved with resolveDataInfoValue.

The function takes its data in memory, and PipelineManager passes it both sourceInfo values. A function that also accepts the name of a .dat file declares 'supportsFile', true and 'dataMode', 'either' and reads that file with the functions in Reading and writing .dat files; Analysis/Filters/normalizeLPF.m is a complete example.

The output is a single-precision array (the input is single precision, and movmean keeps its class), so PipelineManager saves it as smoothed.dat with the axes and frame rate of the input data.

Calling the updated function directly, as in Test before using real data, needs the two values that PipelineManager would otherwise supply:

data = single(reshape(1:24, [2 3 4]));
out = smoothInTime(data, tempdir, 'WindowSec', 0.2, ...
    'FrameRateHz', 10, 'DimNames', {'Y','X','T'});

Checking the updated function

  1. Query the declaration: info = smoothInTime('pipelineInfo') must return a structure without an error. A function whose query fails is reported as broken when PipelineManager scans the folders, not skipped.
  2. Copy the function to Analysis/<Category>/ and open Pipeline Manager. The function must appear under that category. If it does not, read the warning that PipelineManager printed in the MATLAB Command Window when it scanned the folders: NoPipelineInfo means that the function does not answer the 'pipelineInfo' query.
  3. Add the function to a pipeline on a small Save Folder and run the pipeline check that PipelineManager performs before it executes (the preflight). It reports an error if a required sourceInfo value cannot be found in the data.
  4. Run the pipeline and open the saved output in DataViewer.

Troubleshooting

SymptomLikely cause and fix
Function is not listedName/file mismatch, invalid pipelineInfo, excluded folder, or parser error. Query it directly and inspect the error. A function written for an earlier release is skipped with the warning NoPipelineInfo: see Updating custom functions for PipelineManager.
Connection is unavailableSemantic types do not match. Correct the declared type; do not weaken it merely to force a connection.
Filename input failssupportsFile was declared without a tested file branch. Set it false or implement and test file streaming.
Output is not savedCheck output mode, managed filename, leaf policy, and whether a file-manifest function returned actual relative filenames.
UMT validation failsValue dimensions, dimNames, labels, kind, or event information disagree.
Umitoolbox:saveData:missingDimNames or Umitoolbox:saveData:missingFrameRateA direct saveData call for numeric data must name the axes with 'DimNames' and, for data with a T axis, pass 'FrameRateHz' or 'Info'.
Umitoolbox:PipelineManager:unresolvedDimNamesPipelineManager cannot infer the axes of a returned array (for example Y-X-F). Write the file with saveData and 'DimNames', or return a UMT structure.
Umitoolbox:<function>:unsupportedLayoutThe .dat input has axes that the function does not support. Check the layouts listed in its assertDatLayout call.
Umitoolbox:<function>:missingFrameRateHzThe data carry no frame rate and none was passed. Declare a 'FrameRateHz' sourceInfo input, or pass 'FrameRateHz' in a direct call.
Wrong metadata is usedResolve metadata from the exact data file, not the first file found in the Save Folder.
Fresh-folder configuration is rejectedConfirm the folder has no AcqInfos.mat, the DAG has exactly one root initializer, companions are roots, and all other stream nodes descend from the initializer.
Fresh initialization fails at run timeThe initializer failed or returned without a valid current AcqInfos.mat. Fix the importer; do not create metadata in PipelineManager or a companion.

Final checklist

  • Function name, filename, discovery name, and output filenames are unique and consistent.
  • Input positions and name-value options match the MATLAB signature.
  • RAM/file support describes implemented behavior.
  • Semantic types and output modes are accurate.
  • Dimensions and UMT metadata describe the returned value.
  • The output is saved as .dat only if all its dimensions are Y, X, T, E, or F and it comes from a single recording; otherwise it is a .umt file (Choosing the output format).
  • Frame rate, axes, and exposure come from sourceInfo inputs or the data file, never from AcqInfos.mat; .dat files are read and written only through loadMetaData, loadData, spatialSlabIO, and saveData.
  • freshSaveFolderRole is changed from none only for a true initializer or companion.
  • No authoritative metadata is edited by an ordinary processing or companion function.
  • Direct test, discovery test, and a small PipelineManager run pass.

Back to top

Copyright © LabeoTech 2026. This work is licensed under the GNU General Public License, Version 3.0.