From 21bd14388b875fbf66206439ad5edc084067dc67 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Thu, 14 Nov 2024 17:19:27 +0100 Subject: [PATCH] Use NWBInspector when testing livescript tutorials (#618) * Add function for running nwbinspector on tutorial files during testing #484 * Fix intro tutorial * Add nwbInspector check to ignore list in TutorialTest * Fix timestamps with wrong length in Behavior tutorial * Update behavior tutorial. - Add note about why dimension of SpatialSeries data is transposed wrt the type specification - Fix typos * Update images tutorial - Add explanation of why arrays are transposed relative to the type documentation - Fix dimension order of AbstractFeatureSeries * Add 'cell_id' to types.core.IntracellularElectrode in Icephys tutorial * Update run_tests.yml * Update TutorialTest.m * Update TutorialTest.m * Update TutorialTest.m * Try o set up python path * ... * ... * .. * ... * test * ... * Update TutorialTest.m * Fix TutorialTest * Tests are working on GItHub Actions * Update TutorialTest.m * Fix * Update untypedSetTest.m Suppress output in test * Add fixture for clearing generated types when running tests * Fix typo * Improve Fixture description and improve function names * Use OutOfProcess execution mode for python in matlan for github action - Simplifies the reading of nwb files using pynwb in TutorialTest * Update run_tests.yml * Added sentence about using timestamps * Ignore NWBInspector result for specific dataset in ecephys tutorial --- +tests/+fixtures/ResetGeneratedTypesFixture.m | 16 + +tests/+sanity/GenerationTest.m | 10 +- +tests/+system/PyNWBIOTest.m | 2 +- +tests/+unit/TutorialTest.m | 255 +++++++++++---- +tests/+unit/untypedSetTest.m | 10 +- +tests/+util/addFolderToPythonPath.m | 13 + +tests/+util/getProjectDirectory.m | 3 + +tests/requirements.txt | 3 +- .github/workflows/run_tests.yml | 6 +- nwbtest.m | 1 + tutorials/behavior.mlx | Bin 8205 -> 8412 bytes tutorials/html/behavior.html | 163 ++++++++-- tutorials/html/icephys.html | 295 +++++++++--------- tutorials/html/images.html | 44 +-- tutorials/html/intro.html | 232 +------------- tutorials/icephys.mlx | Bin 709614 -> 707612 bytes tutorials/images.mlx | Bin 8588 -> 8753 bytes tutorials/intro.mlx | Bin 221751 -> 218837 bytes 18 files changed, 571 insertions(+), 482 deletions(-) create mode 100644 +tests/+fixtures/ResetGeneratedTypesFixture.m create mode 100644 +tests/+util/addFolderToPythonPath.m create mode 100644 +tests/+util/getProjectDirectory.m diff --git a/+tests/+fixtures/ResetGeneratedTypesFixture.m b/+tests/+fixtures/ResetGeneratedTypesFixture.m new file mode 100644 index 00000000..312881c7 --- /dev/null +++ b/+tests/+fixtures/ResetGeneratedTypesFixture.m @@ -0,0 +1,16 @@ +classdef ResetGeneratedTypesFixture < matlab.unittest.fixtures.Fixture + % ResetGeneratedTypesFixture - Fixture for resetting generated NWB classes. + % + % ResetGeneratedTypesFixture clears all the generated classes for NWB + % types from the matnwb folder. When the fixture is set up, all generated + % class files for NWB types are deleted. When the fixture is torn down, + % generateCore is called to regenerate the classes for NWB types of the + % latest NWB version + + methods + function setup(fixture) + fixture.addTeardown( @generateCore ) + nwbClearGenerated() + end + end +end diff --git a/+tests/+sanity/GenerationTest.m b/+tests/+sanity/GenerationTest.m index 1b30f54c..d09e9b64 100644 --- a/+tests/+sanity/GenerationTest.m +++ b/+tests/+sanity/GenerationTest.m @@ -5,8 +5,14 @@ methods (TestClassSetup) function setupClass(testCase) - rootPath = fullfile(fileparts(mfilename('fullpath')), '..', '..'); - testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); + + import matlab.unittest.fixtures.PathFixture + import tests.fixtures.ResetGeneratedTypesFixture + + rootPath = tests.util.getProjectDirectory(); + testCase.applyFixture( PathFixture(rootPath) ); + + testCase.applyFixture( ResetGeneratedTypesFixture ); end end diff --git a/+tests/+system/PyNWBIOTest.m b/+tests/+system/PyNWBIOTest.m index d08c2053..c81ab493 100644 --- a/+tests/+system/PyNWBIOTest.m +++ b/+tests/+system/PyNWBIOTest.m @@ -34,7 +34,7 @@ function testInFromPyNWB(testCase) methods function [status, cmdout] = runPyTest(testCase, testName) - setenv('PYTHONPATH', fileparts(mfilename('fullpath'))); + tests.util.addFolderToPythonPath( fileparts(mfilename('fullpath')) ) envPath = fullfile('+tests', 'env.mat'); if 2 == exist(envPath, 'file') diff --git a/+tests/+unit/TutorialTest.m b/+tests/+unit/TutorialTest.m index 2d542129..089a55b7 100644 --- a/+tests/+unit/TutorialTest.m +++ b/+tests/+unit/TutorialTest.m @@ -3,17 +3,27 @@ % % This test will test most tutorial files (while skipping tutorials with % dependencies) If the tutorial creates an nwb file, the test will also try -% to open this with pynwb. -% -% Note: -% - Requires MATLAB XXXX to run py.* commands. -% - pynwb must be installed in the python environment returned by -% pyenv() +% to open this with pynwb and run nwbinspector on the file. + +% Notes: +% - Requires MATLAB 2019b or later to run py.* commands. +% +% - pynwb must be installed in the python environment returned by pyenv() +% +% - Running NWBInspector as a Python package within MATLAB on GitHub runners +% currently encounters compatibility issues between the HDF5 library and +% h5py. As a workaround in this test, the CLI interface is used to run +% NWBInspector and the results are manually parsed. This approach is not +% ideal, and hopefully can be improved upon. properties MatNwbDirectory end + properties (Constant) + NwbInspectorSeverityLevel = 1 + end + properties (TestParameter) % TutorialFile - A cell array where each cell is the name of a % tutorial file. testTutorial will run on each file individually @@ -30,12 +40,22 @@ % SkippedFiles - Name of exported nwb files to skip reading with pynwb SkippedFiles = {'testFileWithDataPipes.nwb'} % does not produce a valid nwb file + + % PythonDependencies - Package dependencies for running pynwb tutorials + PythonDependencies = {'nwbinspector'} + end + + properties (Access = private) + NWBInspectorMode = "python" end methods (TestClassSetup) function setupClass(testCase) + + import tests.fixtures.ResetGeneratedTypesFixture + % Get the root path of the matnwb repository - rootPath = getMatNwbRootDirectory(); + rootPath = tests.util.getProjectDirectory(); tutorialsFolder = fullfile(rootPath, 'tutorials'); testCase.MatNwbDirectory = rootPath; @@ -44,29 +64,16 @@ function setupClass(testCase) testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); testCase.applyFixture(matlab.unittest.fixtures.PathFixture(tutorialsFolder)); - % Note: The following seems to not be working on the azure pipeline - % Keep for reference - - % % % Make sure pynwb is installed in MATLAB's Python Environment - % % args = py.list({py.sys.executable, "-m", "pip", "install", "pynwb"}); - % % py.subprocess.check_call(args); - % % - % % % Add pynwb to MATLAB's python environment path - % % pynwbPath = getenv('PYNWB_PATH'); - % % if count(py.sys.path, pynwbPath) == 0 - % % insert(py.sys.path,int32(0),pynwbPath); - % % end - - % % Alternative: Use python script for reading file with pynwb - setenv('PYTHONPATH', fileparts(mfilename('fullpath'))); - - nwbClearGenerated() - end - end + % Check if it is possible to call py.nwbinspector.* functions. + % When running these tests on Github Actions, calling + % py.nwbinspector does not work, whereas the CLI can be used instead. + try + py.nwbinspector.is_module_installed('nwbinspector'); + catch + testCase.NWBInspectorMode = "CLI"; + end - methods (TestClassTeardown) - function tearDownClass(testCase) %#ok - %generateCore() + testCase.applyFixture( ResetGeneratedTypesFixture ); end end @@ -79,64 +86,182 @@ function setupMethod(testCase) methods (Test) function testTutorial(testCase, tutorialFile) %#ok + % Intentionally capturing output, in order for tests to cover + % code which overloads display methods for nwb types/objects. C = evalc( 'run(tutorialFile)' ); %#ok - testCase.testReadTutorialNwbFileWithPynwb() + + testCase.readTutorialNwbFileWithPynwb() + testCase.inspectTutorialFileWithNwbInspector() end end methods - function testReadTutorialNwbFileWithPynwb(testCase) + function readTutorialNwbFileWithPynwb(testCase) % Retrieve all files generated by tutorial - nwbListing = dir('*.nwb'); + nwbFileNameList = testCase.listNwbFiles(); + for nwbFilename = nwbFileNameList + try + io = py.pynwb.NWBHDF5IO(nwbFilename); + nwbObject = io.read(); + testCase.verifyNotEmpty(nwbObject, 'The NWB file should not be empty.'); + io.close() + catch ME + error(ME.message) + end + end + end + + function inspectTutorialFileWithNwbInspector(testCase) + % Retrieve all files generated by tutorial + nwbFileNameList = testCase.listNwbFiles(); + for nwbFilename = nwbFileNameList + if testCase.NWBInspectorMode == "python" + results = py.list(py.nwbinspector.inspect_nwbfile(nwbfile_path=nwbFilename)); + results = testCase.convertNwbInspectorResultsToStruct(results); + elseif testCase.NWBInspectorMode == "CLI" + [~, m] = system(sprintf('nwbinspector %s --levels importance', nwbFilename)); + results = testCase.parseNWBInspectorTextOutput(m); + end + + if isempty(results) + return + end + + results = testCase.filterNWBInspectorResults(results); + % T = struct2table(results); disp(T) + + for j = 1:numel(results) + testCase.verifyLessThan(results(j).importance, testCase.NwbInspectorSeverityLevel, ... + sprintf('Message: %s\nLocation: %s\n File: %s\n', ... + string(results(j).message), results(j).location, results(j).filepath)) + end + end + end + end - for i = 1:numel(nwbListing) - nwbFilename = nwbListing(i).name; - if any(strcmp(nwbFilename, tests.unit.TutorialTest.SkippedFiles)) - continue + methods (Access = private) + function nwbFileNames = listNwbFiles(testCase) + nwbListing = dir('*.nwb'); + nwbFileNames = string( {nwbListing.name} ); + nwbFileNames = setdiff(nwbFileNames, testCase.SkippedFiles); + assert(isrow(nwbFileNames), 'Expected output to be a row vector') + if ~isscalar(nwbFileNames) + if iscolumn(nwbFileNames) + nwbFileNames = transpose(nwbFileNames); end + end + end + end + methods (Static) + function resultsOut = convertNwbInspectorResultsToStruct(resultsIn) + + resultsOut = tests.unit.TutorialTest.getEmptyNwbInspectorResultStruct(); + + C = cell(resultsIn); + for i = 1:numel(C) + resultsOut(i).importance = double( py.getattr(C{i}.importance, 'value') ); + resultsOut(i).severity = double( py.getattr(C{i}.severity, 'value') ); + try + resultsOut(i).location = string(C{i}.location); + catch + resultsOut(i).location = "N/A"; + end + + resultsOut(i).message = string(C{i}.message); + resultsOut(i).filepath = string(C{i}.file_path); + resultsOut(i).check_name = string(C{i}.check_function_name); + end + end + + function resultsOut = parseNWBInspectorTextOutput(systemCommandOutput) + resultsOut = tests.unit.TutorialTest.getEmptyNwbInspectorResultStruct(); + + importanceLevels = containers.Map(... + ["BEST_PRACTICE_SUGGESTION", ... + "BEST_PRACTICE_VIOLATION", ... + "CRITICAL", ... + "PYNWB_VALIDATION", ... + "ERROR"], 0:4 ); + + lines = splitlines(systemCommandOutput); + count = 0; + for i = 1:numel(lines) + % Example line: + % '.0 Importance.BEST_PRACTICE_VIOLATION: behavior_tutorial.nwb - check_regular_timestamps - 'SpatialSeries' object at location '/processing/behavior/Position/SpatialSeries' + % ^2 ^1 ^2 ^ ^ ^ 3 + % [-----------importance------------] [------filepath------] [------check_name------] [-----------------location----------------] + % Splitting and components is exemplified above. + + if ~isempty(regexp( lines{i}, '^\.\d{1}', 'once' ) ) + count = count+1; + % Split line into separate components + splitLine = strsplit(lines{i}, ':'); + splitLine = [... + strsplit(splitLine{1}, ' '), ... + strsplit(splitLine{2}, '-') ... + ]; + + resultsOut(count).importance = importanceLevels( extractAfter(splitLine{2}, 'Importance.') ); + resultsOut(count).filepath = string(strtrim( splitLine{3} )); + resultsOut(count).check_name = string(strtrim(splitLine{4} )); try - io = py.pynwb.NWBHDF5IO(nwbListing(i).name); - nwbObject = io.read(); - testCase.verifyNotEmpty(nwbObject, 'The NWB file should not be empty.'); - io.close() - - catch ME - if strcmp(ME.identifier, 'MATLAB:undefinedVarOrClass') && ... - contains(ME.message, 'py.pynwb.NWBHDF5IO') - - pythonExecutable = tests.util.getPythonPath(); - cmd = sprintf('"%s" -B -m read_nwbfile_with_pynwb %s',... - pythonExecutable, nwbFilename); - - status = system(cmd); - if status ~= 0 - error('Failed to read NWB file "%s" using pynwb', nwbFilename) - end - else - rethrow(ME) - end + locationInfo = strsplit(splitLine{end}, 'at location'); + resultsOut(count).location = string(strtrim(eval(locationInfo{2}))); + catch + resultsOut(count).location = 'N/A'; end + resultsOut(count).message = string(strtrim(lines{i+1})); + end + end + end - catch ME - error(ME.message) - %testCase.verifyFail(sprintf('Failed to read file %s with error: %s', nwbListing(i).name, ME.message)); + function emptyResults = getEmptyNwbInspectorResultStruct() + emptyResults = struct(... + 'importance', {}, ... + 'severity', {}, ... + 'location', {}, ... + 'filepath', {}, ... + 'check_name', {}, ... + 'ignore', {}); + end + + function resultsOut = filterNWBInspectorResults(resultsIn) + CHECK_IGNORE = [... + "check_image_series_external_file_valid", ... + "check_regular_timestamps" + ]; + + for i = 1:numel(resultsIn) + resultsIn(i).ignore = any(strcmp(CHECK_IGNORE, resultsIn(i).check_name)); + + % Special cases to ignore + if resultsIn(i).location == "/acquisition/ExternalVideos" && ... + resultsIn(i).check_name == "check_timestamps_match_first_dimension" + resultsIn(i).ignore = true; + elseif resultsIn(i).location == "/acquisition/SpikeEvents_Shank0" && ... + resultsIn(i).check_name == "check_data_orientation" + % Data for this example is actually longer in another dimension + % than time. + resultsIn(i).ignore = true; end end + resultsOut = resultsIn; + resultsOut([resultsOut.ignore]) = []; end end end function tutorialNames = listTutorialFiles() % listTutorialFiles - List names of all tutorial files (exclude skipped files) - rootPath = getMatNwbRootDirectory(); - L = dir(fullfile(rootPath, 'tutorials')); + rootPath = tests.util.getProjectDirectory(); + L = cat(1, ... + dir(fullfile(rootPath, 'tutorials', '*.mlx')), ... + dir(fullfile(rootPath, 'tutorials', '*.m')) ... + ); + L( [L.isdir] ) = []; % Ignore folders tutorialNames = setdiff({L.name}, tests.unit.TutorialTest.SkippedTutorials); end - -function folderPath = getMatNwbRootDirectory() - folderPath = fileparts(fileparts(fileparts(mfilename('fullpath')))); -end diff --git a/+tests/+unit/untypedSetTest.m b/+tests/+unit/untypedSetTest.m index 4d18716f..29aad172 100644 --- a/+tests/+unit/untypedSetTest.m +++ b/+tests/+unit/untypedSetTest.m @@ -32,13 +32,15 @@ function testCreateSetFromNvPairsPlusFunctionHandle(testCase) end function testDisplayEmptyObject(testCase) - emptyUntypedSet = types.untyped.Set(); - disp(emptyUntypedSet) + emptyUntypedSet = types.untyped.Set(); %#ok + C = evalc( 'disp(emptyUntypedSet)' ); + testCase.verifyClass(C, 'char') end function testDisplayScalarObject(testCase) - scalarSet = types.untyped.Set('a',1) - disp(scalarSet) + scalarSet = types.untyped.Set('a', 1); %#ok + C = evalc( 'disp(scalarSet)' ); + testCase.verifyClass(C, 'char') end function testGetSetSize(testCase) diff --git a/+tests/+util/addFolderToPythonPath.m b/+tests/+util/addFolderToPythonPath.m new file mode 100644 index 00000000..ac21c350 --- /dev/null +++ b/+tests/+util/addFolderToPythonPath.m @@ -0,0 +1,13 @@ +function addFolderToPythonPath(folderPath) + pythonPath = getenv('PYTHONPATH'); + if isempty(pythonPath) + updatedPythonPath = folderPath; + else + if ~contains(pythonPath, folderPath) + updatedPythonPath = strjoin({pythonPath, folderPath}, pathsep); + else + return + end + end + setenv('PYTHONPATH', updatedPythonPath); +end diff --git a/+tests/+util/getProjectDirectory.m b/+tests/+util/getProjectDirectory.m new file mode 100644 index 00000000..8cdad9fa --- /dev/null +++ b/+tests/+util/getProjectDirectory.m @@ -0,0 +1,3 @@ +function projectDirectory = getProjectDirectory() + projectDirectory = fullfile(fileparts(mfilename('fullpath')), '..', '..'); +end diff --git a/+tests/requirements.txt b/+tests/requirements.txt index da0e7629..cc9afd91 100644 --- a/+tests/requirements.txt +++ b/+tests/requirements.txt @@ -1,2 +1,3 @@ pynwb -hdf5plugin \ No newline at end of file +hdf5plugin +nwbinspector diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 0d6230c5..ad7aee46 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -29,6 +29,7 @@ jobs: python -m pip install -U pip pip install -r +tests/requirements.txt echo "HDF5_PLUGIN_PATH=$(python -c "import hdf5plugin; print(hdf5plugin.PLUGINS_PATH)")" >> "$GITHUB_ENV" + echo $( python -m pip show nwbinspector | grep ^Location: | awk '{print $2}' ) - name: Install MATLAB uses: matlab-actions/setup-matlab@v2 with: @@ -36,7 +37,10 @@ jobs: - name: Run tests uses: matlab-actions/run-command@v2 with: - command: results = assertSuccess(nwbtest); assert(~isempty(results), 'No tests ran'); + command: | + pyenv("ExecutionMode", "OutOfProcess"); + results = assertSuccess(nwbtest); + assert(~isempty(results), 'No tests ran'); - name: Upload JUnit results if: always() uses: actions/upload-artifact@v4 diff --git a/nwbtest.m b/nwbtest.m index 18a5f86a..a1f2ed15 100644 --- a/nwbtest.m +++ b/nwbtest.m @@ -46,6 +46,7 @@ ws = pwd; nwbClearGenerated(); % Clear default files if any. + cleanupObj = onCleanup(@() generateCore); cleaner = onCleanup(@generateCore); % Regenerate core when finished pvcell = struct2pvcell(parser.Unmatched); diff --git a/tutorials/behavior.mlx b/tutorials/behavior.mlx index 33fdc5cd9d1ee4566a05df2fa5eaae149c6ecd44..5894a6f889e9e29c76255b28d3131203f5519ebc 100644 GIT binary patch delta 6491 zcmZ9RRZyH!)2(qE++py+3GN|S2qd_>ySwX)1@|Ek0t^np-NPif1P=sv4MBs$1Wvy1 z)TuiEf3bIUKdZaCs;}0roh-8|+c1cNWA)lLss^eJ23tgI>(tCwzf#6F&h3eHt?v-#01o$TYxHL^Ip z!?%Y8ZfQvWy=FaMYK?2r*)l9SyP{T!uKQynCgqIqyDqdv$v5=-gUb9{3tIBWk>Hee zJmCbF9}+>tqIM@Kb*x%~Bw#Yd)8?G3P?3}}wndXcl^DQXwea%_C1m1Y!lvDrD=^^tGeW$}F2CUx zmU0lwLtv(gC*aU^L-i$#Yk&=g0!DFHx7<-mFl@ju$^pI4gS1|C4=2+^hv!foxW7Bv zMjRom#lTI1H(??Jx~}}gp7@jqXFZRvOLh}7gGPl22q~YZ;I-`4$;9zYUe4WsM98*{ zNAJP#xp#cnBu)oNoAj>LckcJ|w4M6O@gdq1(7xnJ@PyjP)p4&*X0=jYZ*nS101_Hz zA}x$VrOb+BW-=6r77w1PM0FU0eOu%<{;pmI$)J;hJ{{51O84x@QaK?tirLP%7ctie zdzUNVK7<)}0pPNCci)iR`I2MdKJoR`ie`wD6UVOySHXYn*QE0_2GVh8H)jMZfZZ$L z;qU&|C6lVb5A_1GIb2!#az3e307(rLIwr-Sg~HHTh$)a{Ig``IjYZ*%Tro=Xb18ev zUZL$bg{`68L>5N3d|uxvQMrywj75!wV;(;uDKoXmY;dA;Xd&SLwaHF{4Xi61Q(5Dv%;{ zEjB?MEV-7IZ`l5Q9jp+jBB=p21=~clpx^RHDgQHy0*4jr_CnvVyLK72)CCVE58W6N zoo-<+F29{NdrxXl3mx(#!6N7~w~9~&(t#!FD#D9<4!7Kl2^vZQE-<@pu#xxT)*8+eF4(s(Cy;F+F+F(x?EAqwZZz(>Hi66#SDB1nm7*fX z)?53KIz?gwkg!Y#J74S#@?-V+8(I7FFvW73o90j0Y1JY}DZ<-w=ab=_ZMDe@mBTgN zBR}0l7X3pW%!;PMI4YE1iShz^Eye`W89Xa4BFF{N&He}aU-Q1USS7lk`^(5%=xJVr z!2`UlmU7lG29SQ~8wKEZe~No26M)(gC55j>T)=eVn=GyFK!n_#IsM`nK|Qh!#e$G5 z-H{^dMhL++diAf=x2qt*g$At#e1`PR{tqVTsNqzbc#iK#Ou(uwhM#c(@T6tB#^=w- z%WohWi0f!|{%xd&gU#FV);WCim~r$rNX11Z{_UPvv>)g3H)>ynh9rKB>J}?(%l=rY z7h{-=O8P(U2)Zc;iB=OV23-+}MtW?_aE@g@tKFX$m?I)1{)Y*@?Az6m1Ro1yCo38n z{>ZB`6oMc5@s0Rgh#teVRl#-A8N_lwE4J{Hke@ zOI1e~KHaNI<^glSL7i>vcEl~p6vtzUekSAh15o0iNyUvm>NN|c z7jLnz)b<}-mjr(T+Znn&39JxUoMlp+u?kQ8F~uTOKPD^vdE46}G)og2g%HbMEF2sU zC8lPGjtxqegni*lcEj@RpAi$&TN!EolBR%2iYP&BRRB^V{*lN-|abfl?~ z_}Kf><9pENDvsl)h6k2vBxl+tn`DRMw)lGCwAr$%y0&#?=d;6b%1wSJ3Q=Ws>mQm% z84F@8cl$O>_Eu#~Ro)l~UH@mDpyN;^%s4u-S1 zPW*l$PL~y!OC$75E{I1?#SvdBv5d(MrZ^MI%Qj1!oUO#ac-tD(E?Kzj*qoqO9D;$z|la`84iz>a&iD>f=^`3nw^XhT*SGjT^l zL`RKdlYKs>X(K7Iq<(s$FQg|%oZNqqBx&jOMxI^>tn${2z9vglJ&D4hWNC5Dfrsaf zW}PeqrXxl-(E{B+Ng3vbv^g`X{)H=2gLZ=>J{S+=@P#G@5AX_l27V0v!5QSt!M@jE zz-n0ru-lV0Q;6!Jcf~=vFGJ|tcijG#N>$Z3OevlmMz}_0RnA84hd6Q@_T+^$j-gd6 zYlvr{K;b`>x%k$kU7grsi~t`}a<#TUD*9Cy`dAZ2tn2&8EHq}Dtxrsub0&wR zAADey+8{A=^o(+Ib0zRPHkyEQB0G~smi=i4@WruMlyOlf#Y`RI{{q^F#)EMvRay7v z_L0`7U*SVFZump!b)c^6lj4T_<$i|q-%4gKNpR&=UxRIVEvkGD;qp*Es>LR?bAD7Z zQ~_j}H&#QCn#$6NcB;70%vH$&o0`-|1n!Qav4n;krOjKucQHOgQp9OD`A{^it(6Mk z08?mC*0Ewji-!O}WgqQ&Sz!e$f=&znOp?=z3cfID5(c z?pPnC(e#flrHi}^GC;y%7;R>>jm+Kn&br;5K7<={?3**yxvjVlmtqpC+#CCJTB= zC5OXfDL`tT{_|tRJjFHn*$)%2v!} zm5haE4Lx}+N$mO(W4oNuU)miU>Z9D!MYhV}FfTsPVURaj@y+{GO^wPge~bp(&I$e%$~D@}Gq%R9cCPSv-Y7tG;~!ph!v zzdA#041!p5JI7L^G}vqcZJIM+l%ssoS+VHqbg8fOXm}u}XN%7O=jd45sXoU~6oqfA z?1_I1?0#KI|MXS^N#7X=J?Elpf%<*JzkjVVtX3&Lpy8d4;9{c^_P7n7`;-YcFTS#l zn4am1eo&TssURu<@kxTp#d20aC6dK2I5!%fLi-oIJwN1)SJi9~de4lexTOn}zHRK$ zWl=B>rfcEbb498EWcH<&rfi3dHrZj&DV$?1Sk##9H7b@8W5#IePfq}MV1n6W@t#M} zA$Z#%L!#fB>tP0FZ1|3r_;vQRV7*Z938O`W;*_nIAGc%?-TI$!9elm79}B(bnt4Mw zzJ5ZgpXLsd=5r3?>+>CqWniPA<1>)|Ol1QfLBk=}Jl* z#_VNCqgRwfPB8p?avB2IKa0N?+rgbz4&k+6w#!Jwt9Gi+_Qm=!s!Xv^*uqR8n0 zVOfME-uwbAPxg{^SvGKP`@EjVppd@6eucc5lSt(r%Q>8`S|6)wmK7NQiuiW2G68(|hhm@Jygz zjaVJo$E`ExAjzL18hJ#W;CRtupz+2*Siq;h_lwvyXT)bD)#vCh7u z6KH@Cf7-I|P@f6Q$;N@dw=iYtoW}Q0H$q)|svvl)VFdFkjcTtfKjhry$3lLYR)U<% ztC?hgNTfS4$rk zODkS?Ump)&AD#eLmpzlUl$ADO_!OW`2zMaPtK&Xe@FSz(o<%`kF8x-1C{OtB)my(b zR;;A?TNoebiu(a0G###3u#3y_e5#d|X^MuH!khK;o#5_Ue0z9NQu0^0vHf9ILdFGR z9*H0yxj3*-J8R<$H3ISUSbeei+fZcz^r>7ZRDZNMWABBTwxSt{;^t58QBHtK_OB7K zqP=s5K6B#R+dWT(wl5ji@?%gZRd1?PwKD#N{dsxaNi>#UB6Rtq+f5@MGj)zhae6^K zI&6aWhr=b%-vK^nOEaG1trPCi493ju=%K<4M~_I;Haf}9R6SWJ`w%+E zEw%2pOsH`FUatJB*$pf7Pm=)Q0~6GosD;?)-l>6A0FwAC!ER9f%V(S`zverbtxvd8 z=S1Np%hcX{OjGWMJf46oe0WE?`3ap?jKUl2>6lS6Ei4X((!HxC!MOv}lDsqWpafWV zzr*3HGZ#y2xg53z=8Q@0J3)DRshi()jijtu+*D-Ke^bA3I`RRkZDzo*Y-Wp&TtP*P z3L2LUo^|apLl)%Y2b!5~rk&Rkruq)S&1KB5Cs~Y9Tb{!_XkWp-5L}`GXC2y8IWO0z zX9}LbweExIXG!FAA8Tn26q0UWYn#)TjKW&R*xOImt%X`{>U%v-lPo9emu86+8P#VQEUBQX3XFLh0oXjVlS2Webnv35qQtY5xzYNVmg za>`hMWW$!iUzT9}$PVePQ^}mFMqK%X;IC$k*j$T2gWID=WR*W86hx2E)hKoG=!6Gq zF(qnz5H#c+Xf}@%jfr~1qm>%6Is|EDFfy?l3HIQhm&znC5LrAuyvD=A zyBXZjNd>AJRb(zc*Rfc0RDHqEb^pUsFbLLy*UJn2O)ryf$eYaC6c(b(am$x{PS$C1 zFG_iwDD@5kSFHupg5VtXVsyVmVn{V=y}1{GHHzQ9j(HK4?ejs%nGHoFuz?b}9?6%1 zA%0BHrIBAYeLIDCLB)X@*mKEQJqcIK$>GQ|TY#_Yo)Ngyq_seDxzbOX=TU1e{rUZ6 z*(EXmN8$5!^z4j!O@(aYUJC{@e1Advg!Vqx$m~NP{Wm`Q2pr^j%d!mD z!6dfvXKl0*QjV?YvIqGmT_61BNQ!+i%%VMmFD^HX-Qd))bHsYCv-1+}7AYePBROJ? z)7g=*UPB|9M>8Qi?X}U zjmZ|BCuHr75^by7!NCxdECI)&$_d(nqDvrQ7-;QQle22!g9!pjDG;s;hs z3*s17%Q=Zdr64xdBE3=jjkQWt{gFs_;9jqQm}zvzLRNn_%acF$sDMjNa9lJI8F*3p zv$G>5^k>=GgS5dgyn0uH*3R9>>kDq(YxlzLMEePBbxRN1&{#G$5z7(n)Y}|k)ZoZ&!6{1(|SMr zwggHK7UjI}c_u!W{+|Yhy2v1b78*k(2q-yL8^2pIb?_^Tj~Avs!;)NydrvIW-P2R! z=6giUJNlf@8+)1kJ^yqXQ>;En+cK+^G9o>+dc3?(eInrn-yhkJdOkf7IA^`qEj9LK zEhabkbSCb-=;(Cv&+2lasXj1 zRwP~YfSN*xPMeq3vj0MIq@LEHOcXY(DSF0Tuezb#!*iPyHiMjZL$Sr5Gm@`QB8)31 zunVEMwZGbXI`u?wYVLb*VRyh7b?8n^UJ@c3k~yX|FGa#;2_X{+oR`v1=%Ir&rvp zD)>*hT+o%8C;7Ta5%N5Cg!9+rC*Jv9aABF)(DH=0ijSGw(l~fp$jTybr&YO4CO#m_^J0Y-STx}?9yXQi z?SYHM{Cxi&|J}QZ-7*_vTWsqmBs%68bcrE+{<;v=zGitQ-mkYgTY%5uA_6m=KN~-t z$`bS4w)Wk&zl~s5-8w*f$+B%mXF+>j{zQZUKCBxw^l(LfYdduZyag`Otv(l{)jF)` zvcXmckuo%~(V*Sf7>d$3(E>z02^~ALa9|$GSd&woioN1#Y|#5$5cr*{3dP2C9tQ8C zQ&z;`f*Q_P8l5ICvR^0bT4%Tht#4caf5xD>aVxEEG_CQ=+1X#`O$=Z!{mY~Z!?t_~ z=Fnj{x3CLxuj$i+ZT!cvqhi%b;@^MXyAe{r!~zOKM6D^XXuAjuk=!T-IF6K72*8M_ z(TjWbs5hshm*_N$ujO^r^*<`%>OdNcB6=ke7*-z+yW_j)^AWZBAvvKB60WaE~KTT*oI}4>Jda>zeI1Dx* zC5g6|f>@4l!s2Rb*CNO)99<;SafOB?>EFJTV34rG(dU^fzr(VK7QZ?s$~C_wU_|!p zekn>>&S%JM0Z>7yMCNz?fU`oOY9S(NKi2z<>j-JU87aYaTjs_Brx2{7a$AmfM+ljH z*sBqMQ+uw!nZ?5K4#uC8HtOl(rxXGtqy;($Zarvj+YLuYJNqrgxE<$Ni+2rqb3YKjj2YTARNj3xWyZZ$OFb%yyqD z03-f7M(w$Jq@$x%c{z*BHkB6;Yuh53VS;`=K^KWR4Z3D)#Ja+ta)IA}BsYolt^~4> zxv2*|mf>9(-)MHnUjLCDj!Jf*YIA5T(TuJhdOAFo7Idh|zVj2H{AT~L_M-sYR=h-` zMtq~IK0Fr~IzP;~n3B%-ZRyeI-prFJ&8^b7h%QdJwWo{tX9gp5-O2ZkRFa9ezX5&L z7I{lE)D<*uF~j4`98}d@dL>F}EJ>M!^x3xRb6TBi+?*$QS}JrToN~{S*URKpJoR(; zPyWGs#5^p9HUsqep!Q5nl_B+N{j^Yw&xphmy~yog#t0|rZN(G){@e4~N=GtFn$FnM z^NSNJmZnc}@*^(FRk}nsRZB)+Hs@YtT^Qc+DIzEB`Xi+$g{UE=s?^#VN^7Jrux(Xf z-y7z+Fc-a!o1wR!`@t(}o3)uJCeM?RdL3ulZBtKcvs|-0;KUOWqv54qjw~5*pG3u; zpT6mXzmoc&JTRKare#6xcw8WjBIl6rS1I9%a9?@!!;sF7rSa~FS+>=c1E4BGwi~3B zs~l!RAEtRIqzXBG{%B~n1*9_0G&Kjos5MiczRZ;sG{pyMl!jXp>x{_Gqqeo4(s3kZ zE`IB73kEi=(BG*a$N*K2 zO(G+;UppR-@hUXs?M5aA@hFpoIWYth-Sg@0-<0#6vMJF@^ip~$P-9x&9?{CHKM{Ta z7qT7BH%0cOG(JgeE;QCH68q^Ep48Ar6e_FynoT3PvM_;VN<ht~FDnU5E*EL7r>e%FSEd7WAAKwq+E$O*~1{L3i^(tqe2 zM|`7Py_MYznwUryVt!E7IA>n4@@a|jW zHXW$%)@GKtilRM3SXh`%tV_V@%t5`^@2=lVQ`??Z)w+FQ%XlJ!(;QEgCA zrVy^&5l-o$I$-6cAk^5WzGV?i)l zN8^Cg9fkF5LYGE`^{6LDn3lY7`t$bNktclxYVkdnE~jq#F!9MV_z?!8G_ZS|VW&EaWS^eL*XtKo3^oqvJ6knr0k` zBFfjaz$_@vQX!|>?>m9--`7PoX79Ij)hhF73jG{m%;C%@Od4{ z5Y6ITy54a$7ar`wjZS*+o4Z$s0ODax+A_s? zF;Pv_It~toT0V`zk~j6RP!?%{blsE2Ak~s5m~dWl!ChFN4?V)ub@Vl-OS@##Vd2A+ zda#h1oQ%fzWqqG7Zii9wiD!7Q1#dHARQ0w_kC1$blYn6PlC%3v%ec6A4*NWtHGTWR z*JYI9H}Hqoz~ER?>k^{`A2^y?djtE=O^M!fp*0@ut}X})%#VOzohshX%ge1b+HqPx zr5JtQrPQA@GWk$U7Ipd=@C^>8r5I;^N}JgQHB$Eqk?Aty*fFq;_P0+abiv8EckQ$S zy}aT?T4J150CIcR9I;ixpQw`f?Wvo?u`42c%D^H=Zk>lG6=fCYmqmvnE&8~BHhjco zMWTgKV^F_$I@G*XT|c4{+ur-q#WaLXP$n3-Tzxwi-TdEK zsU9y>Pe^hOdwLgTiXO6IHHy)?e9y@TDDTZu*`9NJzlJy37jDFWUs%2REsmTu9}|Xm z`@yk^p~lY-t1wNsEpq^J=$vXy^)>Mz&ZWm)bR5?y68z=&qZ&n->R~*O$cZ>c;rENs@*X^Vb!cZFwC&vWIy~qy%R-uCRwbCCZwjj{zN}Dce~3 zvF<0vh;v^{7D%+k{9^6Jz;b8xZ*Xdg2#9z9008CRqXJ|{X9&ywgPUTB71)GeeZ!a? zJ{6#`r<%^j^YV46!#BgLYFq zTq*M;8b3T;nWED**h#+e9?%YZT%d#d5M5c7`F=^n;)H z1v?Lfm#Au)hfmb0J7WRLD!c9bwRn>kI4X&jDvpR@cR%PA7VjN-@P|XeW07ZEIVDcD zh|2pYe%X;uxDERjcis&k=^U2o1zL8nsioo1?<;$%JoGeL;f9!y>d#bFzk}DS`tK6< zmJUz!uN2331Q<#-?=Ma&9d0gN~9a%LQsby}h716n4H;Zk-kjiRiSpMkaU-dLoYU|!)EhBj@y~1xt!lCvcurtTlPfnQdD=x z5j!{)aRGa#=`xB|SJ0!JGV?O!>*4bqy^e~0^ogWV{na~csc1N`UL@^?c7wNU@?i>I zF#TUmt=~+AiFA=kd!Io1ny~Nl1vsv9n%7=fFu|^3lwvLhVdoD28?TCAS8rtAK)Bw# zoy9Mgv~*rZ2dKtzt{o(yM?pDA@yugiex;NakHa2q)KQs^JtX{DOCs3K!WPlsdYT4T zL;|PHIH2f;ssvvB7x8rW4;nA-N84WCya@7ovM!G@+lyqSrQhUe5#;_wgM7Q_;r4%{c73i`4 zoi&Z}MQ$BtR%CMBmUlE0bX+Rgl9sf0>H+H$=+M=x!uvI&Ht` z0$~PI=_lQB728g?UPtm@Y#5naN0E`L_>+7n+^-;%T7NgVNG3!G%U<$N^P?^%V%XrX zIr%GPu$jl=bS>L?_vzM*H;iS2HheU@(U2g*5=8y?OG=;#dRr5Dvo&VN$(~ajYUos= zxwXII14kHG@W@~8FVvQwf@y})`TW3y6KIIl^SSjr9W%DW2JS-!V*)>AYR*xBU;!L= zl5cmFQqkn-^R2Hh<|-HV(W5OB%n>ynqQ!+Th8{0sgKwcZ0=TJ=CdhBi`^hOSY#Y(~ z`Lfk!dtt?_fB%ZFRFsE0;YF#wb0%m^*F+RY)x8Nkk@IFGzw};B?WO9(&6{Lo0*K)D zyK-M3r{pU}ZSy*wLd{!ibw(yJFHrl!u4L0|PW@~m&tT)jpTxw$1N!U9vM9y*S=Puh zB|NIq0I7I8wZ_#&+!7R`4IVwmq=eDEo27z|h(q)A-`mP4{ztw$?UHyp4$9C3)NVco zdgef|2^UuLfJ3uJ5luzV@?Z2L0VV{{pD(^-$KmUznKtRzsz>sG{P{gF&^v%zGYVG z;KKz>c(J2kY_^Qs=st9_%;BE3+M`sgBaj||-CjbpuSvYU-rg;DB+w&(d<1HK(oPN( z`%Pke$7nsc-(8-GDHyg`l$cm_Srs>guV7PWVDECA?RU@%EN4@vWv?pGt6FJ*S2g@u z+U^-+=jAwq=KC$&>UvwKY_Cn3S*`a`lQ+Cey|U}8ULY}FS{W!f#xBHY25q)xOG}v= z?5f3lZE(8*egpbRvU3J&SYH@dzfuHIdhkwR`UvJ%<01>1mf*@ldMg7)3_n;D`M&OS z2;a~WdBb8UW+ljQFrrf|vRBmyKBxt7gO~^UJwCxpm5F{h6X;`;I(r+6k*eGk647yT zb?rf~l5{j}jvVY@95r#(c3YekhlIm}e{icSMV!7I|H3{q%^6;<&0riiEwgqw%CaLR zGM)7fpeG~I=y(jO)mT9xVQ`9!T1~vsPPZYGMJUQXN~|%VA!rG7H(50@i9yL447C^X z1+`ZZIh72OEL#gtVp#nGa@W6)azfk@)Z=jEfa2ET=Mbh{soZ%M-xIO(K&E`aKfmVF z%gS=(h<@~bG28xt#LWx2x-}7g>KYp}xh>U=a6o{J3-`>(0VaD;0>+hkX2JlS#?P;H zHeQEoX|YdC+Uhl}TJ@aKoL;AWw87<<4^x5$tg)_Wvf%2VB zzXkJ)VB5=k&>F0eIZf!FjM)*yW_ZGm*=6O~WbJ)_K!3T{E7mbF z|M&h_hoknSi$98iVqg@ZYg-#r(L0|@NDahz#q<0>lVoypiv#s`oj|c4g2yvMJ>1g& z4ewQN`(dg#k)4Xp-cU%)%=gushnF8)w+AI_Vxk#Y&&}#16wd{(Z-D=uI80xuE6L#i z03vt*faG5Waj!zWjA+nO>`h81*0|0G1|7Fk5}<<*2?aXUuYl#e5kB&f`i!Sqq6049p z$vaGP1E=l~0^x{{J0W#+>%k^SMMe(din-D~ea>@2tWv(Myp0+i!@SU>TE#Ld@Q^Gr z2qtMpPb11e!pnHpb|NcLz~G+Oji~C?bHrc1Qd95ju}zo*h0D6P-r`Ijs&t~aBMh|| zl4U!eJQ{sj+j-EQ&UmTB+q4T+bE!e!6GygDbN^Aj7BlST;ik{-l3d3_XRqREbkid5 z7H`}^aJ%sQzBmTIa`~Rf#)Ng%@ifA*)Q;3xf` z;06F-|FiKw_0QFR^@^4Fje>^s|N8tN>i-S65hWTElO>W;VkH)nU}5|ZF+!a9hm<4{ hN`m;0WP+a|`S)c=bp8jsNdKE@qNG6XB>RuxzW{TT0Zjk^ diff --git a/tutorials/html/behavior.html b/tutorials/html/behavior.html index e3b1f055..4f70f592 100644 --- a/tutorials/html/behavior.html +++ b/tutorials/html/behavior.html @@ -1,27 +1,125 @@ -Behavior Data

Behavior Data

This tutorial will guide you in writing behavioral data to NWB.

Creating an NWB File

Create an NWBFile object with the required fields (session_description, identifier, and session_start_time) and additional metadata.
nwb = NwbFile( ...
'session_description', 'mouse in open exploration',...
'identifier', 'Mouse5_Day3', ...
'session_start_time', datetime(2018, 4, 25, 2, 30, 3), ...
'general_experimenter', 'My Name', ... % optional
'general_session_id', 'session_1234', ... % optional
'general_institution', 'University of My Institution', ... % optional
'general_related_publications', 'DOI:10.1016/j.neuron.2016.12.011'); % optional
nwb

SpatialSeries: Storing continuous spatial data

SpatialSeries is a subclass of TimeSeries that represents data in space, such as the spatial direction e.g., of gaze or travel or position of an animal over time.
Create data that corresponds to x, y position over time.
position_data = [linspace(0, 10, 50); linspace(0, 8, 50)];
In SpatialSeries data, the first dimension is always time (in seconds), the second dimension represents the x, y position. SpatialSeries data should be stored as one continuous stream as it is acquired, not by trials as is often reshaped fro analysis. Data can be trial-aligned on-the-fly using the trials table. See the trials tutorial for further information.
For position data reference_frame indicates the zero-position, e.g. the 0,0 point might be the bottom-left corner of an enclosure, as viewed fromvteh tracking camera.
timestamps = linspace(0, 50)/ 200;
position_spatial_series = types.core.SpatialSeries( ...
'description', 'Postion (x, y) in an open field.', ...
'data', position_data, ...
'timestamps', timestamps, ...
'reference_frame', '(0,0) is the bottom left corner.' ...
)

Position: Storing position measured over time

To help data analysis and visualiztion tools know that this SpatialSeries obejct represents the position of the subject, store the SpatialSeries object inside a Position object, which can hold one or more SpatialSeries objects.
position = types.core.Position();
position.spatialseries.set('SpatialSeries', position_spatial_series);

Create a Behavior Processing Module

Create a processing module called "behavior" for storing behavioral data in the NWBFile, then add the Position object to the processing module.
behavior_processing_module = types.core.ProcessingModule('description', 'stores behavioral data.');
behavior_processing_module.nwbdatainterface.set("Position", position);
nwb.processing.set("behavior", behavior_processing_module);

CompassDirection: Storing view angle measured over time

Analogous to how position can be stored, we can create a SpatialSeries object for representing the view angle of the subject.
For direction data reference from indicates the zero direction, for instance in this case "straight ahead" is 0 radians.
view_angle_data = linspace(0, 4, 50);
direction_spatial_series = types.core.SpatialSeries( ...
'description', 'View angle of the subject measured in radians.', ...
'data', view_angle_data, ...
'timestamps', timestamps, ...
'reference_frame', 'straight ahead', ...
'data_unit', 'radians' ...
);
direction = types.core.CompassDirection();
direction.spatialseries.set('spatial_series', direction_spatial_series);
We can add a CompassDirection object to the behavior processing module the same way we have added the position data.
%behavior_processing_module = types.core.ProcessingModule("stores behavioral data."); % if you have not already created it
behavior_processing_module.nwbdatainterface.set('CompassDirection', direction);
%nwb.processing.set('behavior', behavior_processing_module); % if you have not already added it

BehaviorTimeSeries: Storing continuous behavior data

BehavioralTimeSeries is an interface for storing continuous behavior data, such as the speed of a subject.
speed_data = linspace(0, 0.4, 50);
 
speed_time_series = types.core.TimeSeries( ...
'data', speed_data, ...
'starting_time_rate', 10.0, ... % Hz
'description', 'he speed of the subject measured over time.', ...
'data_unit', 'm/s' ...
);
 
behavioral_time_series = types.core.BehavioralTimeSeries();
behavioral_time_series.timeseries.set('speed', speed_time_series);
 
%behavior_processing_module = types.core.ProcessingModule("stores behavioral data."); % if you have not already created it
behavior_processing_module.nwbdatainterface.set('BehavioralTimeSeries', behavioral_time_series);
%nwb.processing.set('behavior', behavior_processing_module); % if you have not already added it

BehavioralEvents: Storing behavioral events

BehavioralEvents is an interface for storing behavioral events. We can use it for storing the timing and amount of rewards (e.g. water amount) or lever press times.
reward_amount = [1.0, 1.5, 1.0, 1.5];
event_timestamps = [1.0, 2.0, 5.0, 6.0];
 
time_series = types.core.TimeSeries( ...
'data', reward_amount, ...
'timestamps', event_timestamps, ...
'description', 'The water amount the subject received as a reward.', ...
'data_unit', 'ml' ...
);
 
behavioral_events = types.core.BehavioralEvents();
behavioral_events.timeseries.set('lever_presses', time_series);
 
%behavior_processing_module = types.core.ProcessingModule("stores behavioral data."); % if you have not already created it
behavior_processing_module.nwbdatainterface.set('BehavioralEvents', behavioral_events);
%nwb.processing.set('behavior', behavior_processing_module); % if you have not already added it
Storing only the timestsamps of the events is possible with the ndx-events NWB extension. You can also add labels associated with the events with this extension. You can find information about installation and example usage here.

BehavioralEpochs: Storing intervals of behavior data

BehavioralEpochs is for storing intervals of behavior data. BehavioralEpochs uses IntervalSeries to represent the time intervals. Create an IntervalSeries object that represents the time intervals when hte animal was running. IntervalSeries uses 1 to indicate the beginning of an interval and -1 to indicate the end.
run_intervals = types.core.IntervalSeries( ...
'description', 'Intervals when the animal was running.', ...
'data', [1, -1, 1, -1, 1, -1], ...
'timestamps', [0.5, 1.5, 3.5, 4.0, 7.0, 7.3] ...
);
 
behavioral_epochs = types.core.BehavioralEpochs();
behavioral_epochs.intervalseries.set('running', run_intervals);
You can add more than one IntervalSeries to a BehavioralEpochs object.
sleep_intervals = types.core.IntervalSeries( ...
'description', 'Intervals when the animal was sleeping', ...
'data', [1, -1, 1, -1], ...
'timestamps', [15.0, 30.0, 60.0, 95.0] ...
);
behavioral_epochs.intervalseries.set('sleeping', sleep_intervals);
 
% behavior_processing_module = types.core.ProcessingModule("stores behavioral data.");
% behavior_processing_module.nwbdatainterface.set('BehavioralEvents', behavioral_events);
% nwb.processing.set('behavior', behavior_processing_module);

Another approach: TimeIntervals

Using TimeIntervals to represent time intervals is often preferred over BehavioralEpochs and IntervalSeries. TimeIntervals is a subclass of DynamicTable, which offers flexibility for tabular data by allowing the addition of optional columns which are not defined in the standard.
sleep_intervals = types.core.TimeIntervals( ...
'description', 'Intervals when the animal was sleeping.', ...
'colnames', {'start_time', 'stop_time', 'stage'} ...
);
 
sleep_intervals.addRow('start_time', 0.3, 'stop_time', 0.35, 'stage', 1);
sleep_intervals.addRow('start_time', 0.7, 'stop_time', 0.9, 'stage', 2);
sleep_intervals.addRow('start_time', 1.3, 'stop_time', 3.0, 'stage', 3);
 
nwb.intervals.set('sleep_intervals', sleep_intervals);

EyeTracking: Storing continuous eye-tracking data of gaze direction

EyeTracking is for storing eye-tracking data which represents direction of gaze as measured by an eye tracking algorithm. An EyeTracking object holds one or more SpatialSeries objects that represent the gaze direction over time extracted from a video.
eye_position_data = [linspace(-20, 30, 50); linspace(30, -20, 50)];
 
right_eye_position = types.core.SpatialSeries( ...
'description', 'The position of the right eye measured in degrees.', ...
'data', eye_position_data, ...
'starting_time_rate', 50.0, ... % Hz
'reference_frame', '(0,0) is middle', ...
'data_unit', 'degrees' ...
);
 
left_eye_position = types.core.SpatialSeries( ...
'description', 'The position of the right eye measured in degrees.', ...
'data', eye_position_data, ...
'starting_time_rate', 50.0, ... % Hz
'reference_frame', '(0,0) is middle', ...
'data_unit', 'degrees' ...
);
 
eye_tracking = types.core.EyeTracking();
eye_tracking.spatialseries.set('right_eye_position', right_eye_position);
eye_tracking.spatialseries.set('left_eye_position', left_eye_position);
 
% behavior_processing_module = types.core.ProcessingModule("stores behavioral data.");
behavior_processing_module.nwbdatainterface.set('EyeTracking', eye_tracking);
% nwb.processing.set('behavior', behavior_processing_module);

PupilTracking: Storing continuous eye-tracking data of pupil size

PupilTracking is for storing eye-tracking data which represents pupil size. PupilTracking hold one or more TimeSeries obejcts taht canrepresent different features such as the dilaltion of the pupil measured over time by a pupil tracking algorithm.
pupil_diameter = types.core.TimeSeries( ...
'description', 'Pupil diameter extracted from the video of the right eye.', ...
'data', linspace(0.001, 0.002, 50), ...
'starting_time_rate', 20.0, ... % Hz
'data_unit', 'meters' ...
);
 
pupil_tracking = types.core.PupilTracking();
pupil_tracking.timeseries.set('pupil_diameter', pupil_diameter);
 
% behavior_processing_module = types.core.ProcessingModule("stores behavioral data.");
behavior_processing_module.nwbdatainterface.set('PupilTracking', pupil_tracking);
% nwb.processing.set('behavior', behavior_processing_module);

Writing the behavior data to an NWB file

All of the above commands build an NWBFile object in-memory. To write this file, use nwbExport.
nwbExport(nwb, 'test_behavior.nwb');
+.S3 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 1px solid rgb(217, 217, 217); border-bottom: 0px none rgb(33, 33, 33); border-radius: 4px 4px 0px 0px; padding: 6px 45px 0px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } +.S4 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 0px none rgb(33, 33, 33); border-bottom: 0px none rgb(33, 33, 33); border-radius: 0px; padding: 0px 45px 0px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } +.S5 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 0px none rgb(33, 33, 33); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 0px; padding: 0px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } +.S6 { color: rgb(33, 33, 33); padding: 10px 0px 6px 17px; background: rgb(255, 255, 255) none repeat scroll 0% 0% / auto padding-box border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; overflow-x: hidden; line-height: 17.234px; } +/* Styling that is common to warnings and errors is in diagnosticOutput.css */.embeddedOutputsErrorElement { min-height: 18px; max-height: 550px;} +.embeddedOutputsErrorElement .diagnosticMessage-errorType { overflow: auto;} +.embeddedOutputsErrorElement.inlineElement {} +.embeddedOutputsErrorElement.rightPaneElement {} +/* Styling that is common to warnings and errors is in diagnosticOutput.css */.embeddedOutputsWarningElement { min-height: 18px; max-height: 550px;} +.embeddedOutputsWarningElement .diagnosticMessage-warningType { overflow: auto;} +.embeddedOutputsWarningElement.inlineElement {} +.embeddedOutputsWarningElement.rightPaneElement {} +/* Copyright 2015-2023 The MathWorks, Inc. *//* In this file, styles are not scoped to rtcContainer since they could be in the Dojo Tooltip */.diagnosticMessage-wrapper { font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 12px;} +.diagnosticMessage-wrapper.diagnosticMessage-warningType { /*This fallback value will be used for appdesigner warnings*/ color: var(--rtc-warning-output-color, var(--mw-color-matlabWarning));} +.diagnosticMessage-wrapper.diagnosticMessage-warningType a { /*This fallback value will be used for appdesigner warnings*/ color: var(--rtc-warning-output-color, var(--mw-color-matlabWarning)); text-decoration: underline;} +.rtcThemeDefaultOverride .diagnosticMessage-wrapper.diagnosticMessage-warningType,.rtcThemeDefaultOverride .diagnosticMessage-wrapper.diagnosticMessage-warningType a { color: var(--mw-color-matlabWarning) !important;} +.diagnosticMessage-wrapper.diagnosticMessage-errorType { /*This fallback value will be used in appdesigner error tooltip text*/ color: var(--rtc-error-output-color, var(--mw-color-matlabErrors));} +.diagnosticMessage-wrapper.diagnosticMessage-errorType a { /*This fallback value will be used in appdesigner error tooltip text*/ color: var(--rtc-error-output-color, var(--mw-color-matlabErrors)); text-decoration: underline;} +.rtcThemeDefaultOverride .diagnosticMessage-wrapper.diagnosticMessage-errorType,.rtcThemeDefaultOverride .diagnosticMessage-wrapper.diagnosticMessage-errorType a { color: var(--mw-color-matlabErrors) !important;} +.diagnosticMessage-wrapper .diagnosticMessage-messagePart,.diagnosticMessage-wrapper .diagnosticMessage-causePart { white-space: pre-wrap;} +.diagnosticMessage-wrapper .diagnosticMessage-stackPart { white-space: pre;} +.embeddedOutputsTextElement,.embeddedOutputsVariableStringElement { white-space: pre; word-wrap: initial; min-height: 18px; max-height: 550px;} +.embeddedOutputsTextElement .textElement,.embeddedOutputsVariableStringElement .textElement { overflow: auto;} +.textElement,.rtcDataTipElement .textElement { padding-top: 2px;} +.embeddedOutputsTextElement.inlineElement,.embeddedOutputsVariableStringElement.inlineElement {} +.inlineElement .textElement {} +.embeddedOutputsTextElement.rightPaneElement,.embeddedOutputsVariableStringElement.rightPaneElement { min-height: 16px;} +.rightPaneElement .textElement { padding-top: 2px; padding-left: 9px;} +.S7 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 1px solid rgb(217, 217, 217); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 4px; padding: 6px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } +.S8 { margin: 10px 10px 9px 4px; padding: 0px; line-height: 21px; min-height: 0px; white-space: pre-wrap; color: rgb(33, 33, 33); font-family: Helvetica, Arial, sans-serif; font-style: normal; font-size: 14px; font-weight: 400; text-align: left; } +.S9 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 0px none rgb(33, 33, 33); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 0px 0px 4px 4px; padding: 0px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } +.S10 { margin: 15px 10px 5px 4px; padding: 0px; line-height: 18px; min-height: 0px; white-space: pre-wrap; color: rgb(33, 33, 33); font-family: Helvetica, Arial, sans-serif; font-style: normal; font-size: 17px; font-weight: 700; text-align: left; }

