pyNIBS 0.2024.8__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 (107) hide show
  1. pyNIBS-0.2024.8.dist-info/LICENSE +623 -0
  2. pyNIBS-0.2024.8.dist-info/METADATA +723 -0
  3. pyNIBS-0.2024.8.dist-info/RECORD +107 -0
  4. pyNIBS-0.2024.8.dist-info/WHEEL +5 -0
  5. pyNIBS-0.2024.8.dist-info/top_level.txt +1 -0
  6. pynibs/__init__.py +34 -0
  7. pynibs/coil.py +1367 -0
  8. pynibs/congruence/__init__.py +15 -0
  9. pynibs/congruence/congruence.py +1108 -0
  10. pynibs/congruence/ext_metrics.py +257 -0
  11. pynibs/congruence/stimulation_threshold.py +318 -0
  12. pynibs/data/configuration_exp0.yaml +59 -0
  13. pynibs/data/configuration_linear_MEP.yaml +61 -0
  14. pynibs/data/configuration_linear_RT.yaml +61 -0
  15. pynibs/data/configuration_sigmoid4.yaml +68 -0
  16. pynibs/data/network mapping configuration/configuration guide.md +238 -0
  17. pynibs/data/network mapping configuration/configuration_TEMPLATE.yaml +42 -0
  18. pynibs/data/network mapping configuration/configuration_for_testing.yaml +43 -0
  19. pynibs/data/network mapping configuration/configuration_modelTMS.yaml +43 -0
  20. pynibs/data/network mapping configuration/configuration_reg_isi_05.yaml +43 -0
  21. pynibs/data/network mapping configuration/output_documentation.md +185 -0
  22. pynibs/data/network mapping configuration/recommendations_for_accuracy_threshold.md +77 -0
  23. pynibs/data/neuron/models/L23_PC_cADpyr_biphasic_v1.csv +1281 -0
  24. pynibs/data/neuron/models/L23_PC_cADpyr_monophasic_v1.csv +1281 -0
  25. pynibs/data/neuron/models/L4_LBC_biphasic_v1.csv +1281 -0
  26. pynibs/data/neuron/models/L4_LBC_monophasic_v1.csv +1281 -0
  27. pynibs/data/neuron/models/L4_NBC_biphasic_v1.csv +1281 -0
  28. pynibs/data/neuron/models/L4_NBC_monophasic_v1.csv +1281 -0
  29. pynibs/data/neuron/models/L4_SBC_biphasic_v1.csv +1281 -0
  30. pynibs/data/neuron/models/L4_SBC_monophasic_v1.csv +1281 -0
  31. pynibs/data/neuron/models/L5_TTPC2_cADpyr_biphasic_v1.csv +1281 -0
  32. pynibs/data/neuron/models/L5_TTPC2_cADpyr_monophasic_v1.csv +1281 -0
  33. pynibs/expio/Mep.py +1518 -0
  34. pynibs/expio/__init__.py +8 -0
  35. pynibs/expio/brainsight.py +979 -0
  36. pynibs/expio/brainvis.py +71 -0
  37. pynibs/expio/cobot.py +239 -0
  38. pynibs/expio/exp.py +1876 -0
  39. pynibs/expio/fit_funs.py +287 -0
  40. pynibs/expio/localite.py +1987 -0
  41. pynibs/expio/signal_ced.py +51 -0
  42. pynibs/expio/visor.py +624 -0
  43. pynibs/freesurfer.py +502 -0
  44. pynibs/hdf5_io/__init__.py +10 -0
  45. pynibs/hdf5_io/hdf5_io.py +1857 -0
  46. pynibs/hdf5_io/xdmf.py +1542 -0
  47. pynibs/mesh/__init__.py +3 -0
  48. pynibs/mesh/mesh_struct.py +1394 -0
  49. pynibs/mesh/transformations.py +866 -0
  50. pynibs/mesh/utils.py +1103 -0
  51. pynibs/models/_TMS.py +211 -0
  52. pynibs/models/__init__.py +0 -0
  53. pynibs/muap.py +392 -0
  54. pynibs/neuron/__init__.py +2 -0
  55. pynibs/neuron/neuron_regression.py +284 -0
  56. pynibs/neuron/util.py +58 -0
  57. pynibs/optimization/__init__.py +5 -0
  58. pynibs/optimization/multichannel.py +278 -0
  59. pynibs/optimization/opt_mep.py +152 -0
  60. pynibs/optimization/optimization.py +1445 -0
  61. pynibs/optimization/workhorses.py +698 -0
  62. pynibs/pckg/__init__.py +0 -0
  63. pynibs/pckg/biosig/biosig4c++-1.9.5.src_fixed.tar.gz +0 -0
  64. pynibs/pckg/libeep/__init__.py +0 -0
  65. pynibs/pckg/libeep/pyeep.so +0 -0
  66. pynibs/regression/__init__.py +11 -0
  67. pynibs/regression/dual_node_detection.py +2375 -0
  68. pynibs/regression/regression.py +2984 -0
  69. pynibs/regression/score_types.py +0 -0
  70. pynibs/roi/__init__.py +2 -0
  71. pynibs/roi/roi.py +895 -0
  72. pynibs/roi/roi_structs.py +1233 -0
  73. pynibs/subject.py +1009 -0
  74. pynibs/tensor_scaling.py +144 -0
  75. pynibs/tests/data/InstrumentMarker20200225163611937.xml +19 -0
  76. pynibs/tests/data/TriggerMarkers_Coil0_20200225163443682.xml +14 -0
  77. pynibs/tests/data/TriggerMarkers_Coil1_20200225170337572.xml +6373 -0
  78. pynibs/tests/data/Xdmf.dtd +89 -0
  79. pynibs/tests/data/brainsight_niiImage_nifticoord.txt +145 -0
  80. pynibs/tests/data/brainsight_niiImage_nifticoord_largefile.txt +1434 -0
  81. pynibs/tests/data/brainsight_niiImage_niifticoord_mixedtargets.txt +47 -0
  82. pynibs/tests/data/create_subject_testsub.py +332 -0
  83. pynibs/tests/data/data.hdf5 +0 -0
  84. pynibs/tests/data/geo.hdf5 +0 -0
  85. pynibs/tests/test_coil.py +474 -0
  86. pynibs/tests/test_elements2nodes.py +100 -0
  87. pynibs/tests/test_hdf5_io/test_xdmf.py +61 -0
  88. pynibs/tests/test_mesh_transformations.py +123 -0
  89. pynibs/tests/test_mesh_utils.py +143 -0
  90. pynibs/tests/test_nnav_imports.py +101 -0
  91. pynibs/tests/test_quality_measures.py +117 -0
  92. pynibs/tests/test_regressdata.py +289 -0
  93. pynibs/tests/test_roi.py +17 -0
  94. pynibs/tests/test_rotations.py +86 -0
  95. pynibs/tests/test_subject.py +71 -0
  96. pynibs/tests/test_util.py +24 -0
  97. pynibs/tms_pulse.py +34 -0
  98. pynibs/util/__init__.py +4 -0
  99. pynibs/util/dosing.py +233 -0
  100. pynibs/util/quality_measures.py +562 -0
  101. pynibs/util/rotations.py +340 -0
  102. pynibs/util/simnibs.py +763 -0
  103. pynibs/util/util.py +727 -0
  104. pynibs/visualization/__init__.py +2 -0
  105. pynibs/visualization/para.py +4372 -0
  106. pynibs/visualization/plot_2D.py +137 -0
  107. pynibs/visualization/render_3D.py +347 -0
