diff --git a/+openneuro/+datastore/Participantwise.m b/+openneuro/+datastore/Participantwise.m new file mode 100644 index 0000000..4c59fad --- /dev/null +++ b/+openneuro/+datastore/Participantwise.m @@ -0,0 +1,201 @@ +classdef Participantwise < matlab.io.Datastore + %PARTICIPANTWISE Summary of this class goes here + % Detailed explanation goes here + + properties (Constant, Hidden) + % Dictionary mapping type names to read settings + Type = containers.Map( ... + {'Anatomical NIfTI', 'EEG EDF', 'Functional NIfTI', 'DWI', 'Fieldmap', 'JSON Files', 'TSV Files'}, ... + {struct('ReadFcn', @niftiread, 'FileExtensions', {{'.nii.gz', '.nii'}}, 'FolderPath', 'anat'), ... + struct('ReadFcn', @edfread, 'FileExtensions', {{'.edf'}}, 'FolderPath', 'eeg'), ... + struct('ReadFcn', @niftiread, 'FileExtensions', {{'.nii.gz', '.nii'}}, 'FolderPath', 'func'), ... + struct('ReadFcn', @niftiread, 'FileExtensions', {{'.nii.gz', '.nii'}}, 'FolderPath', 'dwi'), ... + struct('ReadFcn', @niftiread, 'FileExtensions', {{'.nii.gz', '.nii'}}, 'FolderPath', 'fmap'), ... + struct('ReadFcn', @(f) jsondecode(fileread(f)), 'FileExtensions', {{'.json'}}, 'FolderPath', 'anat'), ... + struct('ReadFcn', @readtable, 'FileExtensions', {{'.tsv', '.txt'}}, 'FolderPath', 'anat') ... + } ... + ) + end + + properties (Dependent) + AvailableTypes % List of supported type keys + end + + properties + fileDatastoreObj % Public fileDatastore object for MVP mode + end + + properties (Access = private) + CurrentFileIndex double; + FileSet matlab.io.datastore.DsFileSet; + isMVPMode logical = false; % Track which mode we're in + end + + methods + function keys = get.AvailableTypes(obj) + % List available types from the Type dictionary + keys = obj.Type.keys; + end + + function obj = Participantwise(dataset,spec) + % Constructor supporting both MVP mode and original mode + % + % MVP Mode (dictionary-based spec): + % obj = Participantwise("Anatomical NIfTI") + % + % Original Mode (structure-based spec): + % obj = Participantwise(dataset, filesetSpec) + + if isStringScalar(spec) || (ischar(spec) && isvector(spec)) + % MVP MODE: dictionary-based spec + %typeName = varargin{1}; + + % Validate type name against the Type dictionary using keys() method + if ~isKey(obj.Type, spec) + error('Invalid type specified. Available types are: %s', strjoin(obj.AvailableTypes, ', ')); + end + + obj.isMVPMode = true; + obj = createMVPDatastore(obj, dataset, spec); + + else %if nargin == 2 + % ORIGINAL MODE: structure-based spec + %dataset = varargin{1}; + filesetSpec = spec; %varargin{2}; + + % Validate arguments (original validation) + if ~isa(dataset, 'openneuro.Dataset') + error('First argument must be an openneuro.Dataset object'); + end + if ~isstruct(filesetSpec) + error('Second argument must be a struct'); + end + + obj.isMVPMode = false; + obj = createOriginalDatastore(obj, dataset, filesetSpec); + + % else + % error('Constructor requires either 1 argument (MVP mode) or 2 arguments (original mode). Got %d arguments.', nargin); + end + end + + function reset(obj) + % Reset to the start of the data. + if obj.isMVPMode && ~isempty(obj.fileDatastoreObj) + reset(obj.fileDatastoreObj); + else + reset(obj.FileSet); + end + obj.CurrentFileIndex = 1; + end + + function tf = hasdata(obj) + % Return true if more data is available. + if obj.isMVPMode && ~isempty(obj.fileDatastoreObj) + tf = hasdata(obj.fileDatastoreObj); + else + tf = hasfile(obj.FileSet); + end + end + + function [data, info] = read(obj) + % MVP read method - no arguments, dispatches appropriately + if obj.isMVPMode && ~isempty(obj.fileDatastoreObj) + % MVP mode: dispatch to fileDatastore.read() + [data, info] = read(obj.fileDatastoreObj); + else + % Original mode: use FileSet reading + if ~hasdata(obj) + error(sprintf(['No more data to read.\nUse the reset ',... + 'method to reset the datastore to the start of ' ,... + 'the data. \nBefore calling the read method, ',... + 'check if data is available to read ',... + 'by using the hasdata method.'])) + end + + fileInfoTbl = nextfile(obj.FileSet); + data = MyFileReader(fileInfoTbl); + info.Size = size(data); + info.FileName = fileInfoTbl.FileName; + info.Offset = fileInfoTbl.Offset; + + % Update CurrentFileIndex for tracking progress + if fileInfoTbl.Offset + fileInfoTbl.SplitSize >= ... + fileInfoTbl.FileSize + obj.CurrentFileIndex = obj.CurrentFileIndex + 1; + end + end + end + end + + methods (Access = private) + function obj = createMVPDatastore(obj, dataset, typeName) + % Create MVP mode datastore using Type dictionary + + % Get the specification for this type + spec = obj.Type(typeName); + + % Construct fileDatastore object using the Type specification + %defaultPath = fullfile(pwd, "ds001415/"); + %searchPath = fullfile(defaultPath, 'sub-01', spec.FolderPath); + searchPath = fullfile(dataset.URI,dataset.ParticipantIDs(1), spec.FolderPath); + + try + obj.fileDatastoreObj = fileDatastore( ... + searchPath, ... + 'ReadFcn', spec.ReadFcn, ... + 'IncludeSubfolders', true, ... + 'FileExtensions', spec.FileExtensions ... + ); + catch e + warning('Failed to create MVP datastore:\n%s', getReport(e)); + % Create empty datastore as fallback + obj.fileDatastoreObj = fileDatastore(pwd, 'ReadFcn', @readtable); + obj.fileDatastoreObj.Files = {}; + end + end + + function obj = createOriginalDatastore(obj, dataset, filesetSpec) + % Create original mode datastore (unchanged logic) + + obj.FileSet = matlab.io.datastore.DsFileSet(computeLocations(dataset,filesetSpec),'FileExtensions',filesetSpec.extensionList); + obj.CurrentFileIndex = 1; + reset(obj); + end + end + + methods (Access=protected) + + end +end + +%% LOCAL FUNCTIONS +function data = MyFileReader(fileInfoTbl) +% create a reader object using the FileName +reader = matlab.io.datastore.DsFileReader(fileInfoTbl.FileName); + +% seek to the offset +seek(reader,fileInfoTbl.Offset,'Origin','start-of-file'); + +% read fileInfoTbl.SplitSize amount of data +data = read(reader,fileInfoTbl.SplitSize); +end + + +function locations = computeLocations(dataset,filesetSpec) + +fs = filesetSpec; +if all(cellfun(@isempty,{fs.sessions,fs.tasks,fs.runs},'UniformOutput',true)) % Maybe sessions is enough? Trigger an error if tasks/runs w/o sessions? + % "Core modality" special case + assert(isscalar(fs.extendedModality)); + assert(isa(dataset,'openneuro.Dataset')); + locations = dataset.RootURI + "/" + string(dataset.ID) + "/" + dataset.ParticipantIDs + "/" + fs.extendedModality; + +else % General case (TODO; may encompass core modality special case, i.e., removing need for IF/ELSE) + %TODO +end + +% +% dir(b.encoding.dir + "/" + subjects{1} + "/" + sessions{1} + "/" +folders{1} + "/*" ); + +end \ No newline at end of file diff --git a/+openneuro/Dataset.m b/+openneuro/Dataset.m new file mode 100644 index 0000000..454fb32 --- /dev/null +++ b/+openneuro/Dataset.m @@ -0,0 +1,236 @@ +classdef Dataset < handle %& OpenNeuroDataStore +% Creates data set summary +% (C) Johanna Bayer 01.12.2023 + + properties + ID (1,1) string + ParticipantIDs (1,:) string + ParticipantsInfo table = table % data table: participants.tsv + + about_dataset struct = struct % dataset_description.json + info struct = struct % participants.json + end + + properties (Dependent) + URI (1,1) string + end + + properties (Hidden, Constant) + RootURI string = "s3://openneuro.org"; + coreModalityFilesetSpec = zinitCoreModalityFilesetSpec(); + end + + methods + function obj = Dataset(ID) + % Constructor + obj.ID = ID; + + try + obj.ParticipantsInfo = readtable(obj.URI + "/participants.tsv", 'FileType', 'text'); + obj.ParticipantIDs = string(obj.ParticipantsInfo{:,1}); %TODO: access by column name (for clarity & self-validation) + catch + warning("Participants.tsv not found. Individualized loading of " + ... + "participants will not be available") + end + + try + % Fixed: Use obj.URI instead of undefined dir_base + obj.about_dataset = jsondecode(fileread(obj.URI + "/dataset_description.json")); + catch + warning('Dataset description not found.') + end + + try + % Fixed: Use obj.URI instead of undefined dir_base + obj.info = jsondecode(fileread(obj.URI + "/participants.json")); + catch + warning('Participants.json not found.') + end + end + + function ds = addParticipantwiseDatastore(obj, modality) + % Simple mode: one argument, a key to the 'easy' special cases where core modality gives all the required info to make the datastore + % Future mode(s): additional arguments, e.g., of extended modalities, sessions, runs, etc as needed to make the datastore + + arguments + obj + modality (1,1) string {mustBeMember(modality,["mri" "eeg"])} + end + + if nargin == 2 % simple mode (core modality-driven) + filesetSpec = obj.coreModalityFilesetSpec(modality); + elseif nargin > 2 + error("Currently only cases of MRI anatomical and EEG data types are supported. Other participantwise data subsets coming soon.") + end + + ds = zprvAddParticipantwiseDatastore(obj,filesetSpec); + end + + function ds = Participantwise(obj, typeName) + % MVP method: Create participantwise datastore using Type dictionary + % + % Parameters: + % typeName - String from Type dictionary (e.g., "Anatomical NIfTI") + % + % Usage: + % ds = Dataset('ds001415'); + % anatDS = ds.Participantwise("Anatomical NIfTI"); + + arguments + obj + typeName (1,1) string + end + + % Create Participantwise datastore using the dataset's ID and path + ds = openneuro.datastore.Participantwise(obj,typeName); + end + end + + methods + function uri = get.URI(obj) + uri = obj.RootURI + "/" + obj.ID; + end + end + + methods (Access=protected) + function ds = zprvAddParticipantwiseDatastore(obj,filesetSpec) + ds = openneuro.datastore.Participantwise(obj,filesetSpec); + end + end +end + +%% LOCAL FUNCTIONS + +function dict = zinitCoreModalityFilesetSpec() + % Fixed: Use containers.Map instead of configureDictionary for compatibility + dict = containers.Map(); + + % modality = MRI + s = struct; + s.extendedModality = "anat"; + s.sessions = string.empty(); + s.tasks = string.empty(); + s.runs = string.empty(); + s.extensionList = [".nii.gz" ".json"]; + dict("mri") = s; + + % modality = EEG + s = struct; + s.extendedModality = "eeg"; + s.sessions = string.empty(); + s.tasks = string.empty(); + s.runs = string.empty(); + s.extensionList = [".eeg" ".edf" ".json"]; + dict("eeg") = s; + + % Thus far, (just) these two cases have been identified where datastore contents can be inferred w/ just a modality hint +end + +% classdef Dataset < handle %& OpenNeuroDataStore +% % Creates data set summary +% % (C) Johanna Bayer 01.12.2023 +% +% properties +% ID (1,1) string +% ParticipantIDs (1,:) string +% ParticipantsInfo table = table % data table: participants.tsv +% +% about_dataset struct = struct % dataset_description.json +% info struct = struct % participants.json +% end +% +% properties (Dependent) +% URI (1,1) string +% end +% +% properties (Hidden, Constant) +% RootURI string = "s3://openneuro.org"; +% coreModalityFilesetSpec = zinitCoreModalityFilesetSpec(); +% end +% +% methods +% function obj = Dataset(ID) +% % Constructor +% obj.ID = ID; +% +% try +% obj.ParticipantsInfo = readtable(obj.URI + "/participants.tsv", 'FileType', 'text'); +% obj.ParticipantIDs = string(obj.ParticipantsInfo{:,1}); %TODO: access by column name (for clarity & self-validation) +% catch +% warning("Participants.tsv not found. Individualized loading of " + ... +% "participants will not be available") +% end +% +% try +% % Fixed: Use obj.URI instead of undefined dir_base +% obj.about_dataset = jsondecode(fileread(obj.URI + "/dataset_description.json")); +% catch +% warning('Dataset description not found.') +% end +% +% try +% % Fixed: Use obj.URI instead of undefined dir_base +% obj.info = jsondecode(fileread(obj.URI + "/participants.json")); +% catch +% warning('Participants.json not found.') +% end +% end +% +% function ds = addParticipantwiseDatastore(obj, modality) +% % Simple mode: one argument, a key to the 'easy' special cases where core modality gives all the required info to make the datastore +% % Future mode(s): additional arguments, e.g., of extended modalities, sessions, runs, etc as needed to make the datastore +% +% arguments +% obj (1,1) openneuro.Dataset; +% modality (1,1) string {mustBeMember(modality,["mri" "eeg"])} +% end +% +% if nargin == 2 % simple mode (core modality-driven) +% filesetSpec = obj.coreModalityFilesetSpec(modality); +% elseif nargin > 2 +% error("Currently only cases of MRI anatomical and EEG data types are supported. Other participantwise data subsets coming soon.") +% end +% +% ds = zprvAddParticipantwiseDatastore(obj,filesetSpec); +% end +% end +% +% methods +% function uri = get.URI(obj) +% uri = obj.RootURI + "/" + obj.ID; +% end +% end +% +% methods (Access=protected) +% function ds = zprvAddParticipantwiseDatastore(obj,filesetSpec) +% ds = openneuro.datastore.Participantwise(obj,filesetSpec); +% end +% end +% end +% +% %% LOCAL FUNCTIONS +% +% function dict = zinitCoreModalityFilesetSpec() +% % Fixed: Use containers.Map instead of configureDictionary for compatibility +% dict = containers.Map(); +% +% % modality = MRI +% s = struct; +% s.extendedModality = "anat"; +% s.sessions = string.empty(); +% s.tasks = string.empty(); +% s.runs = string.empty(); +% s.extensionList = [".nii.gz" ".json"]; +% dict("mri") = s; +% +% % modality = EEG +% s = struct; +% s.extendedModality = "eeg"; +% s.sessions = string.empty(); +% s.tasks = string.empty(); +% s.runs = string.empty(); +% s.extensionList = [".eeg" ".edf" ".json"]; +% dict("eeg") = s; +% +% % Thus far, (just) these two cases have been identified where datastore contents can be inferred w/ just a modality hint +% end \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b1ac2d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +*.asv +old_and_untracked/* +test_code.m +OpenNeuroDemo_test.mlx +test_BIDS.mat +test.m ++openneuro_old/* +.gitignore +.DS_Store +.MATLAB* ++openneuro_remote/* ++openneuro_works/* ++openneuro_May.zip ++openneuro.zip +OpenNeuroDemo2.mlx ++openneuro2/* +ds001415/ + + + diff --git a/OpenNeuroDemo.mlx b/OpenNeuroDemo.mlx new file mode 100644 index 0000000..7fe2cef Binary files /dev/null and b/OpenNeuroDemo.mlx differ diff --git a/Openneuro_MVP.mlx b/Openneuro_MVP.mlx new file mode 100644 index 0000000..c6c9244 Binary files /dev/null and b/Openneuro_MVP.mlx differ diff --git a/README.md b/README.md index 6907f44..d625501 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,52 @@ -# 🚧 OpenNEURO for MATLAB 🚧 -(Under construction) +# OpenNEURO for MATLAB A MATLAB® toolbox for accessing remote datasets stored on [OpenNEURO](https://openneuro.org/search) data archive. OpenNEURO only stores [BIDS](https://bids.neuroimaging.io)-compliant datasets, so [OpenNEURO for MATLAB](https://github.com/MATLAB-Community-Toolboxes-at-INCF/openneuro-matlab) is also BIDS-aware. + +**OpenNeuro Matlab Interface** — A lightweight interface for accessing and reading participant-level data from OpenNeuro datasets in MATLAB. + +[![Open in MATLAB Online](https://www.mathworks.com/images/responsive/global/open-in-matlab-online.svg)](https://matlab.mathworks.com/open/github/v1?repo=likeajumprope/OpenNEURO-toolbox) + + +## Usage + + +```matlab +>> ds = openneuro.Dataset('ds001415'); +>> anatDS = ds.Participantwise('JSON Files'); +``` + + +### Input Arguments + +- **`'dsXXXXX'`**: OpenNeuro dataset ID (e.g., `'ds001415'`). +- (`data type`) can be one of the following: + +| Type | Folder | File Extensions | Read Function | +|---------------------|-------------|-----------------------|--------------------| +| `'Anatomical NIfTI'` | `anat` | `.nii`, `.nii.gz` | `niftiread` | +| `'EEG EDF'` | `eeg` | `.edf` | `edfread` | +| `'Functional NIfTI'` | `func` | `.nii`, `.nii.gz` | `niftiread` | +| `'DWI'` | `dwi` | `.nii`, `.nii.gz` | `niftiread` | +| `'Fieldmap'` | `fmap` | `.nii`, `.nii.gz` | `niftiread` | +| `'JSON Files'` | `anat` | `.json` | `@(f) jsondecode(fileread(f))` | +| `'TSV Files'` | `anat` | `.tsv`, `.txt` | `readtable` | + +### Example: Read a JSON file + +```matlab +if anatDS.hasdata() + [data, info] = anatDS.read(); % No arguments as required + fprintf(' ✓ MVP read successful: %s\n', info.Filename); +else + fprintf(' ✗ No data available (normal if dataset folder doesn''t exist)\n'); +end +``` + +## Requirements + +- MATLAB **R2023a** or later + +## License + +(C) 2023 Johanna Bayer