Behavior Data

This tutorial will guide you in writing behavioral data to NWB.

Creating an NWB File

Create an NWBFile object with the required fields (session_description, identifier, and session_start_time) and additional metadata.
nwb = NwbFile( ...
'session_description', 'mouse in open exploration',...
'identifier', 'Mouse5_Day3', ...
'session_start_time', datetime(2018, 4, 25, 2, 30, 3, 'TimeZone', 'local'), ...
'general_experimenter', 'My Name', ... % optional
'general_session_id', 'session_1234', ... % optional
'general_institution', 'University of My Institution', ... % optional
'general_related_publications', 'DOI:10.1016/j.neuron.2016.12.011'); % optional
nwb
nwb =
NwbFile with properties: + + nwb_version: '2.7.0' + file_create_date: [] + identifier: 'Mouse5_Day3' + session_description: 'mouse in open exploration' + session_start_time: {[2018-04-25T02:30:03.000000+02:00]} + timestamps_reference_time: [] + acquisition: [0×1 types.untyped.Set] + analysis: [0×1 types.untyped.Set] + general: [0×1 types.untyped.Set] + general_data_collection: '' + general_devices: [0×1 types.untyped.Set] + general_experiment_description: '' + general_experimenter: 'My Name' + general_extracellular_ephys: [0×1 types.untyped.Set] + general_extracellular_ephys_electrodes: [] + general_institution: 'University of My Institution' + general_intracellular_ephys: [0×1 types.untyped.Set] + general_intracellular_ephys_experimental_conditions: [] + general_intracellular_ephys_filtering: '' + general_intracellular_ephys_intracellular_recordings: [] + general_intracellular_ephys_repetitions: [] + general_intracellular_ephys_sequential_recordings: [] + general_intracellular_ephys_simultaneous_recordings: [] + general_intracellular_ephys_sweep_table: [] + general_keywords: '' + general_lab: '' + general_notes: '' + general_optogenetics: [0×1 types.untyped.Set] + general_optophysiology: [0×1 types.untyped.Set] + general_pharmacology: '' + general_protocol: '' + general_related_publications: 'DOI:10.1016/j.neuron.2016.12.011' + general_session_id: 'session_1234' + general_slices: '' + general_source_script: '' + general_source_script_file_name: '' + general_stimulus: '' + general_subject: [] + general_surgery: '' + general_virus: '' + intervals: [0×1 types.untyped.Set] + intervals_epochs: [] + intervals_invalid_times: [] + intervals_trials: [] + processing: [0×1 types.untyped.Set] + scratch: [0×1 types.untyped.Set] + stimulus_presentation: [0×1 types.untyped.Set] + stimulus_templates: [0×1 types.untyped.Set] + units: [] + +Warning: The following required properties are missing for instance for type "NwbFile": + timestamps_reference_time

