Skip to content
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions changelog.d/20250919_172910_dan_add_keys_to_assoc_coordsys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
For top level release notes, leave all the headers commented out.
-->


### Added

- New `associations.coordsystems` to collate coordsystem files.

<!--
### Changed
- A bullet item for the Changed category.
-->
<!--
### Fixed
- A bullet item for the Fixed category.
-->
<!--
### Deprecated
- A bullet item for the Deprecated category.
-->
<!--
### Removed
- A bullet item for the Removed category.
-->
<!--
### Security
- A bullet item for the Security category.
-->
<!--
### Infrastructure
- A bullet item for the Infrastructure category.
-->
2 changes: 1 addition & 1 deletion src/files/inheritance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function* walkBack<T extends string[]>(
if (candidates.length > 1) {
const exactMatch = candidates.find((file) => {
const { entities } = readEntities(file.name)
return Object.keys(sourceParts.entities).every((entity) =>
return [...Object.keys(sourceParts.entities), ...(targetEntities ?? [])].every((entity) =>
entities[entity] === sourceParts.entities[entity]
)
})
Expand Down
71 changes: 49 additions & 22 deletions src/schema/associations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ import type { Schema as MetaSchema } from '@bids/schema/metaschema'

import type { BIDSFile } from '../types/filetree.ts'
import type { BIDSContext } from './context.ts'
import { loadJSON } from '../files/json.ts'
import { loadTSV } from '../files/tsv.ts'
import { parseBvalBvec } from '../files/dwi.ts'
import { walkBack } from '../files/inheritance.ts'
import { evalCheck } from './applyRules.ts'
import { expressionFunctions } from './expressionLanguage.ts'
import { readEntities } from './entities.ts'

import { readText } from '../files/access.ts'

type LoadFunction = (file: BIDSFile, options: any) => Promise<any>
type MultiLoadFunction = (files: BIDSFile[], options: any) => Promise<any>

function defaultAssociation(file: BIDSFile, _options: any): Promise<{ path: string }> {
return Promise.resolve({ path: file.path })
}
Expand All @@ -23,7 +28,7 @@ function defaultAssociation(file: BIDSFile, _options: any): Promise<{ path: stri
*
* Many associations only consist of a path; this object is for more complex associations.
*/
const associationLookup = {
const associationLookup: Record<string, LoadFunction> = {
events: async (file: BIDSFile, options: { maxRows: number }): Promise<Events> => {
const columns = await loadTSV(file, options.maxRows)
.catch((e) => {
Expand Down Expand Up @@ -86,6 +91,24 @@ const associationLookup = {
}
},
}
const multiAssociationLookup: Record<string, MultiLoadFunction> = {
coordsystems: async (
files: BIDSFile[],
options: any,
): Promise<{ paths: string[]; spaces: string[]; ParentCoordinateSystems: string[] }> => {
const jsons = await Promise.allSettled(
files.map((f) => loadJSON(f).catch(() => ({} as Record<string, unknown>))),
)
const parents = jsons.map((j) =>
j.status === 'fulfilled' ? j.value?.ParentCoordinateSystem : undefined
).filter((p) => p) as string[]
return {
paths: files.map((f) => f.path),
spaces: files.map((f) => readEntities(f.name).entities?.space),
ParentCoordinateSystems: parents,
}
},
}

export async function buildAssociations(
context: BIDSContext,
Expand Down Expand Up @@ -123,33 +146,37 @@ export async function buildAssociations(
rule.target.suffix,
rule.target?.entities ?? [],
).next().value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The EMG examples do not have this case, but with multiple coordsystem.jsons, you could imagine having some at the root and some at the leaf.

We currently strongly assume (and implement with .next().value) that the first level found is the level of the association, which has worked for single-file associations. This probably needs to be relaxed, but the logic will be more intricate. I think you would need to take the first coordsystem.json with a given space entity, not the full collection.

For now this could be noted as a limitation in the validator implementation.

if (Array.isArray(file)) {
file = file[0]
}
} catch (error) {
if (
error && typeof error === 'object' && 'code' in error &&
error.code === 'MULTIPLE_INHERITABLE_FILES'
) {
// @ts-expect-error
} catch (error: any) {
if (error?.code === 'MULTIPLE_INHERITABLE_FILES') {
context.dataset.issues.add(error)
break
continue
} else {
throw error
}
}

if (file) {
// @ts-expect-error
const load = associationLookup[key] ?? defaultAssociation
// @ts-expect-error
associations[key] = await load(file, { maxRows: context.dataset.options?.maxRows }).catch(
(error: any) => {
if (error.code) {
context.dataset.issues.add({ ...error, location: file.path })
}
},
)
if (file && !(Array.isArray(file) && file.length === 0)) {
const options = { maxRows: context.dataset.options?.maxRows }
if (key in multiAssociationLookup) {
const load = multiAssociationLookup[key]
if (!Array.isArray(file)) {
file = [file]
}
associations[key as keyof Associations] = await load(file, options)
} else {
const load = associationLookup[key] ?? defaultAssociation
if (Array.isArray(file)) {
file = file[0]
}
const location = file.path
associations[key as keyof Associations] = await load(file, { maxRows: context.dataset.options?.maxRows }).catch(
(error: any) => {
if (error.code) {
context.dataset.issues.add({ ...error, location })
}
},
)
}
}
}
return Promise.resolve(associations)
Expand Down
4 changes: 4 additions & 0 deletions src/schema/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { loadHeader } from '../files/nifti.ts'
import { buildAssociations } from './associations.ts'
import type { ValidatorOptions } from '../setup/options.ts'
import { logger } from '../utils/logger.ts'
import { datatypeFromDirectory } from '../validators/filenameIdentify.ts'

export class BIDSContextDataset implements Dataset {
#dataset_description: Record<string, unknown> = {}
Expand Down Expand Up @@ -162,6 +163,9 @@ export class BIDSContext implements Context {
this.columns = new ColumnsMap() as Record<string, string[]>
this.json = {}
this.associations = {} as Associations
if (this.dataset.schema.objects) { // schema may be empty for some tests
datatypeFromDirectory(this.dataset.schema, this)
}
}

get schema(): Schema {
Expand Down
2 changes: 1 addition & 1 deletion src/validators/filenameIdentify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type { CheckFunction } from '../types/check.ts'
import { lookupEntityLiteral } from './filenameValidate.ts'

const CHECKS: CheckFunction[] = [
datatypeFromDirectory,
// datatypeFromDirectory,
findRuleMatches,
hasMatch,
cleanContext,
Expand Down
20 changes: 11 additions & 9 deletions src/validators/validateFiles.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { assert, assertEquals } from '@std/assert'
import { filenameIdentify } from './filenameIdentify.ts'
import { filenameValidate } from './filenameValidate.ts'
import { BIDSContext } from '../schema/context.ts'
import { BIDSContext, BIDSContextDataset } from '../schema/context.ts'
import { loadSchema } from '../setup/loadSchema.ts'
import type { GenericSchema, Schema } from '../types/schema.ts'
import type { DatasetIssues } from '../issues/datasetIssues.ts'
import { pathToFile } from '../files/filetree.ts'

const schema = await loadSchema() as unknown as GenericSchema
const schema = await loadSchema()

function validatePath(path: string): DatasetIssues {
const context = new BIDSContext(pathToFile(path))
filenameIdentify(schema, context)
filenameValidate(schema, context)
async function validatePath(path: string): Promise<DatasetIssues> {
const dataset = new BIDSContextDataset({ schema })
const context = new BIDSContext(pathToFile(path), dataset)
await filenameIdentify(schema, context)
await filenameValidate(schema as unknown as GenericSchema, context)
return context.dataset.issues
}

Expand Down Expand Up @@ -54,7 +55,7 @@ Deno.test('test valid paths', async (t) => {
]
for (const filename of validFiles) {
await t.step(filename, async () => {
const issues = validatePath(filename)
const issues = await validatePath(filename)
assertEquals(
issues.get({ location: filename }).length,
0,
Expand Down Expand Up @@ -111,9 +112,10 @@ Deno.test('test invalid paths', async (t) => {
]
for (const filename of invalidFiles) {
await t.step(filename, async () => {
const context = new BIDSContext(pathToFile(filename))
const dataset = new BIDSContextDataset({ schema })
const context = new BIDSContext(pathToFile(filename), dataset)
await filenameIdentify(schema, context)
await filenameValidate(schema, context)
await filenameValidate(schema as unknown as GenericSchema, context)
assert(
context.dataset.issues.get({
location: context.file.path,
Expand Down
Loading