@@ -0,0 +1,137 @@
1
+ import pynibs
2
+ import matplotlib
3
+ import numpy as np
4
+ from matplotlib import pyplot as plt
5
+ from matplotlib import ticker
6
+
7
+
8
+ try:
9
+ matplotlib.use("Qt5Agg")
10
+ except ImportError:
11
+ pass
12
+
13
+
14
+ def plot_io_curve(
15
+ mep_data,
16
+ local_mag_e,
17
+ title="I/O curve",
18
+ y_axis_label="EMG p2p-amplitude, µV",
19
+ x_axis_label="mag(E), V/m, scaled",
20
+ fit_fun=None,
21
+ screenshot_fn=None,
22
+ interactive=False,
23
+ mso=None):
24
+ """
25
+ Plot scattered data of MEP-mag(E) pairs (=> I/O curve).
26
+ Optionally the fit of the proided fit function can be overlaid.
27
+
28
+ Parameters
29
+ ----------
30
+ mep_data: np.ndarray[float], [n_stims]
31
+ The acquired MEPs fo each stimulation.
32
+ local_mag_e: np.ndarray[float], [n_stims]
33
+ The local electric field magnitude at the ROI element the I/O curve should be plotted from.
34
+ title: str, optional
35
+ The title of the plot; default: "I/O curve"
36
+ y_axis_label: str, optional
37
+ The title of the y-axis; default: "EMG p2p-amplitude, µV"
38
+ x_axis_label: str, optional
39
+ The title of the x-axis; default: "mag(E), V/m, scaled"
40
+ fit_fun: Callable, optional
41
+ If provided, the fit of the I/O curve with the provided function will be computed and displayed.
42
+ Currently supported: pynibs.simgoid4, pynibs.sigmoid4_log, pynibs.linear
43
+ screenshot_fn: str, optional
44
+ If provided, output the plotted image to that file location.
45
+ interactive: bool
46
+ If True, spawn plot in blocking window.
47
+ mso : np.ndarray or list, optional
48
+ MSO values.
49
+
50
+ Returns
51
+ -------
52
+ True if successful, False otherwise.
53
+ """
54
+ if interactive:
55
+ matplotlib.use('Qt5Agg')
56
+ else:
57
+ matplotlib.use('Agg')
58
+
59
+ plt.figure(figsize=(11, 11))
60
+ plt.scatter(y=np.log10(mep_data) if fit_fun == pynibs.expio.fit_funs.sigmoid4_log else mep_data,
61
+ x=local_mag_e,
62
+ cmap=plt.cm.get_cmap('spring_r'),
63
+ c=mso if mso is not None else list(range(len(local_mag_e))),
64
+ vmin=0, vmax=100 if mso is not None else len(local_mag_e))
65
+
66
+ plt.scatter(y=np.log10(mep_data[-1]) if fit_fun == pynibs.expio.fit_funs.sigmoid4_log else mep_data[-1],
67
+ x=local_mag_e[-1],
68
+ cmap=plt.cm.get_cmap('spring_r'),
69
+ c=mso[-1] if mso is not None else len(local_mag_e),
70
+ s=500,
71
+ vmin=0, vmax=100 if mso is not None else len(local_mag_e))
72
+
73
+ if fit_fun is not None:
74
+ local_mag_e_np = np.zeros((len(local_mag_e), 1))
75
+ local_mag_e_np[:, 0] = local_mag_e
76
+
77
+ fit_scores, fit_params = pynibs.regression.regress_data(
78
+ e_matrix=np.array(local_mag_e_np),
79
+ mep=mep_data,
80
+ fun=fit_fun,
81
+ n_cpu=16,
82
+ con=None,
83
+ n_refit=10,
84
+ return_fits=True,
85
+ verbose=False,
86
+ pool=None,
87
+ refit_discontinuities=False,
88
+ select_signed_data=False
89
+ )
90
+
91
+ num_samples = 100
92
+ x = np.linspace(0, np.min((120, np.max(local_mag_e))), num_samples)
93
+ if fit_fun == pynibs.expio.fit_funs.sigmoid4_log or fit_fun == pynibs.expio.fit_funs.sigmoid4:
94
+ y = fit_fun(x,
95
+ x0=fit_params[0]['x0'],
96
+ r=fit_params[0]['r'],
97
+ amp=fit_params[0]['amp'],
98
+ y0=fit_params[0]['y0']
99
+ )
100
+ elif fit_fun == pynibs.expio.fit_funs.linear:
101
+ y = fit_fun(x,
102
+ m=fit_params[0]["m"],
103
+ n=fit_params[0]["n"]
104
+ )
105
+ else:
106
+ y = np.zeros(num_samples)
107
+
108
+ title += f", R2: {round(fit_scores[0], 5): <5}"
109
+ plt.plot(x, y, color='black')
110
+ props = dict(boxstyle='round', facecolor='wheat', alpha=0.5)
111
+ # for k, v in fit_params.items()
112
+ text = '\n'.join(f'{k: >7}: {np.round(v,2):2.2f}' for k, v in fit_params[0].items())
113
+ text = f'Fitted params:\n‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\n{text}\n n_zaps: {len(mep_data): >6}'
114
+ text += f'\n% MSO: {int(mso[-1]): >5}' if mso is not None else ''
115
+ plt.text(local_mag_e.max() - 5, 2, text, fontsize=18,
116
+ verticalalignment='top', bbox=props, horizontalalignment='right')
117
+ cb = plt.colorbar()
118
+ cb.ax.set_ylabel("% MSO" if mso is not None else "Stimulation Id", fontsize=18)
119
+
120
+ if screenshot_fn is not None:
121
+ if fit_fun == pynibs.expio.fit_funs.sigmoid4_log:
122
+ tick_log_rev = lambda x, y: int(pow(10, x))
123
+ ytick_formatter = ticker.FuncFormatter(tick_log_rev)
124
+ y_axis_label += ",\n log10-scaled"
125
+ ax = plt.gca()
126
+ ax.yaxis.set_major_formatter(ytick_formatter)
127
+
128
+ plt.title(title, fontsize=32)
129
+ plt.xlabel(x_axis_label, fontsize=30)
130
+ plt.ylabel(y_axis_label, fontsize=30)
131
+ plt.xticks(fontsize=28)
132
+ plt.yticks(fontsize=28)
133
+ plt.savefig(screenshot_fn)
134
+
135
+ if interactive:
136
+ plt.show()
137
+ plt.close()
@@ -0,0 +1,347 @@
1
+ import sys
2
+ import h5py
3
+ import os.path
4
+ import numpy as np
5
+
6
+
7
+ def render_coil_positions(
8
+ coil_conf_set_1_positions,
9
+ coil_conf_set_1_orientations,
10
+ coil_conf_set_2_positions=None,
11
+ coil_conf_set_2_orientations=None,
12
+ coil_conf_set_3_positions=None,
13
+ coil_conf_set_3_orientations=None,
14
+ fn_mesh=None,
15
+ surf_type="skin",
16
+ viewport_dim=(1280, 720),
17
+ camera_polar_coords=(-175, 66, 110),
18
+ screenshot_fn=None,
19
+ interactive=False):
20
+ """
21
+ Plots coil positions (and their orientation) in space,
22
+ optionally also display a second set of coil positions,
23
+ optionally also display a surface (e.g. gray matter or skin)
24
+
25
+ Parameters:
26
+ -----------
27
+ coil_conf_set_1_positions : np.ndarray
28
+ The "center" coordinates of the primary coil configurations
29
+ coil_conf_set_1_orientations : np.ndarray
30
+ Either the "m0", "m1" or "m2" orientation vector of the primary coil position.
31
+ coil_conf_set_2_positions : np.ndarray | None, optional
32
+ The "center" coordinates of the secondary coil configurations. Can be omitted.
33
+ coil_conf_set_2_orientations : np.ndarray | None, optional
34
+ Either the "m0", "m1" or "m2" orientation vector of the secondary coil position. Can be omitted.
35
+ fn_mesh : str, optional
36
+ Path to the mesh hdf5-file containing the geometry information of a head model. Can be omitted.
37
+ surf_type : str, optional
38
+ Name of the surface to display. Currently supported: "skin", "skull", "csf", "gm", "wm"
39
+ Only valid if 'fn_mesh' has been provided.
40
+
41
+ Returns:
42
+ --------
43
+ True if successful, False in case of a fatal error.
44
+ """
45
+ try:
46
+ import mayavi.mlab
47
+ except ModuleNotFoundError:
48
+ print("[render_3D.py] Error: Cannot import module mayavi. Most rendering functions will not work.")
49
+ if "mayavi" in sys.modules:
50
+ if not interactive:
51
+ mayavi.mlab.options.offscreen = True
52
+ else:
53
+ mayavi.mlab.options.offscreen = False
54
+
55
+ if not isinstance(viewport_dim, tuple) or len(viewport_dim) != 2:
56
+ viewport_dim = (1280, 720)
57
+ print(
58
+ "[render_coil_positions] Error: Provided viewport dimensions are not in the required format: (x,y),"
59
+ "expected 2 integer representing x,y coordinates of the viewport. Will use default dimensions instead:"
60
+ "(1280,720)")
61
+
62
+ random_figure_id = int(np.random.default_rng().random(1)[0] * 1e9)
63
+ fig = mayavi.mlab.figure(figure=random_figure_id, bgcolor=(1, 1, 1), engine=None, fgcolor=(0., 0., 0.),
64
+ size=viewport_dim)
65
+
66
+ if fn_mesh is not None and os.path.exists(fn_mesh):
67
+ with h5py.File(fn_mesh, 'r') as mesh_h5:
68
+
69
+ # tissue tags from SimNIBS v2 - v4
70
+ tissue_tag = 1005 # default to skin
71
+
72
+ if surf_type == "wm":
73
+ tissue_tag = 1001
74
+ elif surf_type == "gm":
75
+ tissue_tag = 1002
76
+ elif surf_type == "csf":
77
+ tissue_tag = 1003
78
+ elif surf_type == "skull":
79
+ tissue_tag = 1007
80
+ elif surf_type == "skin":
81
+ tissue_tag = 1005
82
+ else:
83
+ print(f"[render_coil_positions] Error: Unsupported surface type '{surf_type}'. Will "
84
+ f"render skin surface per default.")
85
+
86
+ mesh_tissue_type = np.array(mesh_h5["/mesh/elm/tri_tissue_type"])
87
+ mesh_tris = np.array(mesh_h5["/mesh/elm/node_number_list"][:, :3])
88
+ mesh_pts = np.array(mesh_h5["/mesh/nodes/node_coord"])
89
+ surf_tris_idcs = np.where(mesh_tissue_type == tissue_tag)
90
+ surf_tris = mesh_tris[surf_tris_idcs]
91
+
92
+ scalp_surf = mayavi.mlab.triangular_mesh(
93
+ mesh_pts[:, 0],
94
+ mesh_pts[:, 1],
95
+ mesh_pts[:, 2],
96
+ surf_tris,
97
+ representation='surface',
98
+ opacity=1,
99
+ color=(0.8, 0.8, 0.8)
100
+ )
101
+
102
+ if coil_conf_set_2_positions is not None and coil_conf_set_2_orientations is not None:
103
+ mayavi.mlab.points3d(
104
+ coil_conf_set_2_positions[:, 0],
105
+ coil_conf_set_2_positions[:, 1],
106
+ coil_conf_set_2_positions[:, 2],
107
+ scale_factor=0.3,
108
+ color=(.9, .9, .9)
109
+ )
110
+
111
+ mayavi.mlab.quiver3d(
112
+ coil_conf_set_2_positions[:, 0],
113
+ coil_conf_set_2_positions[:, 1],
114
+ coil_conf_set_2_positions[:, 2],
115
+ coil_conf_set_2_orientations[:, 0],
116
+ coil_conf_set_2_orientations[:, 1],
117
+ coil_conf_set_2_orientations[:, 2],
118
+ mode="arrow",
119
+ scale_factor=1,
120
+ color=(.9, .9, .9)
121
+ )
122
+ mayavi.mlab.points3d(
123
+ coil_conf_set_1_positions[:, 0],
124
+ coil_conf_set_1_positions[:, 1],
125
+ coil_conf_set_1_positions[:, 2],
126
+ scale_factor=0.5,
127
+ color=(1., .0, .0)
128
+ )
129
+
130
+ mayavi.mlab.quiver3d(
131
+ coil_conf_set_1_positions[:, 0],
132
+ coil_conf_set_1_positions[:, 1],
133
+ coil_conf_set_1_positions[:, 2],
134
+ coil_conf_set_1_orientations[:, 0],
135
+ coil_conf_set_1_orientations[:, 1],
136
+ coil_conf_set_1_orientations[:, 2],
137
+ mode="arrow",
138
+ scale_factor=2,
139
+ color=(1., .0, .0)
140
+ )
141
+
142
+ if coil_conf_set_3_positions is not None and coil_conf_set_3_orientations is not None:
143
+ mayavi.mlab.points3d(
144
+ coil_conf_set_3_positions[:, 0],
145
+ coil_conf_set_3_positions[:, 1],
146
+ coil_conf_set_3_positions[:, 2],
147
+ scale_factor=2,
148
+ color=(0.161, 0.812, 0.247)
149
+ )
150
+
151
+ mayavi.mlab.quiver3d(
152
+ coil_conf_set_3_positions[:, 0],
153
+ coil_conf_set_3_positions[:, 1],
154
+ coil_conf_set_3_positions[:, 2],
155
+ coil_conf_set_3_orientations[:, 0],
156
+ coil_conf_set_3_orientations[:, 1],
157
+ coil_conf_set_3_orientations[:, 2],
158
+ mode="arrow",
159
+ scale_factor=4,
160
+ color=(0.161, 0.812, 0.247)
161
+ )
162
+
163
+
164
+ if not isinstance(camera_polar_coords, tuple) or len(camera_polar_coords) != 3:
165
+ print(
166
+ "[render_coil_positions] Error: Provided camera coordinates are not in the required format: (a,p,r), "
167
+ "3 floats "
168
+ "representing azimuthal angle, polar angle and radius. Will use default coordinates instead:"
169
+ "(-175, 66, 110)")
170
+ camera_polar_coords = (-175, 66, 110)
171
+
172
+ mean_pts = np.mean(coil_conf_set_1_positions, axis=0)
173
+ mayavi.mlab.view(
174
+ azimuth=camera_polar_coords[0],
175
+ elevation=camera_polar_coords[1],
176
+ distance=camera_polar_coords[2],
177
+ focalpoint=np.array(mean_pts),
178
+ roll=None, reset_roll=None, figure=None
179
+ )
180
+
181
+ if screenshot_fn is not None:
182
+ # Bug? Must execute the command twice to generate an output screenshot of the requested dimensions.
183
+ # Otherwise its dimensions will be always (300,300).
184
+ mayavi.mlab.savefig(filename=screenshot_fn, figure=fig)
185
+ mayavi.mlab.savefig(filename=screenshot_fn, figure=fig)
186
+
187
+ if interactive:
188
+ mayavi.mlab.show()
189
+
190
+ # In theory, passing a reference of the to-be closed figure should be
191
+ # sufficient as well. In practice, it did not work reliably yielding
192
+ # error after too many calls to that function because of too many open
193
+ # figures. Introducing a (random) figure-ID and using this ID to refer
194
+ # to the figure seemed to have solved the isse.
195
+ mayavi.mlab.close(random_figure_id)
196
+ mayavi.mlab.close(all=True)
197
+
198
+ return True
199
+
200
+ else:
201
+ print("[render_coil_positions] Error: Required module 'mayavi' could be not successfully imported."
202
+ "Is it installed? Cannot execute 'render_data_on_surface'.")
203
+
204
+ return False
205
+
206
+
207
+ def render_data_on_surface(
208
+ points,
209
+ tris,
210
+ data,
211
+ viewport_dim=(1280, 720),
212
+ camera_polar_coords=(-175, 66, 110),
213
+ title=None,
214
+ data_name="Data",
215
+ colormap="jet",
216
+ screenshot_fn=None,
217
+ interactive=False
218
+ ):
219
+ """
220
+ Plots data on surface:
221
+ - If the number of data points equals the number of vertices in the mesh, the data will be displayed as point data.
222
+ - If the number of data points equals the number of triangles in the mesh, the data will be displayed as cell data.
223
+
224
+ Parameters
225
+ ----------
226
+ points : np.darray[float], [n_points x 3]
227
+ Points (vertices) of surface mesh.
228
+ tris : np.array[float], [n_tris x 3]
229
+ Connectivity list of triangles.
230
+ data : np.array[float], [n_tris]
231
+ Data in triangular center
232
+ viewport_dim : Tuple[int, int]
233
+ Size of the viewport. Default: (1280, 720)
234
+ camera_polar_coords : Tuple[float, float, float)
235
+ The coordinate of the camera around the object in polar coordinates: (azimuthal angle, polar angle, radius)
236
+ title : str
237
+ Tile of the rendering window.
238
+ data_name : str
239
+ Name of the visualized data set.
240
+ colormap : str
241
+ Identifier of the desired color map. Available colormaps are:
242
+ 'Accent', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'CMRmap', 'Dark2', 'GnBu', 'Greens', 'Greys'
243
+ 'OrRd', 'Oranges', 'PRGn', 'Paired', 'Pastel1', 'Pastel2', 'PiYG', 'PuBu', 'PuBuGn', 'PuOr',
244
+ 'PuRd', 'Purples', 'RdBu', 'RdGy', 'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Set1', 'Set2', 'Set3'
245
+ 'Spectral', 'Vega10', 'Vega20', 'Vega20b', 'Vega20c', 'Wistia', 'YlGn', 'YlGnBu', 'YlOrBr',
246
+ 'YlOrRd', 'afmhot', 'autumn', 'binary', 'black-white', 'blue-red', 'bone', 'brg', 'bwr',
247
+ 'cool', 'coolwarm', 'copper', 'cubehelix', 'file', 'flag', 'gist_earth', 'gist_gray',
248
+ 'gist_heat', 'gist_ncar', 'gist_rainbow', 'gist_stern', 'gist_yarg', 'gnuplot', 'gnuplot2', 'gray',
249
+ 'hot', 'hsv', 'inferno', 'jet', 'magma', 'nipy_spectral', 'ocean', 'pink', 'plasma', 'prism',
250
+ 'rainbow', 'seismic', 'spectral', 'spring', 'summer', 'terrain', 'viridis', 'winter'
251
+ screenshot_fn : str | None
252
+ If provided a screenshot will be saved to that path.
253
+ interactive : bool
254
+ If true, a blocking window will be spawned.
255
+
256
+ Returns
257
+ -------
258
+ True if successful, False in case of a fatal error.
259
+ """
260
+ try:
261
+ import mayavi.mlab
262
+ except ModuleNotFoundError:
263
+ print("[render_3D.py] Error: Cannot import module mayavi. Most rendering functions will not work.")
264
+ if "mayavi" in sys.modules:
265
+ if not interactive:
266
+ mayavi.mlab.options.offscreen = True
267
+ else:
268
+ mayavi.mlab.options.offscreen = False
269
+
270
+ if not isinstance(viewport_dim, tuple) or len(viewport_dim) != 2:
271
+ viewport_dim = (1280, 720)
272
+ print(
273
+ "[render_data_on_surface] Error: Provided viewport dimensions are not in the required format: (x,y),"
274
+ "expected 2 integer representing x,y coordinates of the viewport. Will use default dimensions instead:"
275
+ "(1280,720)")
276
+
277
+ random_figure_id = int(np.random.default_rng().random(1)[0] * 1e9)
278
+ fig = mayavi.mlab.figure(figure=random_figure_id, bgcolor=(1, 1, 1), engine=None, fgcolor=(0., 0., 0.),
279
+ size=viewport_dim)
280
+ mesh = mayavi.mlab.triangular_mesh(points[:, 0], points[:, 1], points[:, 2], tris, representation='wireframe',
281
+ opacity=0)
282
+
283
+ if data.shape[0] == tris.shape[0]:
284
+ mesh.mlab_source.dataset.cell_data.scalars = data
285
+ mesh.mlab_source.dataset.cell_data.scalars.name = data_name
286
+ mesh.mlab_source.update()
287
+ mesh.parent.update()
288
+ mesh = mayavi.mlab.pipeline.set_active_attribute(mesh, cell_scalars=data_name)
289
+ elif data.shape[0] == points.shape[0]:
290
+ mesh.mlab_source.dataset.point_data.scalars = data
291
+ mesh.mlab_source.dataset.point_data.scalars.name = data_name
292
+ mesh.mlab_source.update()
293
+ mesh.parent.update()
294
+ mesh = mayavi.mlab.pipeline.set_active_attribute(mesh, point_scalars=data_name)
295
+ else:
296
+ print(
297
+ "[render_data_on_surface] Error: Provided data does neither equal the number of ertices nor triangles "
298
+ "of the "
299
+ "surface mesh. Cannot map data onto mesh.")
300
+ return False
301
+
302
+ surf = mayavi.mlab.pipeline.surface(mesh, colormap=colormap, vmin=0, vmax=1)
303
+
304
+ if not isinstance(camera_polar_coords, tuple) or len(camera_polar_coords) != 3:
305
+ print(
306
+ "[render_data_on_surface] Error: Provided camera coordinates are not in the required format: (a,p,r), "
307
+ "3 floats "
308
+ "representing azimuthal angle, polar angle and radius. Will use default coordinates instead:"
309
+ "(-175, 66, 110)")
310
+ camera_polar_coords = (-175, 66, 110)
311
+
312
+ mean_pts = np.mean(points, axis=0)
313
+ mayavi.mlab.view(
314
+ azimuth=camera_polar_coords[0],
315
+ elevation=camera_polar_coords[1],
316
+ distance=camera_polar_coords[2],
317
+ focalpoint=np.array(mean_pts),
318
+ roll=None, reset_roll=None, figure=None
319
+ )
320
+
321
+ mayavi.mlab.colorbar(
322
+ surf,
323
+ title=title
324
+ )
325
+
326
+ if screenshot_fn is not None:
327
+ # Bug? Must execute the command twice to generate an output screenshot of the requested dimensions.
328
+ # Otherwise its dimensions will be always (300,300).
329
+ mayavi.mlab.savefig(filename=screenshot_fn, figure=fig)
330
+ mayavi.mlab.savefig(filename=screenshot_fn, figure=fig)
331
+
332
+ if interactive:
333
+ mayavi.mlab.show()
334
+
335
+ # In theory, passing a reference of the to-be closed figure should be
336
+ # sufficient as well. In practice, it did not work reliably yielding
337
+ # error after too many calls to that function because of too many open
338
+ # figures. Introducing a (random) figure-ID and using this ID to refer
339
+ # to the figure seemed to have solved the isse.
340
+ mayavi.mlab.close(random_figure_id)
341
+
342
+ return True
343
+ else:
344
+ print("[render_data_on_surface] Error: Required module 'mayavi' could not be successfully imported."
345
+ "Is it installed? Cannot execute 'render_data_on_surface'.")
346
+
347
+ return False