Running pyAFQ 2.x defaults in pyAFQ 3.x#

from AFQ.api.group import GroupAFQ
import AFQ.data.fetch as afd
import AFQ.definitions.image as afm
import AFQ.api.bundle_dict as abd

import os.path as op

afd.organize_stanford_data()
2026-05-19 01:01:04,307	INFO util.py:154 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output.

Tractography parameters in the old way#

In pyAFQ 2.x, we used CSD with no asymmetric filtering, and seeded streamlines throughout the white matter instead of on the white matter / gray matter interface.

tracking_params = dict(
    odf_model="csd",
    n_seeds=1,
    random_seeds=False,
    minlen=50,
    directions="prob",
    seed_mask=afm.ScalarImage("dti_fa"),
    seed_threshold=0.2
)

Partial Volume Estimate in the old way#

In pyAFQ 2.x, we did not use PVE and instead thresholded on fractional anisotropy (FA) maps to create seed and stopping masks. Here, we recreate the PVE images using the FA maps. Note there the CSF map is not used in this case. Additionally, in pyAFQ 2.x, the brain mask was calculated using median OTSU. Here, we import it from the Freesurfer segmentation instead.

pve = afm.PVEImages(
    afm.ThresholdedScalarImage(
        "dti_fa",
        upper_bound=0.0),
    afm.ThresholdedScalarImage(
        "dti_fa",
        upper_bound=0.2),
    afm.ThresholdedScalarImage(
        "dti_fa",
        lower_bound=0.2))

bm_def = afm.LabelledImageFile(
    suffix="seg", filters={"scope": "freesurfer"},
    exclusive_labels=[0])

VOF / pAF / CST in the old way#

In pyAFQ 2.x, the vertical occipital fasciculus (VOF) and posterior arcuate fasciculus (pAF) were defined differently. The pAF in 3.0 has an increased restriction that it cannot overlap with the arcuate by more than 30%. The VOF has several changes:

  1. one endpoint ROI instead of both, but there is a minimum length requirement to of 25mm to compensate;

  2. The allowed overlap with the pAF has been reduced;

  3. it must be lateral to the inferior fronto-occipital fasciculus instead of the inferior longitudinal fasciculus;

  4. cleaning has been changed: there is now mahalanobis cleaning on orientation, and isolation forest cleaning instead of mahalanobis for distance. Additionally, in the new version, the inferior endpoints of the corticospinal tracts (CST) were removed, and the superior longitudinal fasciculus (SLF) was broken into three sub-bundles.

