junifer 0.0.4.dev831__py3-none-any.whl → 0.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. junifer/__init__.py +17 -0
  2. junifer/_version.py +2 -2
  3. junifer/api/__init__.py +4 -1
  4. junifer/api/cli.py +91 -1
  5. junifer/api/decorators.py +9 -0
  6. junifer/api/functions.py +56 -10
  7. junifer/api/parser.py +3 -0
  8. junifer/api/queue_context/__init__.py +4 -1
  9. junifer/api/queue_context/gnu_parallel_local_adapter.py +16 -6
  10. junifer/api/queue_context/htcondor_adapter.py +16 -5
  11. junifer/api/queue_context/tests/test_gnu_parallel_local_adapter.py +41 -12
  12. junifer/api/queue_context/tests/test_htcondor_adapter.py +48 -15
  13. junifer/api/res/afni/run_afni_docker.sh +1 -1
  14. junifer/api/res/ants/run_ants_docker.sh +1 -1
  15. junifer/api/res/freesurfer/mri_binarize +3 -0
  16. junifer/api/res/freesurfer/mri_mc +3 -0
  17. junifer/api/res/freesurfer/mri_pretess +3 -0
  18. junifer/api/res/freesurfer/mris_convert +3 -0
  19. junifer/api/res/freesurfer/run_freesurfer_docker.sh +61 -0
  20. junifer/api/res/fsl/run_fsl_docker.sh +1 -1
  21. junifer/api/res/{run_conda.sh → run_conda.bash} +1 -1
  22. junifer/api/res/run_conda.zsh +23 -0
  23. junifer/api/res/run_venv.bash +22 -0
  24. junifer/api/res/{run_venv.sh → run_venv.zsh} +1 -1
  25. junifer/api/tests/test_api_utils.py +4 -2
  26. junifer/api/tests/test_cli.py +83 -0
  27. junifer/api/tests/test_functions.py +27 -2
  28. junifer/configs/__init__.py +1 -1
  29. junifer/configs/juseless/__init__.py +4 -1
  30. junifer/configs/juseless/datagrabbers/__init__.py +10 -1
  31. junifer/configs/juseless/datagrabbers/aomic_id1000_vbm.py +4 -3
  32. junifer/configs/juseless/datagrabbers/camcan_vbm.py +3 -0
  33. junifer/configs/juseless/datagrabbers/ixi_vbm.py +4 -3
  34. junifer/configs/juseless/datagrabbers/tests/test_ucla.py +1 -3
  35. junifer/configs/juseless/datagrabbers/ucla.py +12 -9
  36. junifer/configs/juseless/datagrabbers/ukb_vbm.py +3 -0
  37. junifer/data/__init__.py +21 -1
  38. junifer/data/coordinates.py +10 -19
  39. junifer/data/masks/ukb/UKB_15K_GM_template.nii.gz +0 -0
  40. junifer/data/masks.py +58 -87
  41. junifer/data/parcellations.py +14 -3
  42. junifer/data/template_spaces.py +4 -1
  43. junifer/data/tests/test_masks.py +26 -37
  44. junifer/data/utils.py +3 -0
  45. junifer/datagrabber/__init__.py +18 -1
  46. junifer/datagrabber/aomic/__init__.py +3 -0
  47. junifer/datagrabber/aomic/id1000.py +70 -37
  48. junifer/datagrabber/aomic/piop1.py +69 -36
  49. junifer/datagrabber/aomic/piop2.py +71 -38
  50. junifer/datagrabber/aomic/tests/test_id1000.py +44 -100
  51. junifer/datagrabber/aomic/tests/test_piop1.py +65 -108
  52. junifer/datagrabber/aomic/tests/test_piop2.py +45 -102
  53. junifer/datagrabber/base.py +13 -6
  54. junifer/datagrabber/datalad_base.py +13 -1
  55. junifer/datagrabber/dmcc13_benchmark.py +36 -53
  56. junifer/datagrabber/hcp1200/__init__.py +3 -0
  57. junifer/datagrabber/hcp1200/datalad_hcp1200.py +3 -0
  58. junifer/datagrabber/hcp1200/hcp1200.py +4 -1
  59. junifer/datagrabber/multiple.py +45 -6
  60. junifer/datagrabber/pattern.py +170 -62
  61. junifer/datagrabber/pattern_datalad.py +25 -12
  62. junifer/datagrabber/pattern_validation_mixin.py +388 -0
  63. junifer/datagrabber/tests/test_datalad_base.py +4 -4
  64. junifer/datagrabber/tests/test_dmcc13_benchmark.py +46 -19
  65. junifer/datagrabber/tests/test_multiple.py +161 -84
  66. junifer/datagrabber/tests/test_pattern.py +45 -0
  67. junifer/datagrabber/tests/test_pattern_datalad.py +4 -4
  68. junifer/datagrabber/tests/test_pattern_validation_mixin.py +249 -0
  69. junifer/datareader/__init__.py +4 -1
  70. junifer/datareader/default.py +95 -43
  71. junifer/external/BrainPrint/brainprint/__init__.py +4 -0
  72. junifer/external/BrainPrint/brainprint/_version.py +3 -0
  73. junifer/external/BrainPrint/brainprint/asymmetry.py +91 -0
  74. junifer/external/BrainPrint/brainprint/brainprint.py +441 -0
  75. junifer/external/BrainPrint/brainprint/surfaces.py +258 -0
  76. junifer/external/BrainPrint/brainprint/utils/__init__.py +1 -0
  77. junifer/external/BrainPrint/brainprint/utils/_config.py +112 -0
  78. junifer/external/BrainPrint/brainprint/utils/utils.py +188 -0
  79. junifer/external/__init__.py +1 -1
  80. junifer/external/nilearn/__init__.py +5 -1
  81. junifer/external/nilearn/junifer_connectivity_measure.py +483 -0
  82. junifer/external/nilearn/junifer_nifti_spheres_masker.py +23 -9
  83. junifer/external/nilearn/tests/test_junifer_connectivity_measure.py +1089 -0
  84. junifer/external/nilearn/tests/test_junifer_nifti_spheres_masker.py +76 -1
  85. junifer/markers/__init__.py +23 -1
  86. junifer/markers/base.py +68 -28
  87. junifer/markers/brainprint.py +459 -0
  88. junifer/markers/collection.py +10 -2
  89. junifer/markers/complexity/__init__.py +10 -0
  90. junifer/markers/complexity/complexity_base.py +26 -43
  91. junifer/markers/complexity/hurst_exponent.py +3 -0
  92. junifer/markers/complexity/multiscale_entropy_auc.py +3 -0
  93. junifer/markers/complexity/perm_entropy.py +3 -0
  94. junifer/markers/complexity/range_entropy.py +3 -0
  95. junifer/markers/complexity/range_entropy_auc.py +3 -0
  96. junifer/markers/complexity/sample_entropy.py +3 -0
  97. junifer/markers/complexity/tests/test_hurst_exponent.py +11 -3
  98. junifer/markers/complexity/tests/test_multiscale_entropy_auc.py +11 -3
  99. junifer/markers/complexity/tests/test_perm_entropy.py +11 -3
  100. junifer/markers/complexity/tests/test_range_entropy.py +11 -3
  101. junifer/markers/complexity/tests/test_range_entropy_auc.py +11 -3
  102. junifer/markers/complexity/tests/test_sample_entropy.py +11 -3
  103. junifer/markers/complexity/tests/test_weighted_perm_entropy.py +11 -3
  104. junifer/markers/complexity/weighted_perm_entropy.py +3 -0
  105. junifer/markers/ets_rss.py +27 -42
  106. junifer/markers/falff/__init__.py +3 -0
  107. junifer/markers/falff/_afni_falff.py +5 -2
  108. junifer/markers/falff/_junifer_falff.py +3 -0
  109. junifer/markers/falff/falff_base.py +20 -46
  110. junifer/markers/falff/falff_parcels.py +56 -27
  111. junifer/markers/falff/falff_spheres.py +60 -29
  112. junifer/markers/falff/tests/test_falff_parcels.py +39 -23
  113. junifer/markers/falff/tests/test_falff_spheres.py +39 -23
  114. junifer/markers/functional_connectivity/__init__.py +9 -0
  115. junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py +63 -60
  116. junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py +45 -32
  117. junifer/markers/functional_connectivity/edge_functional_connectivity_spheres.py +49 -36
  118. junifer/markers/functional_connectivity/functional_connectivity_base.py +71 -70
  119. junifer/markers/functional_connectivity/functional_connectivity_parcels.py +34 -25
  120. junifer/markers/functional_connectivity/functional_connectivity_spheres.py +40 -30
  121. junifer/markers/functional_connectivity/tests/test_crossparcellation_functional_connectivity.py +11 -7
  122. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_parcels.py +27 -7
  123. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_spheres.py +28 -12
  124. junifer/markers/functional_connectivity/tests/test_functional_connectivity_parcels.py +35 -11
  125. junifer/markers/functional_connectivity/tests/test_functional_connectivity_spheres.py +36 -62
  126. junifer/markers/parcel_aggregation.py +47 -61
  127. junifer/markers/reho/__init__.py +3 -0
  128. junifer/markers/reho/_afni_reho.py +5 -2
  129. junifer/markers/reho/_junifer_reho.py +4 -1
  130. junifer/markers/reho/reho_base.py +8 -27
  131. junifer/markers/reho/reho_parcels.py +28 -17
  132. junifer/markers/reho/reho_spheres.py +27 -18
  133. junifer/markers/reho/tests/test_reho_parcels.py +8 -3
  134. junifer/markers/reho/tests/test_reho_spheres.py +8 -3
  135. junifer/markers/sphere_aggregation.py +43 -59
  136. junifer/markers/temporal_snr/__init__.py +3 -0
  137. junifer/markers/temporal_snr/temporal_snr_base.py +23 -32
  138. junifer/markers/temporal_snr/temporal_snr_parcels.py +9 -6
  139. junifer/markers/temporal_snr/temporal_snr_spheres.py +9 -6
  140. junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py +6 -3
  141. junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py +6 -3
  142. junifer/markers/tests/test_brainprint.py +58 -0
  143. junifer/markers/tests/test_collection.py +9 -8
  144. junifer/markers/tests/test_ets_rss.py +15 -9
  145. junifer/markers/tests/test_markers_base.py +17 -18
  146. junifer/markers/tests/test_parcel_aggregation.py +93 -32
  147. junifer/markers/tests/test_sphere_aggregation.py +72 -19
  148. junifer/onthefly/__init__.py +4 -1
  149. junifer/onthefly/read_transform.py +3 -0
  150. junifer/pipeline/__init__.py +9 -1
  151. junifer/pipeline/pipeline_step_mixin.py +21 -4
  152. junifer/pipeline/registry.py +3 -0
  153. junifer/pipeline/singleton.py +3 -0
  154. junifer/pipeline/tests/test_registry.py +1 -1
  155. junifer/pipeline/update_meta_mixin.py +3 -0
  156. junifer/pipeline/utils.py +67 -1
  157. junifer/pipeline/workdir_manager.py +3 -0
  158. junifer/preprocess/__init__.py +10 -2
  159. junifer/preprocess/base.py +6 -3
  160. junifer/preprocess/confounds/__init__.py +3 -0
  161. junifer/preprocess/confounds/fmriprep_confound_remover.py +47 -60
  162. junifer/preprocess/confounds/tests/test_fmriprep_confound_remover.py +72 -113
  163. junifer/preprocess/smoothing/__init__.py +9 -0
  164. junifer/preprocess/smoothing/_afni_smoothing.py +119 -0
  165. junifer/preprocess/smoothing/_fsl_smoothing.py +116 -0
  166. junifer/preprocess/smoothing/_nilearn_smoothing.py +69 -0
  167. junifer/preprocess/smoothing/smoothing.py +174 -0
  168. junifer/preprocess/smoothing/tests/test_smoothing.py +94 -0
  169. junifer/preprocess/warping/__init__.py +3 -0
  170. junifer/preprocess/warping/_ants_warper.py +3 -0
  171. junifer/preprocess/warping/_fsl_warper.py +3 -0
  172. junifer/stats.py +4 -1
  173. junifer/storage/__init__.py +9 -1
  174. junifer/storage/base.py +40 -1
  175. junifer/storage/hdf5.py +71 -9
  176. junifer/storage/pandas_base.py +3 -0
  177. junifer/storage/sqlite.py +3 -0
  178. junifer/storage/tests/test_hdf5.py +82 -10
  179. junifer/storage/utils.py +9 -0
  180. junifer/testing/__init__.py +4 -1
  181. junifer/testing/datagrabbers.py +13 -6
  182. junifer/testing/tests/test_partlycloudytesting_datagrabber.py +7 -7
  183. junifer/testing/utils.py +3 -0
  184. junifer/utils/__init__.py +13 -2
  185. junifer/utils/fs.py +3 -0
  186. junifer/utils/helpers.py +32 -1
  187. junifer/utils/logging.py +33 -4
  188. junifer/utils/tests/test_logging.py +8 -0
  189. {junifer-0.0.4.dev831.dist-info → junifer-0.0.5.dist-info}/METADATA +17 -16
  190. junifer-0.0.5.dist-info/RECORD +275 -0
  191. {junifer-0.0.4.dev831.dist-info → junifer-0.0.5.dist-info}/WHEEL +1 -1
  192. junifer/datagrabber/tests/test_datagrabber_utils.py +0 -218
  193. junifer/datagrabber/utils.py +0 -230
  194. junifer/preprocess/ants/__init__.py +0 -4
  195. junifer/preprocess/ants/ants_apply_transforms_warper.py +0 -185
  196. junifer/preprocess/ants/tests/test_ants_apply_transforms_warper.py +0 -56
  197. junifer/preprocess/bold_warper.py +0 -265
  198. junifer/preprocess/fsl/__init__.py +0 -4
  199. junifer/preprocess/fsl/apply_warper.py +0 -179
  200. junifer/preprocess/fsl/tests/test_apply_warper.py +0 -45
  201. junifer/preprocess/tests/test_bold_warper.py +0 -159
  202. junifer-0.0.4.dev831.dist-info/RECORD +0 -257
  203. {junifer-0.0.4.dev831.dist-info → junifer-0.0.5.dist-info}/AUTHORS.rst +0 -0
  204. {junifer-0.0.4.dev831.dist-info → junifer-0.0.5.dist-info}/LICENSE.md +0 -0
  205. {junifer-0.0.4.dev831.dist-info → junifer-0.0.5.dist-info}/entry_points.txt +0 -0
  206. {junifer-0.0.4.dev831.dist-info → junifer-0.0.5.dist-info}/top_level.txt +0 -0
