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 @@ -code issues: 252code issues252 +code issues: 249code issues249 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 @@ -tests: 26 passedtests26 passed +tests: 38 passedtests38 passed 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)