templates = afd.read_templates(as_img=False)
old_vof_paf_cst_slf_definitions = abd.BundleDict({
        'Left Corticospinal': {
            'cross_midline': False,
            'include': [templates['CST_roi2_L'],
                        templates['CST_roi1_L']],
            'exclude': [],
            'space': 'template',
            'prob_map': templates['CST_L_prob_map'],
            'end': templates['CST_L_start'],
            'start': templates['CST_L_end']},
        'Right Corticospinal': {
            'cross_midline': False,
            'include': [templates['CST_roi2_R'],
                        templates['CST_roi1_R']],
            'exclude': [],
            'space': 'template',
            'prob_map': templates['CST_R_prob_map'],
            'end': templates['CST_R_start'],
            'start': templates['CST_R_end']},
        "Left Superior Longitudinal": {
            "cross_midline": False,
            "include": [templates["SLF_roi1_L"], templates["SLF_roi2_L"]],
            "exclude": [templates["SLFt_roi2_L"]],
            "space": "template",
            "prob_map": templates["SLF_L_prob_map"],
            "start": templates["SLF_L_start"],
            "end": templates["SLF_L_end"]},
        "Right Superior Longitudinal": {
            "cross_midline": False,
            "include": [templates["SLF_roi1_R"], templates["SLF_roi2_R"]],
            "exclude": [templates["SLFt_roi2_R"]],
            "space": "template",
            "prob_map": templates["SLF_R_prob_map"],
            "start": templates["SLF_R_start"],
            "end": templates["SLF_R_end"]},
        'Left Posterior Arcuate': {'cross_midline': False,
                                   'include': [templates['SLFt_roi2_L']],
                                   'exclude': [templates['SLF_roi1_L']],
                                   'space': 'template',
                                   'start': templates['pARC_L_start'],
                                   'primary_axis': 'I/S',
                                   'primary_axis_percentage': 40},
        'Right Posterior Arcuate': {'cross_midline': False,
                                    'include': [templates['SLFt_roi2_R']],
                                    'exclude': [templates['SLF_roi1_R']],
                                    'space': 'template',
                                    'start': templates['pARC_R_start'],
                                    'primary_axis': 'I/S',
                                    'primary_axis_percentage': 40},
        'Left Vertical Occipital': {'cross_midline': False,
                                    'space': 'template',
                                    'start': templates['VOF_L_start'],
                                    'end': templates['VOF_L_end'],
                                    'inc_addtol': [4, 0],
                                    'Left Arcuate': {
                                        'node_thresh': 20},
                                    'Left Posterior Arcuate': {
                                        'node_thresh': 1,
                                        'core': 'Anterior'},
                                    'Left Inferior Longitudinal': {
                                        'core': 'Right'},
                                    'primary_axis': 'I/S',
                                    'primary_axis_percentage': 40},
        'Right Vertical Occipital': {'cross_midline': False,
                                     'space': 'template',
                                     'start': templates['VOF_R_start'],
                                     'end': templates['VOF_R_end'],
                                     'inc_addtol': [4, 0],
                                     'Right Arcuate': {
                                         'node_thresh': 20},
                                     'Right Posterior Arcuate': {
                                         'node_thresh': 1,
                                         'core': 'Anterior'},
                                     'Right Inferior Longitudinal': {
                                         'core': 'Left'},
                                     'primary_axis': 'I/S',
                                     'primary_axis_percentage': 40}})

Callosal bundles in the old way#

In pyAFQ 2.x, the callosal bundles were cleaned using mahalnobis instead of isolation forest.

callosal_templates =\
    afd.read_callosum_templates(as_img=False)
callosal_bd = abd.BundleDict({
    'Callosum Anterior Frontal': {
        'cross_midline': True,
        'include': [callosal_templates['R_AntFrontal'],
                    callosal_templates['Callosum_midsag'],
                    callosal_templates['L_AntFrontal']],
        'exclude': [],
        'space': 'template'},
    'Callosum Motor': {
        'cross_midline': True,
        'include': [callosal_templates['R_Motor'],
                    callosal_templates['Callosum_midsag'],
                    callosal_templates['L_Motor']],
        'exclude': [],
        'space': 'template'},
    'Callosum Occipital': {
        'cross_midline': True,
        'include': [callosal_templates['R_Occipital'],
                    callosal_templates['Callosum_midsag'],
                    callosal_templates['L_Occipital']],
        'exclude': [],
        'space': 'template'},
    'Callosum Orbital': {
        'cross_midline': True,
        'include': [callosal_templates['R_Orbital'],
                    callosal_templates['Callosum_midsag'],
                    callosal_templates['L_Orbital']],
        'exclude': [],
        'space': 'template'},
    'Callosum Posterior Parietal': {
        'cross_midline': True,
        'include': [callosal_templates['R_PostParietal'],
                    callosal_templates['Callosum_midsag'],
                    callosal_templates['L_PostParietal']],
        'exclude': [],
        'space': 'template'},
    'Callosum Superior Frontal': {
        'cross_midline': True,
        'include': [callosal_templates['R_SupFrontal'],
                    callosal_templates['Callosum_midsag'],
                    callosal_templates['L_SupFrontal']],
        'exclude': [],
        'space': 'template'},
    'Callosum Superior Parietal': {
        'cross_midline': True,
        'include': [callosal_templates['R_SupParietal'],
                    callosal_templates['Callosum_midsag'],
                    callosal_templates['L_SupParietal']],
        'exclude': [],
        'space': 'template'},
    'Callosum Temporal': {
        'cross_midline': True,
        'include': [callosal_templates['R_Temporal'],
                    callosal_templates['Callosum_midsag'],
                    callosal_templates['L_Temporal']],
        'exclude': [],
        'space': 'template'}})