SpatialSeries: Storing continuous spatial data

SpatialSeries is a subclass of TimeSeries that represents data in space, such as the spatial direction e.g., of gaze or travel or position of an animal over time.
Create data that corresponds to x, y position over time.
position_data = [linspace(0, 10, 50); linspace(0, 8, 50)]; % 2 x nT array
In SpatialSeries data, the first dimension is always time (in seconds), the second dimension represents the x, y position. However, as described in the dimensionMapNoDataPipes tutorial, when a MATLAB array is exported to HDF5, the array is transposed. Therefore, in order to correctly export the data, in MATLAB the last dimension of an array should be time. SpatialSeries data should be stored as one continuous stream as it is acquired, not by trials as is often reshaped for analysis. Data can be trial-aligned on-the-fly using the trials table. See the trials tutorial for further information.
For position data reference_frame indicates the zero-position, e.g. the 0,0 point might be the bottom-left corner of an enclosure, as viewed from the tracking camera.
timestamps = linspace(0, 50, 50)/ 200;
position_spatial_series = types.core.SpatialSeries( ...
'description', 'Postion (x, y) in an open field.', ...
'data', position_data, ...
'timestamps', timestamps, ...
'reference_frame', '(0,0) is the bottom left corner.' ...
)
position_spatial_series =
SpatialSeries with properties: + + reference_frame: '(0,0) is the bottom left corner.' + starting_time_unit: 'seconds' + timestamps_interval: 1 + timestamps_unit: 'seconds' + data: [2×50 double] + comments: 'no comments' + control: [] + control_description: '' + data_continuity: '' + data_conversion: 1 + data_offset: 0 + data_resolution: -1 + data_unit: 'meters' + description: 'Postion (x, y) in an open field.' + starting_time: [] + starting_time_rate: [] + timestamps: [0 0.0051 0.0102 0.0153 0.0204 0.0255 0.0306 0.0357 0.0408 0.0459 0.0510 0.0561 0.0612 0.0663 0.0714 0.0765 0.0816 0.0867 0.0918 0.0969 0.1020 0.1071 0.1122 0.1173 0.1224 0.1276 0.1327 0.1378 0.1429 0.1480 0.1531 … ] (1×50 double) +

