Save GLM to BIDS Troubleshoot and Post-Report Contrast Edits

Hi all,

First-time poster, not-so-long-time programmer. I have two questions related to first-level GLM construction with Nilearn.

  1. Upon successful generation of a GLM via make_glm_report , I am looking to use save_glm_to_bids to save the report in the BIDS structure. However, each time I run the code, I receive the following error message: TypeError: Object of type ndarray is not JSON serializable. Has anyone else experienced such a problem?
save_glm_to_bids(DM_glm, contrast_list, contrast_types = 't', out_dir = GLM_dir, prefix = f'{sub}_task-DM')
  1. Upon generation of a GLM , is there a possible way to add contrasts later on without having to modify the Python script? Does the Nilearn toolbox provide something which could be run directly through the command line to edit contrasts perhaps?

Thanks in advance! All the best!

@jacobtheneuron welcome to neurostars and thanks for your question

to make it easier for others to help you, can you give us more info about what you did and the error you got

for context it is better to give longer chunks of code that just the line that failed: ideally you want to give a code snippet of code that almost anyone can run on their computer to reproduce your problem

at least for your case, enough so we know how each variable was created

from nilearn.interfaces.bids import save_glm_to_bids

DM_glm = ???
contrast_list = ???
GLM_dir = ???
sub = ???

save_glm_to_bids(DM_glm, contrast_list,
                              contrast_types = 't', 
                              out_dir = GLM_dir,
                              prefix = f'{sub}_task-DM')

Also do not hesitate to copy paste the entire error traceback rather than just the final error.

No there is no command line interface for nilearn: you will have to edit some python code for that.

Thanks for the response! I will try and provide some more hopefully relevant info:

DM_glm

GLM_dir = os.path.join(source_dir, 'GLM', sub)
DM_glm = FirstLevelModel(t_r = tr, slice_time_ref = 0, noise_model = 'ar1', standardize = False, 
                      hrf_model = 'spm', drift_model = None, high_pass = 0.01,
                      target_affine=func_img_dm.affine, 
                      mask_img = resampled_brainmask,
                      subject_label = sub, memory_level = 12,
                      minimize_memory = True, verbose = 2, n_jobs = -13,
                      memory = os.path.join(GLM_dir, 'cache'))
DM_glm = DM_glm.fit(run_imgs = smooth_img_path_dm, design_matrices = X) 

contrast_list

# DM contrast design 
  # Easy Decision (VD1, VD2 < VD3, VD4) (3 * VD1 < VD2, VD3, VD4)
  # Difficult Decision (VD1, VD2 > VD3, VD4) (3 * VD1 > VD2, VD3, VD4)
contrast_list = ['- VD1 - VD2 + VD3 + VD4', '(3 * - VD1) + VD2 + VD3 + VD4', 
                 'VD1 + VD2 - VD3 - VD4', '(3 * VD1) - VD2 - VD3 - VD4']

GLM_dir is a directory

sub is the subject naming scheme

The error message in its entirety looks something like this:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[110], line 4
      1 print('--------saving BIDS files--------')
      3 # Save GLM to BIDS 
----> 4 save_glm_to_bids(SnackDM_glm, contrast_list, contrast_types = 't', out_dir = GLM_dir, prefix = f'{sub}_task-SnackDM')

File ~/miniconda3/envs/SNACK/lib/python3.11/site-packages/nilearn/interfaces/bids/glm.py:325, in save_glm_to_bids(model, contrasts, contrast_types, out_dir, prefix)
    322 # Model metadata
    323 # TODO: Determine optimal mapping of model metadata to BIDS fields.
    324 metadata_file = os.path.join(out_dir, f"{prefix}statmap.json")
--> 325 _generate_model_metadata(metadata_file, model)
    327 dset_desc_file = os.path.join(out_dir, "dataset_description.json")
    328 _generate_dataset_description(dset_desc_file, model_level)

File ~/miniconda3/envs/SNACK/lib/python3.11/site-packages/nilearn/interfaces/bids/glm.py:124, in _generate_model_metadata(out_file, model)
    117 model_metadata = {
    118     "Description": "A statistical map generated by Nilearn.",
    119     **data_attributes,
    120     "ModelParameters": model_attributes,
    121 }
    123 with open(out_file, "w") as f_obj:
--> 124     json.dump(model_metadata, f_obj, indent=4, sort_keys=True)

