Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion .github/workflows/deno_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
BIDS_SCHEMA: https://bids-specification--1998.org.readthedocs.build/en/1998/schema.json

jobs:
debug_info:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -52,7 +55,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
allow-net: [true, false]
allow-net: [true] # , false]
fail-fast: false
defaults:
run:
Expand Down
49 changes: 49 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,49 @@
<!--
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

- Implement `associations.coordsystems` to collate multiple `coordsystem.json` files,
as required by BEP 042 (EMG).

<!--
### 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.

-->
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).catch((e: any) => {})
} 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, options).catch(
(error: any) => {
if (error.code) {
context.dataset.issues.add({ ...error, location })
}
},
)
}
}
}
return Promise.resolve(associations)
Expand Down
2 changes: 1 addition & 1 deletion tests/data/bids-examples
Submodule bids-examples updated 78 files
+1 −1 .codespellrc
+7 −5 .github/workflows/validate_datasets.yml
+18 −1 README.md
+7 −0 dataset_listing.tsv
+30 −0 emg_CustomBipolar/README.md
+17 −0 emg_CustomBipolar/dataset_description.json
+21 −0 emg_CustomBipolar/participants.json
+2 −0 emg_CustomBipolar/participants.tsv
+2 −0 emg_CustomBipolar/sub-01/emg/sub-01_task-holdWeight_channels.tsv
+ emg_CustomBipolar/sub-01/emg/sub-01_task-holdWeight_emg.edf
+16 −0 emg_CustomBipolar/sub-01/emg/sub-01_task-holdWeight_emg.json
+32 −0 emg_CustomBipolarFace/README.md
+17 −0 emg_CustomBipolarFace/dataset_description.json
+21 −0 emg_CustomBipolarFace/participants.json
+2 −0 emg_CustomBipolarFace/participants.tsv
+5 −0 emg_CustomBipolarFace/sub-01/emg/sub-01_coordsystem.json
+13 −0 emg_CustomBipolarFace/sub-01/emg/sub-01_electrodes.tsv
+7 −0 emg_CustomBipolarFace/sub-01/emg/sub-01_task-talking_channels.tsv
+ emg_CustomBipolarFace/sub-01/emg/sub-01_task-talking_emg.edf
+16 −0 emg_CustomBipolarFace/sub-01/emg/sub-01_task-talking_emg.json
+13 −0 emg_CustomBipolarFace/sub-01/emg/sub-01_task-talking_events.json
+2 −0 emg_CustomBipolarFace/sub-01/emg/sub-01_task-talking_events.tsv
+30 −0 emg_IndependentMod/README.md
+17 −0 emg_IndependentMod/dataset_description.json
+21 −0 emg_IndependentMod/participants.json
+2 −0 emg_IndependentMod/participants.tsv
+7 −0 emg_IndependentMod/sub-01/emg/sub-01_task-grasping_channels.tsv
+ emg_IndependentMod/sub-01/emg/sub-01_task-grasping_emg.edf
+16 −0 emg_IndependentMod/sub-01/emg/sub-01_task-grasping_emg.json
+31 −0 emg_MultiBodyParts/README.md
+16 −0 emg_MultiBodyParts/dataset_description.json
+21 −0 emg_MultiBodyParts/participants.json
+2 −0 emg_MultiBodyParts/participants.tsv
+5 −0 emg_MultiBodyParts/sub-01/emg/sub-01_space-hand_coordsystem.json
+5 −0 emg_MultiBodyParts/sub-01/emg/sub-01_space-lowerLeg_coordsystem.json
+3 −0 emg_MultiBodyParts/sub-01/emg/sub-01_task-mechPerturbations_channels.tsv
+3 −0 emg_MultiBodyParts/sub-01/emg/sub-01_task-mechPerturbations_electrodes.tsv
+0 −0 emg_MultiBodyParts/sub-01/emg/sub-01_task-mechPerturbations_emg.edf
+17 −0 emg_MultiBodyParts/sub-01/emg/sub-01_task-mechPerturbations_emg.json
+33 −0 emg_TwoHDsEMG/README.md
+16 −0 emg_TwoHDsEMG/dataset_description.json
+21 −0 emg_TwoHDsEMG/participants.json
+2 −0 emg_TwoHDsEMG/participants.tsv
+131 −0 emg_TwoHDsEMG/sub-01/emg/sub-01_electrodes.tsv
+5 −0 emg_TwoHDsEMG/sub-01/emg/sub-01_space-forearm_coordsystem.json
+8 −0 emg_TwoHDsEMG/sub-01/emg/sub-01_space-grid1_coordsystem.json
+8 −0 emg_TwoHDsEMG/sub-01/emg/sub-01_space-grid2_coordsystem.json
+129 −0 emg_TwoHDsEMG/sub-01/emg/sub-01_task-isometric_channels.tsv
+0 −0 emg_TwoHDsEMG/sub-01/emg/sub-01_task-isometric_emg.edf
+20 −0 emg_TwoHDsEMG/sub-01/emg/sub-01_task-isometric_emg.json
+33 −0 emg_TwoWristbands/README.md
+16 −0 emg_TwoWristbands/dataset_description.json
+21 −0 emg_TwoWristbands/participants.json
+2 −0 emg_TwoWristbands/participants.tsv
+5 −0 emg_TwoWristbands/space-leftForearm_coordsystem.json
+5 −0 emg_TwoWristbands/space-rightForearm_coordsystem.json
+33 −0 emg_TwoWristbands/sub-01/emg/sub-01_electrodes.tsv
+33 −0 emg_TwoWristbands/sub-01/emg/sub-01_task-typing_channels.tsv
+0 −0 emg_TwoWristbands/sub-01/emg/sub-01_task-typing_emg.edf
+17 −0 emg_TwoWristbands/sub-01/emg/sub-01_task-typing_emg.json
+37 −0 emg_concurrentIndependentUnits/README.md
+17 −0 emg_concurrentIndependentUnits/dataset_description.json
+21 −0 emg_concurrentIndependentUnits/participants.json
+2 −0 emg_concurrentIndependentUnits/participants.tsv
+ emg_concurrentIndependentUnits/sub-01/emg/sub-01_photo.jpg
+131 −0 emg_concurrentIndependentUnits/sub-01/emg/sub-01_recording-highDensity_electrodes.tsv
+8 −0 emg_concurrentIndependentUnits/sub-01/emg/sub-01_space-grid1_coordsystem.json
+8 −0 emg_concurrentIndependentUnits/sub-01/emg/sub-01_space-grid2_coordsystem.json
+5 −0 emg_concurrentIndependentUnits/sub-01/emg/sub-01_space-lowerLeg_coordsystem.json
+5 −0 emg_concurrentIndependentUnits/sub-01/emg/sub-01_space-thigh_coordsystem.json
+22 −0 emg_concurrentIndependentUnits/sub-01/emg/sub-01_task-jumping_events.json
+16 −0 emg_concurrentIndependentUnits/sub-01/emg/sub-01_task-jumping_events.tsv
+2 −0 emg_concurrentIndependentUnits/sub-01/emg/sub-01_task-jumping_recording-bipolar_channels.tsv
+ emg_concurrentIndependentUnits/sub-01/emg/sub-01_task-jumping_recording-bipolar_emg.edf
+30 −0 emg_concurrentIndependentUnits/sub-01/emg/sub-01_task-jumping_recording-bipolar_emg.json
+129 −0 emg_concurrentIndependentUnits/sub-01/emg/sub-01_task-jumping_recording-highDensity_channels.tsv
+ emg_concurrentIndependentUnits/sub-01/emg/sub-01_task-jumping_recording-highDensity_emg.edf
+30 −0 emg_concurrentIndependentUnits/sub-01/emg/sub-01_task-jumping_recording-highDensity_emg.json
Loading