bundle_info = abd.default_bd() + \
    old_vof_paf_cst_slf_definitions + \
    callosal_bd

Run GroupAFQ with these parameters#

Finally, we can run GroupAFQ with the 2.0 parameters. In sum, we changed: Tractography parameters to use CSD and seed throughout the white matter; PVE images to use FA thresholding; Bundle definitions for VOF, pAF, and CST to use the old definitions; Callosal bundles to use mahalanobis cleaning.

myafq = GroupAFQ(
    bids_path=op.join(afd.afq_home, 'stanford_hardi'),
    dwi_preproc_pipeline='vistasoft',
    t1_preproc_pipeline='freesurfer',
    tracking_params=tracking_params,
    brain_mask_definition=bm_def,
    pve=pve,
    bundle_info=bundle_info)

myafq.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 01 and session 01:
INFO:AFQ:  DWI: /home/runner/AFQ_data/stanford_hardi/derivatives/vistasoft/sub-01/ses-01/dwi/sub-01_ses-01_dwi.nii.gz
INFO:AFQ:  BVAL: /home/runner/AFQ_data/stanford_hardi/derivatives/vistasoft/sub-01/ses-01/dwi/sub-01_ses-01_dwi.bval
INFO:AFQ:  BVEC: /home/runner/AFQ_data/stanford_hardi/derivatives/vistasoft/sub-01/ses-01/dwi/sub-01_ses-01_dwi.bvec
INFO:AFQ:  T1: /home/runner/AFQ_data/stanford_hardi/derivatives/freesurfer/sub-01/ses-01/anat/sub-01_ses-01_T1w.nii.gz
INFO:AFQ:Calculating _desc-brain_mask.nii.gz...
INFO:AFQ:Calculating _desc-T1w_mask.nii.gz...
INFO:AFQ:_desc-T1w_mask.nii.gz completed. Saving to /home/runner/AFQ_data/stanford_hardi/derivatives/afq/sub-01/ses-01/dwi/sub-01_ses-01_desc-T1w_mask.nii.gz
INFO:AFQ:_desc-brain_mask.nii.gz completed. Saving to /home/runner/AFQ_data/stanford_hardi/derivatives/afq/sub-01/ses-01/dwi/sub-01_ses-01_desc-brain_mask.nii.gz
INFO:AFQ:Calculating _desc-masked_T1w.nii.gz...
INFO:AFQ:_desc-masked_T1w.nii.gz completed. Saving to /home/runner/AFQ_data/stanford_hardi/derivatives/afq/sub-01/ses-01/dwi/sub-01_ses-01_desc-masked_T1w.nii.gz
INFO:AFQ:Calculating affine pre-alignment...
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[6], line 10
      6     brain_mask_definition=bm_def,
      7     pve=pve,
      8     bundle_info=bundle_info)
      9 
---> 10 myafq.export_all()

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/AFQ/api/group.py:720, in GroupAFQ.export_all(self, viz, afqbrowser, xforms, indiv)
    694 """Exports all the possible outputs
    695 
    696 Parameters
   (...)    716     Default: True
    717 """
    718 start_time = time()
--> 720 export_all_helper(self, xforms, indiv, viz)
    722 self.combine_profiles()
    723 if afqbrowser:

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/AFQ/api/utils.py:160, in export_all_helper(api_afq_object, xforms, indiv, viz)
    158 if xforms:
    159     try:
