How to add new bundles into pyAFQ (Acoustic Radiations Example)#

pyAFQ is designed to be customizable and extensible. This example shows how you can customize it to define a new bundle based on a definition of waypoint and endpoint ROIs of your design. In this case, we add the acoustic radiations.

We start by importing some of the components that we need for this example and fixing the random seed for reproducibility

import os.path as op
import plotly
import numpy as np

from AFQ.api.group import GroupAFQ
import AFQ.api.bundle_dict as abd
import AFQ.data.fetch as afd
from AFQ.definitions.image import ImageFile, RoiImage
np.random.seed(1234)
2026-05-19 00:54:37,639	INFO util.py:154 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output.

Get dMRI data#

We will analyze one subject from the Healthy Brain Network Processed Open Diffusion Derivatives dataset (HBN-POD2) [1], [2]. We’ll use a fetcher to get preprocessed dMRI data for one of the >2,000 subjects in that study. The data gets organized into a BIDS-compatible format in the ~/AFQ_data/HBN folder:

study_dir = afd.fetch_hbn_preproc(["NDARAA948VFH"])[1]

Define custom BundleDict object#

The BundleDict object holds information about “include” and “exclude” ROIs, as well as endpoint ROIS, and whether the bundle crosses the midline. In this case, the ROIs are all defined in the MNI template space that is used as the default template space in pyAFQ, but, in principle, other template spaces could be used.

The ROIs for the case can be downloaded using a custom fetcher which saves the ROIs to a folder and creates a dictionary of paths to the ROIs:

ar_rois = afd.read_ar_templates()

bundles = abd.BundleDict({
    "Left Acoustic Radiation": {
        "start": ar_rois["AAL_Thal_L"],
        "end": ar_rois["AAL_TempSup_L"],
        "cross_midline": False,
    },
    "Right Acoustic Radiation": {
        "start": ar_rois["AAL_Thal_R"],
        "end": ar_rois["AAL_TempSup_R"],
        "cross_midline": False
    }
})
  0%|          | 0/16 MB [00:00]
 12%|█▎        | 2/16 MB [00:00]
 31%|███▏      | 5/16 MB [00:00]
 62%|██████▎   | 10/16 MB [00:00]
 94%|█████████▍| 15/16 MB [00:00]
100%|██████████| 16/16 MB [00:00]

  0%|          | 0/16 MB [00:00]
 12%|█▎        | 2/16 MB [00:00]
 25%|██▌       | 4/16 MB [00:00]
 44%|████▍     | 7/16 MB [00:00]
 62%|██████▎   | 10/16 MB [00:00]
 81%|████████▏ | 13/16 MB [00:00]
100%|██████████| 16/16 MB [00:00]

  0%|          | 0/16 MB [00:00]
 12%|█▎        | 2/16 MB [00:00]
 31%|███▏      | 5/16 MB [00:00]
 62%|██████▎   | 10/16 MB [00:00]
 88%|████████▊ | 14/16 MB [00:00]
100%|██████████| 16/16 MB [00:00]

  0%|          | 0/16 MB [00:00]
 12%|█▎        | 2/16 MB [00:00]
 25%|██▌       | 4/16 MB [00:00]
 38%|███▊      | 6/16 MB [00:00]
 62%|██████▎   | 10/16 MB [00:00]
 94%|█████████▍| 15/16 MB [00:00]
100%|██████████| 16/16 MB [00:00]

Define GroupAFQ object#

HBN POD2 have been processed with qsiprep [3]_. This means that a brain mask has already been computer for them. As you can see in other examples, these data also have a mapping calculated for them, which can also be incorporated into processing. However, in this case, we will let pyAFQ calculate its own SyN-based mapping so that the combine_bundle method can be used below to create a montage visualization.

For tractography, we use CSD-based probabilistic tractography seeding extensively (n_seeds=4 means 81 seeds per voxel!), but only within the ROIs and not throughout the white matter. This is controlled by passing "seed_mask": RoiImage() in the tracking_params dict. The custom bundles are passed as bundle_info=bundles. The call to my_afq.export_all() initiates the pipeline.

my_afq = GroupAFQ(
    bids_path=study_dir,
    dwi_preproc_pipeline="qsiprep",
    participant_labels=["NDARAA948VFH"],
    output_dir=op.join(study_dir, "derivatives", "afq_ar"),
    tracking_params={"n_seeds": 4,
                     "directions": "prob",
                     "odf_model": "CSD",
                     "seed_mask": RoiImage(use_endpoints=True)},
    bundle_info=bundles)