Position: Storing position measured over time

To help data analysis and visualization tools know that this SpatialSeries object represents the position of the subject, store the SpatialSeries object inside a Position object, which can hold one or more SpatialSeries objects.
position = types.core.Position();
position.spatialseries.set('SpatialSeries', position_spatial_series);

Create a Behavior Processing Module

Create a processing module called "behavior" for storing behavioral data in the NWBFile, then add the Position object to the processing module.
behavior_processing_module = types.core.ProcessingModule('description', 'stores behavioral data.');
behavior_processing_module.nwbdatainterface.set("Position", position);
nwb.processing.set("behavior", behavior_processing_module);

CompassDirection: Storing view angle measured over time

Analogous to how position can be stored, we can create a SpatialSeries object for representing the view angle of the subject.
For direction data reference_frame indicates the zero direction, for instance in this case "straight ahead" is 0 radians.
view_angle_data = linspace(0, 4, 50);
direction_spatial_series = types.core.SpatialSeries( ...
'description', 'View angle of the subject measured in radians.', ...
'data', view_angle_data, ...
'timestamps', timestamps, ...
'reference_frame', 'straight ahead', ...
'data_unit', 'radians' ...
);
direction = types.core.CompassDirection();
direction.spatialseries.set('spatial_series', direction_spatial_series);
We can add a CompassDirection object to the behavior processing module the same way we have added the position data.
%behavior_processing_module = types.core.ProcessingModule("stores behavioral data."); % if you have not already created it
behavior_processing_module.nwbdatainterface.set('CompassDirection', direction);
%nwb.processing.set('behavior', behavior_processing_module); % if you have not already added it