--> 160         api_afq_object.export("b0_warped")
    161     except Exception as e:
    162         api_afq_object.logger.warning(
    163             (
    164                 "Failed to export warped b0. This could be because your "
   (...)    167             )
    168         )

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/AFQ/api/group.py:647, in GroupAFQ.export(self, attr_name, collapse)
    645         to_calc_list.append((subject, session))
    646     else:
--> 647         results[subject][session] = val_from_plan(plans_dict, attr_name)
    649 # if some need to be calculated, do those in parallel
    650 if to_calc_list:

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/AFQ/api/utils.py:146, in val_from_plan(plan, attr_name)
    144 def val_from_plan(plan, attr_name):
    145     try:
--> 146         return plan[attr_name]
    147     except Exception as err:
    148         current_err = err

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/pcollections/_lazy.py:381, in ldict.__getitem__(self, key)
    379 v = pdict.__getitem__(self, key)
    380 if isinstance(v, lazy):
--> 381     v = v()
    382 return v

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/pcollections/_lazy.py:143, in lazy.__call__(self)
    141     val = self.value
    142 else:
--> 143     val = part()
    144     # We've successfully calculated the value; set the members
    145     # appropriately.
    146     object.__setattr__(self, 'value', val)

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/immlib/workflow/_core.py:855, in plan._source_lookup(inputtup, calctup, src)
    853     (cidx, oidx) = src
    854     lazycalc = calctup[cidx]
--> 855     val = lazycalc()[oidx]
    856 else:
    857     val = inputtup[src]()

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/pcollections/_lazy.py:143, in lazy.__call__(self)
    141     val = self.value
    142 else:
--> 143     val = part()
    144     # We've successfully calculated the value; set the members
    145     # appropriately.
    146     object.__setattr__(self, 'value', val)

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/immlib/workflow/_core.py:884, in plan._make_calctup.<locals>.<lambda>(c, args)
    881 # We take advantage of Python's weak closures here:
    882 calctup = ()
    883 calctup = tuple(
--> 884     lazy(lambda c,args: f(inputtup, calctup, c, args), c, args)
    885     for (c,args) in zip(calcdata.calcs, calcdata.args))
    886 return calctup

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/immlib/workflow/_core.py:868, in plan._call_calc(inputtup, calctup, c, args)
    866 kwargs = {}
    867 c = to_calc(c)
--> 868 for (p,arg) in zip(c.signature.parameters.values(), argvals):
    869     if p.kind == p.POSITIONAL_ONLY:
    870         args.append[arg]

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/immlib/workflow/_core.py:855, in plan._source_lookup(inputtup, calctup, src)
    853     (cidx, oidx) = src
    854     lazycalc = calctup[cidx]
--> 855     val = lazycalc()[oidx]
    856 else:
    857     val = inputtup[src]()

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/pcollections/_lazy.py:143, in lazy.__call__(self)
    141     val = self.value
    142 else:
--> 143     val = part()
    144     # We've successfully calculated the value; set the members
    145     # appropriately.
    146     object.__setattr__(self, 'value', val)

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/immlib/workflow/_core.py:884, in plan._make_calctup.<locals>.<lambda>(c, args)
    881 # We take advantage of Python's weak closures here:
    882 calctup = ()
    883 calctup = tuple(
--> 884     lazy(lambda c,args: f(inputtup, calctup, c, args), c, args)
    885     for (c,args) in zip(calcdata.calcs, calcdata.args))
    886 return calctup

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/immlib/workflow/_core.py:873, in plan._call_calc(inputtup, calctup, c, args)
    871     else:
    872         kwargs[p.name] = arg
--> 873 r = c.eager_call(*args, **kwargs)
    874 if is_amap(r):
    875     return tuple(map(r.__getitem__, c.outputs))

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/immlib/workflow/_core.py:448, in calc.eager_call(self, *args, **kwargs)
    432 """Eagerly calls the given calculation using the arguments.
    433 
    434 ``c.eager_call(...)`` returns the result of calling the calculation
   (...)    444 calc.eager_mapcall, calc.lazy_call, calc.lazy_mapcall
    445 """
    446 # Now we just pass these arguments along (the function itself has been
    447 # given the caching code via decorators already).