my_afq.export_all()
INFO:bidsschematools:No schema path specified, defaulting to the bundled schema, `/opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/bidsschematools/data/schema.json`.
INFO:AFQ:Using the following files for subject NDARAA948VFH and session HBNsiteRU:
INFO:AFQ:  DWI: /home/runner/AFQ_data/HBN/derivatives/qsiprep/sub-NDARAA948VFH/ses-HBNsiteRU/dwi/sub-NDARAA948VFH_ses-HBNsiteRU_acq-64dir_space-T1w_desc-preproc_dwi.nii.gz
INFO:AFQ:  BVAL: /home/runner/AFQ_data/HBN/derivatives/qsiprep/sub-NDARAA948VFH/ses-HBNsiteRU/dwi/sub-NDARAA948VFH_ses-HBNsiteRU_acq-64dir_space-T1w_desc-preproc_dwi.bval
INFO:AFQ:  BVEC: /home/runner/AFQ_data/HBN/derivatives/qsiprep/sub-NDARAA948VFH/ses-HBNsiteRU/dwi/sub-NDARAA948VFH_ses-HBNsiteRU_acq-64dir_space-T1w_desc-preproc_dwi.bvec
INFO:AFQ:  T1: /home/runner/AFQ_data/HBN/derivatives/qsiprep/sub-NDARAA948VFH/anat/sub-NDARAA948VFH_desc-preproc_T1w.nii.gz
WARNING:AFQ:It is recommended to provide CSF/GM/WM segmentations using PVEImage or PVEImages in AFQ.definitions.image. Otherwise, SynthSeg2 will be used
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[4], line 1
----> 1 my_afq = GroupAFQ(
      2     bids_path=study_dir,
      3     dwi_preproc_pipeline="qsiprep",
      4     participant_labels=["NDARAA948VFH"],

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/AFQ/api/group.py:432, in GroupAFQ.__init__(self, bids_path, bids_filters, dwi_preproc_pipeline, t1_preproc_pipeline, participant_labels, output_dir, parallel_params, bids_layout_kwargs, logging_level, **kwargs)
    422 self.valid_ses_list.append(str(session))
    424 this_pAFQ_inputs = _ParticipantAFQInputs(
    425     dwi_data_file,
    426     bval_file,
   (...)    430     this_kwargs,
    431 )
--> 432 this_pAFQ = ParticipantAFQ(
    433     this_pAFQ_inputs.dwi_data_file,
    434     this_pAFQ_inputs.bval_file,
    435     this_pAFQ_inputs.bvec_file,
    436     this_pAFQ_inputs.t1_file,
    437     this_pAFQ_inputs.results_dir,
    438     **this_pAFQ_inputs.kwargs,
    439 )
    440 self.plans_dict[subject][str(session)] = this_pAFQ.plans_dict
    441 self.pAFQ_list.append(this_pAFQ)

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/AFQ/api/participant.py:129, in ParticipantAFQ.__init__(self, dwi_data_file, bval_file, bvec_file, t1_file, output_dir, logging_level, **kwargs)
    117 self.og_kwargs = kwargs.copy()
    119 self.kwargs = dict(
    120     dwi_data_file=dwi_data_file,
    121     bval_file=bval_file,
   (...)    127     **kwargs,
    128 )
--> 129 self.make_workflow()

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/AFQ/api/participant.py:151, in ParticipantAFQ.make_workflow(self)
    136     plans = {  # if using SLR map, do tractography first
    137         "structural": get_structural_plan(self.kwargs),
    138         "data": get_data_plan(self.kwargs),
   (...)    143         "viz": get_viz_plan(self.kwargs),
    144     }
    145 else:
    146     plans = {  # Otherwise, do mapping first
    147         "structural": get_structural_plan(self.kwargs),
    148         "data": get_data_plan(self.kwargs),
    149         "tissue": get_tissue_plan(self.kwargs),
    150         "mapping": get_mapping_plan(self.kwargs),
--> 151         "tractography": get_tractography_plan(self.kwargs),
    152         "segmentation": get_segmentation_plan(self.kwargs),
    153         "viz": get_viz_plan(self.kwargs),
    154     }
    156 # Fill in defaults not already set
    157 for _, kwargs_in_section in kwargs_descriptors.items():

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/AFQ/tasks/tractography.py:246, in get_tractography_plan(kwargs)
    240 n_seeds = kwargs["tracking_params"]["n_seeds"]
    241 if (
    242     kwargs["tracking_params"]["random_seeds"]
    243     and isinstance(n_seeds, int)
    244     and n_seeds <= 20
    245 ):
--> 246     raise ValueError(
    247         "Using random seeds with a low number of seeds is not recommended."
    248         " Please increase n_seeds or set random_seeds to False."
    249         " A recommended number of seeds when using random seeds is 1e7."
    250     )
    252 return immlib.plan(**tractography_tasks)

ValueError: Using random seeds with a low number of seeds is not recommended. Please increase n_seeds or set random_seeds to False. A recommended number of seeds when using random seeds is 1e7.

Interactive bundle visualization#

Another way to examine the outputs is to export the individual bundle figures, which show the streamlines, as well as the ROIs used to define the bundle. This is an html file, which contains an interactive figure that can be navigated, zoomed, rotated, etc.

bundle_html = my_afq.export("indiv_bundles_figures")
plotly.io.show(bundle_html["NDARAA948VFH"]["Left Acoustic Radiation"])

References#

.. [1] Alexander LM, Escalera J, Ai L, et al. An open resource for transdiagnostic research in pediatric mental health and learning disorders. Sci Data. 2017;4:170181.

.. [2] Richie-Halford A, Cieslak M, Ai L, et al. An analysis-ready and quality controlled resource for pediatric brain white-matter research. Scientific Data. 2022;9(1):1-27.

.. [3] Cieslak M, Cook PA, He X, et al. QSIPrep: an integrative platform for preprocessing and reconstructing diffusion MRI data. Nat Methods. 2021;18(7):775-778.