BehaviorTimeSeries: Storing continuous behavior data

BehavioralTimeSeries is an interface for storing continuous behavior data, such as the speed of a subject.
speed_data = linspace(0, 0.4, 50);
 
speed_time_series = types.core.TimeSeries( ...
'data', speed_data, ...
'starting_time', 1.0, ... % NB: Important to set starting_time when using starting_time_rate
'starting_time_rate', 10.0, ... % Hz
'description', 'he speed of the subject measured over time.', ...
'data_unit', 'm/s' ...
);
 
behavioral_time_series = types.core.BehavioralTimeSeries();
behavioral_time_series.timeseries.set('speed', speed_time_series);
 
%behavior_processing_module = types.core.ProcessingModule("stores behavioral data."); % if you have not already created it
behavior_processing_module.nwbdatainterface.set('BehavioralTimeSeries', behavioral_time_series);
%nwb.processing.set('behavior', behavior_processing_module); % if you have not already added it

BehavioralEvents: Storing behavioral events

BehavioralEvents is an interface for storing behavioral events. We can use it for storing the timing and amount of rewards (e.g. water amount) or lever press times.
reward_amount = [1.0, 1.5, 1.0, 1.5];
event_timestamps = [1.0, 2.0, 5.0, 6.0];
 
time_series = types.core.TimeSeries( ...
'data', reward_amount, ...
'timestamps', event_timestamps, ...
'description', 'The water amount the subject received as a reward.', ...
'data_unit', 'ml' ...
);
 
behavioral_events = types.core.BehavioralEvents();
behavioral_events.timeseries.set('lever_presses', time_series);
 
%behavior_processing_module = types.core.ProcessingModule("stores behavioral data."); % if you have not already created it
behavior_processing_module.nwbdatainterface.set('BehavioralEvents', behavioral_events);
%nwb.processing.set('behavior', behavior_processing_module); % if you have not already added it
Storing only the timestamps of the events is possible with the ndx-events NWB extension. You can also add labels associated with the events with this extension. You can find information about installation and example usage here.

BehavioralEpochs: Storing intervals of behavior data

BehavioralEpochs is for storing intervals of behavior data. BehavioralEpochs uses IntervalSeries to represent the time intervals. Create an IntervalSeries object that represents the time intervals when the animal was running. IntervalSeries uses 1 to indicate the beginning of an interval and -1 to indicate the end.
run_intervals = types.core.IntervalSeries( ...
'description', 'Intervals when the animal was running.', ...
'data', [1, -1, 1, -1, 1, -1], ...
'timestamps', [0.5, 1.5, 3.5, 4.0, 7.0, 7.3] ...
);
 
behavioral_epochs = types.core.BehavioralEpochs();
behavioral_epochs.intervalseries.set('running', run_intervals);
You can add more than one IntervalSeries to a BehavioralEpochs object.
sleep_intervals = types.core.IntervalSeries( ...
'description', 'Intervals when the animal was sleeping', ...
'data', [1, -1, 1, -1], ...
'timestamps', [15.0, 30.0, 60.0, 95.0] ...
);
behavioral_epochs.intervalseries.set('sleeping', sleep_intervals);
 
% behavior_processing_module = types.core.ProcessingModule("stores behavioral data.");
% behavior_processing_module.nwbdatainterface.set('BehavioralEvents', behavioral_events);
% nwb.processing.set('behavior', behavior_processing_module);

Another approach: TimeIntervals

Using TimeIntervals to represent time intervals is often preferred over BehavioralEpochs and IntervalSeries. TimeIntervals is a subclass of DynamicTable, which offers flexibility for tabular data by allowing the addition of optional columns which are not defined in the standard DynamicTable class.
sleep_intervals = types.core.TimeIntervals( ...
'description', 'Intervals when the animal was sleeping.', ...
'colnames', {'start_time', 'stop_time', 'stage'} ...
);
 
sleep_intervals.addRow('start_time', 0.3, 'stop_time', 0.35, 'stage', 1);
sleep_intervals.addRow('start_time', 0.7, 'stop_time', 0.9, 'stage', 2);
sleep_intervals.addRow('start_time', 1.3, 'stop_time', 3.0, 'stage', 3);
 
nwb.intervals.set('sleep_intervals', sleep_intervals);

EyeTracking: Storing continuous eye-tracking data of gaze direction

EyeTracking is for storing eye-tracking data which represents direction of gaze as measured by an eye tracking algorithm. An EyeTracking object holds one or more SpatialSeries objects that represent the gaze direction over time extracted from a video.
eye_position_data = [linspace(-20, 30, 50); linspace(30, -20, 50)];
 
right_eye_position = types.core.SpatialSeries( ...
'description', 'The position of the right eye measured in degrees.', ...
'data', eye_position_data, ...
'starting_time', 1.0, ... % NB: Important to set starting_time when using starting_time_rate
'starting_time_rate', 50.0, ... % Hz
'reference_frame', '(0,0) is middle', ...
'data_unit', 'degrees' ...
);
 
left_eye_position = types.core.SpatialSeries( ...
'description', 'The position of the right eye measured in degrees.', ...
'data', eye_position_data, ...
'starting_time', 1.0, ... % NB: Important to set starting_time when using starting_time_rate
'starting_time_rate', 50.0, ... % Hz
'reference_frame', '(0,0) is middle', ...
'data_unit', 'degrees' ...
);
 
eye_tracking = types.core.EyeTracking();
eye_tracking.spatialseries.set('right_eye_position', right_eye_position);
eye_tracking.spatialseries.set('left_eye_position', left_eye_position);
 
% behavior_processing_module = types.core.ProcessingModule("stores behavioral data.");
behavior_processing_module.nwbdatainterface.set('EyeTracking', eye_tracking);
% nwb.processing.set('behavior', behavior_processing_module);

PupilTracking: Storing continuous eye-tracking data of pupil size

PupilTracking is for storing eye-tracking data which represents pupil size. PupilTracking holds one or more TimeSeries objects that can represent different features such as the dilation of the pupil measured over time by a pupil tracking algorithm.
pupil_diameter = types.core.TimeSeries( ...
'description', 'Pupil diameter extracted from the video of the right eye.', ...
'data', linspace(0.001, 0.002, 50), ...
'starting_time', 1.0, ... % NB: Important to set starting_time when using starting_time_rate
'starting_time_rate', 20.0, ... % Hz
'data_unit', 'meters' ...
);
 
pupil_tracking = types.core.PupilTracking();
pupil_tracking.timeseries.set('pupil_diameter', pupil_diameter);
 
% behavior_processing_module = types.core.ProcessingModule("stores behavioral data.");
behavior_processing_module.nwbdatainterface.set('PupilTracking', pupil_tracking);
% nwb.processing.set('behavior', behavior_processing_module);

Writing the behavior data to an NWB file

All of the above commands build an NWBFile object in-memory. To write this file, use nwbExport.
% Save to tutorials/tutorial_nwb_files folder
nwbFilePath = misc.getTutorialNwbFilePath('behavior_tutorial.nwb');
nwbExport(nwb, nwbFilePath);
fprintf('Exported NWB file to "%s"\n', 'behavior_tutorial.nwb')
Exported NWB file to "behavior_tutorial.nwb"

\ No newline at end of file diff --git a/tutorials/html/icephys.html b/tutorials/html/icephys.html index c992dd45..5b095148 100644 --- a/tutorials/html/icephys.html +++ b/tutorials/html/icephys.html @@ -1,42 +1,47 @@ -Intracellular electrophysiology

Intracellular electrophysiology

Table of Contents
Creating an NWBFile +.embeddedOutputsTextElement.rightPaneElement,.embeddedOutputsVariableStringElement.rightPaneElement { min-height: 16px;} +.rightPaneElement .textElement { padding-top: 2px; padding-left: 9px;}

Intracellular electrophysiology

The following tutorial describes storage of intracellular electrophysiology data in NWB. NWB supports storage of the time series describing the stimulus and response, information about the electrode and device used, as well as metadata about the organization of the experiment.
Illustration of the hierarchy of metadata tables used to describe the organization of intracellular electrophysiology experiments.

Creating an NWBFile

When creating an NWB file, the first step is to create the NWBFile, which you can create using the NwbFile command.
session_start_time = datetime(2018, 3, 1, 12, 0, 0, 'TimeZone', 'local');
nwbfile = NwbFile( ...
'session_description', 'my first synthetic recording', ...
'identifier', 'EXAMPLE_ID', ...
'session_start_time', session_start_time, ...
'general_experimenter', 'Dr. Bilbo Baggins', ...
'general_lab', 'Bag End Laboratory', ...
'general_institution', 'University of Middle Earth at the Shire', ...
'general_experiment_description', 'I went on an adventure with thirteen dwarves to reclaim vast treasures.', ...
'general_session_id', 'LONELYMTN' ...
);

Device metadata

Device metadata is represented by Device objects.
device = types.core.Device();
nwbfile.general_devices.set('Heka ITC-1600', device);

Electrode metadata

Intracellular electrode metadata is represented by IntracellularElectrode objects. Create an electrode object, which requires a link to the device of the previous step. Then add it to the NWB file.
electrode = types.core.IntracellularElectrode( ...
'description', 'a mock intracellular electrode', ...
'device', types.untyped.SoftLink(device) ...
);
nwbfile.general_intracellular_ephys.set('elec0', electrode);

Stimulus and response data

Intracellular stimulus and response data are represented with subclasses of PatchClampSeries. A stimulus is described by a time series representing voltage or current stimulation with a particular set of parameters. There are two classes for representing stimulus data:
The response is then described by a time series representing voltage or current recorded from a single cell using a single intracellular electrode via one of the following classes:
Below we create a simple example stimulus/response recording data pair.
ccss = types.core.VoltageClampStimulusSeries( ...
'data', [1, 2, 3, 4, 5], ...
'starting_time', 123.6, ...
'starting_time_rate', 10e3, ...
'electrode', types.untyped.SoftLink(electrode), ...
'gain', 0.02, ...
'sweep_number', uint64(15), ...
'stimulus_description', 'N/A' ...
);
nwbfile.stimulus_presentation.set('ccss', ccss);
vcs = types.core.VoltageClampSeries( ...
'data', [0.1, 0.2, 0.3, 0.4, 0.5], ...
'data_conversion', 1e-12, ...
'data_resolution', NaN, ...
'starting_time', 123.6, ...
'starting_time_rate', 20e3, ...
'electrode', types.untyped.SoftLink(electrode), ...
'gain', 0.02, ...
'capacitance_slow', 100e-12, ...
'resistance_comp_correction', 70.0, ...
'stimulus_description', 'N/A', ...
'sweep_number', uint64(15) ...
);
nwbfile.acquisition.set('vcs', vcs);

Adding an intracellular recording

