pyNIBS 0.2024.8__py3-none-any.whl → 0.2026.1__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.
- pynibs/__init__.py +26 -14
- pynibs/coil/__init__.py +6 -0
- pynibs/{coil.py → coil/coil.py} +213 -543
- pynibs/coil/export.py +508 -0
- pynibs/congruence/__init__.py +4 -1
- pynibs/congruence/congruence.py +37 -45
- pynibs/congruence/ext_metrics.py +40 -11
- pynibs/congruence/stimulation_threshold.py +1 -2
- pynibs/expio/Mep.py +120 -370
- pynibs/expio/__init__.py +10 -0
- pynibs/expio/brainsight.py +34 -37
- pynibs/expio/cobot.py +25 -25
- pynibs/expio/exp.py +10 -7
- pynibs/expio/fit_funs.py +3 -0
- pynibs/expio/invesalius.py +70 -0
- pynibs/expio/localite.py +190 -91
- pynibs/expio/neurone.py +139 -0
- pynibs/expio/signal_ced.py +345 -2
- pynibs/expio/visor.py +16 -15
- pynibs/freesurfer.py +34 -33
- pynibs/hdf5_io/hdf5_io.py +149 -132
- pynibs/hdf5_io/xdmf.py +35 -31
- pynibs/mesh/__init__.py +1 -1
- pynibs/mesh/mesh_struct.py +77 -92
- pynibs/mesh/transformations.py +121 -21
- pynibs/mesh/utils.py +191 -99
- pynibs/models/_TMS.py +2 -1
- pynibs/muap.py +1 -2
- pynibs/neuron/__init__.py +10 -0
- pynibs/neuron/models/mep.py +566 -0
- pynibs/neuron/neuron_regression.py +98 -8
- pynibs/optimization/__init__.py +12 -2
- pynibs/optimization/{optimization.py → coil_opt.py} +157 -133
- pynibs/optimization/multichannel.py +1174 -24
- pynibs/optimization/workhorses.py +7 -8
- pynibs/regression/__init__.py +4 -2
- pynibs/regression/dual_node_detection.py +229 -219
- pynibs/regression/regression.py +92 -61
- pynibs/roi/__init__.py +4 -1
- pynibs/roi/roi_structs.py +19 -21
- pynibs/roi/{roi.py → roi_utils.py} +56 -33
- pynibs/subject.py +24 -14
- pynibs/util/__init__.py +20 -4
- pynibs/util/dosing.py +4 -5
- pynibs/util/quality_measures.py +39 -38
- pynibs/util/rotations.py +116 -9
- pynibs/util/{simnibs.py → simnibs_io.py} +29 -19
- pynibs/util/{util.py → utils.py} +20 -22
- pynibs/visualization/para.py +4 -4
- pynibs/visualization/render_3D.py +4 -4
- pynibs-0.2026.1.dist-info/METADATA +105 -0
- pynibs-0.2026.1.dist-info/RECORD +69 -0
- {pyNIBS-0.2024.8.dist-info → pynibs-0.2026.1.dist-info}/WHEEL +1 -1
- pyNIBS-0.2024.8.dist-info/METADATA +0 -723
- pyNIBS-0.2024.8.dist-info/RECORD +0 -107
- pynibs/data/configuration_exp0.yaml +0 -59
- pynibs/data/configuration_linear_MEP.yaml +0 -61
- pynibs/data/configuration_linear_RT.yaml +0 -61
- pynibs/data/configuration_sigmoid4.yaml +0 -68
- pynibs/data/network mapping configuration/configuration guide.md +0 -238
- pynibs/data/network mapping configuration/configuration_TEMPLATE.yaml +0 -42
- pynibs/data/network mapping configuration/configuration_for_testing.yaml +0 -43
- pynibs/data/network mapping configuration/configuration_modelTMS.yaml +0 -43
- pynibs/data/network mapping configuration/configuration_reg_isi_05.yaml +0 -43
- pynibs/data/network mapping configuration/output_documentation.md +0 -185
- pynibs/data/network mapping configuration/recommendations_for_accuracy_threshold.md +0 -77
- pynibs/data/neuron/models/L23_PC_cADpyr_biphasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L23_PC_cADpyr_monophasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_LBC_biphasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_LBC_monophasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_NBC_biphasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_NBC_monophasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_SBC_biphasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_SBC_monophasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L5_TTPC2_cADpyr_biphasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L5_TTPC2_cADpyr_monophasic_v1.csv +0 -1281
- pynibs/tests/data/InstrumentMarker20200225163611937.xml +0 -19
- pynibs/tests/data/TriggerMarkers_Coil0_20200225163443682.xml +0 -14
- pynibs/tests/data/TriggerMarkers_Coil1_20200225170337572.xml +0 -6373
- pynibs/tests/data/Xdmf.dtd +0 -89
- pynibs/tests/data/brainsight_niiImage_nifticoord.txt +0 -145
- pynibs/tests/data/brainsight_niiImage_nifticoord_largefile.txt +0 -1434
- pynibs/tests/data/brainsight_niiImage_niifticoord_mixedtargets.txt +0 -47
- pynibs/tests/data/create_subject_testsub.py +0 -332
- pynibs/tests/data/data.hdf5 +0 -0
- pynibs/tests/data/geo.hdf5 +0 -0
- pynibs/tests/test_coil.py +0 -474
- pynibs/tests/test_elements2nodes.py +0 -100
- pynibs/tests/test_hdf5_io/test_xdmf.py +0 -61
- pynibs/tests/test_mesh_transformations.py +0 -123
- pynibs/tests/test_mesh_utils.py +0 -143
- pynibs/tests/test_nnav_imports.py +0 -101
- pynibs/tests/test_quality_measures.py +0 -117
- pynibs/tests/test_regressdata.py +0 -289
- pynibs/tests/test_roi.py +0 -17
- pynibs/tests/test_rotations.py +0 -86
- pynibs/tests/test_subject.py +0 -71
- pynibs/tests/test_util.py +0 -24
- /pynibs/{regression/score_types.py → neuron/models/m1_montbrio.py} +0 -0
- {pyNIBS-0.2024.8.dist-info → pynibs-0.2026.1.dist-info/licenses}/LICENSE +0 -0
- {pyNIBS-0.2024.8.dist-info → pynibs-0.2026.1.dist-info}/top_level.txt +0 -0
pynibs/{coil.py → coil/coil.py}
RENAMED
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
"""
|
|
2
|
-
All functions to operate on TMS coils go here, for example to create ``.xdmf`` files to visualize coil positions.
|
|
3
|
-
"""
|
|
4
1
|
import os
|
|
2
|
+
import tqdm
|
|
5
3
|
import copy
|
|
6
4
|
import math
|
|
7
5
|
import h5py
|
|
8
|
-
import shutil
|
|
9
6
|
import random
|
|
7
|
+
import shutil
|
|
10
8
|
import itertools
|
|
11
|
-
import pandas as pd
|
|
12
9
|
import numpy as np
|
|
13
|
-
import matplotlib.pyplot as plt
|
|
14
10
|
from scipy.spatial import Delaunay
|
|
15
11
|
from collections import OrderedDict
|
|
12
|
+
from matplotlib import pyplot as plt
|
|
16
13
|
|
|
17
14
|
import pynibs
|
|
18
15
|
|
|
@@ -59,7 +56,7 @@ def check_coil_position(points, hull):
|
|
|
59
56
|
valid : bool
|
|
60
57
|
Validity of coil position:
|
|
61
58
|
TRUE: valid
|
|
62
|
-
FALSE:
|
|
59
|
+
FALSE: invalid
|
|
63
60
|
"""
|
|
64
61
|
# make Delaunay grid if not already passed
|
|
65
62
|
if not isinstance(hull, Delaunay):
|
|
@@ -117,7 +114,7 @@ def calc_coil_transformation_matrix(LOC_mean, ORI_mean, LOC_var, ORI_var, V):
|
|
|
117
114
|
\\end{bmatrix}
|
|
118
115
|
"""
|
|
119
116
|
# calculate rotation matrix for angle variation (angle_var in deg)
|
|
120
|
-
rotation_matrix = pynibs.euler_angles_to_rotation_matrix(ORI_var * np.pi / 180.)
|
|
117
|
+
rotation_matrix = pynibs.util.rotations.euler_angles_to_rotation_matrix(ORI_var * np.pi / 180.)
|
|
121
118
|
|
|
122
119
|
# determine new orientation
|
|
123
120
|
ori = np.dot(ORI_mean, rotation_matrix)
|
|
@@ -196,7 +193,7 @@ def calc_coil_position_pdf(fn_rescon=None, fn_simpos=None, fn_exp=None, orientat
|
|
|
196
193
|
folder_pdfplots = ''
|
|
197
194
|
|
|
198
195
|
if fn_rescon and fn_simpos:
|
|
199
|
-
positions_all, conditions, position_list, _, _ = pynibs.read_exp_stimulations(fn_rescon, fn_simpos)
|
|
196
|
+
positions_all, conditions, position_list, _, _ = pynibs.expio.read_exp_stimulations(fn_rescon, fn_simpos)
|
|
200
197
|
|
|
201
198
|
# sort POSITIONS according to CONDITIONS, idx_con is alphabetically sorted, first index of condition[*]
|
|
202
199
|
# appeareance
|
|
@@ -382,7 +379,8 @@ def test_coil_position_gpc(parameters):
|
|
|
382
379
|
triangles = node_number_list[elm_type == 2, 0:3]
|
|
383
380
|
|
|
384
381
|
triangles = triangles[triangles_regions == 5]
|
|
385
|
-
surface_points = pynibs.unique_rows(np.reshape(points[triangles],
|
|
382
|
+
surface_points = pynibs.util.utils.unique_rows(np.reshape(points[triangles],
|
|
383
|
+
(3 * triangles.shape[0], 3)))
|
|
386
384
|
limits_scaling_factor = .1
|
|
387
385
|
|
|
388
386
|
# Generate Delaunay triangulation object
|
|
@@ -501,12 +499,12 @@ def get_invalid_coil_parameters(param_dict, coil_position_mean, svd_v, del_obj,
|
|
|
501
499
|
limits_theta = param_dict['theta']['limits']
|
|
502
500
|
limits_phi = param_dict['phi']['limits']
|
|
503
501
|
|
|
504
|
-
limits_pos_x = pynibs.add_center(limits_pos_x)
|
|
505
|
-
limits_pos_y = pynibs.add_center(limits_pos_y)
|
|
506
|
-
limits_pos_z = pynibs.add_center(limits_pos_z)
|
|
507
|
-
limits_psi = pynibs.add_center(limits_psi)
|
|
508
|
-
limits_theta = pynibs.add_center(limits_theta)
|
|
509
|
-
limits_phi = pynibs.add_center(limits_phi)
|
|
502
|
+
limits_pos_x = pynibs.util.utils.add_center(limits_pos_x)
|
|
503
|
+
limits_pos_y = pynibs.util.utils.add_center(limits_pos_y)
|
|
504
|
+
limits_pos_z = pynibs.util.utils.add_center(limits_pos_z)
|
|
505
|
+
limits_psi = pynibs.util.utils.add_center(limits_psi)
|
|
506
|
+
limits_theta = pynibs.util.utils.add_center(limits_theta)
|
|
507
|
+
limits_phi = pynibs.util.utils.add_center(limits_phi)
|
|
510
508
|
|
|
511
509
|
temp_list = [[0, 1, 2], [0, 1, 2], [0, 1, 2], [0, 1, 2], [0, 1, 2], [0, 1, 2]]
|
|
512
510
|
combinations = list(itertools.product(*temp_list))
|
|
@@ -582,513 +580,10 @@ def get_invalid_coil_parameters(param_dict, coil_position_mean, svd_v, del_obj,
|
|
|
582
580
|
return fail_params
|
|
583
581
|
|
|
584
582
|
|
|
585
|
-
def create_stimsite_hdf5(fn_exp, fn_hdf, conditions_selected=None, sep="_", merge_sites=False, fix_angles=False,
|
|
586
|
-
data_dict=None, conditions_ignored=None):
|
|
587
|
-
"""
|
|
588
|
-
Reads results_conditions and creates an hdf5/xdmf pair with condition-wise centers of stimulation sites and
|
|
589
|
-
coil directions as data.
|
|
590
|
-
|
|
591
|
-
Parameters
|
|
592
|
-
----------
|
|
593
|
-
fn_exp : str
|
|
594
|
-
Path to results.csv.
|
|
595
|
-
fn_hdf : str
|
|
596
|
-
Path where to write file. Gets overridden if already existing.
|
|
597
|
-
conditions_selected : str or list of str, optional
|
|
598
|
-
List of conditions returned by the function, the others are omitted.
|
|
599
|
-
If None, all conditions are returned.
|
|
600
|
-
sep: str, default: "_"
|
|
601
|
-
Separator between condition label and angle (e.g. M1_0, or M1-0).
|
|
602
|
-
merge_sites : bool
|
|
603
|
-
If true, only one coil center per site is generated.
|
|
604
|
-
fix_angles : bool
|
|
605
|
-
rename 22.5 -> 0, 0 -> -45, 67.5 -> 90, 90 -> 135.
|
|
606
|
-
data_dict : dict ofnp.ndarray of float [n_stimsites] (optional), default: None
|
|
607
|
-
Dictionary containing data corresponding to the stimulation sites (keys).
|
|
608
|
-
conditions_ignored : str or list of str, optional
|
|
609
|
-
Conditions, which are not going to be included in the plot.
|
|
610
|
-
|
|
611
|
-
Returns
|
|
612
|
-
-------
|
|
613
|
-
<Files> : hdf5/xdmf file pair
|
|
614
|
-
Contains information about condition-wise stimulation sites and coil directions (fn_hdf)
|
|
615
|
-
|
|
616
|
-
Example
|
|
617
|
-
-------
|
|
618
|
-
.. code-block:: python
|
|
619
|
-
|
|
620
|
-
pynibs.create_stimsite_hdf5('/exp/1/experiment_corrected.csv',
|
|
621
|
-
'/stimsite', True, True)
|
|
622
|
-
"""
|
|
623
|
-
assert not fn_hdf.endswith('/')
|
|
624
|
-
|
|
625
|
-
exp = pynibs.read_csv(fn_exp)
|
|
626
|
-
|
|
627
|
-
exp_cond = pynibs.sort_by_condition(exp, conditions_selected=conditions_selected) # []
|
|
628
|
-
|
|
629
|
-
# get the unique conditions in the correct order
|
|
630
|
-
conds = [c['condition'][0] for c in exp_cond]
|
|
631
|
-
|
|
632
|
-
# remove conds
|
|
633
|
-
conds_temp = []
|
|
634
|
-
exp_cond_temp = []
|
|
635
|
-
|
|
636
|
-
if type(conditions_ignored) is not list:
|
|
637
|
-
conditions_ignored = [conditions_ignored]
|
|
638
|
-
|
|
639
|
-
for i_c, c in enumerate(conds):
|
|
640
|
-
ignore = False
|
|
641
|
-
for ci in list(conditions_ignored):
|
|
642
|
-
if c == ci:
|
|
643
|
-
ignore = True
|
|
644
|
-
|
|
645
|
-
if not ignore:
|
|
646
|
-
conds_temp.append(conds[i_c])
|
|
647
|
-
exp_cond_temp.append(exp_cond[i_c])
|
|
648
|
-
|
|
649
|
-
exp_cond = exp_cond_temp
|
|
650
|
-
conds = conds_temp
|
|
651
|
-
|
|
652
|
-
# hardcoded row #3 is condition
|
|
653
|
-
cond_idx = np.linspace(0, len(exp_cond), 1)[:, np.newaxis]
|
|
654
|
-
|
|
655
|
-
centers = []
|
|
656
|
-
m0 = []
|
|
657
|
-
m1 = []
|
|
658
|
-
m2 = []
|
|
659
|
-
|
|
660
|
-
for i_cond in range(len(exp_cond)):
|
|
661
|
-
centers.append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 3])
|
|
662
|
-
m0.append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 0])
|
|
663
|
-
m1.append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 1])
|
|
664
|
-
m2.append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 2])
|
|
665
|
-
|
|
666
|
-
# split conds to angles and sites: M1_90 -> M1, 90
|
|
667
|
-
angles = np.array([sp.split(sep)[-1] for sp in conds]).astype(np.float64)
|
|
668
|
-
sites = np.array([sp.split(sep)[0] for sp in conds])
|
|
669
|
-
sites_unique = np.unique(sites)
|
|
670
|
-
|
|
671
|
-
# average the center positions of a stimulation site over all orientations
|
|
672
|
-
if merge_sites:
|
|
673
|
-
|
|
674
|
-
# generate sites dict
|
|
675
|
-
centers_sites = dict()
|
|
676
|
-
|
|
677
|
-
for site in sites_unique:
|
|
678
|
-
centers_sites[site] = []
|
|
679
|
-
|
|
680
|
-
# gather all orientations and put them to the corresponding sites
|
|
681
|
-
for i_cond, site in enumerate(sites):
|
|
682
|
-
centers_sites[site].append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 3])
|
|
683
|
-
|
|
684
|
-
# determine average position over all orientations for each site
|
|
685
|
-
for site in sites_unique:
|
|
686
|
-
centers_sites[site] = np.mean(np.vstack(centers_sites[site]), axis=0)
|
|
687
|
-
|
|
688
|
-
# write it back to centers
|
|
689
|
-
for i_cond, site in enumerate(sites):
|
|
690
|
-
centers[i_cond] = centers_sites[site]
|
|
691
|
-
|
|
692
|
-
centers = np.vstack(centers)
|
|
693
|
-
m0 = np.vstack(m0)
|
|
694
|
-
m1 = np.vstack(m1)
|
|
695
|
-
m2 = np.vstack(m2)
|
|
696
|
-
|
|
697
|
-
# enumerate sites, as paraview does not plot string array data
|
|
698
|
-
sites_idx = np.array(list(range(len(sites))))[:, np.newaxis]
|
|
699
|
-
|
|
700
|
-
angles[angles[:] == 675.] = 67.5
|
|
701
|
-
angles[angles[:] == 225.] = 22.5
|
|
702
|
-
|
|
703
|
-
if fix_angles:
|
|
704
|
-
# rename wrong angle names
|
|
705
|
-
angles_cor = np.copy(angles)
|
|
706
|
-
angles_cor[angles == 0] = -45.
|
|
707
|
-
angles_cor[angles == 22.5] = 0.
|
|
708
|
-
angles_cor[angles == 67.5] = 90.
|
|
709
|
-
angles_cor[angles == 90] = 135.
|
|
710
|
-
angles = angles_cor
|
|
711
|
-
|
|
712
|
-
# write hdf5 file
|
|
713
|
-
if not fn_hdf.endswith('.hdf5'):
|
|
714
|
-
fn_hdf += '.hdf5'
|
|
715
|
-
f = h5py.File(fn_hdf, 'w')
|
|
716
|
-
f.create_dataset('centers', data=centers.astype(np.float64))
|
|
717
|
-
f.create_dataset('m0', data=m0.astype(np.float64))
|
|
718
|
-
f.create_dataset('m1', data=m1.astype(np.float64))
|
|
719
|
-
f.create_dataset('m2', data=m2.astype(np.float64))
|
|
720
|
-
f.create_dataset('cond', data=np.string_(conds)) # this is a string array, not xdmf compatible
|
|
721
|
-
f.create_dataset('cond_idx', data=cond_idx)
|
|
722
|
-
f.create_dataset('angles', data=angles)
|
|
723
|
-
f.create_dataset('sites', data=np.string_(sites)) # this is a string array, not xdmf compatible
|
|
724
|
-
f.create_dataset('sites_idx', data=sites_idx)
|
|
725
|
-
|
|
726
|
-
data = None
|
|
727
|
-
if data_dict is not None:
|
|
728
|
-
data = np.zeros((len(list(data_dict.keys())), 1))
|
|
729
|
-
for i_data, cond in enumerate(conds):
|
|
730
|
-
data[i_data, 0] = data_dict[cond]
|
|
731
|
-
f.create_dataset('data', data=data)
|
|
732
|
-
f.close()
|
|
733
|
-
|
|
734
|
-
# write .xdmf file
|
|
735
|
-
f = open(fn_hdf[:-4] + 'xdmf', 'w')
|
|
736
|
-
fn_hdf = os.path.basename(fn_hdf) # relative links
|
|
737
|
-
|
|
738
|
-
# header
|
|
739
|
-
f.write('<?xml version="1.0"?>\n')
|
|
740
|
-
f.write('<!DOCTYPE Xdmf SYSTEM "Xdmf.dtd" []>\n')
|
|
741
|
-
f.write('<Xdmf Version="2.0" xmlns:xi="http://www.w3.org/2001/XInclude">\n')
|
|
742
|
-
f.write('<Domain>\n')
|
|
743
|
-
f.write('<Grid\nCollectionType="Spatial"\nGridType="Collection"\nName="Collection">\n')
|
|
744
|
-
|
|
745
|
-
# one grid for coil dipole nodes...store data hdf5.
|
|
746
|
-
#######################################################
|
|
747
|
-
f.write('<Grid Name="stimsites" GridType="Uniform">\n')
|
|
748
|
-
f.write('<Topology NumberOfElements="' + str(centers.shape[0]) +
|
|
749
|
-
'" TopologyType="Polyvertex" Name="Tri">\n')
|
|
750
|
-
f.write('<DataItem Format="XML" Dimensions="' + str(centers.shape[0]) + ' 1">\n')
|
|
751
|
-
# f.write(hdf5_fn + ':' + path + '/triangle_number_list\n')
|
|
752
|
-
np.savetxt(f, list(range(centers.shape[0])), fmt='%d', delimiter=' ') # 1 2 3 4 ... N_Points
|
|
753
|
-
f.write('</DataItem>\n')
|
|
754
|
-
f.write('</Topology>\n')
|
|
755
|
-
|
|
756
|
-
# nodes
|
|
757
|
-
f.write('<Geometry GeometryType="XYZ">\n')
|
|
758
|
-
f.write('<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 3">\n')
|
|
759
|
-
f.write(fn_hdf + ':' + '/centers\n')
|
|
760
|
-
f.write('</DataItem>\n')
|
|
761
|
-
f.write('</Geometry>\n')
|
|
762
|
-
|
|
763
|
-
# data
|
|
764
|
-
# dipole magnitude
|
|
765
|
-
# the 4 vectors
|
|
766
|
-
for i in range(3):
|
|
767
|
-
f.write('<Attribute Name="dir_' + str(i) + '" AttributeType="Vector" Center="Cell">\n')
|
|
768
|
-
f.write('<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 3">\n')
|
|
769
|
-
f.write(fn_hdf + ':' + '/m' + str(i) + '\n')
|
|
770
|
-
f.write('</DataItem>\n')
|
|
771
|
-
f.write('</Attribute>\n\n')
|
|
772
|
-
|
|
773
|
-
# angles
|
|
774
|
-
f.write('<Attribute Name="angles" AttributeType="Scalar" Center="Cell">\n')
|
|
775
|
-
f.write('<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 1">\n')
|
|
776
|
-
f.write(fn_hdf + ':' + '/angles\n')
|
|
777
|
-
f.write('</DataItem>\n')
|
|
778
|
-
f.write('</Attribute>\n\n')
|
|
779
|
-
|
|
780
|
-
# data
|
|
781
|
-
if data_dict is not None:
|
|
782
|
-
f.write('<Attribute Name="data" AttributeType="Scalar" Center="Cell">\n')
|
|
783
|
-
f.write('<DataItem Format="HDF" Dimensions="' + str(data.shape[0]) + ' 1">\n')
|
|
784
|
-
f.write(fn_hdf + ':' + '/data\n')
|
|
785
|
-
f.write('</DataItem>\n')
|
|
786
|
-
f.write('</Attribute>\n\n')
|
|
787
|
-
|
|
788
|
-
# site idx
|
|
789
|
-
f.write('<Attribute Name="sites_idx" AttributeType="Scalar" Center="Cell">\n')
|
|
790
|
-
f.write('<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 1">\n')
|
|
791
|
-
f.write(fn_hdf + ':' + '/sites_idx\n')
|
|
792
|
-
f.write('</DataItem>\n')
|
|
793
|
-
f.write('</Attribute>\n\n')
|
|
794
|
-
|
|
795
|
-
f.write('</Grid>\n')
|
|
796
|
-
# end coil dipole data
|
|
797
|
-
|
|
798
|
-
# footer
|
|
799
|
-
f.write('</Grid>\n')
|
|
800
|
-
f.write('</Domain>\n')
|
|
801
|
-
f.write('</Xdmf>\n')
|
|
802
|
-
f.close()
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
def create_stimsite_from_list(fn_hdf, poslist, datanames=None, data=None, overwrite=False):
|
|
806
|
-
"""
|
|
807
|
-
This takes a list of matsimnibs-style coil position and orientations and creates an .hdf5 + .xdmf tuple
|
|
808
|
-
for all positions.
|
|
809
|
-
|
|
810
|
-
Centers and coil orientations are written to disk, with optional data for each coil configuration.
|
|
811
|
-
|
|
812
|
-
Parameters
|
|
813
|
-
----------
|
|
814
|
-
fn_hdf: str
|
|
815
|
-
Filename for the .hdf5 file. The .xdmf is saved with the same basename.
|
|
816
|
-
Folder should already exist.
|
|
817
|
-
poslist: list of np.ndarray
|
|
818
|
-
(4,4) Positions.
|
|
819
|
-
datanames: str or list of str, optional
|
|
820
|
-
Dataset names for ``data``.
|
|
821
|
-
data: np.ndarray, optional
|
|
822
|
-
Dataset array with shape = ``(len(poslist.pos), len(datanames())``.
|
|
823
|
-
overwrite : bool, defaul: False
|
|
824
|
-
Overwrite existing files.
|
|
825
|
-
"""
|
|
826
|
-
centers = []
|
|
827
|
-
m0 = []
|
|
828
|
-
m1 = []
|
|
829
|
-
m2 = []
|
|
830
|
-
if data is not None:
|
|
831
|
-
assert isinstance(data, np.ndarray)
|
|
832
|
-
|
|
833
|
-
for lst in poslist:
|
|
834
|
-
centers.append(lst[0:3, 3])
|
|
835
|
-
m0.append(lst[0:3, 0])
|
|
836
|
-
m1.append(lst[0:3, 1])
|
|
837
|
-
m2.append(lst[0:3, 2])
|
|
838
|
-
|
|
839
|
-
centers = np.vstack(centers)
|
|
840
|
-
m0 = np.vstack(m0)
|
|
841
|
-
m1 = np.vstack(m1)
|
|
842
|
-
m2 = np.vstack(m2)
|
|
843
|
-
|
|
844
|
-
write_coil_pos_hdf5(fn_hdf, centers, m0, m1, m2, datanames=datanames, data=data, overwrite=overwrite)
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
def create_stimsite_from_tmslist(fn_hdf, poslist, datanames=None, data=None, overwrite=False):
|
|
848
|
-
"""
|
|
849
|
-
This takes a :py:class:simnibs.sim_struct.TMSLIST from simnibs and creates an .hdf5 + .xdmf tuple for all positions.
|
|
850
|
-
|
|
851
|
-
Centers and coil orientations are written to disk, with optional data for each coil configuration.
|
|
852
|
-
|
|
853
|
-
Parameters
|
|
854
|
-
----------
|
|
855
|
-
fn_hdf: str
|
|
856
|
-
Filename for the .hdf5 file. The .xdmf is saved with the same basename.
|
|
857
|
-
Folder should already exist.
|
|
858
|
-
poslist: simnibs.sim_struct.TMSLIST
|
|
859
|
-
poslist.pos[*].matsimnibs have to be set.
|
|
860
|
-
datanames: str or list of str, optional
|
|
861
|
-
Dataset names for ``data``.
|
|
862
|
-
data: np.ndarray, optional
|
|
863
|
-
Dataset array with shape = ``(len(poslist.pos), len(datanames())``.
|
|
864
|
-
overwrite : bool, default: False
|
|
865
|
-
Overwrite existing files
|
|
866
|
-
"""
|
|
867
|
-
centers = []
|
|
868
|
-
m0 = []
|
|
869
|
-
m1 = []
|
|
870
|
-
m2 = []
|
|
871
|
-
assert poslist.pos
|
|
872
|
-
if data is not None:
|
|
873
|
-
assert isinstance(data, np.ndarray)
|
|
874
|
-
for pos in poslist.pos:
|
|
875
|
-
assert pos.matsimnibs is not None
|
|
876
|
-
pos.matsimnibs = np.array(pos.matsimnibs)
|
|
877
|
-
centers.append(pos.matsimnibs[0:3, 3])
|
|
878
|
-
m0.append(pos.matsimnibs[0:3, 0])
|
|
879
|
-
m1.append(pos.matsimnibs[0:3, 1])
|
|
880
|
-
m2.append(pos.matsimnibs[0:3, 2])
|
|
881
|
-
|
|
882
|
-
centers = np.vstack(centers)
|
|
883
|
-
m0 = np.vstack(m0)
|
|
884
|
-
m1 = np.vstack(m1)
|
|
885
|
-
m2 = np.vstack(m2)
|
|
886
|
-
|
|
887
|
-
write_coil_pos_hdf5(fn_hdf, centers, m0, m1, m2, datanames=datanames, data=data, overwrite=overwrite)
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
def create_stimsite_from_exp_hdf5(fn_exp, fn_hdf, datanames=None, data=None, overwrite=False):
|
|
891
|
-
"""
|
|
892
|
-
This takes an experiment.hdf5 file and creates an .hdf5 + .xdmf tuple for all coil positions for visualization.
|
|
893
|
-
|
|
894
|
-
Parameters
|
|
895
|
-
----------
|
|
896
|
-
fn_exp : str
|
|
897
|
-
Path to experiment.hdf5
|
|
898
|
-
fn_hdf : str
|
|
899
|
-
Filename for the resulting .hdf5 file. The .xdmf is saved with the same basename.
|
|
900
|
-
Folder should already exist.
|
|
901
|
-
datanames : str or list of str, optional
|
|
902
|
-
Dataset names for ``data``
|
|
903
|
-
data : np.ndarray, optional
|
|
904
|
-
Dataset array with shape = ``(len(poslist.pos), len(datanames())``.
|
|
905
|
-
overwrite : bool, default: False
|
|
906
|
-
Overwrite existing files.
|
|
907
|
-
"""
|
|
908
|
-
df_stim = pd.read_hdf(fn_exp, "stim_data")
|
|
909
|
-
|
|
910
|
-
matsimnibs = np.zeros((4, 4, df_stim.shape[0]))
|
|
911
|
-
|
|
912
|
-
for i in range(df_stim.shape[0]):
|
|
913
|
-
matsimnibs[:, :, i] = df_stim["coil_mean"].iloc[i]
|
|
914
|
-
|
|
915
|
-
create_stimsite_from_matsimnibs(fn_hdf=fn_hdf,
|
|
916
|
-
matsimnibs=matsimnibs,
|
|
917
|
-
datanames=datanames,
|
|
918
|
-
data=data,
|
|
919
|
-
overwrite=overwrite)
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
def create_stimsite_from_matsimnibs(fn_hdf, matsimnibs, datanames=None, data=None, overwrite=False):
|
|
923
|
-
"""
|
|
924
|
-
This takes a matsimnibs array and creates an .hdf5 + .xdmf tuple for all coil positions for visualization.
|
|
925
|
-
|
|
926
|
-
Centers and coil orientations are written disk.
|
|
927
|
-
|
|
928
|
-
Parameters
|
|
929
|
-
----------
|
|
930
|
-
fn_hdf: str
|
|
931
|
-
Filename for the .hdf5 file. The .xdmf is saved with the same basename.
|
|
932
|
-
Folder should already exist.
|
|
933
|
-
matsimnibs: np.ndarray
|
|
934
|
-
(4, 4, n_pos)
|
|
935
|
-
Matsimnibs matrices containing the coil orientation (x,y,z) and position (p)
|
|
936
|
-
|
|
937
|
-
.. math::
|
|
938
|
-
\\begin{bmatrix}
|
|
939
|
-
| & | & | & | \\\\
|
|
940
|
-
x & y & z & p \\\\
|
|
941
|
-
| & | & | & | \\\\
|
|
942
|
-
0 & 0 & 0 & 1 \\\\
|
|
943
|
-
\\end{bmatrix}
|
|
944
|
-
datanames: str or list of str, optional
|
|
945
|
-
Dataset names for ``data``.
|
|
946
|
-
data: np.ndarray, optional
|
|
947
|
-
(len(poslist.pos), len(datanames).
|
|
948
|
-
overwrite : bool, default: False
|
|
949
|
-
Overwrite existing files.
|
|
950
|
-
"""
|
|
951
|
-
matsimnibs = np.atleast_3d(matsimnibs)
|
|
952
|
-
n_pos = matsimnibs.shape[2]
|
|
953
|
-
centers = np.zeros((n_pos, 3))
|
|
954
|
-
m0 = np.zeros((n_pos, 3))
|
|
955
|
-
m1 = np.zeros((n_pos, 3))
|
|
956
|
-
m2 = np.zeros((n_pos, 3))
|
|
957
|
-
if data is not None:
|
|
958
|
-
assert isinstance(data, np.ndarray)
|
|
959
|
-
|
|
960
|
-
for i in range(matsimnibs.shape[2]):
|
|
961
|
-
centers[i, :] = matsimnibs[0:3, 3, i]
|
|
962
|
-
m0[i, :] = matsimnibs[0:3, 0, i]
|
|
963
|
-
m1[i, :] = matsimnibs[0:3, 1, i]
|
|
964
|
-
m2[i, :] = matsimnibs[0:3, 2, i]
|
|
965
|
-
|
|
966
|
-
write_coil_pos_hdf5(fn_hdf, centers, m0, m1, m2, datanames=datanames, data=data, overwrite=overwrite)
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
def write_coil_pos_hdf5(fn_hdf, centers, m0, m1, m2, datanames=None, data=None, overwrite=False):
|
|
970
|
-
"""
|
|
971
|
-
Creates a ``.hdf5`` + ``.xdmf`` file tuple for all coil positions.
|
|
972
|
-
Coil centers and coil orientations are saved, and - optionally - data for each position if ``data`` and
|
|
973
|
-
``datanames`` are provided.
|
|
974
|
-
|
|
975
|
-
Parameters
|
|
976
|
-
----------
|
|
977
|
-
fn_hdf : str
|
|
978
|
-
Filename for the .hdf5 file. The .xdmf is saved with the same basename.
|
|
979
|
-
Folder should already exist.
|
|
980
|
-
centers : np.ndarray of float
|
|
981
|
-
(n_pos, 3) Coil positions.
|
|
982
|
-
m0 : np.ndarray of float
|
|
983
|
-
(n_pos, 3) Coil orientation x-axis (looking at the active (patient) side of the coil pointing to the right).
|
|
984
|
-
m1 : np.ndarray of float
|
|
985
|
-
(n_pos, 3) Coil orientation y-axis (looking at the active side of the coil pointing up away from the handle).
|
|
986
|
-
m2 : np.ndarray of float
|
|
987
|
-
(n_pos, 3) Coil orientation z-axis (looking at the active (patient) side of the coil pointing to the patient).
|
|
988
|
-
datanames : str or list of str, optional
|
|
989
|
-
(n_data) Dataset names for ``data``
|
|
990
|
-
data : np.ndarray, optional
|
|
991
|
-
(n_pos, n_data) Dataset array with (len(poslist.pos), len(datanames()).
|
|
992
|
-
overwrite : bool, default: False
|
|
993
|
-
Overwrite existing files.
|
|
994
|
-
"""
|
|
995
|
-
n_pos = centers.shape[0]
|
|
996
|
-
if isinstance(datanames, str):
|
|
997
|
-
datanames = [datanames]
|
|
998
|
-
|
|
999
|
-
if data is not None:
|
|
1000
|
-
if datanames is None:
|
|
1001
|
-
raise ValueError("Provide datanames= with data= argument.")
|
|
1002
|
-
if isinstance(datanames, str):
|
|
1003
|
-
datanames = [datanames]
|
|
1004
|
-
if len(data.shape) <= 1:
|
|
1005
|
-
data = np.atleast_1d(data)[:, np.newaxis]
|
|
1006
|
-
assert data.shape == (n_pos, len(datanames))
|
|
1007
|
-
if datanames is not None and data is None:
|
|
1008
|
-
raise ValueError("Provide data= with datanames= argument.")
|
|
1009
|
-
|
|
1010
|
-
m0_reshaped = np.hstack((m0, np.zeros((n_pos, 1)))).T[:, np.newaxis, :]
|
|
1011
|
-
m1_reshaped = np.hstack((m1, np.zeros((n_pos, 1)))).T[:, np.newaxis, :]
|
|
1012
|
-
m2_reshaped = np.hstack((m2, np.zeros((n_pos, 1)))).T[:, np.newaxis, :]
|
|
1013
|
-
centers_reshaped = np.hstack((centers, np.ones((n_pos, 1)))).T[:, np.newaxis, :]
|
|
1014
|
-
|
|
1015
|
-
matsimnibs = np.concatenate((m0_reshaped, m1_reshaped, m2_reshaped, centers_reshaped), axis=1)
|
|
1016
|
-
|
|
1017
|
-
# write hdf5 file
|
|
1018
|
-
if not fn_hdf.endswith('.hdf5'):
|
|
1019
|
-
fn_hdf += '.hdf5'
|
|
1020
|
-
if os.path.exists(fn_hdf) and not overwrite:
|
|
1021
|
-
raise OSError(fn_hdf + " already exists. Set overwrite flag for create_stimsite_from_poslist.")
|
|
1022
|
-
|
|
1023
|
-
with h5py.File(fn_hdf, 'w') as f:
|
|
1024
|
-
f.create_dataset('centers', data=centers.astype(np.float64))
|
|
1025
|
-
f.create_dataset('m0', data=m0.astype(np.float64))
|
|
1026
|
-
f.create_dataset('m1', data=m1.astype(np.float64))
|
|
1027
|
-
f.create_dataset('m2', data=m2.astype(np.float64))
|
|
1028
|
-
f.create_dataset("matsimnibs", data=matsimnibs)
|
|
1029
|
-
|
|
1030
|
-
if data is not None:
|
|
1031
|
-
for i, col in enumerate(data.T):
|
|
1032
|
-
f.create_dataset('/data/' + datanames[i], data=col)
|
|
1033
|
-
|
|
1034
|
-
# write .xdmf file
|
|
1035
|
-
with open(fn_hdf[:-4] + 'xdmf', 'w') as f:
|
|
1036
|
-
fn_hdf = os.path.basename(fn_hdf) # relative links
|
|
1037
|
-
|
|
1038
|
-
# header
|
|
1039
|
-
f.write('<?xml version="1.0"?>\n')
|
|
1040
|
-
f.write('<!DOCTYPE Xdmf SYSTEM "Xdmf.dtd" []>\n')
|
|
1041
|
-
f.write('<Xdmf Version="2.0" xmlns:xi="http://www.w3.org/2001/XInclude">\n')
|
|
1042
|
-
f.write('<Domain>\n')
|
|
1043
|
-
f.write('<Grid CollectionType="Spatial" GridType="Collection" Name="Collection">\n')
|
|
1044
|
-
|
|
1045
|
-
# one grid for coil dipole nodes...store data hdf5.
|
|
1046
|
-
#######################################################
|
|
1047
|
-
f.write('<Grid Name="stimsites" GridType="Uniform">\n')
|
|
1048
|
-
f.write(f'<Topology NumberOfElements="{centers.shape[0]}" TopologyType="Polyvertex" Name="Tri">\n')
|
|
1049
|
-
f.write(f'\t<DataItem Format="XML" Dimensions="{centers.shape[0]} 1">\n')
|
|
1050
|
-
np.savetxt(f, list(range(centers.shape[0])), fmt='\t%d', delimiter=' ') # 1 2 3 4 ... N_Points
|
|
1051
|
-
f.write('\t</DataItem>\n')
|
|
1052
|
-
f.write('</Topology>\n\n')
|
|
1053
|
-
|
|
1054
|
-
# nodes
|
|
1055
|
-
f.write('<Geometry GeometryType="XYZ">\n')
|
|
1056
|
-
f.write(f'\t<DataItem Format="HDF" Dimensions="{centers.shape[0]} 3">\n')
|
|
1057
|
-
f.write(f'\t{fn_hdf}:/centers\n')
|
|
1058
|
-
f.write('\t</DataItem>\n')
|
|
1059
|
-
f.write('</Geometry>\n\n')
|
|
1060
|
-
|
|
1061
|
-
# data
|
|
1062
|
-
# dipole magnitude
|
|
1063
|
-
# the 4 vectors
|
|
1064
|
-
for i in range(3):
|
|
1065
|
-
f.write(f'\t\t<Attribute Name="dir_{i}" AttributeType="Vector" Center="Cell">\n')
|
|
1066
|
-
f.write(f'\t\t\t<DataItem Format="HDF" Dimensions="{centers.shape[0]} 3">\n')
|
|
1067
|
-
f.write(f'\t\t\t{fn_hdf}:/m{i}\n')
|
|
1068
|
-
f.write('\t\t\t</DataItem>\n')
|
|
1069
|
-
f.write('\t\t</Attribute>\n\n')
|
|
1070
|
-
|
|
1071
|
-
if data is not None:
|
|
1072
|
-
for i, col in enumerate(data.T):
|
|
1073
|
-
f.write(f'\t\t<Attribute Name="{datanames[i]}" AttributeType="Scalar" Center="Cell">\n')
|
|
1074
|
-
f.write('\t\t\t<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 1">\n')
|
|
1075
|
-
f.write(f'\t\t\t{fn_hdf}:/data/{datanames[i]}\n')
|
|
1076
|
-
f.write('\t\t\t</DataItem>\n')
|
|
1077
|
-
f.write('\t\t</Attribute>\n\n')
|
|
1078
|
-
|
|
1079
|
-
f.write('</Grid>\n')
|
|
1080
|
-
# end coil dipole data
|
|
1081
|
-
|
|
1082
|
-
# footer
|
|
1083
|
-
f.write('</Grid>\n')
|
|
1084
|
-
f.write('</Domain>\n')
|
|
1085
|
-
f.write('</Xdmf>\n')
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
583
|
def sort_opt_coil_positions(fn_coil_pos_opt, fn_coil_pos, fn_out_hdf5=None, root_path="/0/0/", verbose=False,
|
|
1089
584
|
print_output=False):
|
|
1090
585
|
"""
|
|
1091
|
-
Sorts coil positions according to Traveling Salesman problem
|
|
586
|
+
Sorts coil positions according to Traveling Salesman problem.
|
|
1092
587
|
|
|
1093
588
|
Parameters
|
|
1094
589
|
----------
|
|
@@ -1185,8 +680,8 @@ def sort_opt_coil_positions(fn_coil_pos_opt, fn_coil_pos, fn_out_hdf5=None, root
|
|
|
1185
680
|
else:
|
|
1186
681
|
# Euclidean distance
|
|
1187
682
|
distances[from_counter][to_counter] = (int(
|
|
1188
|
-
|
|
1189
|
-
|
|
683
|
+
math.hypot((from_node[0] - to_node[0]),
|
|
684
|
+
(from_node[1] - to_node[1]))))
|
|
1190
685
|
return distances
|
|
1191
686
|
|
|
1192
687
|
def get_routes(s, r, m):
|
|
@@ -1293,7 +788,7 @@ def random_walk_coil(start_mat, n_steps, fn_mesh_hdf5, angles_dev=3, distance_lo
|
|
|
1293
788
|
|
|
1294
789
|
Parameters
|
|
1295
790
|
----------
|
|
1296
|
-
start_mat : np.
|
|
791
|
+
start_mat : np.ndarray
|
|
1297
792
|
(4, 4) SimNIBS matsimnibs.
|
|
1298
793
|
n_steps : int
|
|
1299
794
|
Number of steps to walk.
|
|
@@ -1332,10 +827,10 @@ def random_walk_coil(start_mat, n_steps, fn_mesh_hdf5, angles_dev=3, distance_lo
|
|
|
1332
827
|
# walk angles
|
|
1333
828
|
for idx, axis in enumerate(['x', 'y', 'z']):
|
|
1334
829
|
if angles_dev[idx] != 0:
|
|
1335
|
-
mat_rot = pynibs.rotate_matsimnibs_euler(axis=axis,
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
830
|
+
mat_rot = pynibs.util.rotations.rotate_matsimnibs_euler(axis=axis,
|
|
831
|
+
angle=np.random.normal(scale=angles_dev[idx]),
|
|
832
|
+
matsimnibs=mat_rot,
|
|
833
|
+
metric=angles_metric)
|
|
1339
834
|
mat[:3, :3] = mat_rot[:3, :3]
|
|
1340
835
|
mats.append(mat)
|
|
1341
836
|
|
|
@@ -1345,23 +840,198 @@ def random_walk_coil(start_mat, n_steps, fn_mesh_hdf5, angles_dev=3, distance_lo
|
|
|
1345
840
|
assert distance_low >= 0 and distance_high >= 0
|
|
1346
841
|
assert distance_high >= distance_low
|
|
1347
842
|
distances = np.random.uniform(low=distance_low, high=distance_high, size=n_steps + 1)
|
|
1348
|
-
mats = pynibs.coil_distance_correction_matsimnibs(matsimnibs=mats,
|
|
1349
|
-
|
|
1350
|
-
|
|
843
|
+
mats = pynibs.expio.coil_distance_correction_matsimnibs(matsimnibs=mats,
|
|
844
|
+
fn_mesh_hdf5=fn_mesh_hdf5,
|
|
845
|
+
distance=distances)
|
|
1351
846
|
|
|
1352
847
|
if coil_pos_fn is not None:
|
|
1353
|
-
pynibs.write_coil_pos_hdf5(fn_hdf=os.path.splitext(coil_pos_fn)[0],
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
pynibs.write_coil_sequence_xdmf(coil_pos_fn,
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
848
|
+
pynibs.coil.write_coil_pos_hdf5(fn_hdf=os.path.splitext(coil_pos_fn)[0],
|
|
849
|
+
centers=mats[:3, 3, :].T,
|
|
850
|
+
m0=mats[:3, 0, :].T,
|
|
851
|
+
m1=mats[:3, 1, :].T,
|
|
852
|
+
m2=mats[:3, 2, :].T,
|
|
853
|
+
overwrite=True)
|
|
854
|
+
|
|
855
|
+
pynibs.coil.write_coil_sequence_xdmf(coil_pos_fn,
|
|
856
|
+
np.arange(n_steps + 1),
|
|
857
|
+
vec1=mats[:3, 0, :].T,
|
|
858
|
+
vec2=mats[:3, 1, :].T,
|
|
859
|
+
vec3=mats[:3, 2, :].T,
|
|
860
|
+
output_xdmf=f"{os.path.splitext(coil_pos_fn)[0]}.xdmf")
|
|
1366
861
|
|
|
1367
862
|
return mats
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def shift_coil_to_skin(mesh, coil, matsimnibs, dist=1, dist_neigh=50, coil_elm_idx=None, verbose=False):
|
|
866
|
+
"""
|
|
867
|
+
Shifts the coil position to the skin surface, also works for bent coil models.
|
|
868
|
+
|
|
869
|
+
Parameters
|
|
870
|
+
----------
|
|
871
|
+
mesh : simnibs.mesh_io.Mesh or str
|
|
872
|
+
SimNIBS .msh.
|
|
873
|
+
coil : simnibs.simulation.tms_coil.tms_coil.TmsCoil or str
|
|
874
|
+
.tcd coil model.
|
|
875
|
+
matsimnibs : np.ndarray
|
|
876
|
+
(4, 4) SimNIBS matsimnibs coil position / orientation.
|
|
877
|
+
dist : float, default: 0
|
|
878
|
+
Target (minimum) coil-skin distance.
|
|
879
|
+
dist_neigh : float, default: 50
|
|
880
|
+
If set, only mesh nodes with a distance < dist_neigh are considered for
|
|
881
|
+
the distance calculation to speed up computation.
|
|
882
|
+
coil_elm_idx : list of int, optional
|
|
883
|
+
Which coil elements to use.
|
|
884
|
+
verbose : bool, default: False
|
|
885
|
+
Print output messages.
|
|
886
|
+
|
|
887
|
+
Returns
|
|
888
|
+
-------
|
|
889
|
+
matsimnibs_shifted : np.ndarray
|
|
890
|
+
(4, 4) Shifted coil position / orientation.
|
|
891
|
+
min_dist : float
|
|
892
|
+
(Minimum) coil-skin distance before shifting.
|
|
893
|
+
"""
|
|
894
|
+
import trimesh
|
|
895
|
+
import simnibs
|
|
896
|
+
if isinstance(mesh, str):
|
|
897
|
+
mesh = simnibs.mesh_io.read_msh(mesh)
|
|
898
|
+
mesh = mesh.crop_mesh(1005)
|
|
899
|
+
|
|
900
|
+
if isinstance(coil, str):
|
|
901
|
+
from simnibs.simulation.tms_coil.tms_coil import TmsCoil
|
|
902
|
+
coil = TmsCoil.from_file(coil)
|
|
903
|
+
|
|
904
|
+
if verbose:
|
|
905
|
+
pynibs.matsim2paraview(matsimnibs)
|
|
906
|
+
|
|
907
|
+
casing = coil.get_mesh(
|
|
908
|
+
coil_affine=matsimnibs,
|
|
909
|
+
include_optimization_points=False,
|
|
910
|
+
include_coil_elements=False,
|
|
911
|
+
)
|
|
912
|
+
if coil_elm_idx is not None:
|
|
913
|
+
mesh_case = trimesh.Trimesh(vertices=casing.nodes.node_coord,
|
|
914
|
+
faces=casing.elm.node_number_list[coil_elm_idx, :3] - 1)
|
|
915
|
+
casing = casing.nodes.node_coord[casing.elm.node_number_list - 1][coil_elm_idx, :3, :]
|
|
916
|
+
else:
|
|
917
|
+
mesh_case = trimesh.Trimesh(vertices=casing.nodes.node_coord, faces=casing.elm.node_number_list[:, :3] - 1)
|
|
918
|
+
casing = casing.nodes.node_coord[casing.elm.node_number_list - 1][:, :3, :]
|
|
919
|
+
|
|
920
|
+
if dist_neigh is not None:
|
|
921
|
+
close_mesh_nodes = []
|
|
922
|
+
for mesh_node_i, mesh_node in tqdm.tqdm(enumerate(mesh.nodes.node_coord),
|
|
923
|
+
leave=False, total=mesh.nodes.node_coord.shape[0],
|
|
924
|
+
desc=f"Finding close mesh nodes to coil {casing.shape[0]}"):
|
|
925
|
+
if mesh_node_i % 10 != 0:
|
|
926
|
+
# speed this up by subsampling
|
|
927
|
+
continue
|
|
928
|
+
closest = pynibs.util.rotations.points_to_triangles(mesh_node[np.newaxis, :], casing)
|
|
929
|
+
|
|
930
|
+
# if (np.linalg.norm(mesh_node - coil_nodes, axis=1) < dist_neigh).any():
|
|
931
|
+
if (np.linalg.norm(mesh_node - closest[0, ::], axis=1) < dist_neigh).any():
|
|
932
|
+
close_mesh_nodes.append(mesh_node_i + 1)
|
|
933
|
+
if verbose:
|
|
934
|
+
print(f"{len(close_mesh_nodes)} close mesh nodes found.")
|
|
935
|
+
|
|
936
|
+
mesh = mesh.crop_mesh(nodes=close_mesh_nodes)
|
|
937
|
+
|
|
938
|
+
ray_directions = np.repeat(matsimnibs[:3, 2][np.newaxis, :], mesh.nodes.node_coord.shape[0], axis=0)
|
|
939
|
+
loc, index_ray, index_tri = mesh_case.ray.intersects_location(ray_origins=mesh.nodes.node_coord,
|
|
940
|
+
ray_directions=-ray_directions)
|
|
941
|
+
|
|
942
|
+
min_dist = np.linalg.norm(loc - mesh.nodes.node_coord[index_ray], axis=1).min()
|
|
943
|
+
if verbose:
|
|
944
|
+
print(f"Minimum distance to cortex surface: {min_dist} mm")
|
|
945
|
+
|
|
946
|
+
assert min_dist > 0, f"Coil touches head: {min_dist}"
|
|
947
|
+
|
|
948
|
+
# apply the target distance
|
|
949
|
+
min_dist -= dist
|
|
950
|
+
|
|
951
|
+
# shift coil placement in the z-coil axis direction towards the head
|
|
952
|
+
add = min_dist * matsimnibs[:3, 2]
|
|
953
|
+
# print(f"vec: {np.round(matsimnibs[:3, 2], 2)}: by {np.round(add,2)} mm. Mindist: {np.round(min_dist,2)}")
|
|
954
|
+
matsimnibs[:3, 3] += add
|
|
955
|
+
|
|
956
|
+
return matsimnibs, min_dist
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
def shift_many_coil_placements(mesh, coil, matsimnibs, dist=1, dist_neigh=50, flat_coil=False,
|
|
960
|
+
z_cut_off=None, verbose=False):
|
|
961
|
+
"""
|
|
962
|
+
Shifts many coil placements to the cortex surface.
|
|
963
|
+
|
|
964
|
+
Parameters
|
|
965
|
+
----------
|
|
966
|
+
mesh : simnibs.mesh_io.Mesh or str
|
|
967
|
+
SimNIBD .msh.
|
|
968
|
+
coil : simnibs.simulation.tms_coil.tms_coil.TmsCoil or str
|
|
969
|
+
.tcd coil model.
|
|
970
|
+
matsimnibs : np.ndarray
|
|
971
|
+
(4, 4, N) SimNIBS matsimnibs coil position / orientation.
|
|
972
|
+
dist : float, default: 0
|
|
973
|
+
Target (minimum) coil-skin distance.
|
|
974
|
+
dist_neigh : float, default: 50
|
|
975
|
+
If set, only mesh nodes with a distance < dist_neigh are considered for
|
|
976
|
+
the distance calculation to speed up computation.
|
|
977
|
+
flat_coil : bool, default: False
|
|
978
|
+
If True, the coil is assumed to be flat and shifts for the same position are applied across all rotations.
|
|
979
|
+
z_cut_off : float, optional
|
|
980
|
+
Cut coil case at this z-offset to speed up computation
|
|
981
|
+
verbose : bool, default: False
|
|
982
|
+
Print output messages.
|
|
983
|
+
|
|
984
|
+
Returns
|
|
985
|
+
-------
|
|
986
|
+
matsimnibs : np.ndarray
|
|
987
|
+
(4, 4, N) Shifted coil positions / orientations.
|
|
988
|
+
"""
|
|
989
|
+
import simnibs
|
|
990
|
+
if isinstance(mesh, str):
|
|
991
|
+
mesh = simnibs.mesh_io.read_msh(mesh)
|
|
992
|
+
mesh = mesh.crop_mesh(1005)
|
|
993
|
+
if isinstance(coil, str):
|
|
994
|
+
from simnibs.simulation.tms_coil.tms_coil import TmsCoil
|
|
995
|
+
coil = TmsCoil.from_file(coil)
|
|
996
|
+
|
|
997
|
+
if isinstance(matsimnibs, str):
|
|
998
|
+
with h5py.File(matsimnibs, 'r') as f:
|
|
999
|
+
matsimnibs = f['matsimnibs'][:]
|
|
1000
|
+
|
|
1001
|
+
shifts = np.zeros((3, matsimnibs.shape[2]))
|
|
1002
|
+
|
|
1003
|
+
# just look at bottom side of coil to speed up things
|
|
1004
|
+
if z_cut_off is not None:
|
|
1005
|
+
casing = coil.get_mesh(
|
|
1006
|
+
coil_affine=None,
|
|
1007
|
+
include_optimization_points=False,
|
|
1008
|
+
include_coil_elements=False,
|
|
1009
|
+
)
|
|
1010
|
+
casing = casing.nodes.node_coord[casing.elm.node_number_list - 1][:, :3, :]
|
|
1011
|
+
bottom_coil_idx = np.squeeze(np.argwhere(np.sum(casing[:, :, 2] > z_cut_off, axis=1) > 1))
|
|
1012
|
+
assert bottom_coil_idx.shape[0] > 0, "No coil elements found above z_cut_off"
|
|
1013
|
+
else:
|
|
1014
|
+
bottom_coil_idx = None
|
|
1015
|
+
|
|
1016
|
+
# perform shift_coil_to_cortex for each matsimnibs
|
|
1017
|
+
for idx in tqdm.tqdm(range(matsimnibs.shape[2]), position=1, desc="Shift coils"):
|
|
1018
|
+
if (shifts[:, idx] != 0).any():
|
|
1019
|
+
# let's skip the ones we already know
|
|
1020
|
+
continue
|
|
1021
|
+
matsim = matsimnibs[:, :, idx].copy()
|
|
1022
|
+
|
|
1023
|
+
# get shifted coil placement for this idx
|
|
1024
|
+
matsim_shifted, _ = shift_coil_to_skin(mesh, coil, matsim, dist=dist, dist_neigh=dist_neigh,
|
|
1025
|
+
coil_elm_idx=bottom_coil_idx, verbose=verbose)
|
|
1026
|
+
|
|
1027
|
+
if flat_coil:
|
|
1028
|
+
# let's see if there are other coil placements with the same position
|
|
1029
|
+
same_pos_idx = \
|
|
1030
|
+
np.where(np.sum(matsimnibs[:3, 3, :] == matsimnibs[:3, 3, idx][:, np.newaxis], axis=0) == 3)[0]
|
|
1031
|
+
else:
|
|
1032
|
+
same_pos_idx = np.array([idx])
|
|
1033
|
+
shifts[:, same_pos_idx] = matsim_shifted[:3, 3][:, np.newaxis]
|
|
1034
|
+
|
|
1035
|
+
matsimnibs[:3, 3, :] = shifts
|
|
1036
|
+
|
|
1037
|
+
return matsimnibs
|