@@ -5,7 +5,7 @@
5
5
  # License: AGPL
6
6
 
7
7
  from pathlib import Path
8
- from typing import Dict, List, Optional
8
+ from typing import Dict, List, Optional, Union
9
9
 
10
10
  import nibabel as nib
11
11
  import pandas as pd
@@ -15,6 +15,9 @@ from ..pipeline import PipelineStepMixin, UpdateMetaMixin
15
15
  from ..utils.logging import logger, warn_with_log
16
16
 
17
17
 
18
+ __all__ = ["DefaultDataReader"]
19
+
20
+
18
21
  # Map each file extension to a type
19
22
  _extensions = {
20
23
  ".nii": "NIFTI",
@@ -102,57 +105,106 @@ class DefaultDataReader(PipelineStepMixin, UpdateMetaMixin):
102
105
  if params is None:
103
106
  params = {}
104
107
  # For each type of data, try to read it
105
- for type_ in input.keys():
106
- # Skip Warp data type
107
- if type_ == "Warp":
108
+ for type_key, type_val in input.items():
109
+ # Skip Warp and FreeSurfer data type
110
+ if type_key in ["Warp", "FreeSurfer"]:
108
111
  continue
109
112
 
110
113
  # Check for malformed datagrabber specification
111
- if "path" not in input[type_]:
114
+ if "path" not in type_val:
112
115
  warn_with_log(
113
- f"Input type {type_} does not provide a path. Skipping."
116
+ f"Input type {type_key} does not provide a path. Skipping."
114
117
  )
115
118
  continue
116
119
 
117
- # Retrieve actual path
118
- t_path = input[type_]["path"]
119
- # Retrieve loading params for the data type
120
- t_params = params.get(type_, {})
121
-
122
- # Convert str to Path
123
- if not isinstance(t_path, Path):
124
- t_path = Path(t_path)
125
- out[type_]["path"] = t_path
126
-
127
- logger.info(f"Reading {type_} from {t_path.as_posix()}")
128
- # Initialize variable for file data
129
- fread = None
130
- # Lowercase path
131
- fname = t_path.name.lower()
132
- # Loop through extensions to find the correct one
133
- for ext, ftype in _extensions.items():
134
- if fname.endswith(ext):
135
- logger.info(f"{type_} is type {ftype}")
136
- # Retrieve reader function
137
- reader_func = _readers[ftype]["func"]
138
- # Retrieve reader function params
139
- reader_params = _readers[ftype]["params"]
140
- # Update reader function params
141
- if reader_params is not None:
142
- t_params.update(reader_params)
143
- logger.debug(f"Calling {reader_func} with {t_params}")
120
+ # Iterate to check for nested "types" like mask;
121
+ # need to copy to avoid runtime error for changing dict size
122
+ for k, v in type_val.copy().items():
123
+ # Read data for base data type
124
+ if k == "path":
125
+ # Convert str to Path
126
+ if not isinstance(v, Path):
127
+ v = Path(v)
128
+ # Update path
129
+ out[type_key]["path"] = v
130
+ logger.info(f"Reading {type_key} from {v.absolute()!s}")
131
+ # Retrieve loading params for the data type
132
+ t_params = params.get(type_key, {})
144
133
  # Read data
145
- fread = reader_func(t_path, **t_params)
146
- break
147
- # If no file data is found due to unknown extension
148
- if fread is None:
149
- logger.info(
150
- f"Unknown file type {t_path.as_posix()}, skipping reading"
151
- )
134
+ out[type_key]["data"] = _read_data(
135
+ data_type=type_key, path=v, read_params=t_params
136
+ )
137
+ # Read data for nested data type
138
+ if isinstance(v, dict) and "path" in v:
139
+ # Set path
140
+ nested_path = v["path"]
141
+ # Convert str to Path
142
+ if not isinstance(nested_path, Path):
143
+ nested_path = Path(nested_path)
144
+ # Update path
145
+ out[type_key][k]["path"] = nested_path
146
+ # Set nested type key for easier access
147
+ nested_type = f"{type_key}.{k}"
148
+ logger.info(
149
+ f"Reading {nested_type} from "
150
+ f"{nested_path.absolute()!s}"
151
+ )
152
+ # Retrieve loading params for the nested data type
153
+ nested_params = params.get(nested_type, {})
154
+ # Read data
155
+ out[type_key][k]["data"] = _read_data(
156
+ data_type=nested_type,
157
+ path=nested_path,
158
+ read_params=nested_params,
159
+ )
152
160
 
153
- # Set file data for output
154
- out[type_]["data"] = fread
155
161
  # Update metadata for step
156
- self.update_meta(out[type_], "datareader")
162
+ self.update_meta(out[type_key], "datareader")
157
163
 
158
164
  return out
165
+
166
+
167
+ def _read_data(
168
+ data_type: str, path: Path, read_params: Dict
169
+ ) -> Union[nib.Nifti1Image, pd.DataFrame, None]:
170
+ """Read data for data type.
171
+
172
+ Parameters
173
+ ----------
174
+ data_type : str
175
+ The data type being read.
176
+ path : pathlib.Path
177
+ The path to read data from.
178
+ read_params : dict
179
+ Parameters for reader function.
180
+
181
+ Returns
182
+ -------
183
+ nibabel.Nifti1Image or pandas.DataFrame or pandas.TextFileReader or None
184
+ The data loaded in memory if file type is known else None.
185
+
186
+ """
187
+ # Initialize variable for file data
188
+ fread = None
189
+ # Lowercase path
190
+ fname = path.name.lower()
191
+ # Loop through extensions to find the correct one
192
+ for ext, ftype in _extensions.items():
193
+ if fname.endswith(ext):
194
+ logger.info(f"{data_type} is of type {ftype}")
195
+ # Retrieve reader function
196
+ reader_func = _readers[ftype]["func"]
197
+ # Retrieve reader function params
198
+ reader_params = _readers[ftype]["params"]
199
+ # Update reader function params
200
+ if reader_params is not None:
201
+ read_params.update(reader_params)
202
+ logger.debug(f"Calling {reader_func!s} with {read_params}")
203
+ # Read data
204
+ fread = reader_func(path, **read_params)
205
+ break
206
+ # If no file data is found due to unknown extension
207
+ if fread is None:
208
+ logger.info(f"Unknown file type {path.absolute()!s}, skipping reading")
209
+
210
+ return fread
@@ -0,0 +1,4 @@
1
+ from ._version import __version__ # noqa: F401
2
+ from .brainprint import compute_asymmetry, compute_brainprint # noqa: F401
3
+ from .surfaces import surf_to_vtk # noqa: F401
4
+ from .utils._config import sys_info # noqa: F401
@@ -0,0 +1,3 @@
1
+ """Version number."""
2
+
3
+ __version__ = "0.4.0"
@@ -0,0 +1,91 @@
1
+ """
2
+ Contains asymmetry estimation functionality.
3
+ """
4
+ from typing import Dict
5
+
6
+ import numpy as np
7
+ from lapy import shapedna
8
+
9
+
10
+ def compute_asymmetry(
11
+ eigenvalues, distance: str = "euc", skip_cortex: bool = False
12
+ ) -> Dict[str, float]:
13
+ """
14
+ Compute lateral shape distances from BrainPrint analysis results.
15
+
16
+ Parameters
17
+ ----------
18
+ eigenvalues : _type_
19
+ BrainPrint analysis results.
20
+ distance : str, optional
21
+ ShapeDNA distance, by default "euc".
22
+ skip_cortex : bool, optional
23
+ Whether to skip white matter and pial surfaces, by default False.
24
+
25
+ Returns
26
+ -------
27
+ Dict[str, float]
28
+ {left_label}_{right_label}, distance.
29
+ """
30
+ # Define structures
31
+
32
+ # combined and individual aseg labels:
33
+ # - Left Striatum: left Caudate + Putamen + Accumbens
34
+ # - Right Striatum: right Caudate + Putamen + Accumbens
35
+ # - CorpusCallosum: 5 subregions combined
36
+ # - Cerebellum: brainstem + (left+right) cerebellum WM and GM
37
+ # - Ventricles: (left+right) lat.vent + inf.lat.vent + choroidplexus + 3rdVent + CSF
38
+ # - Lateral-Ventricle: lat.vent + inf.lat.vent + choroidplexus
39
+ # - 3rd-Ventricle: 3rd-Ventricle + CSF
40
+
41
+ structures_left_right = [
42
+ ("Left-Striatum", "Right-Striatum"),
43
+ ("Left-Lateral-Ventricle", "Right-Lateral-Ventricle"),
44
+ (
45
+ "Left-Cerebellum-White-Matter",
46
+ "Right-Cerebellum-White-Matter",
47
+ ),
48
+ ("Left-Cerebellum-Cortex", "Right-Cerebellum-Cortex"),
49
+ ("Left-Thalamus-Proper", "Right-Thalamus-Proper"),
50
+ ("Left-Caudate", "Right-Caudate"),
51
+ ("Left-Putamen", "Right-Putamen"),
52
+ ("Left-Pallidum", "Right-Pallidum"),
53
+ ("Left-Hippocampus", "Right-Hippocampus"),
54
+ ("Left-Amygdala", "Right-Amygdala"),
55
+ ("Left-Accumbens-area", "Right-Accumbens-area"),
56
+ ("Left-VentralDC", "Right-VentralDC"),
57
+ ]
58
+
59
+ cortex_2d_left_right = [
60
+ ("lh-white-2d", "rh-white-2d"),
61
+ ("lh-pial-2d", "rh-pial-2d"),
62
+ ]
63
+
64
+ structures = structures_left_right
65
+ if not skip_cortex:
66
+ structures += cortex_2d_left_right
67
+
68
+ distances = dict()
69
+ for left_label, right_label in structures:
70
+ left_eigenvalues, right_eigenvalues = (
71
+ eigenvalues[left_label][2:],
72
+ eigenvalues[right_label][2:],
73
+ )
74
+ has_nan = np.isnan(left_eigenvalues).any() or np.isnan(right_eigenvalues).any()
75
+ key = f"{left_label}_{right_label}"
76
+ if has_nan:
77
+ message = (
78
+ "NaNs found for {left_label} or {right_label}, "
79
+ "skipping asymmetry computation...".format(
80
+ left_label=left_label, right_label=right_label
81
+ )
82
+ )
83
+ print(message)
84
+ distances[key] = np.nan
85
+ else:
86
+ distances[key] = shapedna.compute_distance(
87
+ left_eigenvalues,
88
+ right_eigenvalues,
89
+ dist=distance,
90
+ )
91
+ return distances