The IntracellularRecordingsTable relates electrode, stimulus and response pairs and describes metadata specific to individual recordings.
Illustration of the structure of the IntracellularRecordingsTable
ic_rec_table = types.core.IntracellularRecordingsTable( ...
'categories', {'electrodes', 'stimuli', 'responses'}, ...
'colnames', {'recordings_tag'}, ...
'description', [ ...
'A table to group together a stimulus and response from a single ', ...
'electrode and a single simultaneous recording and for storing ', ...
'metadata about the intracellular recording.'], ...
'id', types.hdmf_common.ElementIdentifiers('data', int64([0, 1, 2])), ...
'recordings_tag', types.hdmf_common.VectorData( ...
'data', repmat({'Tag'}, 3, 1), ...
'description', 'Column for storing a custom recordings tag' ...
) ...
);
ic_rec_table.electrodes = types.core.IntracellularElectrodesTable( ...
'description', 'Table for storing intracellular electrode related metadata.', ...
'colnames', {'electrode'}, ...
'id', types.hdmf_common.ElementIdentifiers( ...
'data', int64([0, 1, 2]) ...
), ...
'electrode', types.hdmf_common.VectorData( ...
'data', repmat(types.untyped.ObjectView(electrode), 3, 1), ...
'description', 'Column for storing the reference to the intracellular electrode' ...
) ...
);
ic_rec_table.stimuli = types.core.IntracellularStimuliTable( ...
'description', 'Table for storing intracellular stimulus related metadata.', ...
'colnames', {'stimulus'}, ...
'id', types.hdmf_common.ElementIdentifiers( ...
'data', int64([0, 1, 2]) ...
), ...
'stimulus', types.core.TimeSeriesReferenceVectorData( ...
'description', 'Column storing the reference to the recorded stimulus for the recording (rows)', ...
'data', struct( ...
'idx_start', [0, 1, -1], ...
'count', [5, 3, -1], ...
'timeseries', [ ...
types.untyped.ObjectView(ccss), ...
types.untyped.ObjectView(ccss), ...
types.untyped.ObjectView(vcs) ...
] ...
)...
)...
);
ic_rec_table.responses = types.core.IntracellularResponsesTable( ...
'description', 'Table for storing intracellular response related metadata.', ...
'colnames', {'response'}, ...
'id', types.hdmf_common.ElementIdentifiers( ...
'data', int64([0, 1, 2]) ...
), ...
'response', types.core.TimeSeriesReferenceVectorData( ...
'description', 'Column storing the reference to the recorded response for the recording (rows)', ...
'data', struct( ...
'idx_start', [0, 2, 0], ...
'count', [5, 3, 5], ...
'timeseries', [ ...
types.untyped.ObjectView(vcs), ...
types.untyped.ObjectView(vcs), ...
types.untyped.ObjectView(vcs) ...
] ...
)...
)...
);
The IntracellularRecordingsTable table is not just a DynamicTable but an AlignedDynamicTable. The AlignedDynamicTable type is itself a DynamicTable that may contain an arbitrary number of additional DynamicTable, each of which defines a "category." This is similar to a table with “sub-headings”. In the case of the IntracellularRecordingsTable, we have three predefined categories, i.e., electrodes, stimuli, and responses. We can also dynamically add new categories to the table. As each category corresponds to a DynamicTable, this means we have to create a new DynamicTable and add it to our table.
% add category
ic_rec_table.categories = [ic_rec_table.categories, {'recording_lab_data'}];
ic_rec_table.dynamictable.set( ...
'recording_lab_data', types.hdmf_common.DynamicTable( ...
'description', 'category table for lab-specific recording metadata', ...
'colnames', {'location'}, ...
'id', types.hdmf_common.ElementIdentifiers( ...
'data', int64([0, 1, 2]) ...
), ...
'location', types.hdmf_common.VectorData( ...
'data', {'Mordor', 'Gondor', 'Rohan'}, ...
'description', 'Recording location in Middle Earth' ...
) ...
) ...
);
In an AlignedDynamicTable all category tables must align with the main table, i.e., all tables must have the same number of rows and rows are expected to correspond to each other by index.
We can also add custom columns to any of the subcategory tables, i.e., the electrodes, stimuli, and responses tables, and any custom subcategory tables. All we need to do is indicate the name of the category we want to add the column to.
% Add voltage threshold as column of electrodes table
ic_rec_table.electrodes.colnames = [ic_rec_table.electrodes.colnames {'voltage_threshold'}];
ic_rec_table.electrodes.vectordata.set('voltage_threshold', types.hdmf_common.VectorData( ...
'data', [0.1, 0.12, 0.13], ...
'description', 'Just an example column on the electrodes category table' ...
) ...
);
nwbfile.general_intracellular_ephys_intracellular_recordings = ic_rec_table;

Hierarchical organization of recordings

To describe the organization of intracellular experiments, the metadata is organized hierarchically in a sequence of tables. All of the tables are so-called DynamicTables enabling users to add columns for custom metadata. Storing data in hierarchical tables has the advantage that it allows us to avoid duplication of metadata. E.g., for a single experiment we only need to describe the metadata that is constant across an experimental condition as a single row in the SimultaneousRecordingsTable without having to replicate the same information across all repetitions and sequential-, simultaneous-, and individual intracellular recordings. For analysis, this means that we can easily focus on individual aspects of an experiment while still being able to easily access information about information from related tables. All of these tables are optional, but to use one you must use all of the lower level tables, even if you only need a single row.

Add a simultaneous recording

The SimultaneousRecordingsTable groups intracellular recordings from the IntracellularRecordingsTable together that were recorded simultaneously from different electrodes and/or cells and describes metadata that is constant across the simultaneous recordings. In practice a simultaneous recording is often also referred to as a sweep. This example adds a custom column, "simultaneous_recording_tag."
% create simultaneous recordings table with custom column
% 'simultaneous_recording_tag'
[recordings_vector_data, recordings_vector_index] = util.create_indexed_column( ...
{[0, 1, 2],}, ...
'Column with references to one or more rows in the IntracellularRecordingsTable table', ...
ic_rec_table);
ic_sim_recs_table = types.core.SimultaneousRecordingsTable( ...
'description', [ ...
'A table for grouping different intracellular recordings from ', ...
'the IntracellularRecordingsTable table together that were recorded ', ...
'simultaneously from different electrodes.'...
], ...
'colnames', {'recordings', 'simultaneous_recording_tag'}, ...
'id', types.hdmf_common.ElementIdentifiers( ...
'data', int64(12) ...
), ...
'recordings', recordings_vector_data, ...
'recordings_index', recordings_vector_index, ...
'simultaneous_recording_tag', types.hdmf_common.VectorData( ...
'description', 'A custom tag for simultaneous_recordings', ...
'data', {'LabTag1'} ...
) ...
);
Depending on the lab workflow, it may be useful to add complete columns to a table after we have already populated the table with rows. That would be done like so:
ic_sim_recs_table.colnames = [ic_sim_recs_table.colnames, {'simultaneous_recording_type'}];
ic_sim_recs_table.vectordata.set( ...
'simultaneous_recording_type', types.hdmf_common.VectorData(...
'description', 'Description of the type of simultaneous_recording', ...
'data', {'SimultaneousRecordingType1'} ...
) ...
);
nwbfile.general_intracellular_ephys_simultaneous_recordings = ic_sim_recs_table;

Add a sequential recording

The SequentialRecordingsTable groups simultaneously recorded intracellular recordings from the SimultaneousRecordingsTable together and describes metadata that is constant across the simultaneous recordings. In practice a sequential recording is often also referred to as a sweep sequence. A common use of sequential recordings is to group together simultaneous recordings where a sequence of stimuli of the same type with varying parameters have been presented in a sequence (e.g., a sequence of square waveforms with varying amplitude).
[simultaneous_recordings_vector_data, simultaneous_recordings_vector_index] = util.create_indexed_column( ...
{0,}, ...
'Column with references to one or more rows in the SimultaneousRecordingsTable table', ...
ic_sim_recs_table);
sequential_recordings = types.core.SequentialRecordingsTable( ...
'description', [ ...
'A table for grouping different intracellular recording ', ...
'simultaneous_recordings from the SimultaneousRecordingsTable ', ...
'table together. This is typically used to group together ', ...
'simultaneous_recordings where the a sequence of stimuli of ', ...
'the same type with varying parameters have been presented in ', ...
'a sequence.' ...
], ...
'colnames', {'simultaneous_recordings', 'stimulus_type'}, ...
'id', types.hdmf_common.ElementIdentifiers( ...
'data', int64(15) ...
), ...
'simultaneous_recordings', simultaneous_recordings_vector_data, ...
'simultaneous_recordings_index', simultaneous_recordings_vector_index, ...
'stimulus_type', types.hdmf_common.VectorData( ...
'description', 'Column storing the type of stimulus used for the sequential recording', ...
'data', {'square'} ...
) ...
);
nwbfile.general_intracellular_ephys_sequential_recordings = sequential_recordings;

Add repetitions table

The RepetitionsTable groups sequential recordings from the SequentialRecordingsTable. In practice, a repetition is often also referred to a run. A typical use of the RepetitionsTable is to group sets of different stimuli that are applied in sequence that may be repeated.
[sequential_recordings_vector_data, sequential_recordings_vector_index] = util.create_indexed_column( ...
{0,}, ...
'Column with references to one or more rows in the SequentialRecordingsTable table', ...
sequential_recordings);
nwbfile.general_intracellular_ephys_repetitions = types.core.RepetitionsTable( ...
'description', [ ...
'A table for grouping different intracellular recording sequential ', ...
'recordings together. With each SimultaneousRecording typically ', ...
'representing a particular type of stimulus, the RepetitionsTable ', ...
'table is typically used to group sets of stimuli applied in sequence.' ...
], ...
'colnames', {'sequential_recordings'}, ...
'id', types.hdmf_common.ElementIdentifiers( ...
'data', int64(17) ...
), ...
'sequential_recordings', sequential_recordings_vector_data, ...
'sequential_recordings_index', sequential_recordings_vector_index ...
);

Add experimental condition table

The ExperimentalConditionsTable groups repetitions of intracellular recording from the RepetitionsTable together that belong to the same experimental conditions.
[repetitions_vector_data, repetitions_vector_index] = util.create_indexed_column( ...
{0, 0}, ...
'Column with references to one or more rows in the RepetitionsTable table', ...
nwbfile.general_intracellular_ephys_repetitions);
nwbfile.general_intracellular_ephys_experimental_conditions = types.core.ExperimentalConditionsTable( ...
'description', [ ...
'A table for grouping different intracellular recording ', ...
'repetitions together that belong to the same experimental ', ...
'conditions.' ...
], ...
'colnames', {'repetitions', 'tag'}, ...
'id', types.hdmf_common.ElementIdentifiers( ...
'data', int64([19, 21]) ...
), ...
'repetitions', repetitions_vector_data, ...
'repetitions_index', repetitions_vector_index, ...
'tag', types.hdmf_common.VectorData( ...
'description', 'integer tag for a experimental condition', ...
'data', [1,3] ...
) ...
);

Write the NWB file

nwbExport(nwbfile, 'test_new_icephys.nwb');

Read the NWB file

nwbfile2 = nwbRead('test_new_icephys.nwb', 'ignorecache')
nwbfile2 =
NwbFile with properties: +Read the NWB file
The following tutorial describes storage of intracellular electrophysiology data in NWB. NWB supports storage of the time series describing the stimulus and response, information about the electrode and device used, as well as metadata about the organization of the experiment.
Illustration of the hierarchy of metadata tables used to describe the organization of intracellular electrophysiology experiments.

Creating an NWBFile

When creating an NWB file, the first step is to create the NWBFile, which you can create using the NwbFile command.
session_start_time = datetime(2018, 3, 1, 12, 0, 0, 'TimeZone', 'local');
 
 
nwbfile = NwbFile( ...
'session_description', 'my first synthetic recording', ...
'identifier', 'EXAMPLE_ID', ...
'session_start_time', session_start_time, ...
'general_experimenter', 'Dr. Bilbo Baggins', ...
'general_lab', 'Bag End Laboratory', ...
'general_institution', 'University of Middle Earth at the Shire', ...
'general_experiment_description', 'I went on an adventure with thirteen dwarves to reclaim vast treasures.', ...
'general_session_id', 'LONELYMTN' ...
);
 

Device metadata

Device metadata is represented by Device objects.
 
device = types.core.Device();
nwbfile.general_devices.set('Heka ITC-1600', device);

Electrode metadata

Intracellular electrode metadata is represented by IntracellularElectrode objects. Create an electrode object, which requires a link to the device of the previous step. Then add it to the NWB file.
electrode = types.core.IntracellularElectrode( ...
'description', 'a mock intracellular electrode', ...
'device', types.untyped.SoftLink(device), ...
'cell_id', 'a very interesting cell' ...
);
nwbfile.general_intracellular_ephys.set('elec0', electrode);

Stimulus and response data

Intracellular stimulus and response data are represented with subclasses of PatchClampSeries. A stimulus is described by a time series representing voltage or current stimulation with a particular set of parameters. There are two classes for representing stimulus data:
The response is then described by a time series representing voltage or current recorded from a single cell using a single intracellular electrode via one of the following classes:
Below we create a simple example stimulus/response recording data pair.
ccss = types.core.VoltageClampStimulusSeries( ...
'data', [1, 2, 3, 4, 5], ...
'starting_time', 123.6, ...
'starting_time_rate', 10e3, ...
'electrode', types.untyped.SoftLink(electrode), ...
'gain', 0.02, ...
'sweep_number', uint64(15), ...
'stimulus_description', 'N/A' ...
);
 
nwbfile.stimulus_presentation.set('ccss', ccss);
 
vcs = types.core.VoltageClampSeries( ...
'data', [0.1, 0.2, 0.3, 0.4, 0.5], ...
'data_conversion', 1e-12, ...
'data_resolution', NaN, ...
'starting_time', 123.6, ...
'starting_time_rate', 20e3, ...
'electrode', types.untyped.SoftLink(electrode), ...
'gain', 0.02, ...
'capacitance_slow', 100e-12, ...
'resistance_comp_correction', 70.0, ...
'stimulus_description', 'N/A', ...
'sweep_number', uint64(15) ...
);
nwbfile.acquisition.set('vcs', vcs);

Adding an intracellular recording

The IntracellularRecordingsTable relates electrode, stimulus and response pairs and describes metadata specific to individual recordings.
Illustration of the structure of the IntracellularRecordingsTable
ic_rec_table = types.core.IntracellularRecordingsTable( ...
'categories', {'electrodes', 'stimuli', 'responses'}, ...
'colnames', {'recordings_tag'}, ...
'description', [ ...
'A table to group together a stimulus and response from a single ', ...
'electrode and a single simultaneous recording and for storing ', ...
'metadata about the intracellular recording.'], ...
'id', types.hdmf_common.ElementIdentifiers('data', int64([0, 1, 2])), ...
'recordings_tag', types.hdmf_common.VectorData( ...
'data', repmat({'Tag'}, 3, 1), ...
'description', 'Column for storing a custom recordings tag' ...
) ...
);
 
ic_rec_table.electrodes = types.core.IntracellularElectrodesTable( ...
'description', 'Table for storing intracellular electrode related metadata.', ...
'colnames', {'electrode'}, ...
'id', types.hdmf_common.ElementIdentifiers( ...
'data', int64([0, 1, 2]) ...
), ...
'electrode', types.hdmf_common.VectorData( ...
'data', repmat(types.untyped.ObjectView(electrode), 3, 1), ...
'description', 'Column for storing the reference to the intracellular electrode' ...
) ...
);
 
ic_rec_table.stimuli = types.core.IntracellularStimuliTable( ...
'description', 'Table for storing intracellular stimulus related metadata.', ...
'colnames', {'stimulus'}, ...
'id', types.hdmf_common.ElementIdentifiers( ...
'data', int64([0, 1, 2]) ...
), ...
'stimulus', types.core.TimeSeriesReferenceVectorData( ...
'description', 'Column storing the reference to the recorded stimulus for the recording (rows)', ...
'data', struct( ...
'idx_start', [0, 1, -1], ...
'count', [5, 3, -1], ...
'timeseries', [ ...
types.untyped.ObjectView(ccss), ...
types.untyped.ObjectView(ccss), ...
types.untyped.ObjectView(vcs) ...
] ...
)...
)...
);
 
