Skip to content

Commit 6ad289d

Browse files
authored
Merge pull request #191 from adelavega/enh/fixed-effects
Add fixed effects - FEMA contrasts
2 parents a05b6b0 + 8c054f3 commit 6ad289d

File tree

5 files changed

+72
-32
lines changed

5 files changed

+72
-32
lines changed

examples/models/ds000003/models/model-001_smdl.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@
5555
}
5656
]
5757
},
58+
{
59+
"Level": "subject",
60+
"DummyContrasts": {
61+
"Type": "FEMA"
62+
}
63+
},
5864
{
5965
"Level": "dataset",
6066
"DummyContrasts": {

fitlins/interfaces/nistats.py

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def _run_interface(self, runtime):
135135
out_ents = self.inputs.contrast_info[0]['entities']
136136
fname_fmt = os.path.join(runtime.cwd, '{}_{}.nii.gz').format
137137
for name, weights, contrast_type in prepare_contrasts(
138-
self.inputs.contrast_info, mat.columns.tolist()):
138+
self.inputs.contrast_info, mat.columns):
139139
contrast_metadata.append(
140140
{'contrast': name,
141141
'stat': contrast_type,
@@ -178,12 +178,12 @@ def _match(query, metadata):
178178
class SecondLevelModel(NistatsBaseInterface, SecondLevelEstimatorInterface, SimpleInterface):
179179
def _run_interface(self, runtime):
180180
from nistats import second_level_model as level2
181+
from nistats.contrasts import compute_fixed_effects
182+
181183
smoothing_fwhm = self.inputs.smoothing_fwhm
182184
if not isdefined(smoothing_fwhm):
183185
smoothing_fwhm = None
184186

185-
model = level2.SecondLevelModel(smoothing_fwhm=smoothing_fwhm)
186-
187187
effect_maps = []
188188
variance_maps = []
189189
stat_maps = []
@@ -196,46 +196,76 @@ def _run_interface(self, runtime):
196196
# Only keep files which match all entities for contrast
197197
stat_metadata = _flatten(self.inputs.stat_metadata)
198198
input_effects = _flatten(self.inputs.effect_maps)
199+
input_variances = _flatten(self.inputs.variance_maps)
199200

200201
filtered_effects = []
202+
filtered_variances = []
201203
names = []
202-
for m, eff in zip(stat_metadata, input_effects):
204+
for m, eff, var in zip(stat_metadata, input_effects, input_variances):
203205
if _match(out_ents, m):
204206
filtered_effects.append(eff)
207+
filtered_variances.append(var)
205208
names.append(m['contrast'])
206209

207-
# Dummy code contrast of input effects
208-
design_matrix = pd.get_dummies(names)
210+
mat = pd.get_dummies(names)
211+
contrasts = prepare_contrasts(self.inputs.contrast_info, mat.columns)
209212

210-
# Fit single model for all inputs
211-
model.fit(filtered_effects, design_matrix=design_matrix)
213+
# Only fit model if any non-FEMA contrasts at this level
214+
if any(c[2] != 'FEMA' for c in contrasts):
215+
if len(filtered_effects) < 2:
216+
raise RuntimeError(
217+
"At least two inputs are required for a 't' for 'F' "
218+
"second level contrast")
219+
model = level2.SecondLevelModel(smoothing_fwhm=smoothing_fwhm)
220+
model.fit(filtered_effects, design_matrix=mat)
212221

213-
for name, weights, contrast_type in prepare_contrasts(
214-
self.inputs.contrast_info, design_matrix.columns.to_list()):
222+
for name, weights, contrast_type in contrasts:
215223
contrast_metadata.append(
216224
{'contrast': name,
217225
'stat': contrast_type,
218226
**out_ents})
219227

220-
maps = model.compute_contrast(
221-
second_level_contrast=weights,
222-
second_level_stat_type=contrast_type,
223-
output_type='all')
228+
# Pass-through happens automatically as it can handle 1 input
229+
if contrast_type == 'FEMA':
230+
# Filter effects and variances based on weights
231+
ix = weights[0].astype(bool)
232+
233+
ffx_res = compute_fixed_effects(
234+
np.array(filtered_effects)[ix],
235+
np.array(filtered_variances)[ix]
236+
)
237+
238+
maps = {
239+
'effect_size': ffx_res[0],
240+
'effect_variance': ffx_res[1],
241+
'stat': ffx_res[2]
242+
}
243+
else:
244+
maps = model.compute_contrast(
245+
second_level_contrast=weights,
246+
second_level_stat_type=contrast_type,
247+
output_type='all'
248+
)
224249

225250
for map_type, map_list in (('effect_size', effect_maps),
226251
('effect_variance', variance_maps),
227252
('z_score', zscore_maps),
228253
('p_value', pvalue_maps),
229254
('stat', stat_maps)):
230-
fname = fname_fmt(name, map_type)
231-
maps[map_type].to_filename(fname)
232-
map_list.append(fname)
255+
if map_type in maps:
256+
fname = fname_fmt(name, map_type)
257+
maps[map_type].to_filename(fname)
258+
map_list.append(fname)
233259

234260
self._results['effect_maps'] = effect_maps
235261
self._results['variance_maps'] = variance_maps
236262
self._results['stat_maps'] = stat_maps
237-
self._results['zscore_maps'] = zscore_maps
238-
self._results['pvalue_maps'] = pvalue_maps
239263
self._results['contrast_metadata'] = contrast_metadata
240264

265+
# These are "optional" as fixed effects do not support these
266+
if zscore_maps:
267+
self._results['zscore_maps'] = zscore_maps
268+
if pvalue_maps:
269+
self._results['pvalue_maps'] = pvalue_maps
270+
241271
return runtime

fitlins/interfaces/utils.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ def _list_outputs(self):
3232
outputs = self._outputs().get()
3333
for key in self._fields:
3434
val = getattr(self.inputs, key)
35-
if self._check_lengths is True:
36-
self._calculate_length(val)
37-
outputs[key] = [elem for sublist in val for elem in sublist]
35+
# Allow for empty inputs
36+
if isdefined(val):
37+
if self._check_lengths is True:
38+
self._calculate_length(val)
39+
outputs[key] = [elem for sublist in val for elem in sublist]
3840
self._lengths = None
3941

4042
return outputs
@@ -72,12 +74,14 @@ def _run_interface(self, runtime):
7274
self._results.update({'metadata': [], 'out': []})
7375
for key in self._fields:
7476
val = getattr(self.inputs, key)
75-
if len(val) != n:
76-
raise ValueError(f"List lengths must match metadata. Failing list: {key}")
77-
for md, obj in zip(orig_metadata, val):
78-
metadata = md.copy()
79-
metadata.update(md_map.get(key, {}))
80-
self._results['metadata'].append(metadata)
81-
self._results['out'].append(obj)
77+
# Allow for missing values
78+
if isdefined(val):
79+
if len(val) != n:
80+
raise ValueError(f"List lengths must match metadata. Failing list: {key}")
81+
for md, obj in zip(orig_metadata, val):
82+
metadata = md.copy()
83+
metadata.update(md_map.get(key, {}))
84+
self._results['metadata'].append(metadata)
85+
self._results['out'].append(obj)
8286

8387
return runtime

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
nistats @ git+https://github.com/nistats/nistats.git@b69e127020df05aa8ba540f9a8d799e1dad3d602
1+
nistats @ git+https://github.com/nistats/nistats.git@e88a5f

setup.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ install_requires =
2828
nilearn>=0.4
2929
pandas>=0.19
3030
tables>=3.2.1
31-
nistats>=0.0.1b0
32-
pybids @ git+https://github.com/bids-standard/pybids.git@24d2032020fd407e5bcbbbf20943726f48717574
31+
nistats@git+https://github.com/nistats/nistats.git@e88a5f
32+
pybids>=0.10.0
3333
jinja2
3434

3535
[options.extras_require]

0 commit comments

Comments
 (0)