@yibeichan/claude-skills 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/cli.js +272 -0
- package/install.py +240 -0
- package/package.json +44 -0
- package/skills/bidsapp-nidm-standards/SKILL.md +202 -0
- package/skills/bidsapp-nidm-standards/references/babs_config.md +20 -0
- package/skills/bidsapp-nidm-standards/references/cli_arguments.md +76 -0
- package/skills/bidsapp-nidm-standards/references/container_patterns.md +53 -0
- package/skills/bidsapp-nidm-standards/references/nidm_integration.md +403 -0
- package/skills/bidsapp-nidm-standards/references/repo_structure.md +121 -0
- package/skills/bidsapp-nidm-standards/references/testing_patterns.md +82 -0
- package/skills/dicom2fmriprep/SKILL.md +377 -0
- package/skills/dicom2fmriprep/evals/evals.json +26 -0
- package/skills/dicom2fmriprep/references/babs-details.md +407 -0
- package/skills/dicom2fmriprep/references/fmriprep-details.md +250 -0
- package/skills/dicom2fmriprep/references/heudiconv-details.md +243 -0
- package/skills/fmri-ssm/SKILL.md +317 -0
- package/skills/fmri-ssm/references/code_templates.md +1570 -0
- package/skills/fmri-ssm/references/downstream_analysis.md +680 -0
- package/skills/fmri-ssm/references/group_inference.md +608 -0
- package/skills/fmri-ssm/references/hrf_modeling.md +447 -0
- package/skills/fmri-ssm/references/model_catalog.md +436 -0
- package/skills/fmri-ssm/references/paradigm_guide.md +406 -0
- package/skills/fmri-ssm/references/preprocessing.md +614 -0
- package/skills/fmri-ssm.zip +0 -0
- package/skills/neuroimaging-qc/SKILL.md +203 -0
- package/skills/neuroimaging-qc/references/eeg_qc.md +400 -0
- package/skills/neuroimaging-qc/references/fmri_qc.md +343 -0
- package/skills/neuroimaging-qc/references/fnirs_qc.md +430 -0
- package/skills/neuroimaging-qc/references/structural_qc.md +454 -0
- package/skills/neuroimaging-qc/scripts/parse_fmriprep_confounds.py +153 -0
- package/skills/neuroimaging-qc/scripts/parse_mriqc.py +114 -0
- package/skills/neuroimaging-qc/scripts/qc_report.py +295 -0
- package/skills/scientific-writer/SKILL.md +202 -0
- package/skills/scientific-writer/references/citation_styles.md +163 -0
- package/skills/scientific-writer/references/field_conventions.md +245 -0
- package/skills/scientific-writer/references/figures_tables.md +225 -0
- package/skills/scientific-writer/references/reporting_guidelines.md +225 -0
- package/skills.json +54 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# NIDM Integration Reference
|
|
2
|
+
|
|
3
|
+
## NIDM Workflow
|
|
4
|
+
|
|
5
|
+
The standard NIDM integration follows this sequence:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
1. Check --skip-nidm flag
|
|
9
|
+
├─ If true: Skip NIDM processing entirely
|
|
10
|
+
└─ If false: Continue to step 2
|
|
11
|
+
|
|
12
|
+
2. Locate input NIDM file
|
|
13
|
+
└─ Path: <nidm-input-dir>/[sub-{id}/[ses-{session}/]]<filename>.ttl
|
|
14
|
+
|
|
15
|
+
3. Copy NIDM file to output (if exists)
|
|
16
|
+
├─ Destination: <output_dir>/<bidsapp_name>/nidm/sub-{id}/[ses-{session}/]<filename>.ttl
|
|
17
|
+
└─ If not found: Log warning and continue
|
|
18
|
+
|
|
19
|
+
4. Run analysis (unless --skip-<analysis>)
|
|
20
|
+
└─ Output to: <output_dir>/<bidsapp_name>/<analysis>/...
|
|
21
|
+
|
|
22
|
+
5. Extract metrics from analysis outputs
|
|
23
|
+
└─ Analysis-specific metric extraction
|
|
24
|
+
|
|
25
|
+
6. Integrate metrics into NIDM file
|
|
26
|
+
├─ If copied file exists: Overwrite with integrated data
|
|
27
|
+
└─ If no existing file: Create new NIDM file from scratch
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## NIDM File Naming
|
|
31
|
+
|
|
32
|
+
### With Sessions
|
|
33
|
+
```
|
|
34
|
+
sub-{id}/ses-{session}/sub-{id}_ses-{session}.ttl
|
|
35
|
+
```
|
|
36
|
+
Example: `sub-01/ses-baseline/sub-01_ses-baseline.ttl`
|
|
37
|
+
|
|
38
|
+
### Without Sessions (Session-less)
|
|
39
|
+
```
|
|
40
|
+
sub-{id}/sub-{id}.ttl
|
|
41
|
+
```
|
|
42
|
+
Example: `sub-01/sub-01.ttl`
|
|
43
|
+
|
|
44
|
+
**Important**: Session-less data uses `sub-01.ttl`, NOT `sub-01_ses-baseline.ttl`
|
|
45
|
+
|
|
46
|
+
## NIDM File Organization
|
|
47
|
+
|
|
48
|
+
### Input NIDM Directory Structure
|
|
49
|
+
|
|
50
|
+
**Option 1: Single file** (for dataset-level NIDM)
|
|
51
|
+
```
|
|
52
|
+
<nidm-input-dir>/
|
|
53
|
+
└── nidm.ttl
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Option 2: Per-subject files with sessions**
|
|
57
|
+
```
|
|
58
|
+
<nidm-input-dir>/
|
|
59
|
+
├── sub-01/
|
|
60
|
+
│ ├── ses-baseline/
|
|
61
|
+
│ │ └── sub-01_ses-baseline.ttl
|
|
62
|
+
│ └── ses-followup/
|
|
63
|
+
│ └── sub-01_ses-followup.ttl
|
|
64
|
+
└── sub-02/
|
|
65
|
+
└── ses-baseline/
|
|
66
|
+
└── sub-02_ses-baseline.ttl
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Option 3: Per-subject files without sessions**
|
|
70
|
+
```
|
|
71
|
+
<nidm-input-dir>/
|
|
72
|
+
├── sub-01/
|
|
73
|
+
│ └── sub-01.ttl
|
|
74
|
+
└── sub-02/
|
|
75
|
+
└── sub-02.ttl
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Output NIDM Directory Structure
|
|
79
|
+
|
|
80
|
+
**With sessions:**
|
|
81
|
+
```
|
|
82
|
+
<output_dir>/<bidsapp_name>/nidm/
|
|
83
|
+
├── dataset_description.json
|
|
84
|
+
└── sub-{id}/
|
|
85
|
+
└── ses-{session}/
|
|
86
|
+
└── sub-{id}_ses-{session}.ttl
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Without sessions:**
|
|
90
|
+
```
|
|
91
|
+
<output_dir>/<bidsapp_name>/nidm/
|
|
92
|
+
├── dataset_description.json
|
|
93
|
+
└── sub-{id}/
|
|
94
|
+
└── sub-{id}.ttl
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## NIDM File Format
|
|
98
|
+
|
|
99
|
+
NIDM files use Turtle (.ttl) RDF format. Basic structure:
|
|
100
|
+
|
|
101
|
+
```turtle
|
|
102
|
+
@prefix prov: <http://www.w3.org/ns/prov#> .
|
|
103
|
+
@prefix nidm: <http://purl.org/nidash/nidm#> .
|
|
104
|
+
@prefix freesurfer: <http://purl.org/nidash/freesurfer#> .
|
|
105
|
+
|
|
106
|
+
# Software Agent
|
|
107
|
+
<software/freesurfer> a prov:Agent, prov:SoftwareAgent ;
|
|
108
|
+
prov:type nidm:FreeSurfer ;
|
|
109
|
+
nidm:softwareVersion "8.0.0" .
|
|
110
|
+
|
|
111
|
+
# Processing Activity
|
|
112
|
+
<activity/recon-all> a prov:Activity ;
|
|
113
|
+
prov:wasAssociatedWith <software/freesurfer> ;
|
|
114
|
+
prov:used <input/T1w> ;
|
|
115
|
+
prov:generated <output/segmentation> .
|
|
116
|
+
|
|
117
|
+
# Metrics
|
|
118
|
+
<output/segmentation> a prov:Entity ;
|
|
119
|
+
freesurfer:LeftHippocampalVolume "3847.2"^^xsd:float ;
|
|
120
|
+
freesurfer:RightHippocampalVolume "3912.5"^^xsd:float .
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## NIDM Copy Implementation
|
|
124
|
+
|
|
125
|
+
### Copy Function Template
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
import shutil
|
|
129
|
+
from pathlib import Path
|
|
130
|
+
import logging
|
|
131
|
+
|
|
132
|
+
logger = logging.getLogger(__name__)
|
|
133
|
+
|
|
134
|
+
def copy_nidm_file(nidm_input_dir, output_dir, bidsapp_name, subject_id, session_id=None):
|
|
135
|
+
"""
|
|
136
|
+
Copy existing NIDM file to output directory.
|
|
137
|
+
|
|
138
|
+
If no existing NIDM file is found, logs a warning and returns None.
|
|
139
|
+
The NIDM converter should handle creating a new file from scratch.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
nidm_input_dir: Path to input NIDM directory
|
|
143
|
+
output_dir: Base output directory
|
|
144
|
+
bidsapp_name: Name of BIDSapp (e.g., 'freesurfer_nidm')
|
|
145
|
+
subject_id: Subject ID without 'sub-' prefix
|
|
146
|
+
session_id: Session ID without 'ses-' prefix (optional, None for session-less)
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Path to copied NIDM file in output directory, or None if no input file found
|
|
150
|
+
"""
|
|
151
|
+
# Determine input NIDM file path
|
|
152
|
+
input_dir = Path(nidm_input_dir)
|
|
153
|
+
|
|
154
|
+
# Try per-subject structure
|
|
155
|
+
if session_id:
|
|
156
|
+
# With sessions
|
|
157
|
+
filename = f"sub-{subject_id}_ses-{session_id}.ttl"
|
|
158
|
+
input_file = input_dir / f"sub-{subject_id}" / f"ses-{session_id}" / filename
|
|
159
|
+
if not input_file.exists():
|
|
160
|
+
# Try generic nidm.ttl name
|
|
161
|
+
input_file = input_dir / f"sub-{subject_id}" / f"ses-{session_id}" / "nidm.ttl"
|
|
162
|
+
else:
|
|
163
|
+
# Without sessions - use sub-{id}.ttl
|
|
164
|
+
filename = f"sub-{subject_id}.ttl"
|
|
165
|
+
input_file = input_dir / f"sub-{subject_id}" / filename
|
|
166
|
+
if not input_file.exists():
|
|
167
|
+
# Try generic nidm.ttl name
|
|
168
|
+
input_file = input_dir / f"sub-{subject_id}" / "nidm.ttl"
|
|
169
|
+
|
|
170
|
+
# Fall back to dataset-level file
|
|
171
|
+
if not input_file.exists():
|
|
172
|
+
input_file = input_dir / "nidm.ttl"
|
|
173
|
+
|
|
174
|
+
if not input_file.exists():
|
|
175
|
+
logger.warning(
|
|
176
|
+
f"No existing NIDM file found in {nidm_input_dir} for sub-{subject_id}"
|
|
177
|
+
+ (f" ses-{session_id}" if session_id else "")
|
|
178
|
+
+ ". NIDM conversion will create a new file from scratch."
|
|
179
|
+
)
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
# Create output directory
|
|
183
|
+
nidm_output_dir = Path(output_dir) / bidsapp_name / "nidm"
|
|
184
|
+
if session_id:
|
|
185
|
+
subject_nidm_dir = nidm_output_dir / f"sub-{subject_id}" / f"ses-{session_id}"
|
|
186
|
+
output_filename = f"sub-{subject_id}_ses-{session_id}.ttl"
|
|
187
|
+
else:
|
|
188
|
+
# Session-less: sub-{id}.ttl
|
|
189
|
+
subject_nidm_dir = nidm_output_dir / f"sub-{subject_id}"
|
|
190
|
+
output_filename = f"sub-{subject_id}.ttl"
|
|
191
|
+
|
|
192
|
+
subject_nidm_dir.mkdir(parents=True, exist_ok=True)
|
|
193
|
+
output_file = subject_nidm_dir / output_filename
|
|
194
|
+
|
|
195
|
+
# Copy file
|
|
196
|
+
shutil.copy2(input_file, output_file)
|
|
197
|
+
logger.info(f"Copied NIDM file from {input_file} to {output_file}")
|
|
198
|
+
|
|
199
|
+
return output_file
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## NIDM Integration Implementation
|
|
203
|
+
|
|
204
|
+
### Integration Function Template
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
import rdflib
|
|
208
|
+
from pathlib import Path
|
|
209
|
+
from datetime import datetime
|
|
210
|
+
import logging
|
|
211
|
+
|
|
212
|
+
logger = logging.getLogger(__name__)
|
|
213
|
+
|
|
214
|
+
def integrate_metrics_to_nidm(nidm_file, metrics, software_name, software_version,
|
|
215
|
+
subject_id, session_id=None, output_dir=None,
|
|
216
|
+
bidsapp_name=None):
|
|
217
|
+
"""
|
|
218
|
+
Integrate analysis metrics into NIDM file.
|
|
219
|
+
|
|
220
|
+
If nidm_file is None (no existing file), creates a new NIDM file from scratch.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
nidm_file: Path to NIDM .ttl file (can be None)
|
|
224
|
+
metrics: Dictionary of metrics to add
|
|
225
|
+
software_name: Name of analysis software (e.g., 'FreeSurfer')
|
|
226
|
+
software_version: Version of analysis software
|
|
227
|
+
subject_id: Subject ID without 'sub-' prefix
|
|
228
|
+
session_id: Session ID without 'ses-' prefix (optional)
|
|
229
|
+
output_dir: Base output directory (required if nidm_file is None)
|
|
230
|
+
bidsapp_name: BIDSapp name (required if nidm_file is None)
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Path to the created/updated NIDM file
|
|
234
|
+
"""
|
|
235
|
+
# Determine output file path
|
|
236
|
+
if nidm_file is None:
|
|
237
|
+
if output_dir is None or bidsapp_name is None:
|
|
238
|
+
raise ValueError("output_dir and bidsapp_name required when creating new NIDM file")
|
|
239
|
+
|
|
240
|
+
# Create new NIDM file
|
|
241
|
+
nidm_output_dir = Path(output_dir) / bidsapp_name / "nidm"
|
|
242
|
+
if session_id:
|
|
243
|
+
subject_nidm_dir = nidm_output_dir / f"sub-{subject_id}" / f"ses-{session_id}"
|
|
244
|
+
output_filename = f"sub-{subject_id}_ses-{session_id}.ttl"
|
|
245
|
+
else:
|
|
246
|
+
subject_nidm_dir = nidm_output_dir / f"sub-{subject_id}"
|
|
247
|
+
output_filename = f"sub-{subject_id}.ttl"
|
|
248
|
+
|
|
249
|
+
subject_nidm_dir.mkdir(parents=True, exist_ok=True)
|
|
250
|
+
nidm_file = subject_nidm_dir / output_filename
|
|
251
|
+
|
|
252
|
+
logger.info(f"Creating new NIDM file at {nidm_file}")
|
|
253
|
+
g = rdflib.Graph()
|
|
254
|
+
else:
|
|
255
|
+
# Load existing NIDM graph
|
|
256
|
+
logger.info(f"Loading existing NIDM file from {nidm_file}")
|
|
257
|
+
g = rdflib.Graph()
|
|
258
|
+
g.parse(nidm_file, format='turtle')
|
|
259
|
+
|
|
260
|
+
# Define namespaces
|
|
261
|
+
PROV = rdflib.Namespace('http://www.w3.org/ns/prov#')
|
|
262
|
+
NIDM = rdflib.Namespace('http://purl.org/nidash/nidm#')
|
|
263
|
+
ANALYSIS = rdflib.Namespace(f'http://purl.org/nidash/{software_name.lower()}#')
|
|
264
|
+
XSD = rdflib.Namespace('http://www.w3.org/2001/XMLSchema#')
|
|
265
|
+
|
|
266
|
+
# Bind namespaces
|
|
267
|
+
g.bind('prov', PROV)
|
|
268
|
+
g.bind('nidm', NIDM)
|
|
269
|
+
g.bind(software_name.lower(), ANALYSIS)
|
|
270
|
+
g.bind('xsd', XSD)
|
|
271
|
+
|
|
272
|
+
# Create activity node
|
|
273
|
+
timestamp = datetime.now().isoformat()
|
|
274
|
+
activity_id = rdflib.URIRef(f'activity/{software_name.lower()}-{timestamp}')
|
|
275
|
+
g.add((activity_id, rdflib.RDF.type, PROV.Activity))
|
|
276
|
+
g.add((activity_id, PROV.startedAtTime, rdflib.Literal(datetime.now(), datatype=XSD.dateTime)))
|
|
277
|
+
|
|
278
|
+
# Create software agent
|
|
279
|
+
software_id = rdflib.URIRef(f'software/{software_name.lower()}')
|
|
280
|
+
g.add((software_id, rdflib.RDF.type, PROV.SoftwareAgent))
|
|
281
|
+
g.add((software_id, NIDM.softwareVersion, rdflib.Literal(software_version)))
|
|
282
|
+
g.add((activity_id, PROV.wasAssociatedWith, software_id))
|
|
283
|
+
|
|
284
|
+
# Create output entity with metrics
|
|
285
|
+
output_id = rdflib.URIRef(f'output/{software_name.lower()}-result-{timestamp}')
|
|
286
|
+
g.add((output_id, rdflib.RDF.type, PROV.Entity))
|
|
287
|
+
g.add((activity_id, PROV.generated, output_id))
|
|
288
|
+
|
|
289
|
+
# Add subject/session info
|
|
290
|
+
g.add((output_id, NIDM.subjectID, rdflib.Literal(f"sub-{subject_id}")))
|
|
291
|
+
if session_id:
|
|
292
|
+
g.add((output_id, NIDM.sessionID, rdflib.Literal(f"ses-{session_id}")))
|
|
293
|
+
|
|
294
|
+
# Add metrics
|
|
295
|
+
for metric_name, metric_value in metrics.items():
|
|
296
|
+
predicate = ANALYSIS[metric_name]
|
|
297
|
+
if isinstance(metric_value, float):
|
|
298
|
+
obj = rdflib.Literal(metric_value, datatype=XSD.float)
|
|
299
|
+
elif isinstance(metric_value, int):
|
|
300
|
+
obj = rdflib.Literal(metric_value, datatype=XSD.integer)
|
|
301
|
+
else:
|
|
302
|
+
obj = rdflib.Literal(metric_value)
|
|
303
|
+
g.add((output_id, predicate, obj))
|
|
304
|
+
|
|
305
|
+
# Save graph (overwrites if existing, creates if new)
|
|
306
|
+
g.serialize(destination=str(nidm_file), format='turtle')
|
|
307
|
+
logger.info(f"NIDM file saved to {nidm_file}")
|
|
308
|
+
|
|
309
|
+
return nidm_file
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Complete NIDM Processing Workflow
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
def process_nidm(nidm_input_dir, output_dir, bidsapp_name, subject_id, session_id,
|
|
316
|
+
skip_nidm, analysis_outputs, software_name, software_version):
|
|
317
|
+
"""
|
|
318
|
+
Complete NIDM processing workflow.
|
|
319
|
+
|
|
320
|
+
Handles both cases:
|
|
321
|
+
1. Existing NIDM file: Copy → Extract metrics → Integrate (overwrite)
|
|
322
|
+
2. No existing file: Warning → Extract metrics → Create new file
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
if skip_nidm:
|
|
326
|
+
logger.info("Skipping NIDM processing (--skip-nidm flag set)")
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
# Step 1: Try to copy existing NIDM file
|
|
330
|
+
nidm_file = copy_nidm_file(nidm_input_dir, output_dir, bidsapp_name,
|
|
331
|
+
subject_id, session_id)
|
|
332
|
+
|
|
333
|
+
# Step 2: Extract metrics from analysis outputs
|
|
334
|
+
logger.info("Extracting metrics from analysis outputs")
|
|
335
|
+
metrics = extract_metrics(analysis_outputs, subject_id, session_id)
|
|
336
|
+
|
|
337
|
+
# Step 3: Integrate metrics into NIDM file
|
|
338
|
+
# This works whether nidm_file exists or is None
|
|
339
|
+
nidm_file = integrate_metrics_to_nidm(
|
|
340
|
+
nidm_file,
|
|
341
|
+
metrics,
|
|
342
|
+
software_name,
|
|
343
|
+
software_version,
|
|
344
|
+
subject_id,
|
|
345
|
+
session_id,
|
|
346
|
+
output_dir,
|
|
347
|
+
bidsapp_name
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
logger.info(f"NIDM processing complete: {nidm_file}")
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Analysis-Specific Metric Extraction
|
|
354
|
+
|
|
355
|
+
Each analysis tool requires custom metric extraction. Examples:
|
|
356
|
+
|
|
357
|
+
### FreeSurfer Metrics
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
def extract_freesurfer_metrics(freesurfer_dir):
|
|
361
|
+
"""Extract metrics from FreeSurfer outputs."""
|
|
362
|
+
metrics = {}
|
|
363
|
+
|
|
364
|
+
# Read aseg.stats for volumetric measures
|
|
365
|
+
aseg_stats = freesurfer_dir / "stats" / "aseg.stats"
|
|
366
|
+
if aseg_stats.exists():
|
|
367
|
+
# Parse aseg.stats file
|
|
368
|
+
# Extract hippocampal volumes, cortical volumes, etc.
|
|
369
|
+
pass
|
|
370
|
+
|
|
371
|
+
# Read aparc.stats for cortical measures
|
|
372
|
+
for hemi in ['lh', 'rh']:
|
|
373
|
+
aparc_stats = freesurfer_dir / "stats" / f"{hemi}.aparc.stats"
|
|
374
|
+
if aparc_stats.exists():
|
|
375
|
+
# Parse cortical thickness, surface area, etc.
|
|
376
|
+
pass
|
|
377
|
+
|
|
378
|
+
return metrics
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### MRIQC Metrics
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
def extract_mriqc_metrics(mriqc_output_dir):
|
|
385
|
+
"""Extract metrics from MRIQC JSON outputs."""
|
|
386
|
+
import json
|
|
387
|
+
|
|
388
|
+
metrics = {}
|
|
389
|
+
|
|
390
|
+
# Find JSON file with IQMs
|
|
391
|
+
json_files = list(mriqc_output_dir.glob("sub-*_T1w.json"))
|
|
392
|
+
if json_files:
|
|
393
|
+
with open(json_files[0]) as f:
|
|
394
|
+
iqms = json.load(f)
|
|
395
|
+
|
|
396
|
+
# Extract image quality metrics
|
|
397
|
+
metrics['SNR'] = iqms.get('snr_total')
|
|
398
|
+
metrics['CNR'] = iqms.get('cnr')
|
|
399
|
+
metrics['EFC'] = iqms.get('efc')
|
|
400
|
+
# Add more metrics as needed
|
|
401
|
+
|
|
402
|
+
return metrics
|
|
403
|
+
```
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Repository Structure Reference
|
|
2
|
+
|
|
3
|
+
## Standard Directory Layout
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
<bidsapp_name>/ # e.g., freesurfer_bidsapp, mriqc-nidm_bidsapp
|
|
7
|
+
├── src/ # Source code
|
|
8
|
+
│ ├── <analysis_name>/ # Main analysis package
|
|
9
|
+
│ │ ├── __init__.py
|
|
10
|
+
│ │ ├── run.py # CLI entry point
|
|
11
|
+
│ │ ├── <analysis>_runner.py # Core analysis execution
|
|
12
|
+
│ │ ├── utils.py # Utility functions
|
|
13
|
+
│ │ └── validators.py # Input validation
|
|
14
|
+
│ └── nidm/ # NIDM conversion package
|
|
15
|
+
│ ├── __init__.py
|
|
16
|
+
│ ├── nidm_converter.py # Main NIDM conversion logic
|
|
17
|
+
│ ├── nidm_utils.py # NIDM utilities
|
|
18
|
+
│ └── data/ # NIDM reference data
|
|
19
|
+
│ ├── ontology_mappings.json
|
|
20
|
+
│ └── software_metadata.csv
|
|
21
|
+
├── tests/ # Test suite
|
|
22
|
+
│ ├── __init__.py
|
|
23
|
+
│ ├── test_<analysis>.py # Analysis tests
|
|
24
|
+
│ ├── test_nidm.py # NIDM conversion tests
|
|
25
|
+
│ ├── test_integration.py # End-to-end tests
|
|
26
|
+
│ └── fixtures/ # Test data
|
|
27
|
+
│ ├── bids_data/
|
|
28
|
+
│ └── nidm_data/
|
|
29
|
+
├── examples/ # Usage examples
|
|
30
|
+
│ ├── example_usage.sh # Basic usage script
|
|
31
|
+
│ └── babs_integration.sh # BABS workflow example
|
|
32
|
+
├── Dockerfile # Docker container definition
|
|
33
|
+
├── Singularity # Singularity/Apptainer definition
|
|
34
|
+
├── setup.py # Package setup and metadata
|
|
35
|
+
├── setup.cfg # Setup configuration
|
|
36
|
+
├── requirements.txt # Python dependencies
|
|
37
|
+
├── VERSION # Version number (e.g., 0.1.0)
|
|
38
|
+
├── LICENSE # MIT license
|
|
39
|
+
├── README.md # Usage documentation
|
|
40
|
+
├── .gitignore # Git ignore patterns
|
|
41
|
+
└── .github/ # GitHub workflows (optional)
|
|
42
|
+
└── workflows/
|
|
43
|
+
└── test.yml # CI/CD tests
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## File Responsibilities
|
|
47
|
+
|
|
48
|
+
### src/<analysis_name>/run.py
|
|
49
|
+
- CLI argument parsing using argparse
|
|
50
|
+
- Input validation
|
|
51
|
+
- Orchestrate analysis and NIDM workflows
|
|
52
|
+
- Error handling and logging
|
|
53
|
+
|
|
54
|
+
### src/<analysis_name>/<analysis>_runner.py
|
|
55
|
+
- Execute the actual analysis tool
|
|
56
|
+
- Handle tool-specific configurations
|
|
57
|
+
- Manage intermediate outputs
|
|
58
|
+
- Extract metrics for NIDM
|
|
59
|
+
|
|
60
|
+
### src/nidm/nidm_converter.py
|
|
61
|
+
- Copy input NIDM file to output directory
|
|
62
|
+
- Extract relevant metrics from analysis outputs
|
|
63
|
+
- Integrate metrics into NIDM file (overwrite)
|
|
64
|
+
- Validate NIDM output
|
|
65
|
+
|
|
66
|
+
### Dockerfile
|
|
67
|
+
- Define container image
|
|
68
|
+
- Install analysis tool dependencies
|
|
69
|
+
- Install Python package
|
|
70
|
+
- Set entry point to run.py
|
|
71
|
+
|
|
72
|
+
### Singularity
|
|
73
|
+
- Define Apptainer/Singularity container
|
|
74
|
+
- Use existing base images where possible
|
|
75
|
+
- Install analysis tool and Python package
|
|
76
|
+
- Configure for HPC execution
|
|
77
|
+
|
|
78
|
+
### setup.py
|
|
79
|
+
- Package metadata (name, version, author)
|
|
80
|
+
- Define entry points for CLI commands
|
|
81
|
+
- Specify dependencies
|
|
82
|
+
- Include package data files
|
|
83
|
+
|
|
84
|
+
## Key Naming Conventions
|
|
85
|
+
|
|
86
|
+
- **Repository name**: `<tool>_bidsapp` or `<tool>-nidm_bidsapp`
|
|
87
|
+
- Examples: `freesurfer_bidsapp`, `mriqc-nidm_bidsapp`, `ants_bidsapp`
|
|
88
|
+
|
|
89
|
+
- **Package name**: `<tool>_nidm` (in setup.py)
|
|
90
|
+
- Examples: `freesurfer_nidm`, `mriqc_nidm`, `ants_nidm`
|
|
91
|
+
|
|
92
|
+
- **Output directory**: `<tool>_nidm` or `<tool>-nidm`
|
|
93
|
+
- Must match package name for consistency
|
|
94
|
+
|
|
95
|
+
- **Container name**: `<tool>-nidm-bidsapp-<version>`
|
|
96
|
+
- Example: `mriqc-nidm-bidsapp-0-1-0`
|
|
97
|
+
|
|
98
|
+
## Modularity Requirements
|
|
99
|
+
|
|
100
|
+
The NIDM conversion component must be:
|
|
101
|
+
- **Self-contained**: All NIDM logic in `src/nidm/` directory
|
|
102
|
+
- **Reusable**: Can be imported and used independently
|
|
103
|
+
- **Analysis-specific**: Tailored to the specific analysis tool's outputs
|
|
104
|
+
- **Consistent API**: Standard function signatures across all BIDSapps
|
|
105
|
+
|
|
106
|
+
Example NIDM module API:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# In src/nidm/nidm_converter.py
|
|
110
|
+
def copy_nidm_input(input_dir, output_dir, subject_id, session_id):
|
|
111
|
+
"""Copy existing NIDM file to output directory."""
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
def extract_metrics(analysis_output_dir, subject_id, session_id):
|
|
115
|
+
"""Extract relevant metrics from analysis outputs."""
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
def integrate_to_nidm(nidm_file, metrics):
|
|
119
|
+
"""Integrate extracted metrics into NIDM file."""
|
|
120
|
+
pass
|
|
121
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Testing Patterns
|
|
2
|
+
|
|
3
|
+
## Test Structure
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
tests/
|
|
7
|
+
├── __init__.py
|
|
8
|
+
├── test_<analysis>.py # Analysis-specific tests
|
|
9
|
+
├── test_nidm.py # NIDM conversion tests
|
|
10
|
+
├── test_integration.py # End-to-end tests
|
|
11
|
+
└── fixtures/ # Test data
|
|
12
|
+
├── bids_data/ # Minimal BIDS dataset
|
|
13
|
+
└── nidm_data/ # Sample NIDM files
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Test Coverage Requirements
|
|
17
|
+
|
|
18
|
+
1. **CLI Argument Parsing**
|
|
19
|
+
- Valid arguments accepted
|
|
20
|
+
- Invalid arguments rejected
|
|
21
|
+
- Default values correct
|
|
22
|
+
|
|
23
|
+
2. **BIDS Validation**
|
|
24
|
+
- Valid BIDS datasets accepted
|
|
25
|
+
- Invalid datasets rejected
|
|
26
|
+
- Required files detected
|
|
27
|
+
|
|
28
|
+
3. **Analysis Execution**
|
|
29
|
+
- Tool runs successfully with valid inputs
|
|
30
|
+
- Error handling for invalid inputs
|
|
31
|
+
- Output files created
|
|
32
|
+
|
|
33
|
+
4. **NIDM Conversion**
|
|
34
|
+
- Input NIDM file copied correctly
|
|
35
|
+
- Metrics extracted from analysis outputs
|
|
36
|
+
- NIDM file updated with new metrics
|
|
37
|
+
- TTL format valid
|
|
38
|
+
|
|
39
|
+
5. **Output Structure**
|
|
40
|
+
- Correct directory structure created
|
|
41
|
+
- BIDS-compliant derivatives
|
|
42
|
+
- dataset_description.json files present
|
|
43
|
+
|
|
44
|
+
## Example Tests
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import pytest
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
|
|
50
|
+
def test_cli_args():
|
|
51
|
+
"""Test CLI argument parsing."""
|
|
52
|
+
from <package>.run import parse_args
|
|
53
|
+
|
|
54
|
+
args = parse_args([
|
|
55
|
+
'/data/bids',
|
|
56
|
+
'/data/output',
|
|
57
|
+
'participant',
|
|
58
|
+
'--participant-label', '01',
|
|
59
|
+
'--nidm-input-dir', '/data/nidm'
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
assert args.bids_dir == '/data/bids'
|
|
63
|
+
assert args.participant_label == ['01']
|
|
64
|
+
|
|
65
|
+
def test_nidm_copy(tmp_path):
|
|
66
|
+
"""Test NIDM file copying."""
|
|
67
|
+
from <package>.nidm.nidm_converter import copy_nidm_file
|
|
68
|
+
|
|
69
|
+
# Setup test data
|
|
70
|
+
nidm_input = tmp_path / "nidm_input"
|
|
71
|
+
nidm_input.mkdir()
|
|
72
|
+
(nidm_input / "nidm.ttl").write_text("test nidm content")
|
|
73
|
+
|
|
74
|
+
# Copy file
|
|
75
|
+
output = tmp_path / "output"
|
|
76
|
+
nidm_file = copy_nidm_file(nidm_input, output, "test_app", "01")
|
|
77
|
+
|
|
78
|
+
assert nidm_file.exists()
|
|
79
|
+
assert nidm_file.read_text() == "test nidm content"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Use pytest fixtures for test data and temporary directories.
|