ic_rec_table.responses = types.core.IntracellularResponsesTable( ...
'description', 'Table for storing intracellular response related metadata.', ...
'colnames', {'response'}, ...
'id', types.hdmf_common.ElementIdentifiers( ...
'data', int64([0, 1, 2]) ...
), ...
'response', types.core.TimeSeriesReferenceVectorData( ...
'description', 'Column storing the reference to the recorded response for the recording (rows)', ...
'data', struct( ...
'idx_start', [0, 2, 0], ...
'count', [5, 3, 5], ...
'timeseries', [ ...
types.untyped.ObjectView(vcs), ...
types.untyped.ObjectView(vcs), ...
types.untyped.ObjectView(vcs) ...
] ...
)...
)...
);
 
The IntracellularRecordingsTable table is not just a DynamicTable but an AlignedDynamicTable. The AlignedDynamicTable type is itself a DynamicTable that may contain an arbitrary number of additional DynamicTable, each of which defines a "category." This is similar to a table with “sub-headings”. In the case of the IntracellularRecordingsTable, we have three predefined categories, i.e., electrodes, stimuli, and responses. We can also dynamically add new categories to the table. As each category corresponds to a DynamicTable, this means we have to create a new DynamicTable and add it to our table.
% add category
ic_rec_table.categories = [ic_rec_table.categories, {'recording_lab_data'}];
ic_rec_table.dynamictable.set( ...
'recording_lab_data', types.hdmf_common.DynamicTable( ...
'description', 'category table for lab-specific recording metadata', ...
'colnames', {'location'}, ...
'id', types.hdmf_common.ElementIdentifiers( ...
'data', int64([0, 1, 2]) ...
), ...
'location', types.hdmf_common.VectorData( ...
'data', {'Mordor', 'Gondor', 'Rohan'}, ...
'description', 'Recording location in Middle Earth' ...
) ...
) ...
);
In an AlignedDynamicTable all category tables must align with the main table, i.e., all tables must have the same number of rows and rows are expected to correspond to each other by index.
We can also add custom columns to any of the subcategory tables, i.e., the electrodes, stimuli, and responses tables, and any custom subcategory tables. All we need to do is indicate the name of the category we want to add the column to.
% Add voltage threshold as column of electrodes table
ic_rec_table.electrodes.colnames = [ic_rec_table.electrodes.colnames {'voltage_threshold'}];
ic_rec_table.electrodes.vectordata.set('voltage_threshold', types.hdmf_common.VectorData( ...
'data', [0.1, 0.12, 0.13], ...
'description', 'Just an example column on the electrodes category table' ...
) ...
);
 
nwbfile.general_intracellular_ephys_intracellular_recordings = ic_rec_table;

Hierarchical organization of recordings

To describe the organization of intracellular experiments, the metadata is organized hierarchically in a sequence of tables. All of the tables are so-called DynamicTables enabling users to add columns for custom metadata. Storing data in hierarchical tables has the advantage that it allows us to avoid duplication of metadata. E.g., for a single experiment we only need to describe the metadata that is constant across an experimental condition as a single row in the SimultaneousRecordingsTable without having to replicate the same information across all repetitions and sequential-, simultaneous-, and individual intracellular recordings. For analysis, this means that we can easily focus on individual aspects of an experiment while still being able to easily access information about information from related tables. All of these tables are optional, but to use one you must use all of the lower level tables, even if you only need a single row.

Add a simultaneous recording

The SimultaneousRecordingsTable groups intracellular recordings from the IntracellularRecordingsTable together that were recorded simultaneously from different electrodes and/or cells and describes metadata that is constant across the simultaneous recordings. In practice a simultaneous recording is often also referred to as a sweep. This example adds a custom column, "simultaneous_recording_tag."
% create simultaneous recordings table with custom column
% 'simultaneous_recording_tag'
 
[recordings_vector_data, recordings_vector_index] = util.create_indexed_column( ...
{[0, 1, 2],}, ...
'Column with references to one or more rows in the IntracellularRecordingsTable table', ...
ic_rec_table);
 
ic_sim_recs_table = types.core.SimultaneousRecordingsTable( ...
'description', [ ...
'A table for grouping different intracellular recordings from ', ...
'the IntracellularRecordingsTable table together that were recorded ', ...
'simultaneously from different electrodes.'...
], ...
'colnames', {'recordings', 'simultaneous_recording_tag'}, ...
'id', types.hdmf_common.ElementIdentifiers( ...
'data', int64(12) ...
), ...
'recordings', recordings_vector_data, ...
'recordings_index', recordings_vector_index, ...
'simultaneous_recording_tag', types.hdmf_common.VectorData( ...
'description', 'A custom tag for simultaneous_recordings', ...
'data', {'LabTag1'} ...
) ...
);
 
Depending on the lab workflow, it may be useful to add complete columns to a table after we have already populated the table with rows. That would be done like so:
ic_sim_recs_table.colnames = [ic_sim_recs_table.colnames, {'simultaneous_recording_type'}];
ic_sim_recs_table.vectordata.set( ...
'simultaneous_recording_type', types.hdmf_common.VectorData(...
'description', 'Description of the type of simultaneous_recording', ...
'data', {'SimultaneousRecordingType1'} ...
) ...
);
 
nwbfile.general_intracellular_ephys_simultaneous_recordings = ic_sim_recs_table;

Add a sequential recording

The SequentialRecordingsTable groups simultaneously recorded intracellular recordings from the SimultaneousRecordingsTable together and describes metadata that is constant across the simultaneous recordings. In practice a sequential recording is often also referred to as a sweep sequence. A common use of sequential recordings is to group together simultaneous recordings where a sequence of stimuli of the same type with varying parameters have been presented in a sequence (e.g., a sequence of square waveforms with varying amplitude).
[simultaneous_recordings_vector_data, simultaneous_recordings_vector_index] = util.create_indexed_column( ...
{0,}, ...
'Column with references to one or more rows in the SimultaneousRecordingsTable table', ...
ic_sim_recs_table);
 
sequential_recordings = types.core.SequentialRecordingsTable( ...
'description', [ ...
'A table for grouping different intracellular recording ', ...
'simultaneous_recordings from the SimultaneousRecordingsTable ', ...
'table together. This is typically used to group together ', ...
'simultaneous_recordings where the a sequence of stimuli of ', ...
'the same type with varying parameters have been presented in ', ...
'a sequence.' ...
], ...
'colnames', {'simultaneous_recordings', 'stimulus_type'}, ...
'id', types.hdmf_common.ElementIdentifiers( ...
'data', int64(15) ...
), ...
'simultaneous_recordings', simultaneous_recordings_vector_data, ...
'simultaneous_recordings_index', simultaneous_recordings_vector_index, ...
'stimulus_type', types.hdmf_common.VectorData( ...
'description', 'Column storing the type of stimulus used for the sequential recording', ...
'data', {'square'} ...
) ...
);
 
nwbfile.general_intracellular_ephys_sequential_recordings = sequential_recordings;

Add repetitions table

The RepetitionsTable groups sequential recordings from the SequentialRecordingsTable. In practice, a repetition is often also referred to a run. A typical use of the RepetitionsTable is to group sets of different stimuli that are applied in sequence that may be repeated.
[sequential_recordings_vector_data, sequential_recordings_vector_index] = util.create_indexed_column( ...
{0,}, ...
'Column with references to one or more rows in the SequentialRecordingsTable table', ...
sequential_recordings);
 
 
nwbfile.general_intracellular_ephys_repetitions = types.core.RepetitionsTable( ...
'description', [ ...
'A table for grouping different intracellular recording sequential ', ...
'recordings together. With each SimultaneousRecording typically ', ...
'representing a particular type of stimulus, the RepetitionsTable ', ...
'table is typically used to group sets of stimuli applied in sequence.' ...
], ...
'colnames', {'sequential_recordings'}, ...
'id', types.hdmf_common.ElementIdentifiers( ...
'data', int64(17) ...
), ...
'sequential_recordings', sequential_recordings_vector_data, ...
'sequential_recordings_index', sequential_recordings_vector_index ...
);

Add experimental condition table

The ExperimentalConditionsTable groups repetitions of intracellular recording from the RepetitionsTable together that belong to the same experimental conditions.
[repetitions_vector_data, repetitions_vector_index] = util.create_indexed_column( ...
{0, 0}, ...
'Column with references to one or more rows in the RepetitionsTable table', ...
nwbfile.general_intracellular_ephys_repetitions);
 
nwbfile.general_intracellular_ephys_experimental_conditions = types.core.ExperimentalConditionsTable( ...
'description', [ ...
'A table for grouping different intracellular recording ', ...
'repetitions together that belong to the same experimental ', ...
'conditions.' ...
], ...
'colnames', {'repetitions', 'tag'}, ...
'id', types.hdmf_common.ElementIdentifiers( ...
'data', int64([19, 21]) ...
), ...
'repetitions', repetitions_vector_data, ...
'repetitions_index', repetitions_vector_index, ...
'tag', types.hdmf_common.VectorData( ...
'description', 'integer tag for a experimental condition', ...
'data', [1,3] ...
) ...
);

Write the NWB file

nwbExport(nwbfile, 'test_new_icephys.nwb');

Read the NWB file

nwbfile2 = nwbRead('test_new_icephys.nwb', 'ignorecache')
nwbfile2 =
NwbFile with properties: - nwb_version: {'2.6.0'} + nwb_version: '2.7.0' file_create_date: [1×1 types.untyped.DataStub] - identifier: {'EXAMPLE_ID'} - session_description: {'my first synthetic recording'} - session_start_time: 2018-03-01T12:00:00.000000-05:00 - timestamps_reference_time: 2018-03-01T12:00:00.000000-05:00 + identifier: 'EXAMPLE_ID' + session_description: 'my first synthetic recording' + session_start_time: [1×1 types.untyped.DataStub] + timestamps_reference_time: [1×1 types.untyped.DataStub] acquisition: [1×1 types.untyped.Set] analysis: [0×1 types.untyped.Set] general: [0×1 types.untyped.Set] - general_data_collection: [] + general_data_collection: '' general_devices: [1×1 types.untyped.Set] - general_experiment_description: {'I went on an adventure with thirteen dwarves to reclaim vast treasures.'} + general_experiment_description: 'I went on an adventure with thirteen dwarves to reclaim vast treasures.' general_experimenter: [1×1 types.untyped.DataStub] general_extracellular_ephys: [0×1 types.untyped.Set] general_extracellular_ephys_electrodes: [] - general_institution: {'University of Middle Earth at the Shire'} + general_institution: 'University of Middle Earth at the Shire' general_intracellular_ephys: [1×1 types.untyped.Set] general_intracellular_ephys_experimental_conditions: [1×1 types.core.ExperimentalConditionsTable] - general_intracellular_ephys_filtering: [] + general_intracellular_ephys_filtering: '' general_intracellular_ephys_intracellular_recordings: [1×1 types.core.IntracellularRecordingsTable] general_intracellular_ephys_repetitions: [1×1 types.core.RepetitionsTable] general_intracellular_ephys_sequential_recordings: [1×1 types.core.SequentialRecordingsTable] general_intracellular_ephys_simultaneous_recordings: [1×1 types.core.SimultaneousRecordingsTable] general_intracellular_ephys_sweep_table: [] - general_keywords: [] - general_lab: {'Bag End Laboratory'} - general_notes: [] + general_keywords: '' + general_lab: 'Bag End Laboratory' + general_notes: '' general_optogenetics: [0×1 types.untyped.Set] general_optophysiology: [0×1 types.untyped.Set] - general_pharmacology: [] - general_protocol: [] - general_related_publications: [] - general_session_id: {'LONELYMTN'} - general_slices: [] - general_source_script: [] - general_source_script_file_name: [] - general_stimulus: [] + general_pharmacology: '' + general_protocol: '' + general_related_publications: '' + general_session_id: 'LONELYMTN' + general_slices: '' + general_source_script: '' + general_source_script_file_name: '' + general_stimulus: '' general_subject: [] - general_surgery: [] - general_virus: [] + general_surgery: '' + general_virus: '' intervals: [0×1 types.untyped.Set] intervals_epochs: [] intervals_invalid_times: [] @@ -98,10 +103,12 @@ stimulus_presentation: [1×1 types.untyped.Set] stimulus_templates: [0×1 types.untyped.Set] units: [] -

+
+
\ No newline at end of file +--> +
\ No newline at end of file diff --git a/tutorials/html/images.html b/tutorials/html/images.html index 7f7c05fc..bbdaecdf 100644 --- a/tutorials/html/images.html +++ b/tutorials/html/images.html @@ -39,20 +39,20 @@ .S12 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 0px none rgb(33, 33, 33); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 0px 0px 4px 4px; padding: 0px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } .S13 { margin: 15px 10px 5px 4px; padding: 0px; line-height: 18px; min-height: 0px; white-space: pre-wrap; color: rgb(33, 33, 33); font-family: Helvetica, Arial, sans-serif; font-style: normal; font-size: 17px; font-weight: 700; text-align: left; } .S14 { margin: 10px 10px 9px 4px; padding: 0px; line-height: 21px; min-height: 0px; white-space: pre-wrap; color: rgb(33, 33, 33); font-family: Helvetica, Arial, sans-serif; font-style: normal; font-size: 14px; font-weight: 400; text-align: left; } -.S15 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 1px solid rgb(217, 217, 217); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 4px; padding: 6px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; }

Storing Image Data in NWB

Image data can be a collection of individual images or movie segments (as a movie is simply a series of images), about the subject, the environment, the presented stimuli, or other parts related to the experiment. This tutorial focuses in particular on the usage of:

Create an NWB File

nwb = NwbFile( ...
'session_description', 'mouse in open exploration',...
'identifier', 'Mouse5_Day3', ...
'session_start_time', datetime(2018, 4, 25, 2, 30, 3, 'TimeZone', 'local'), ...
'timestamps_reference_time', datetime(2018, 4, 25, 3, 0, 45, 'TimeZone', 'local'), ...
'general_experimenter', 'LastName, FirstName', ... % optional
'general_session_id', 'session_1234', ... % optional
'general_institution', 'University of My Institution', ... % optional
'general_related_publications', 'DOI:10.1016/j.neuron.2016.12.011' ... % optional
);
nwb
nwb =
NwbFile with properties: +.S15 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 1px solid rgb(217, 217, 217); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 4px; padding: 6px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; }

Storing Image Data in NWB

Image data can be a collection of individual images or movie segments (as a movie is simply a series of images), about the subject, the environment, the presented stimuli, or other parts related to the experiment. This tutorial focuses in particular on the usage of:

Create an NWB File

nwb = NwbFile( ...
'session_description', 'mouse in open exploration',...
'identifier', 'Mouse5_Day3', ...
'session_start_time', datetime(2018, 4, 25, 2, 30, 3, 'TimeZone', 'local'), ...
'timestamps_reference_time', datetime(2018, 4, 25, 3, 0, 45, 'TimeZone', 'local'), ...
'general_experimenter', 'LastName, FirstName', ... % optional
'general_session_id', 'session_1234', ... % optional
'general_institution', 'University of My Institution', ... % optional
'general_related_publications', 'DOI:10.1016/j.neuron.2016.12.011' ... % optional
);
nwb
nwb =
NwbFile with properties: - nwb_version: '2.6.0' + nwb_version: '2.7.0' file_create_date: [] identifier: 'Mouse5_Day3' session_description: 'mouse in open exploration' @@ -101,7 +101,7 @@ stimulus_presentation: [0×1 types.untyped.Set] stimulus_templates: [0×1 types.untyped.Set] units: [] -