--> 448 res = self.function(*args, **kwargs)
    449 # Now interpret the result.
    450 outs = self.outputs

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/AFQ/tasks/mapping.py:132, in mapping(base_fname, dwi_data_file, reg_subject, data_imap, mapping_definition)
    128 if not isinstance(mapping_definition, Definition):
    129     raise TypeError(
    130         "mapping must be a mapping defined" + " in `AFQ.definitions.mapping`"
    131     )
--> 132 return mapping_definition.get_for_subses(
    133     base_fname,
    134     data_imap["dwi"],
    135     dwi_data_file,
    136     reg_subject,
    137     reg_template,
    138     tmpl_name,
    139 )

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/AFQ/definitions/mapping.py:339, in SynMap.get_for_subses(self, base_fname, dwi, dwi_data_file, reg_subject, reg_template, tmpl_name, subject_sls, template_sls)
    337 start_time = time()
    338 if self.use_prealign:
--> 339     reg_prealign = self.prealign(reg_subject, reg_template)
    340 else:
    341     reg_prealign = None

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/AFQ/definitions/mapping.py:216, in GeneratedMapMixin.prealign(self, reg_subject, reg_template)
    214 def prealign(self, reg_subject, reg_template):
    215     logger.info("Calculating affine pre-alignment...")
--> 216     _, aff = affine_registration(reg_subject, reg_template, **self.affine_kwargs)
    217     return aff

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/dipy/testing/decorators.py:201, in warning_for_keywords.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
    194 # Check if the current version is within the warning range
    195 if (
    196     version.parse(from_version)
    197     <= version.parse(current_version)
    198     <= version.parse(until_version)
    199 ):
    200     # Convert positional to keyword arguments and issue a warning
--> 201     return convert_positional_to_keyword(func, args, kwargs)
    203 # If the version is greater than the until_version,
    204 # pass the arguments as they are
    205 elif version.parse(current_version) > version.parse(until_version):

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/dipy/testing/decorators.py:192, in warning_for_keywords.<locals>.decorator.<locals>.wrapper.<locals>.convert_positional_to_keyword(func, args, kwargs)
    182         warnings.warn(
    183             f"Pass {positionally_passed_kwonly_args} as keyword args. "
    184             f"From version {until_version} passing these as positional "
   (...)    187             stacklevel=3,
    188         )
    190     return func(*positional_args, **corrected_kwargs)
--> 192 return func(*args, **kwargs)

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/dipy/align/_public.py:599, in affine_registration(moving, static, moving_affine, static_affine, pipeline, starting_affine, metric, level_iters, sigmas, factors, ret_metric, moving_mask, static_mask, optimizer_options, **metric_kwargs)
    597     else:
    598         transform = _METHOD_DICT[func][1]()
--> 599         xform, xopt, fopt = affreg.optimize(
    600             static,
    601             moving,
    602             transform,
    603             None,
    604             static_grid2world=static_affine,
    605             moving_grid2world=moving_affine,
    606             starting_affine=starting_affine,
    607             ret_metric=True,
    608             static_mask=static_mask,
    609             moving_mask=moving_mask,
    610         )
    611         starting_affine = xform.affine
    613 # Copy the final affine into a final variable

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/dipy/testing/decorators.py:201, in warning_for_keywords.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
    194 # Check if the current version is within the warning range
    195 if (
    196     version.parse(from_version)
    197     <= version.parse(current_version)
    198     <= version.parse(until_version)
    199 ):
    200     # Convert positional to keyword arguments and issue a warning
