diff --git a/.github/badges/code_issues.svg b/.github/badges/code_issues.svg
index d0441a4..c0ee5dd 100644
--- a/.github/badges/code_issues.svg
+++ b/.github/badges/code_issues.svg
@@ -1 +1 @@
-
+
diff --git a/.github/badges/tests.svg b/.github/badges/tests.svg
index d9e3818..97f46fd 100644
--- a/.github/badges/tests.svg
+++ b/.github/badges/tests.svg
@@ -1 +1 @@
-
+
diff --git a/.github/workflows/test-code.yml b/.github/workflows/test-code.yml
index a4a187c..37c6984 100644
--- a/.github/workflows/test-code.yml
+++ b/.github/workflows/test-code.yml
@@ -21,6 +21,7 @@ concurrency:
permissions:
contents: write
checks: write
+ pull-requests: write
security-events: write
jobs:
diff --git a/code/+nansen/+module/+twophoton/+autosegmentation/+flufinder/+detect/findUniqueRoisFromComponents.m b/code/+nansen/+module/+twophoton/+autosegmentation/+flufinder/+detect/findUniqueRoisFromComponents.m
index 07b3cb8..da00b2b 100644
--- a/code/+nansen/+module/+twophoton/+autosegmentation/+flufinder/+detect/findUniqueRoisFromComponents.m
+++ b/code/+nansen/+module/+twophoton/+autosegmentation/+flufinder/+detect/findUniqueRoisFromComponents.m
@@ -58,17 +58,28 @@
% Boolean for all remaining components that wasn't taken care of yet
remaining = true(numel(S), 1);
- % Create a "sum projection" image of all components.
- uniquePixelList = unique(allPixelIndices);
-
+ % Create a "sum projection" image of all components: the number of
+ % components each pixel belongs to. accumarray counts every pixel,
+ % including the last unique one, and copes with zero or one pixel
+ % (histcounts needs at least two bin edges).
componentImage = zeros(imageSize);
- % Using a histogram here is faster than a for loop. % (Maybe only if there are 100ks of components...)
- [N,E] = histcounts(allPixelIndices, uniquePixelList);
- componentImage(E(1:end-1)) = N;
+ if ~isempty(allPixelIndices)
+ componentImage(:) = accumarray(allPixelIndices(:), 1, [numel(componentImage), 1]);
+ end
summary.ComponentImageInit = componentImage;
+ summary.ComponentImageFinished = componentImage;
%imviewer(componentImage) % Todo: return this as part of summary
+ if isempty(S) || isempty(allPixelIndices)
+ % Nothing to pick from; the merge and tagging below need rois.
+ warning('on', 'stats:linkage:NonMonotonicTree')
+ if nargout == 1
+ clear summary
+ end
+ return
+ end
+
mask = false(imageSize);
nRois = 0;
@@ -83,7 +94,12 @@
% Find peak in the summed component image. This will be the most
% active roi among candidates
- [~, peakInd] = max(componentImage(:));
+ [peakValue, peakInd] = max(componentImage(:));
+ if peakValue <= 0
+ % Every remaining pixel has been cleared; without this guard
+ % the loop would spin on an empty peak forever.
+ break
+ end
% Find all components that contain this peak
containsPeak = regionInd(allPixelIndices==peakInd);
diff --git a/code/+nansen/+module/+twophoton/+integration/+caiman/deconvolve.m b/code/+nansen/+module/+twophoton/+integration/+caiman/deconvolve.m
index 68b55e2..4470994 100644
--- a/code/+nansen/+module/+twophoton/+integration/+caiman/deconvolve.m
+++ b/code/+nansen/+module/+twophoton/+integration/+caiman/deconvolve.m
@@ -30,11 +30,7 @@
% todo...
prevstr = [];
starttime = tic;
- if isempty(gcp('nocreate'))
- dispProgress = true;
- else
- dispProgress = false;
- end
+ dispProgress = ~isParallelPoolRunning();
if nRois < 5
dispProgress = false;
@@ -141,3 +137,11 @@
fprintf(msg)
end
end
+
+function tf = isParallelPoolRunning()
+%isParallelPoolRunning True if a parallel pool is open
+%
+% gcp belongs to the Parallel Computing Toolbox, which the module lists
+% as optional, so it must not be called when the toolbox is absent.
+ tf = exist('gcp', 'file') == 2 && ~isempty(gcp('nocreate'));
+end
diff --git a/code/+nansen/+module/+twophoton/+integration/+extract/@Processor/Processor.m b/code/+nansen/+module/+twophoton/+integration/+extract/@Processor/Processor.m
index c660da5..63c6c42 100644
--- a/code/+nansen/+module/+twophoton/+integration/+extract/@Processor/Processor.m
+++ b/code/+nansen/+module/+twophoton/+integration/+extract/@Processor/Processor.m
@@ -109,20 +109,6 @@ function onCompletion(obj)
obj.mergeResults()
end
- % % % Todo: The following should be a separate session method...
- % % if isempty(obj.OriginalStack)
- % % sourceStack = obj.SourceStack;
- % % else
- % % sourceStack = obj.OriginalStack;
- % % end
- % %
- % % % Get temporal segments
- % % tExtracor = nansen.module.twophoton.integration.extract.ProcessorT(...
- % % sourceStack, obj.Options, obj.MergedResults);
- % % tExtracor.Options.Run.numFramesPerPart = 2000;
- % % tExtracor.DataIoModel = obj.DataIoModel;
- % %
- % % tExtracor.runMethod()
end
%obj.createRoiClassificationData()
diff --git a/code/+nansen/+module/+twophoton/+integration/+extract/@ProcessorT/ProcessorT.m b/code/+nansen/+module/+twophoton/+integration/+extract/@ProcessorT/ProcessorT.m
deleted file mode 100644
index 2c46436..0000000
--- a/code/+nansen/+module/+twophoton/+integration/+extract/@ProcessorT/ProcessorT.m
+++ /dev/null
@@ -1,134 +0,0 @@
-classdef ProcessorT < nansen.stack.ImageStackProcessor
-
-% This class creates the following data variables:
-%
-% * ExtractTemporalWeights : Extracted temporal components
-% (numRois x numTimepoints) single matrix
-
- % Rename to ExtractT
-
- % Todo: Run per channel and plane...
-
- properties (Constant, Hidden)
- DATA_SUBFOLDER = fullfile('roi_data', 'autosegmentation_extract')
- VARIABLE_PREFIX = 'ExtractTemporal'
- end
-
- properties (Constant)
- MethodName = 'EXTRACT (Signal Extraction)'
- IsManual = false % Does method require manual supervision
- IsQueueable = true % Can method be added to a queue
- OptionsManager nansen.manage.OptionsManager = ...
- nansen.OptionsManager('nansen.module.twophoton.integration.extract.Processor')
- end
-
- properties %Options
- end
-
- properties (Access = private)
- SegmentationResults cell % nPlanes x nChannels
- ExtractConfig cell % nPlanes x nChannels
- %Results % cell array with cell for each part..
- end
-
- methods (Static)
- function S = getDefaultOptions()
- end
- end
-
- methods % Structor
-
- function obj = ProcessorT(varargin)
-
- obj@nansen.stack.ImageStackProcessor(varargin{:})
- obj.SegmentationResults = varargin{3};
-
- % Todo: Need to assign options and output from extract!
-
- if ~nargout
- obj.runMethod()
- clear obj
- end
- end
- end
-
- methods (Access = protected)
-
- function onInitialization(obj)
- %onInitialization Initialize variables
-
- filePath = obj.getDataFilePath('ExtractTemporalWeights_temp', '-w',...
- 'Subfolder', obj.DATA_SUBFOLDER, 'IsInternal', true);
-
- if isfile(filePath)
- obj.Results = obj.loadData('ExtractTemporalWeights_temp');
- end
-
- % Todo: Load from file....
- if isempty(obj.Results)
- obj.Results = cell(1, obj.NumParts);
- end
-
- if ~isa(obj.SegmentationResults, 'cell')
- obj.SegmentationResults = {obj.SegmentationResults};
- end
- assert(isa(obj.SegmentationResults{1}, 'struct'), ...
- 'Expected segmentation results to be a cell array of structs')
- obj.ExtractConfig = cell(size(obj.SegmentationResults));
-
- for i = 1:numel(obj.SegmentationResults)
- spatialWeights = obj.SegmentationResults{i}.spatial_weights;
- [h, w, n] = size(spatialWeights);
-
- config = obj.SegmentationResults{i}.config;
- config.max_iter = 0;
- config.num_iter_stop_quality_checks=0;
- config.S_init = reshape(spatialWeights, h * w, n);
- config.verbose = 0;
-
- obj.ExtractConfig{i} = config;
- end
- end
-
- function onCompletion(obj)
- % Todo: Fix this, need to run on a longer stack to ensure the temporal chunks are correctly merged...
- % Todo: Adapt to work for multi-plane / multi-channel
- % Concatenate temporal signals....
-
- T = cat(2, obj.Results{:});
- signalArray = cat(3, obj.Results{:});
-
- obj.MergedResults
- % Save temporal profiles
- obj.saveData('ExtractTemporalWeights', T, 'Subfolder', ...
- obj.DATA_SUBFOLDER, 'IsInternal', true)
- end
- end
-
- methods (Access = protected) % Implement methods from ImageStackProcessor
-
- function [YOut, results] = processPart(obj, Y)
-
- iC = obj.StackIterator.CurrentIterationC;
- iZ = obj.StackIterator.CurrentIterationZ;
- config = obj.ExtractConfig{iZ, iC};
-
- [~, results, ~] = run_extract(Y, config);
-
- YOut = [];
- %obj.Results{obj.CurrentPart} = T;
- %obj.saveData('ExtractTemporalWeights_temp', obj.Results)
- %results = [];
- end
-
- function tf = checkIfPartIsFinished(obj, partNumber)
- tf = ~isempty(obj.Results{partNumber});
- end
- end
-
- methods (Access = private)
-
- function loadResults(obj)
- end
- end
-end
diff --git a/code/+nansen/+module/+twophoton/+integration/+suite2p/Suite2pCorrected.m b/code/+nansen/+module/+twophoton/+integration/+suite2p/Suite2pCorrected.m
index f2d5dea..22fb234 100644
--- a/code/+nansen/+module/+twophoton/+integration/+suite2p/Suite2pCorrected.m
+++ b/code/+nansen/+module/+twophoton/+integration/+suite2p/Suite2pCorrected.m
@@ -13,8 +13,7 @@
methods % Structors
function obj = Suite2pCorrected(filePath, varargin)
- import('nansen.module.twophoton.integration.suite2p.Suite2pCorrected')
- filePath = Suite2pCorrected.lookForMultipartFiles(filePath);
+ filePath = nansen.module.twophoton.integration.suite2p.Suite2pCorrected.lookForMultipartFiles(filePath);
obj@nansen.stack.virtual.TiffMultiPart(filePath, varargin{:})
end
diff --git a/code/+nansen/+module/+twophoton/+io/+mscan/MDF.m b/code/+nansen/+module/+twophoton/+io/+mscan/MDF.m
index b95b60d..b990216 100644
--- a/code/+nansen/+module/+twophoton/+io/+mscan/MDF.m
+++ b/code/+nansen/+module/+twophoton/+io/+mscan/MDF.m
@@ -122,36 +122,6 @@ function writeFrames(obj, frameIndex, data)
methods (Access = private)
- function resolveDataSizeAndDimensionArrangement(obj, stackSize)
- % Todo: Remove when this is merged into main branch from
- % dev-imagestack-5d
-
- % Find singleton dimensions.
- isSingleton = stackSize == 1;
-
- % Get arrangement of dimensions of data
- try
- % Note subclasses might implement this as a constant
- % property. If it's not implemented, use default DDA
- dataDimensionArrangement = obj.DATA_DIMENSION_ARRANGEMENT;
- catch
- dataDimensionArrangement = obj.DEFAULT_DIMENSION_ARRANGEMENT;
- end
-
- % Get order of dimensions of data
- [~, ~, dimensionOrder] = intersect( obj.DEFAULT_DIMENSION_ARRANGEMENT, ...
- dataDimensionArrangement, 'stable' );
-
- % Rearrange beased on dimension order
- isSingleton_(dimensionOrder) = isSingleton;
- dataSize(dimensionOrder) = stackSize;
-
- % Assign size and dimension arrangement for data excluding
- % singleton dimension.
- obj.DataSize = dataSize(~isSingleton_);
- obj.DataDimensionArrangement = dataDimensionArrangement(~isSingleton_);
- end
-
function mdfParams = getScanParameters(obj)
import nansen.module.twophoton.io.mscan.getScanParameters
mdfParams = getScanParameters(obj.mfile);
diff --git a/code/+nansen/+module/+twophoton/+io/+sciscan/SciScanRaw.m b/code/+nansen/+module/+twophoton/+io/+sciscan/SciScanRaw.m
index ff98462..6cd74b4 100644
--- a/code/+nansen/+module/+twophoton/+io/+sciscan/SciScanRaw.m
+++ b/code/+nansen/+module/+twophoton/+io/+sciscan/SciScanRaw.m
@@ -431,7 +431,7 @@ function getChannelColors(obj)
inistring = fileread(iniFilePath);
sciscanVars = {'external.start.trigger.enable', 'aocard.model'};
- isValid = contains(inistring, sciscanVars);
+ isValid = all(contains(inistring, sciscanVars));
end
end
diff --git a/code/+nansen/+module/+twophoton/+io/+thorlabs/ThorLabsTiffs.m b/code/+nansen/+module/+twophoton/+io/+thorlabs/ThorLabsTiffs.m
index 7139eca..837e794 100644
--- a/code/+nansen/+module/+twophoton/+io/+thorlabs/ThorLabsTiffs.m
+++ b/code/+nansen/+module/+twophoton/+io/+thorlabs/ThorLabsTiffs.m
@@ -43,9 +43,7 @@
methods % Structors
function obj = ThorLabsTiffs(filePath, varargin)
- import('nansen.module.twophoton.io.thorlabs.ThorLabsTiffs')
-
- filePath = ThorLabsTiffs.lookForMultipartFiles(filePath);
+ filePath = nansen.module.twophoton.io.thorlabs.ThorLabsTiffs.lookForMultipartFiles(filePath);
obj@nansen.stack.data.VirtualArray(filePath, varargin{:})
end
diff --git a/code/+nansen/+module/+twophoton/+roisignals/+process/+dff/dffChenEtAl2013.m b/code/+nansen/+module/+twophoton/+roisignals/+process/+dff/dffChenEtAl2013.m
index 846a4aa..5577614 100644
--- a/code/+nansen/+module/+twophoton/+roisignals/+process/+dff/dffChenEtAl2013.m
+++ b/code/+nansen/+module/+twophoton/+roisignals/+process/+dff/dffChenEtAl2013.m
@@ -16,8 +16,8 @@
fPil = squeeze( mean(signalArray(:, 2:end, :), 2) );
end
- npil_true0 = prctile(fPil, 20);
+ npil_true0 = prctile(fPil, 20, 1);
f_true = fRoi - (0.7*fPil) + npil_true0;
- f_true0 = prctile(f_true, 20);
+ f_true0 = prctile(f_true, 20, 1);
dff_true = (f_true - f_true0) ./ f_true0;
end
diff --git a/code/+nansen/+module/+twophoton/+roisignals/+process/+dff/dffRoiMinusDffNpil.m b/code/+nansen/+module/+twophoton/+roisignals/+process/+dff/dffRoiMinusDffNpil.m
index f739db8..2ec7543 100644
--- a/code/+nansen/+module/+twophoton/+roisignals/+process/+dff/dffRoiMinusDffNpil.m
+++ b/code/+nansen/+module/+twophoton/+roisignals/+process/+dff/dffRoiMinusDffNpil.m
@@ -51,6 +51,13 @@
if ~isa(deltaDff, 'double') || ~isa(dffNpil, 'double')
deltaDff = double(deltaDff); dffNpil = double(dffNpil);
end
+ if exist('sgolayfilt', 'file') ~= 2
+ error('NANSEN:TwoPhoton:SignalProcessingToolboxRequired', ...
+ ['The dF/F method "dffRoiMinusDffNpil" uses sgolayfilt from the ', ...
+ 'Signal Processing Toolbox, which is not installed. Install the ', ...
+ 'toolbox, set smoothSignals to true, or choose another dffFcn ', ...
+ 'such as dffClassic.'])
+ end
baselineCorrection = sgolayfilt(deltaDff, 3, 11);
dff = dffRoi - sgolayfilt(dffNpil, 3, 11) + baselineCorrection;
end
diff --git a/code/+nansen/+module/+twophoton/+roisignals/deconvolveDff.m b/code/+nansen/+module/+twophoton/+roisignals/deconvolveDff.m
index 27f1dd9..965cf75 100644
--- a/code/+nansen/+module/+twophoton/+roisignals/deconvolveDff.m
+++ b/code/+nansen/+module/+twophoton/+roisignals/deconvolveDff.m
@@ -1,4 +1,20 @@
function [dec, den, opt] = deconvolveDff(dff, varargin)
+%deconvolveDff Deconvolve dF/F signals with the selected method
+%
+% [dec, den, opt] = deconvolveDff(dff) deconvolves dff, a numSamples x
+% numRois array as returned by computeDff, and returns the deconvolved
+% (dec) and denoised (den) signals in the same shape, plus the options
+% used (opt). A vector is treated as a single ROI in either orientation.
+%
+% [dec, den, opt] = deconvolveDff(dff, Name, Value, ...) or
+% deconvolveDff(dff, options) sets the deconvolution parameters, see
+% getDeconvolutionParameters. The deconvolutionMethod parameter selects
+% a function from the process.deconvolve package by name.
+%
+% P = deconvolveDff() returns the default parameters.
+%
+% See also nansen.module.twophoton.roisignals.computeDff,
+% nansen.module.twophoton.roisignals.getDeconvolutionParameters
[P, V] = nansen.module.twophoton.roisignals.getDeconvolutionParameters();
P.deconvolutionMethod = 'caiman';
@@ -12,17 +28,19 @@
deconvPackage = 'nansen.module.twophoton.roisignals.process.deconvolve';
deconvFunction = str2func( strjoin({deconvPackage, params.deconvolutionMethod}, '.') );
- % dff must be nRois x nSamples for deconvolution method...
- wasTransposed = false;
- if size(dff, 1) > size(dff, 2)
- dff = transpose(dff);
- wasTransposed = true;
+ % The deconvolution methods want numRois x numSamples. The input
+ % contract is numSamples x numRois (computeDff's output); a vector is
+ % one ROI regardless of orientation. Shape is not guessed from which
+ % dimension is larger, so recordings with more ROIs than samples are
+ % deconvolved correctly.
+ if isvector(dff)
+ dffForMethod = reshape(dff, 1, []);
+ else
+ dffForMethod = transpose(dff);
end
- [dec, den, opt] = deconvFunction(dff, params);
+ [dec, den, opt] = deconvFunction(dffForMethod, params);
- if wasTransposed
- dec = transpose(dec);
- den = transpose(den);
- end
+ dec = reshape(transpose(dec), size(dff));
+ den = reshape(transpose(den), size(dff));
end
diff --git a/code/+nansen/+module/+twophoton/+roisignals/extractF.m b/code/+nansen/+module/+twophoton/+roisignals/extractF.m
index 2510e80..1a5cfe3 100644
--- a/code/+nansen/+module/+twophoton/+roisignals/extractF.m
+++ b/code/+nansen/+module/+twophoton/+roisignals/extractF.m
@@ -3,7 +3,8 @@
%
% signals = extractF(imageStack, roiArray) extracts signals for rois in
% roiArray from image frames in imageData using default settings.
-% roiArray must be a nansen.RoiArray object and imageData can be a
+% roiArray must be an array of RoI objects (or a roimanager.roiGroup)
+% and imageData can be a
% numeric array of three or more dimensions or a nansen.ImageStack
% object.
%
diff --git a/tests/+twophotontest/DffTest.m b/tests/+twophotontest/DffTest.m
index 156466f..15ab753 100644
--- a/tests/+twophotontest/DffTest.m
+++ b/tests/+twophotontest/DffTest.m
@@ -82,14 +82,26 @@ function testChenDffSubtractsWeightedNeuropil(testCase)
fRoi = squeeze(signalArray(:, 1, :));
fPil = squeeze(signalArray(:, 2, :));
- fTrue = fRoi - 0.7*fPil + prctile(fPil, 20);
- fTrue0 = prctile(fTrue, 20);
+ fTrue = fRoi - 0.7*fPil + prctile(fPil, 20, 1);
+ fTrue0 = prctile(fTrue, 20, 1);
expected = (fTrue - fTrue0) ./ fTrue0;
testCase.verifySize(dff, [testCase.NumSamples, testCase.NumRois])
testCase.verifyEqual(dff, expected, 'AbsTol', testCase.AbsTol)
end
+ function testChenDffOfASingleRoiMatchesTheMultiRoiResult(testCase)
+ % The baselines must be taken along time for each ROI, so the
+ % first ROI's dF/F must not depend on the other ROIs being
+ % present.
+ signalArray = testCase.createRoiSignals(2);
+ dffAll = testCase.computeDff(signalArray, 'dffFcn', 'dffChenEtAl2013');
+ dffOne = testCase.computeDff(signalArray(:, :, 1), 'dffFcn', 'dffChenEtAl2013');
+
+ testCase.verifySize(dffOne, [testCase.NumSamples, 1])
+ testCase.verifyEqual(dffOne, dffAll(:, 1), 'AbsTol', testCase.AbsTol)
+ end
+
function testRoiMinusNeuropilDffRuns(testCase)
% dffRoiMinusDffNpil uses sgolayfilt from the Signal Processing
% Toolbox, which the module manifest lists as optional.
diff --git a/tests/+twophotontest/FluFinderDetectionTest.m b/tests/+twophotontest/FluFinderDetectionTest.m
new file mode 100644
index 0000000..5a0f4d1
--- /dev/null
+++ b/tests/+twophotontest/FluFinderDetectionTest.m
@@ -0,0 +1,58 @@
+classdef FluFinderDetectionTest < matlab.unittest.TestCase
+%FluFinderDetectionTest FluFinder's component picking on small inputs
+%
+% Exercises nansen.module.twophoton.autosegmentation.flufinder.detect.
+% findUniqueRoisFromComponents with hand-built component stats, the
+% struct array that getBwComponentStats returns (Area, Centroid,
+% PixelIdxList). Covers the degenerate inputs that used to error in
+% histcounts or spin forever: no components, and a single repeated
+% component.
+%
+% Run tests:
+% runtests('twophotontest.FluFinderDetectionTest')
+
+ properties (Constant, Access = private)
+ ImageSize = [48, 48]
+ end
+
+ methods (Access = private)
+
+ function S = createComponents(testCase, center, radius, numObservations)
+ %createComponents The same disk observed numObservations times
+ [columns, rows] = meshgrid(1:testCase.ImageSize(2), 1:testCase.ImageSize(1));
+ mask = (columns - center(1)).^2 + (rows - center(2)).^2 <= radius^2;
+ pixelIndices = find(mask);
+ S = repmat(struct('Area', numel(pixelIndices), 'Centroid', center, ...
+ 'PixelIdxList', pixelIndices), numObservations, 1);
+ end
+
+ function rois = findRois(testCase, S)
+ rois = nansen.module.twophoton.autosegmentation.flufinder.detect. ...
+ findUniqueRoisFromComponents(testCase.ImageSize, S);
+ end
+ end
+
+ methods (Test)
+
+ function testNoComponentsGivesNoRois(testCase)
+ S = struct('Area', {}, 'Centroid', {}, 'PixelIdxList', {});
+ rois = testCase.findRois(S);
+ testCase.verifyClass(rois, 'RoI')
+ testCase.verifyEmpty(rois)
+ end
+
+ function testRepeatedComponentGivesOneRoi(testCase)
+ S = testCase.createComponents([20, 24], 5, 3);
+ rois = testCase.findRois(S);
+ testCase.verifyClass(rois, 'RoI')
+ testCase.verifyNumElements(rois, 1)
+ end
+
+ function testTwoSeparatedComponentsGiveTwoRois(testCase)
+ S = [testCase.createComponents([14, 14], 5, 3); ...
+ testCase.createComponents([34, 34], 5, 3)];
+ rois = testCase.findRois(S);
+ testCase.verifyNumElements(rois, 2)
+ end
+ end
+end
diff --git a/tests/+twophotontest/ReaderResolutionTest.m b/tests/+twophotontest/ReaderResolutionTest.m
index ae5411a..2a60c08 100644
--- a/tests/+twophotontest/ReaderResolutionTest.m
+++ b/tests/+twophotontest/ReaderResolutionTest.m
@@ -17,6 +17,15 @@
% runtests('twophotontest.ReaderResolutionTest')
properties (TestParameter)
+ ReaderClass = { ...
+ 'nansen.module.twophoton.io.sciscan.SciScanRaw', ...
+ 'nansen.module.twophoton.io.scanimage.ScanImageTiff', ...
+ 'nansen.module.twophoton.io.scanimage.ScanImageMultiRoiTiff', ...
+ 'nansen.module.twophoton.io.prairieview.PrairieViewTiffs', ...
+ 'nansen.module.twophoton.io.thorlabs.ThorLabsTiffs', ...
+ 'nansen.module.twophoton.io.mscan.MDF', ...
+ 'nansen.module.twophoton.integration.suite2p.Suite2pCorrected'}
+
FilenameCase = struct( ...
'prairieView', struct( ...
'filename', 'TSeries-01012020-0001_Cycle00001_Ch1_000001.ome.tif', ...
@@ -59,6 +68,30 @@ function writeText(testCase, filePath, text)
methods (Test)
+ function testReaderClassLoads(testCase, ReaderClass)
+ % A reader whose class definition cannot be loaded (for example
+ % an override of a sealed superclass method) is unusable from
+ % every entry point, so every reader must at least load.
+ metaClass = meta.class.fromName(ReaderClass);
+ testCase.verifyNotEmpty(metaClass, ...
+ sprintf('Reader class "%s" did not load', ReaderClass))
+ end
+
+ function testSciScanPredicateReturnsScalarLogical(testCase)
+ folderPath = testCase.makeFixtureFolder();
+ baseName = '20200101_12_00_00_recording';
+ rawFilePath = fullfile(folderPath, [baseName, '.raw']);
+ testCase.writeText(rawFilePath, '');
+ testCase.writeText(fullfile(folderPath, [baseName, '.ini']), sprintf( ...
+ 'external.start.trigger.enable = TRUE\naocard.model = PCI-6110\n'));
+
+ isValid = nansen.module.twophoton.io.sciscan.SciScanRaw.fileCheck(rawFilePath);
+
+ testCase.verifyClass(isValid, 'logical')
+ testCase.verifySize(isValid, [1, 1])
+ testCase.verifyTrue(isValid)
+ end
+
function testFilenameExpressionReaders(testCase, FilenameCase)
className = testCase.resolve(FilenameCase.filename);
testCase.verifyEqual(className, FilenameCase.className)