OpticalSeries: Storing series of images as stimuli

OpticalSeries is for time series of images that were presented to the subject as stimuli. We will create an OpticalSeries object with the name "StimulusPresentation" representing what images were shown to the subject and at what times.
Image data can be stored either in the HDF5 file or as an external image file. For this tutorial, we will use fake image data with shape of ('RGB', 'y', 'x', 'time') = (200, 50, 50, 3). As in all TimeSeries, the first dimension is time. The second and third dimensions represent x and y. The fourth dimension represents the RGB value (length of 3) for color images.
NWB differentiates between acquired data and data that was presented as stimulus. We can add it to the NWBFile object as stimulus data.
If the sampling rate is constant, use rate and starting_time to specify time. For irregularly sampled recordings, use timestamps to specify time for each sample image.
image_data = randi(255, [3, 50, 50, 200]);
optical_series = types.core.OpticalSeries( ...
'distance', 0.7, ... % required
'field_of_view', [0.2, 0.3, 0.7], ... % required
'orientation', 'lower left', ... % required
'data', image_data, ...
'data_unit', 'n.a.', ...
'starting_time_rate', 1.0, ...
'starting_time', 0.0, ...
'description', 'The images presented to the subject as stimuli' ...
);
 
nwb.stimulus_presentation.set('StimulusPresentation', optical_series);

AbstractFeatureSeries: Storing features of visual stimuli

While it is usually recommended to store the entire image data as an OpticalSeries, sometimes it is useful to store features of the visual stimuli instead of or in addition to the raw image data. For example, you may want to store the mean luminance of the image, the contrast, or the spatial frequency. This can be done using an instance of AbstractFeatureSeries. This class is a general container for storing time series of features that are derived from the raw image data.
% Create some fake feature data
feature_data = rand(200, 3); % 200 time points, 3 features
 
% Create an AbstractFeatureSeries object
abstract_feature_series = types.core.AbstractFeatureSeries( ...
'data', feature_data, ...
'timestamps', linspace(0, 1, 200), ...
'description', 'Features of the visual stimuli', ...
'features', {'luminance', 'contrast', 'spatial frequency'}, ...
'feature_units', {'n.a.', 'n.a.', 'cycles/degree'} ...
);
% Add the AbstractFeatureSeries to the NWBFile
nwb.stimulus_presentation.set('StimulusFeatures', abstract_feature_series);

ImageSeries: Storing series of images as acquisition

ImageSeries is a general container for time series of images acquired during the experiment. Image data can be stored either in the HDF5 file or as an external image file. When color images are stored in the HDF5 file the color channel order is expected to be RGB.
image_data = randi(255, [3, 50, 50, 200]);
behavior_images = types.core.ImageSeries( ...
'data', image_data, ...
'description', 'Image data of an animal in environment', ...
'data_unit', 'n.a.', ...
'starting_time_rate', 1.0, ...
'starting_time', 0.0 ...
);
 
nwb.acquisition.set('ImageSeries', behavior_images);

External Files

External files (e.g. video files of the behaving animal) can be added to the NWBFile by creating an ImageSeries object using the external_file attribute that specifies the path to the external file(s) on disk. The file(s) path must be relative to the path of the NWB file. Either external_file or data must be specified, but not both. external_file can be a cell array of multiple video files.
The starting_frame attribute serves as an index to indicate the starting frame of each external file, allowing you to skip the beginning of videos.
external_files = {'video1.pmp4', 'video2.pmp4'};
 
timestamps = [0.0, 0.04, 0.07, 0.1, 0.14, 0.16, 0.21];
behavior_external_file = types.core.ImageSeries( ...
'description', 'Behavior video of animal moving in environment', ...
'data_unit', 'n.a.', ...
'external_file', external_files, ...
'format', 'external', ...
'external_file_starting_frame', [0, 2, 4], ...
'timestamps', timestamps ...
);
 
nwb.acquisition.set('ExternalVideos', behavior_external_file);

Static Images

Static images can be stored in an NWBFile object by creating an RGBAImage, RGBImage or GrayscaleImage object with the image data. All of these image types provide an optional description parameter to include text description about the image and the resolution parameter to specify the pixels/cm resolution of the image.

RGBAImage: for color images with transparency

RGBAImage is for storing data of color image with transparency. data must be 3D where the first and second dimensions represent x and y. The third dimension has length 4 and represents the RGBA value.
image_data = randi(255, [4, 200, 200]);
 
rgba_image = types.core.RGBAImage( ...
'data', image_data, ... % required
'resolution', 70.0, ...
'description', 'RGBA image' ...
);

RGBImage: for color images

RGBImage is for storing data of RGB color image. data must be 3D where the first and second dimensions represent x and y. The third dimension has length 3 and represents the RGB value.
image_data = randi(255, [3, 200, 200]);
 
rgb_image = types.core.RGBImage( ...
'data', image_data, ... % required
'resolution', 70.0, ...
'description', 'RGB image' ...
);

GrayscaleImage: for grayscale images

GrayscaleImage is for storing grayscale image data. data must be 2D where the first and second dimensions represent x and y.
image_data = randi(255, [200, 200]);
 
grayscale_image = types.core.GrayscaleImage( ...
'data', image_data, ... % required
'resolution', 70.0, ...
'description', 'Grayscale image' ...
);

Images: a container for images

Add the images to an Images container that accepts any of these image types.
image_collection = types.core.Images( ...
'description', 'A collection of logo images presented to the subject.'...
);
 
image_collection.image.set('rgba_image', rgba_image);
image_collection.image.set('rgb_image', rgb_image);
image_collection.image.set('grayscale_image', grayscale_image);
 
nwb.acquisition.set('image_collection', image_collection);

Index Series for Repeated Images

You may want to set up a time series of images where some images are repeated many times. You could create an ImageSeries that repeats the data each time the image is shown, but that would be inefficient, because it would store the same data multiple times. A better solution would be to store the unique images once and reference those images. This is how IndexSeries works. First, create an Images container with the order of images defined using an ImageReferences. Then create an IndexSeries that indexes into the Images.
rgbImage = imread('street2.jpg');
grayImage = uint8(sum(double(rgbImage), 3) ./ double(max(max(max(rgbImage)))));
GsStreet = types.core.GrayscaleImage(...
'data', grayImage, ...
'description', 'grayscale image of a street.', ...
'resolution', 28 ...
);
 
RgbStreet = types.core.RGBImage( ...
'data', rgbImage, ...
'resolution', 28, ...
'description', 'RGB Street' ...
);
 
ImageOrder = types.core.ImageReferences(...
'data', [types.untyped.ObjectView(RgbStreet), types.untyped.ObjectView(GsStreet)] ...
);
Images = types.core.Images( ...
'gs_face', GsStreet, ...
'rgb_face', RgbStreet, ...
'description', 'A collection of streets.', ...
'order_of_images', ImageOrder ...
);
 
types.core.IndexSeries(...
'data', [0, 1, 0, 1], ... % NOTE: 0-indexed
'indexed_images', Images, ...
'timestamps', [0.1, 0.2, 0.3, 0.4] ...
)
ans =
IndexSeries with properties: +

OpticalSeries: Storing series of images as stimuli

OpticalSeries is for time series of images that were presented to the subject as stimuli. We will create an OpticalSeries object with the name "StimulusPresentation" representing what images were shown to the subject and at what times.
Image data can be stored either in the HDF5 file or as an external image file. For this tutorial, we will use fake image data with shape of ('time', 'x', 'y', 'RGB') = (200, 50, 50, 3). As in all TimeSeries, the first dimension is time. The second and third dimensions represent x and y. The fourth dimension represents the RGB value (length of 3) for color images. Please note: As described in the dimensionMapNoDataPipes tutorial, when a MATLAB array is exported to HDF5, the array is transposed. Therefore, in order to correctly export the data, we will need to create a transposed array, where the dimensions are in reverse order compared to the type specification.
NWB differentiates between acquired data and data that was presented as stimulus. We can add it to the NWBFile object as stimulus data.
If the sampling rate is constant, use rate and starting_time to specify time. For irregularly sampled recordings, use timestamps to specify time for each sample image.
image_data = randi(255, [3, 50, 50, 200]); % NB: Array is transposed
optical_series = types.core.OpticalSeries( ...
'distance', 0.7, ... % required
'field_of_view', [0.2, 0.3, 0.7], ... % required
'orientation', 'lower left', ... % required
'data', image_data, ...
'data_unit', 'n.a.', ...
'starting_time_rate', 1.0, ...
'starting_time', 0.0, ...
'description', 'The images presented to the subject as stimuli' ...
);
 
nwb.stimulus_presentation.set('StimulusPresentation', optical_series);

AbstractFeatureSeries: Storing features of visual stimuli

While it is usually recommended to store the entire image data as an OpticalSeries, sometimes it is useful to store features of the visual stimuli instead of or in addition to the raw image data. For example, you may want to store the mean luminance of the image, the contrast, or the spatial frequency. This can be done using an instance of AbstractFeatureSeries. This class is a general container for storing time series of features that are derived from the raw image data.
% Create some fake feature data
feature_data = rand(3, 200); % 200 time points, 3 features
 
% Create an AbstractFeatureSeries object
abstract_feature_series = types.core.AbstractFeatureSeries( ...
'data', feature_data, ...
'timestamps', linspace(0, 1, 200), ...
'description', 'Features of the visual stimuli', ...
'features', {'luminance', 'contrast', 'spatial frequency'}, ...
'feature_units', {'n.a.', 'n.a.', 'cycles/degree'} ...
);
% Add the AbstractFeatureSeries to the NWBFile
nwb.stimulus_presentation.set('StimulusFeatures', abstract_feature_series);

ImageSeries: Storing series of images as acquisition

ImageSeries is a general container for time series of images acquired during the experiment. Image data can be stored either in the HDF5 file or as an external image file. When color images are stored in the HDF5 file the color channel order is expected to be RGB.
image_data = randi(255, [3, 50, 50, 200]);
behavior_images = types.core.ImageSeries( ...
'data', image_data, ...
'description', 'Image data of an animal in environment', ...
'data_unit', 'n.a.', ...
'starting_time_rate', 1.0, ...
'starting_time', 0.0 ...
);
 
nwb.acquisition.set('ImageSeries', behavior_images);

External Files

External files (e.g. video files of the behaving animal) can be added to the NWBFile by creating an ImageSeries object using the external_file attribute that specifies the path to the external file(s) on disk. The file(s) path must be relative to the path of the NWB file. Either external_file or data must be specified, but not both. external_file can be a cell array of multiple video files.
The starting_frame attribute serves as an index to indicate the starting frame of each external file, allowing you to skip the beginning of videos.
external_files = {'video1.pmp4', 'video2.pmp4'};
 
timestamps = [0.0, 0.04, 0.07, 0.1, 0.14, 0.16, 0.21];
behavior_external_file = types.core.ImageSeries( ...
'description', 'Behavior video of animal moving in environment', ...
'data_unit', 'n.a.', ...
'external_file', external_files, ...
'format', 'external', ...
'external_file_starting_frame', [0, 2, 4], ...
'timestamps', timestamps ...
);
 
nwb.acquisition.set('ExternalVideos', behavior_external_file);

Static Images

Static images can be stored in an NWBFile object by creating an RGBAImage, RGBImage or GrayscaleImage object with the image data. All of these image types provide an optional description parameter to include text description about the image and the resolution parameter to specify the pixels/cm resolution of the image.

RGBAImage: for color images with transparency

RGBAImage is for storing data of color image with transparency. data must be 3D where the first and second dimensions represent x and y. The third dimension has length 4 and represents the RGBA value.
image_data = randi(255, [4, 200, 200]);
 
rgba_image = types.core.RGBAImage( ...
'data', image_data, ... % required
'resolution', 70.0, ...
'description', 'RGBA image' ...
);

RGBImage: for color images

RGBImage is for storing data of RGB color image. data must be 3D where the first and second dimensions represent x and y. The third dimension has length 3 and represents the RGB value.
image_data = randi(255, [3, 200, 200]);
 
rgb_image = types.core.RGBImage( ...
'data', image_data, ... % required
'resolution', 70.0, ...
'description', 'RGB image' ...
);

GrayscaleImage: for grayscale images

GrayscaleImage is for storing grayscale image data. data must be 2D where the first and second dimensions represent x and y.
image_data = randi(255, [200, 200]);
 
grayscale_image = types.core.GrayscaleImage( ...
'data', image_data, ... % required
'resolution', 70.0, ...
'description', 'Grayscale image' ...
);

Images: a container for images

Add the images to an Images container that accepts any of these image types.
image_collection = types.core.Images( ...
'description', 'A collection of logo images presented to the subject.'...
);
 
image_collection.image.set('rgba_image', rgba_image);
image_collection.image.set('rgb_image', rgb_image);
image_collection.image.set('grayscale_image', grayscale_image);
 
nwb.acquisition.set('image_collection', image_collection);

Index Series for Repeated Images

You may want to set up a time series of images where some images are repeated many times. You could create an ImageSeries that repeats the data each time the image is shown, but that would be inefficient, because it would store the same data multiple times. A better solution would be to store the unique images once and reference those images. This is how IndexSeries works. First, create an Images container with the order of images defined using an ImageReferences. Then create an IndexSeries that indexes into the Images.
rgbImage = imread('street2.jpg');
grayImage = uint8(sum(double(rgbImage), 3) ./ double(max(max(max(rgbImage)))));
GsStreet = types.core.GrayscaleImage(...
'data', grayImage, ...
'description', 'grayscale image of a street.', ...
'resolution', 28 ...
);
 
RgbStreet = types.core.RGBImage( ...
'data', rgbImage, ...
'resolution', 28, ...
'description', 'RGB Street' ...
);
 
ImageOrder = types.core.ImageReferences(...
'data', [types.untyped.ObjectView(RgbStreet), types.untyped.ObjectView(GsStreet)] ...
);
Images = types.core.Images( ...
'gs_face', GsStreet, ...
'rgb_face', RgbStreet, ...
'description', 'A collection of streets.', ...
'order_of_images', ImageOrder ...
);
 
types.core.IndexSeries(...
'data', [0, 1, 0, 1], ... % NOTE: 0-indexed
'indexed_images', Images, ...
'timestamps', [0.1, 0.2, 0.3, 0.4] ...
)
ans =
IndexSeries with properties: indexed_images: [1×1 types.core.Images] indexed_timeseries: [] @@ -121,7 +121,7 @@ starting_time: [] starting_time_rate: [] timestamps: [0.1000 0.2000 0.3000 0.4000] -
Here data contains the (0-indexed) index of the displayed image as they are ordered in the ImageReference.

Writing the images to an NWB File

Now use nwbExport to write the file.
nwbExport(nwb, "images_test.nwb");
+
Here data contains the (0-indexed) index of the displayed image as they are ordered in the ImageReference.

Writing the images to an NWB File

Now use nwbExport to write the file.
nwbExport(nwb, "images_test.nwb");