2424import os
2525from pathlib import Path
2626import json
27+ from hashlib import sha1
2728
2829import numpy as np
2930import nibabel as nb
4647
4748@pytest .mark .parametrize ("out_path_base" , [None , "fmriprep" ])
4849@pytest .mark .parametrize (
49- "source,input_files,entities,expectation" ,
50+ "source,input_files,entities,expectation,checksum " ,
5051 [
5152 (
5253 T1W_PATH ,
5354 ["anat.nii.gz" ],
5455 {"desc" : "preproc" },
5556 "sub-100185/anat/sub-100185_desc-preproc_T1w.nii.gz" ,
57+ "7c047921def32da260df4a985019b9f5231659fa" ,
5658 ),
5759 (
5860 T1W_PATH ,
5961 ["anat.nii.gz" ],
6062 {"desc" : "preproc" , "space" : "MNI" },
6163 "sub-100185/anat/sub-100185_space-MNI_desc-preproc_T1w.nii.gz" ,
64+ "b22399f50ce454049d5d074457a92ab13e7fdf8c" ,
6265 ),
6366 (
6467 T1W_PATH ,
6568 ["anat.nii.gz" ],
6669 {"desc" : "preproc" , "space" : "MNI" , "resolution" : "native" },
6770 "sub-100185/anat/sub-100185_space-MNI_desc-preproc_T1w.nii.gz" ,
71+ "b22399f50ce454049d5d074457a92ab13e7fdf8c" ,
6872 ),
6973 (
7074 T1W_PATH ,
7175 ["anat.nii.gz" ],
7276 {"desc" : "preproc" , "space" : "MNI" , "resolution" : "high" },
7377 "sub-100185/anat/sub-100185_space-MNI_res-high_desc-preproc_T1w.nii.gz" ,
78+ "b22399f50ce454049d5d074457a92ab13e7fdf8c" ,
7479 ),
7580 (
7681 T1W_PATH ,
7782 ["tfm.txt" ],
7883 {"from" : "fsnative" , "to" : "T1w" , "suffix" : "xfm" },
7984 "sub-100185/anat/sub-100185_from-fsnative_to-T1w_mode-image_xfm.txt" ,
85+ "da39a3ee5e6b4b0d3255bfef95601890afd80709" ,
8086 ),
8187 (
8288 T1W_PATH ,
8389 ["tfm.h5" ],
8490 {"from" : "MNI152NLin2009cAsym" , "to" : "T1w" , "suffix" : "xfm" },
8591 "sub-100185/anat/sub-100185_from-MNI152NLin2009cAsym_to-T1w_mode-image_xfm.h5" ,
92+ "da39a3ee5e6b4b0d3255bfef95601890afd80709" ,
8693 ),
8794 (
8895 T1W_PATH ,
8996 ["anat.nii.gz" ],
9097 {"desc" : "brain" , "suffix" : "mask" },
9198 "sub-100185/anat/sub-100185_desc-brain_mask.nii.gz" ,
99+ "d425f0096b6b6d1252973e48b31d760c0b1bdc11" ,
92100 ),
93101 (
94102 T1W_PATH ,
95103 ["anat.nii.gz" ],
96104 {"desc" : "brain" , "suffix" : "mask" , "space" : "MNI" },
97105 "sub-100185/anat/sub-100185_space-MNI_desc-brain_mask.nii.gz" ,
106+ "a2a6efa16eb23173d0ee64779de879711bc74643" ,
98107 ),
99108 (
100109 T1W_PATH ,
101110 ["anat.surf.gii" ],
102111 {"suffix" : "pial" , "hemi" : "L" },
103112 "sub-100185/anat/sub-100185_hemi-L_pial.surf.gii" ,
113+ "da39a3ee5e6b4b0d3255bfef95601890afd80709" ,
104114 ),
105115 (
106116 T1W_PATH ,
110120 f"sub-100185/anat/sub-100185_desc-{ s } _dseg.nii"
111121 for s in ("aseg" , "aparcaseg" )
112122 ],
123+ ["a235cdf59f9bf077ba30bf2523a56508e3a5aabb" ,
124+ "a235cdf59f9bf077ba30bf2523a56508e3a5aabb" ],
113125 ),
114126 (
115127 T1W_PATH ,
119131 f"sub-100185/anat/sub-100185_desc-preproc_T1w.{ ext } "
120132 for ext in ("nii" , "json" )
121133 ],
134+ ["25c107d4a3e6f98e48aa752c5bbd88ab8e8d069f" ,
135+ "da39a3ee5e6b4b0d3255bfef95601890afd80709" ],
122136 ),
123137 (
124138 T1W_PATH ,
128142 f"sub-100185/anat/sub-100185_label-{ lab } _probseg.nii.gz"
129143 for lab in ("GM" , "WM" , "CSF" )
130144 ],
145+ ["7c047921def32da260df4a985019b9f5231659fa" ] * 3 ,
131146 ),
132147 # BOLD data
133148 (
134149 BOLD_PATH ,
135150 ["aroma.csv" ],
136151 {"suffix" : "AROMAnoiseICs" },
137152 "sub-100185/func/sub-100185_task-machinegame_run-1_AROMAnoiseICs.csv" ,
153+ "da39a3ee5e6b4b0d3255bfef95601890afd80709" ,
138154 ),
139155 (
140156 BOLD_PATH ,
141157 ["confounds.tsv" ],
142158 {"suffix" : "regressors" , "desc" : "confounds" },
143159 "sub-100185/func/sub-100185_task-machinegame_run-1_desc-confounds_regressors.tsv" ,
160+ "da39a3ee5e6b4b0d3255bfef95601890afd80709" ,
144161 ),
145162 (
146163 BOLD_PATH ,
147164 ["mixing.tsv" ],
148165 {"suffix" : "mixing" , "desc" : "MELODIC" },
149166 "sub-100185/func/sub-100185_task-machinegame_run-1_desc-MELODIC_mixing.tsv" ,
167+ "da39a3ee5e6b4b0d3255bfef95601890afd80709" ,
150168 ),
151169 (
152170 BOLD_PATH ,
153171 ["lh.func.gii" ],
154172 {"space" : "fsaverage" , "density" : "10k" , "hemi" : "L" },
155173 "sub-100185/func/sub-100185_task-machinegame_run-1_"
156174 "space-fsaverage_den-10k_hemi-L_bold.func.gii" ,
175+ "da39a3ee5e6b4b0d3255bfef95601890afd80709" ,
157176 ),
158177 (
159178 BOLD_PATH ,
160179 ["hcp.dtseries.nii" ],
161180 {"space" : "fsLR" , "density" : "91k" },
162181 "sub-100185/func/sub-100185_task-machinegame_run-1_"
163182 "space-fsLR_den-91k_bold.dtseries.nii" ,
183+ "53d9b486d08fec5a952f68fcbcddb38a72818d4c" ,
164184 ),
165185 (
166186 BOLD_PATH ,
167187 ["ref.nii" ],
168188 {"space" : "MNI" , "suffix" : "boldref" },
169189 "sub-100185/func/sub-100185_task-machinegame_run-1_space-MNI_boldref.nii" ,
190+ "53d9b486d08fec5a952f68fcbcddb38a72818d4c" ,
170191 ),
171192 (
172193 BOLD_PATH ,
173194 ["dseg.nii" ],
174195 {"space" : "MNI" , "suffix" : "dseg" , "desc" : "aseg" },
175196 "sub-100185/func/sub-100185_task-machinegame_run-1_space-MNI_desc-aseg_dseg.nii" ,
197+ "6d2cae7f56c246d7934e2e21e7b472ecc63a4257" ,
176198 ),
177199 (
178200 BOLD_PATH ,
179201 ["mask.nii" ],
180202 {"space" : "MNI" , "suffix" : "mask" , "desc" : "brain" },
181203 "sub-100185/func/sub-100185_task-machinegame_run-1_space-MNI_desc-brain_mask.nii" ,
204+ "c365991854931181a1444d6803f5289448e7e266" ,
182205 ),
183206 (
184207 BOLD_PATH ,
185208 ["bold.nii" ],
186209 {"space" : "MNI" , "desc" : "preproc" },
187210 "sub-100185/func/sub-100185_task-machinegame_run-1_space-MNI_desc-preproc_bold.nii" ,
211+ "aa1eed935e6a8dcca646b0c78ee57218e30e2974" ,
188212 ),
189213 # Nondeterministic order - do we really need this to work, or we can stay safe with
190214 # MapNodes?
197221 ["anat.html" ],
198222 {"desc" : "conform" , "datatype" : "figures" },
199223 "sub-100185/figures/sub-100185_desc-conform_T1w.html" ,
224+ "da39a3ee5e6b4b0d3255bfef95601890afd80709" ,
200225 ),
201226 (
202227 BOLD_PATH ,
203228 ["aroma.csv" ],
204229 {"suffix" : "AROMAnoiseICs" , "extension" : "h5" },
205230 ValueError ,
231+ None ,
206232 ),
207233 (
208234 T1W_PATH ,
209235 ["anat.nii.gz" ] * 3 ,
210236 {"desc" : "preproc" , "space" : "MNI" },
211237 ValueError ,
238+ None ,
212239 ),
213240 (
214241 "sub-07/ses-preop/anat/sub-07_ses-preop_T1w.nii.gz" ,
215242 ["tfm.h5" ],
216243 {"from" : "orig" , "to" : "target" , "suffix" : "xfm" },
217244 "sub-07/ses-preop/anat/sub-07_ses-preop_from-orig_to-target_mode-image_xfm.h5" ,
245+ "da39a3ee5e6b4b0d3255bfef95601890afd80709" ,
218246 ),
219247 (
220248 "sub-07/ses-preop/anat/sub-07_ses-preop_run-1_T1w.nii.gz" ,
221249 ["tfm.txt" ],
222250 {"from" : "orig" , "to" : "T1w" , "suffix" : "xfm" },
223251 "sub-07/ses-preop/anat/sub-07_ses-preop_run-1_from-orig_to-T1w_mode-image_xfm.txt" ,
252+ "da39a3ee5e6b4b0d3255bfef95601890afd80709" ,
224253 ),
225254 ],
226255)
@@ -232,6 +261,7 @@ def test_DerivativesDataSink_build_path(
232261 input_files ,
233262 entities ,
234263 expectation ,
264+ checksum ,
235265 dismiss_entities ,
236266):
237267 """Check a few common derivatives generated by NiPreps."""
@@ -296,9 +326,12 @@ def test_DerivativesDataSink_build_path(
296326 output = dds .run ().outputs .out_file
297327 if isinstance (output , str ):
298328 output = [output ]
329+ checksum = [checksum ]
299330
300331 for out , exp in zip (output , expectation ):
301332 assert Path (out ).relative_to (tmp_path ) == Path (base ) / exp
333+ for out , chksum in zip (output , checksum ):
334+ assert sha1 (Path (out ).read_bytes ()).hexdigest () == chksum
302335
303336
304337def test_DerivativesDataSink_dtseries_json_hack (tmp_path ):
@@ -522,6 +555,52 @@ def test_DerivativesDataSink_fmapid(tmp_path):
522555 assert dds .outputs .out_file .endswith ("sub-36_fmapid-auto00000_desc-pepolar_fieldmap.svg" )
523556
524557
558+ @pytest .mark .parametrize ("dtype" , ("i2" , "u2" , "f4" ))
559+ def test_DerivativesDataSink_values (tmp_path , dtype ):
560+ # We use static checksums above, which ensures we don't break things, but
561+ # pins the tests to specific values.
562+ # Here we use random values, check that the values are preserved, and then
563+ # the checksums are unchanged across two runs.
564+ fname = str (tmp_path / "source.nii.gz" )
565+ rng = np .random .default_rng ()
566+ hdr = nb .Nifti1Header ()
567+ hdr .set_qform (np .eye (4 ), code = 1 )
568+ hdr .set_sform (np .eye (4 ), code = 1 )
569+ nb .Nifti1Image (rng .uniform (500 , 2000 , (5 , 5 , 5 )), np .eye (4 ), hdr ).to_filename (fname )
570+
571+ orig_data = np .asanyarray (nb .load (fname ).dataobj )
572+ expected = np .rint (orig_data ) if dtype [0 ] in "iu" else orig_data
573+
574+ dds = bintfs .DerivativesDataSink (
575+ base_directory = str (tmp_path ),
576+ keep_dtype = True ,
577+ data_dtype = dtype ,
578+ desc = "preproc" ,
579+ source_file = T1W_PATH ,
580+ in_file = fname ,
581+ ).run ()
582+
583+ out_file = Path (dds .outputs .out_file )
584+
585+ nii = nb .load (out_file )
586+ assert np .allclose (nii .dataobj , expected )
587+
588+ checksum = sha1 (out_file .read_bytes ()).hexdigest ()
589+ out_file .unlink ()
590+
591+ # Rerun to ensure determinism with non-zero data
592+ dds = bintfs .DerivativesDataSink (
593+ base_directory = str (tmp_path ),
594+ keep_dtype = True ,
595+ data_dtype = dtype ,
596+ desc = "preproc" ,
597+ source_file = T1W_PATH ,
598+ in_file = fname ,
599+ ).run ()
600+
601+ assert sha1 (out_file .read_bytes ()).hexdigest () == checksum
602+
603+
525604@pytest .mark .parametrize ("field" , ["RepetitionTime" , "UndefinedField" ])
526605def test_ReadSidecarJSON_connection (testdata_dir , field ):
527606 """
0 commit comments