--> 201     return convert_positional_to_keyword(func, args, kwargs)
    203 # If the version is greater than the until_version,
    204 # pass the arguments as they are
    205 elif version.parse(current_version) > version.parse(until_version):

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/dipy/testing/decorators.py:192, in warning_for_keywords.<locals>.decorator.<locals>.wrapper.<locals>.convert_positional_to_keyword(func, args, kwargs)
    182         warnings.warn(
    183             f"Pass {positionally_passed_kwonly_args} as keyword args. "
    184             f"From version {until_version} passing these as positional "
   (...)    187             stacklevel=3,
    188         )
    190     return func(*positional_args, **corrected_kwargs)
--> 192 return func(*args, **kwargs)

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/dipy/align/imaffine.py:1331, in AffineRegistration.optimize(***failed resolving arguments***)
   1328 else:
   1329     self.options["maxiter"] = max_iter
-> 1331 opt = Optimizer(
   1332     self.metric.distance_and_gradient,
   1333     self.params0,
   1334     method=self.method,
   1335     jac=True,
   1336     options=self.options,
   1337 )
   1338 params = opt.xopt
   1340 # Update starting_affine matrix with optimal parameters

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/dipy/testing/decorators.py:201, in warning_for_keywords.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
    194 # Check if the current version is within the warning range
    195 if (
    196     version.parse(from_version)
    197     <= version.parse(current_version)
    198     <= version.parse(until_version)
    199 ):
    200     # Convert positional to keyword arguments and issue a warning
--> 201     return convert_positional_to_keyword(func, args, kwargs)
    203 # If the version is greater than the until_version,
    204 # pass the arguments as they are
    205 elif version.parse(current_version) > version.parse(until_version):

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/dipy/testing/decorators.py:192, in warning_for_keywords.<locals>.decorator.<locals>.wrapper.<locals>.convert_positional_to_keyword(func, args, kwargs)
    182         warnings.warn(
    183             f"Pass {positionally_passed_kwonly_args} as keyword args. "
    184             f"From version {until_version} passing these as positional "
   (...)    187             stacklevel=3,
    188         )
    190     return func(*positional_args, **corrected_kwargs)
--> 192 return func(*args, **kwargs)

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/dipy/core/optimize.py:164, in Optimizer.__init__(self, fun, x0, args, method, jac, hess, hessp, bounds, constraints, tol, callback, options, evolution)
    148     res = minimize(
    149         fun,
    150         x0,
   (...)    160         options=options,
    161     )
    163 else:
--> 164     res = minimize(
    165         fun,
    166         x0,
    167         args,
    168         method,
    169         jac,
    170         hess,
    171         hessp,
    172         bounds,
    173         constraints,
    174         tol,
    175         callback,
    176         options,
    177     )
    179 self.res = res

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/scipy/optimize/_minimize.py:784, in minimize(fun, x0, args, method, jac, hess, hessp, bounds, constraints, tol, callback, options)
    781     res = _minimize_newtoncg(fun, x0, args, jac, hess, hessp, callback,
    782                              **options)
    783 elif meth == 'l-bfgs-b':
--> 784     res = _minimize_lbfgsb(fun, x0, args, jac, bounds,
    785                            callback=callback, **options)
    786 elif meth == 'tnc':
    787     res = _minimize_tnc(fun, x0, args, jac, bounds, callback=callback,
    788                         **options)

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/scipy/optimize/_lbfgsb_py.py:469, in _minimize_lbfgsb(fun, x0, args, jac, bounds, disp, maxcor, ftol, gtol, eps, maxfun, maxiter, iprint, callback, maxls, finite_diff_rel_step, workers, **unknown_options)
    461 _lbfgsb.setulb(m, x, low_bnd, upper_bnd, nbd, f, g, factr, pgtol, wa,
    462                iwa, task, lsave, isave, dsave, maxls, ln_task)
    464 if task[0] == 3:
    465     # The minimization routine wants f and g at the current x.
    466     # Note that interruptions due to maxfun are postponed
    467     # until the completion of the current minimization iteration.
    468     # Overwrite f and g:
