diff --git a/bids-validator/src/files/dwi.ts b/bids-validator/src/files/dwi.ts new file mode 100644 index 000000000..3d992a1ad --- /dev/null +++ b/bids-validator/src/files/dwi.ts @@ -0,0 +1,29 @@ +/* + * DWI + * Module for parsing DWI-associated files + */ +const normalizeEOL = (str: string): string => + str.replace(/\r\n/g, '\n').replace(/\r/g, '\n') + +export function parseBval(contents: string): number[][] { + // BVAL files are a single row of numbers, and may contain + // trailing whitespace + return [contents + .split(/\s+/) + .filter((x) => x !== '') + .map((x) => Number(x))] +} + +export function parseBvec(contents: string): number[][] { + // BVEC files are a matrix of numbers, with each row being + // a different axis + return normalizeEOL(contents) + .split(/\s*\n/) + .filter((x) => x !== '') + .map((row) => + row + .split(/\s+/) + .filter((x) => x !== '') + .map((x) => Number(x)), + ) +} diff --git a/bids-validator/src/schema/associations.ts b/bids-validator/src/schema/associations.ts index 4460d3cdf..bc9b50a1b 100644 --- a/bids-validator/src/schema/associations.ts +++ b/bids-validator/src/schema/associations.ts @@ -7,6 +7,7 @@ import { FileTree } from '../types/filetree.ts' import { BIDSContext } from './context.ts' import { readEntities } from './entities.ts' import { parseTSV } from '../files/tsv.ts' +import { parseBval, parseBvec } from '../files/dwi.ts' // type AssociationsLookup = Record => { - return file - .text() - .then((text) => parseTSV(text) as ContextAssociationsEvents) + load: async (file: BIDSFile): Promise => { + const text = await file.text() + const columns = parseTSV(text) + return { + path: file.path, + onset: columns.get('onset') || [], + } }, }, aslcontext: { + suffix: 'aslcontext', extensions: ['.tsv'], inherit: true, - load: (file: BIDSFile): Promise => { - return Promise.resolve({ path: file.path, n_rows: 0, volume_type: [] }) + load: async ( + file: BIDSFile, + ): Promise => { + const contents = await file.text() + const columns = parseTSV(contents) + return { + path: file.path, + n_rows: columns.get('volume_type')?.length || 0, + volume_type: columns.get('volume_type') || [], + } }, }, m0scan: { + suffix: 'm0scan', extensions: ['.nii', '.nii.gz'], inherit: false, load: (file: BIDSFile): Promise => { @@ -46,13 +61,15 @@ const associationLookup = { }, }, magnitude: { + suffix: 'magnitude', extensions: ['.nii', '.nii.gz'], inherit: false, load: (file: BIDSFile): Promise => { - return Promise.resolve({ path: file.path, onset: 'silly' }) + return Promise.resolve({ path: file.path }) }, }, magnitude1: { + suffix: 'magnitude1', extensions: ['.nii', '.nii.gz'], inherit: false, load: (file: BIDSFile): Promise => { @@ -60,26 +77,51 @@ const associationLookup = { }, }, bval: { - extensions: ['.nii', '.nii.gz'], + suffix: 'dwi', + extensions: ['.bval'], inherit: true, - load: (file: BIDSFile): Promise => { - return Promise.resolve({ path: file.path, n_cols: 0 }) + load: async (file: BIDSFile): Promise => { + const contents = await file.text() + const columns = parseBval(contents) + return { + path: file.path, + n_cols: columns ? columns[0].length : 0, + } }, }, bvec: { - extensions: ['.nii', '.nii.gz'], + suffix: 'dwi', + extensions: ['.bvec'], inherit: true, - load: (file: BIDSFile): Promise => { - return Promise.resolve({ path: file.path, n_cols: 0 }) + load: async (file: BIDSFile): Promise => { + const contents = await file.text() + const columns = parseBvec(contents) + return { + path: file.path, + n_cols: columns ? columns[0].length : 0, + } }, }, channels: { + suffix: 'channels', extensions: ['.tsv'], inherit: true, - load: (file: BIDSFile): Promise => { - return file - .text() - .then((text) => parseTSV(text) as ContextAssociationsEvents) + load: async (file: BIDSFile): Promise => { + const contents = await file.text() + const columns = parseTSV(contents) + return { + path: file.path, + type: columns.get('type') || [], + short_channel: columns.get('short_channel') || [], + } + }, + }, + coordsystem: { + suffix: 'coordsystem', + extensions: ['.json'], + inherit: true, + load: (file: BIDSFile): Promise => { + return Promise.resolve({ path: file.path }) }, }, } @@ -90,9 +132,9 @@ export async function buildAssociations( ): Promise { const associations: ContextAssociations = {} for (const key in associationLookup as typeof associationLookup) { - const { extensions, inherit } = + const { suffix, extensions, inherit } = associationLookup[key as keyof typeof associationLookup] - const paths = getPaths(fileTree, source, key, extensions) + const paths = getPaths(fileTree, source, suffix, extensions) if (paths.length === 0) { continue } diff --git a/bids-validator/src/schema/context.ts b/bids-validator/src/schema/context.ts index 284781f38..65cb6c4f7 100644 --- a/bids-validator/src/schema/context.ts +++ b/bids-validator/src/schema/context.ts @@ -5,6 +5,7 @@ import { ContextSubject, ContextAssociations, ContextNiftiHeader, + ContextData, } from '../types/context.ts' import { BIDSFile } from '../types/file.ts' import { FileTree } from '../types/filetree.ts' @@ -12,6 +13,7 @@ import { ColumnsMap } from '../types/columns.ts' import { BIDSEntities, readEntities } from './entities.ts' import { DatasetIssues } from '../issues/datasetIssues.ts' import { parseTSV } from '../files/tsv.ts' +import { parseBval, parseBvec } from '../files/dwi.ts' import { loadHeader } from '../files/nifti.ts' import { buildAssociations } from './associations.ts' import { ValidatorOptions } from '../setup/options.ts' @@ -66,6 +68,7 @@ export class BIDSContext implements Context { columns: ColumnsMap associations: ContextAssociations nifti_header?: ContextNiftiHeader + data?: ContextData constructor( fileTree: FileTree, @@ -171,6 +174,27 @@ export class BIDSContext implements Context { } } + // Currently un-specified bit of context needed for bval/bvec + async loadData(): Promise { + let parser + if (this.file.path.endsWith('.bval')) { + parser = parseBval + } else if (this.file.path.endsWith('.bvec')) { + parser = parseBvec + } + if (parser) { + this.data = await this.file + .text() + .then(parser as (value: string) => number[][]) + .then((data) => { + return { + n_rows: data.length, + n_cols: data ? data[0].length : 0, + } + }).then((ret) => {console.log(ret); return ret}) + } + } + async loadColumns(): Promise { if (this.extension !== '.tsv') { return @@ -198,6 +222,7 @@ export class BIDSContext implements Context { this.loadSidecar(), this.loadColumns(), this.loadAssociations(), + this.loadData(), ]) this.loadNiftiHeader() } diff --git a/bids-validator/src/types/context.ts b/bids-validator/src/types/context.ts index 5849d6e31..c54a89b06 100644 --- a/bids-validator/src/types/context.ts +++ b/bids-validator/src/types/context.ts @@ -48,6 +48,14 @@ export interface ContextAssociationsBvec { path: string n_cols: number } +export interface ContextAssociationsChannels { + path?: string + type?: string[] + short_channel?: string[] +} +export interface ContextAssociationsCoordsystem { + path: string +} export interface ContextAssociations { events?: ContextAssociationsEvents aslcontext?: ContextAssociationsAslcontext @@ -56,6 +64,8 @@ export interface ContextAssociations { magnitude1?: ContextAssociationsMagnitude1 bval?: ContextAssociationsBval bvec?: ContextAssociationsBvec + channels?: ContextAssociationsChannels + coordsystem?: ContextAssociationsCoordsystem } export interface ContextNiftiHeaderDimInfo { freq: number @@ -74,6 +84,10 @@ export interface ContextNiftiHeader { qform_code: number sform_code: number } +export interface ContextData { + n_cols?: number + n_rows?: number +} export interface Context { dataset: ContextDataset subject: ContextSubject @@ -88,4 +102,5 @@ export interface Context { columns: object json: object nifti_header?: ContextNiftiHeader + data?: ContextData }