File ~/miniconda3/lib/python3.11/json/__init__.py:179, in dump(obj, fp, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, default, sort_keys, **kw)
    173     iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii,
    174         check_circular=check_circular, allow_nan=allow_nan, indent=indent,
    175         separators=separators,
    176         default=default, sort_keys=sort_keys, **kw).iterencode(obj)
    177 # could accelerate with writelines in some versions of Python, at
    178 # a debuggability cost
--> 179 for chunk in iterable:
    180     fp.write(chunk)

File ~/miniconda3/lib/python3.11/json/encoder.py:432, in _make_iterencode.<locals>._iterencode(o, _current_indent_level)
    430     yield from _iterencode_list(o, _current_indent_level)
    431 elif isinstance(o, dict):
--> 432     yield from _iterencode_dict(o, _current_indent_level)
    433 else:
    434     if markers is not None:

File ~/miniconda3/lib/python3.11/json/encoder.py:406, in _make_iterencode.<locals>._iterencode_dict(dct, _current_indent_level)
    404         else:
    405             chunks = _iterencode(value, _current_indent_level)
--> 406         yield from chunks
    407 if newline_indent is not None:
    408     _current_indent_level -= 1

File ~/miniconda3/lib/python3.11/json/encoder.py:406, in _make_iterencode.<locals>._iterencode_dict(dct, _current_indent_level)
    404         else:
    405             chunks = _iterencode(value, _current_indent_level)
--> 406         yield from chunks
    407 if newline_indent is not None:
    408     _current_indent_level -= 1

File ~/miniconda3/lib/python3.11/json/encoder.py:439, in _make_iterencode.<locals>._iterencode(o, _current_indent_level)
    437         raise ValueError("Circular reference detected")
    438     markers[markerid] = o
--> 439 o = _default(o)
    440 yield from _iterencode(o, _current_indent_level)
    441 if markers is not None:

File ~/miniconda3/lib/python3.11/json/encoder.py:180, in JSONEncoder.default(self, o)
    161 def default(self, o):
    162     """Implement this method in a subclass such that it returns
    163     a serializable object for ``o``, or calls the base implementation
    164     (to raise a ``TypeError``).
   (...)
    178 
    179     """
--> 180     raise TypeError(f'Object of type {o.__class__.__name__} '
    181                     f'is not JSON serializable')

TypeError: Object of type ndarray is not JSON serializable

Hope this helps! Thanks again!

So we have found the solution to Problem 1 in which there appears to be a problem when specifying .affine rather than leaving as default (None)! Has something like this come up before?

As for the second question, I didn’t understand what I was asking (apologies). My real second question is: if I wish to add contrasts later, is there a way to skip the re-fitting of a GLM to each subject? Or must the entire script be run again?

Thanks again and all the best!

I can get the same error with this code adapted from one of our tests.

In your code can I ask what is func_img_dm supposed to be.

target_affine=func_img_dm.affine

Original test

Adapted as a minimal example to reproduce

import numpy as np
from pathlib import Path

from nilearn._utils.data_gen import (
    generate_fake_fmri_data_and_design,
)
from nilearn.glm.first_level import FirstLevelModel
from nilearn.interfaces.bids import (
    save_glm_to_bids,
)

shapes, rk = [(7, 8, 9, 15)], 3
mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
    shapes,
    rk,
)

# To try different values
# target_affine = None
# target_affine = fmri_data[0].affine
target_affine = mask.affine

single_run_model = FirstLevelModel(
    mask_img=None,
    minimize_memory=False,
    target_affine=target_affine
).fit(
    fmri_data[0],
    design_matrices=design_matrices[0],
)

contrasts = {
    "effects of interest": np.eye(rk),
}
contrast_types = {
    "effects of interest": "F",
}
save_glm_to_bids(
    model=single_run_model,
    contrasts=contrasts,
    contrast_types=contrast_types,
    out_dir=Path.cwd() / "tmp",
    prefix="sub-01_ses-01_task-nback",
)

I don’t think we have an easy way around this at the moment.

It relates to an old issue we have where we say that BIDS could help with avoiding this kind of problem but we never got around to implement it (yet).

OK save_glm_to_bids will try to save all the parameters of the model to json but our code cannot does not convert numpy arrays (like the affine) into something more JSON friendly hence the error.

So defo a bug.

opened an issue

this is the bug fix:

hopefully it will be in the next nilearn release

For some additional info, func_dm_img is a smoothed 4D image. Maybe the affine of a 3D (or mean) smoothed image would work better? Regardless thanks again for the assistance!

1 Like

OK we just merged the bug fix so hopefully in the next release, this should not happen anymore.

1 Like