--> 469     f, g = func_and_grad(x)
    470 elif task[0] == 1:
    471     # new iteration
    472     n_iterations += 1

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/scipy/optimize/_differentiable_functions.py:412, in ScalarFunction.fun_and_grad(self, x)
    410 if not np.array_equal(x, self.x):
    411     self._update_x(x)
--> 412 self._update_fun()
    413 self._update_grad()
    414 return self.f, self.g

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/scipy/optimize/_differentiable_functions.py:362, in ScalarFunction._update_fun(self)
    360 def _update_fun(self):
    361     if not self.f_updated:
--> 362         fx = self._wrapped_fun(self.x)
    363         self._nfev += 1
    364         if fx < self._lowest_f:

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/scipy/_lib/_util.py:603, in _ScalarFunctionWrapper.__call__(self, x)
    600 def __call__(self, x):
    601     # Send a copy because the user may overwrite it.
    602     # The user of this class might want `x` to remain unchanged.
--> 603     fx = self.f(np.copy(x), *self.args)
    604     self.nfev += 1
    606     # Make sure the function returns a true scalar

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/scipy/optimize/_optimize.py:80, in MemoizeJac.__call__(self, x, *args)
     78 def __call__(self, x, *args):
     79     """ returns the function value """
---> 80     self._compute_if_needed(x, *args)
     81     return self._value

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/scipy/optimize/_optimize.py:74, in MemoizeJac._compute_if_needed(self, x, *args)
     72 if not np.all(x == self.x) or self._value is None or self.jac is None:
     73     self.x = np.asarray(x).copy()
---> 74     fg = self.fun(x, *args)
     75     self.jac = fg[1]
     76     self._value = fg[0]

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/dipy/align/imaffine.py:888, in MutualInformationMetric.distance_and_gradient(self, params)
    868 r"""Numeric value of the metric and its gradient at given parameters.
    869 
    870 Parameters
   (...)    885 
    886 """
    887 try:
--> 888     self._update_mutual_information(params, update_gradient=True)
    889 except (AffineInversionError, AffineInvalidValuesError):
    890     return np.inf, 0 * self.metric_grad

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/dipy/testing/decorators.py:201, in warning_for_keywords.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
    194 # Check if the current version is within the warning range
    195 if (
    196     version.parse(from_version)
    197     <= version.parse(current_version)
    198     <= version.parse(until_version)
    199 ):
    200     # Convert positional to keyword arguments and issue a warning
--> 201     return convert_positional_to_keyword(func, args, kwargs)
    203 # If the version is greater than the until_version,
    204 # pass the arguments as they are
    205 elif version.parse(current_version) > version.parse(until_version):

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/dipy/testing/decorators.py:192, in warning_for_keywords.<locals>.decorator.<locals>.wrapper.<locals>.convert_positional_to_keyword(func, args, kwargs)
    182         warnings.warn(
    183             f"Pass {positionally_passed_kwonly_args} as keyword args. "
    184             f"From version {until_version} passing these as positional "
   (...)    187             stacklevel=3,
    188         )
    190     return func(*positional_args, **corrected_kwargs)
--> 192 return func(*args, **kwargs)

File /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages/dipy/align/imaffine.py:782, in MutualInformationMetric._update_mutual_information(self, params, update_gradient)
    777 if self.sampling_proportion is None:  # Dense case
    778     # Compute the gradient of moving img. at physical points
    779     # associated with the >>static image's grid<< cells
    780     # The image gradient must be eval. at current moved points
    781     grid_to_world = current_affine.dot(self.static_grid2world)
--> 782     mgrad, inside = vf.gradient(
    783         self.moving,
    784         self.moving_world2grid,
    785         self.moving_spacing,
    786         self.static.shape,
    787         grid_to_world,
    788     )
    789     # The Jacobian must be evaluated at the pre-aligned points
    790     H.update_gradient_dense(
    791         params,
    792         self.transform,
   (...)    798         mmask=moving_mask_values,
    799     )

KeyboardInterrupt: