Skip to content

Commit 4c9b4b6

Browse files
committed
streamline
1 parent 41e5bdf commit 4c9b4b6

File tree

8 files changed

+113
-81
lines changed

8 files changed

+113
-81
lines changed

README.md

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,15 @@ functions to project the 3D locations to 2D space and plot them.
2828

2929
## How to work with it
3030

31-
- `git clone` the repository (or download as .zip and unpack)
31+
- `git clone` the repository (or download as `.zip` and unpack)
3232
- `cd eeg_positions`
3333
- Using your python environment of choice, install the package and its
3434
dependencies locally using `pip install -e .`
3535
- Run the tests using `pytest` (you might have to `pip install pytest` first)
36-
- Calculate and plot electrodes by calling `python eeg_positions.py` in the
37-
`eeg_positions/eeg_positions` directory
36+
- Calculate and plot electrodes by calling `python eeg_positions/calc_positions.py`
3837
- Check out `contour_labels.py` for the order how electrodes are computed
39-
- ... and see `utils.py` for the `find_point_at_fraction` function that is
40-
the core of the computations.
38+
- ... and see `utils.py` for the `find_point_at_fraction` function that is the
39+
core of the computations.
4140

4241
## References
4342

@@ -46,6 +45,19 @@ functions to project the 3D locations to 2D space and plot them.
4645
112(4), 713-719. doi:
4746
[10.1016/S1388-2457(00)00527-7](https://www.biosemi.com/publications/pdf/Oostenveld2001b.pdf)
4847

48+
## Acknowledgements
49+
50+
Explicit thanks to:
51+
52+
- Robert Oostenveld for writing his [blog post](http://robertoostenveld.nl/electrode/)
53+
on electrodes
54+
- Ed Williams for the helpful correspondence and discussions about
55+
"intermediate points on a great circle" (see his
56+
[aviation formulary](http://www.edwilliams.org/avform.htm#Intermediate))
57+
- "N. Bach" and "Nominal Animal" who helped me to figure out the math for
58+
the `find_point_at_fraction` function (see this
59+
[math.stackexchange.com post](https://math.stackexchange.com/questions/2800845/find-intermediate-points-on-small-circle-of-a-sphere/2805204#2805204))
60+
4961
---
5062

5163
# Examples
@@ -89,13 +101,13 @@ montage.plot()
89101

90102
## Interactively viewing 3D coordinates
91103

92-
reproduce by running `python eeg_positions.py`
104+
reproduce by running `python eeg_positions/calc_positions.py`
93105

94106
![img: coordinate system](./images/3d_view.png)
95107

96108
## Projections to 2D
97109

98-
reproduce by running `python eeg_positions.py`
110+
reproduce by running `python eeg_positions/calc_positions.py`
99111

100112
### 10-20 system
101113
![img: coordinate system](./images/1020.png)
@@ -115,8 +127,10 @@ reproduce by running `python eeg_positions.py`
115127
### 3D Axes and Cartesian Coordinate System
116128

117129
- Imagine the x-axis pointing roughly towards the viewer with increasing values
118-
- The y-axis is orthogonal to the x-axis, pointing to the right of the viewer with increasing values
119-
- The z-axis is orthogonal to the xy-plane and pointing vertically up with increasing values
130+
- The y-axis is orthogonal to the x-axis, pointing to the right of the viewer
131+
with increasing values
132+
- The z-axis is orthogonal to the xy-plane and pointing vertically up with
133+
increasing values
120134

121135
![img: coordinate system](./images/coords_cartesian.png)
122136

@@ -132,15 +146,16 @@ sphere:
132146

133147
## Cartesian Coordinates
134148

135-
- The left preauricular point = (-1, 0, 0) ... coincides with T9
136-
- The right preauricular point = (1, 0, 0) ... coincides with T10
137-
- The nasion = (0, 1, 0) ... coincides with Nz
138-
- The inion = (0, -1, 0) ... coincides with Iz
139-
- The vertex = (0, 0, 1) ... coincides with Cz
149+
- The left preauricular point = `(-1, 0, 0)` ... coincides with T9
150+
- The right preauricular point = `(1, 0, 0)` ... coincides with T10
151+
- The nasion = `(0, 1, 0)` ... coincides with Nz
152+
- The inion = `(0, -1, 0)` ... coincides with Iz
153+
- The vertex = `(0, 0, 1)` ... coincides with Cz
140154

141-
Hence, the equator of the sphere goes through T9, T10, and Nz.
155+
Hence, the equator of the sphere goes through T9, T10, Nz and Iz.
142156

143-
**Note that these are ASSUMPTIONS**. It would be equally valid to assume the equator going through T7, T8, and Fpz.
157+
**Note that these are ASSUMPTIONS**. It would be equally valid to assume the
158+
equator going through T7, T8, Fpz, and Oz.
144159

145160
# EEG Electrode Position Data
146161

eeg_positions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
"""Initialize eeg_positions."""
2+
3+
__version__ = '1.0.0'

eeg_positions/eeg_positions.py renamed to eeg_positions/calc_positions.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88

99
import pandas as pd
1010

11-
from utils import (get_xyz, find_point_at_fraction, plot_spherical_head,
12-
plot_2d_head, stereographic_projection)
13-
from contour_labels import all_contours, system1020, system1010, system1005
11+
from eeg_positions.utils import (get_xyz, find_point_at_fraction,
12+
plot_spherical_head, plot_2d_head,
13+
stereographic_projection)
14+
from eeg_positions.contour_labels import (ALL_CONTOURS, SYSTEM1020, SYSTEM1010,
15+
SYSTEM1005)
1416

1517

1618
if __name__ == '__main__':
@@ -32,7 +34,7 @@
3234

3335
# Calculate all positions
3436
# -----------------------
35-
for contour in all_contours:
37+
for contour in ALL_CONTOURS:
3638

3739
if len(contour) == 21:
3840
midpoint_idx = 10
@@ -49,15 +51,15 @@
4951

5052
# Calculate all other points at fractions of distance
5153
# see `contour_labels.py` and `test_contour_labels.py`
52-
other_points = {}
54+
other_ps = {}
5355
for i, label in enumerate(contour):
54-
other_points[label] = find_point_at_fraction(p1,
55-
p2,
56-
p3,
57-
f=i/(len(contour)-1))
56+
other_ps[label] = find_point_at_fraction(p1,
57+
p2,
58+
p3,
59+
frac=i/(len(contour)-1))
5860

5961
# Append to data frame
60-
tmp = pd.DataFrame.from_dict(other_points, orient='index')
62+
tmp = pd.DataFrame.from_dict(other_ps, orient='index')
6163
tmp.columns = ['x', 'y', 'z']
6264
tmp['label'] = tmp.index
6365
df = df.append(tmp, ignore_index=True, sort=True)
@@ -71,19 +73,19 @@
7173
fname_template = os.path.join(fpath, '..', 'data', 'standard_{}.tsv')
7274

7375
# First in 3D, then in 2D for each system
74-
for system, fmt in zip([system1020, system1010, system1005],
76+
for system, fmt in zip([SYSTEM1020, SYSTEM1010, SYSTEM1005],
7577
['1020', '1010', '1005']):
7678

7779
idx = df.label.isin(system)
7880
system_df = df.loc[idx, :]
79-
system_df.sort_values(by='label', inplace=True)
81+
system_df = system_df.sort_values(by='label')
8082
system_df.to_csv(fname_template.format(fmt), sep='\t', na_rep='n/a',
8183
index=False, float_format='%.4f')
8284

8385
# Now in 2D using stereographic projection
84-
xs, ys = stereographic_projection(system_df.values[:, 1],
85-
system_df.values[:, 2],
86-
system_df.values[:, 3])
86+
xs, ys = stereographic_projection(system_df.to_numpy()[:, 1],
87+
system_df.to_numpy()[:, 2],
88+
system_df.to_numpy()[:, 3])
8789
system_df = system_df.loc[:, ['label', 'x', 'y']]
8890
system_df['x'] = xs
8991
system_df['x'] = ys
@@ -108,11 +110,11 @@
108110
# 2D
109111
fig2, ax2 = plot_2d_head()
110112

111-
xs, ys = stereographic_projection(df.x, df.y, df.z)
113+
xs, ys = stereographic_projection(df['x'], df['y'], df['z'])
112114

113115
ax2.scatter(xs, ys, marker='.', color='r')
114116

115-
for lab, x, y in zip(list(df.label), xs, ys):
117+
for lab, x, y in zip(list(df['label']), xs, ys):
116118
ax2.annotate(lab, xy=(x, y), fontsize=5)
117119

118120
ax2.set_title('standard_{}'.format(system))

eeg_positions/contour_labels.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
'POO8']
115115

116116
# List of lists. Note the specific ordering.
117-
all_contours = [sagittal_Nz_Cz_Iz,
117+
ALL_CONTOURS = [sagittal_Nz_Cz_Iz,
118118
horizontal_Nz_T10_Iz, horizontal_Nz_T9_Iz,
119119
coronal_T9_Cz_T10,
120120
horizontal_NFpz_T10h_OIz, horizontal_NFpz_T9h_OIz,
@@ -125,17 +125,20 @@
125125
contour_PO, contour_POO]
126126

127127
# Defining which electrodes belong into which standard system
128-
system1020 = ['Fp1', 'Fpz', 'Fp2', 'F7', 'F3', 'Fz', 'F4', 'F8',
128+
SYSTEM1020 = ['Fp1', 'Fpz', 'Fp2', 'F7', 'F3', 'Fz', 'F4', 'F8',
129129
'T7', 'C3', 'Cz', 'C4', 'T8', 'P7', 'P3', 'Pz', 'P4', 'P8',
130130
'O1', 'Oz', 'O2']
131131

132-
system1010 = system1020 + ['Nz', 'AF7', 'AFz', 'AF8', 'F9', 'F5', 'F1', 'F2',
132+
SYSTEM1010 = SYSTEM1020 + ['Nz', 'AF7', 'AFz', 'AF8', 'F9', 'F5', 'F1', 'F2',
133133
'F6', 'F10', 'FT9', 'FT7', 'FC5', 'FC3', 'FC1',
134134
'FCz', 'FC2', 'FC4', 'FC6', 'FT8', 'FT10', 'T9',
135135
'C5', 'C1', 'C2', 'C6', 'T10', 'TP7', 'CP5', 'CP3',
136136
'CP1', 'CPz', 'CP2', 'CP4', 'CP6', 'TP8', 'P9',
137137
'P5', 'P1', 'P2', 'P6', 'P10', 'PO9', 'PO7', 'POz',
138138
'PO8', 'PO10', 'I1', 'Iz', 'I2']
139139

140-
system1005 = list(set([label for contour in all_contours
141-
for label in contour]))
140+
SYSTEM1005 = list()
141+
for contour in ALL_CONTOURS:
142+
for label in contour:
143+
if label not in SYSTEM1005:
144+
SYSTEM1005.append(label)
Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
"""Test whether the contour labels are complete."""
22

3-
from eeg_positions.contour_labels import (all_contours, system1020, system1010,
4-
system1005)
3+
from eeg_positions.contour_labels import (ALL_CONTOURS, SYSTEM1020, SYSTEM1010,
4+
SYSTEM1005)
55

66

77
def test_contour_lengths():
88
"""Check that we have 17 or 21 electrode per contour."""
9-
for contour in all_contours:
9+
for contour in ALL_CONTOURS:
1010
assert len(contour) in [17, 21]
1111

1212

1313
def test_system_labels():
1414
"""Check if systems have the correct number of labels."""
15-
assert len(system1005) == 345
16-
assert len(system1020) == 21
17-
assert len(system1010) == 71
18-
19-
20-
if __name__ == '__main__':
21-
print(all_contours)
15+
assert len(SYSTEM1005) == 345
16+
assert len(SYSTEM1020) == 21
17+
assert len(SYSTEM1010) == 71

eeg_positions/tests/test_utilities.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,16 @@ def test_find_point_at_fraction():
5252
p1 = (1., 0., 0.)
5353
p2 = (0., 0., 1.)
5454
p3 = (-1., 0., 0.)
55-
p = find_point_at_fraction(p1, p2, p3, f=0.)
56-
assert p == p1
57-
p = find_point_at_fraction(p1, p2, p3, f=1.)
58-
assert p == p3
59-
p = find_point_at_fraction(p1, p2, p3, f=0.5)
60-
assert p == p2
55+
point = find_point_at_fraction(p1, p2, p3, frac=0.)
56+
assert point == p1
57+
point = find_point_at_fraction(p1, p2, p3, frac=1.)
58+
assert point == p3
59+
point = find_point_at_fraction(p1, p2, p3, frac=0.5)
60+
assert point == p2
6161

6262
# Assert error when equal points
6363
with pytest.raises(ValueError):
64-
find_point_at_fraction(p1, p1, p3, 0.5)
64+
find_point_at_fraction(p1, p1, p3, frac=0.5)
6565

6666

6767
def test_stereographic_projection():
@@ -73,7 +73,7 @@ def test_stereographic_projection():
7373
df = pd.DataFrame(data)
7474

7575
# We know where Cz and Nz should be in 2D:
76-
xs, ys = stereographic_projection(df.x, df.y, df.z)
76+
xs, ys = stereographic_projection(df['x'], df['y'], df['z'])
7777
np.testing.assert_allclose(xs, np.array((0, 0)))
7878
np.testing.assert_allclose(ys, np.array((0, 1)))
7979

eeg_positions/utils.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,28 @@
77
from mpl_toolkits.mplot3d import Axes3D # noqa: F401
88

99

10-
def find_point_at_fraction(p1, p2, p3, f):
10+
def find_point_at_fraction(p1, p2, p3, frac):
1111
"""Find a point on an arc spanned by three points.
1212
1313
Given three points `p1`, `p2` and `p3` on a sphere with origin (0, 0, 0),
14-
find the coordinates of a point `p` at a fraction `f` of the overall
14+
find the coordinates of a `point` at a fraction `frac` of the overall
1515
distance on an arc spanning from `p1` over `p2` to `p3`. Given this
16-
assumption, for fractions of zero, `p` will equal `p1`; for fractions of
17-
one, `p` will equal `p3`; and for fractions of one half, `p` will equal
18-
`p2` [1]_.
16+
assumption, for fractions of zero, `point` will equal `p1`; for fractions
17+
of one, `point` will equal `p3`; and for fractions of one half, `point`
18+
will equal `p2` [1]_.
1919
2020
Parameters
2121
----------
2222
p1, p2, p3 : tuple
2323
Each tuple containing x, y, z cartesian coordinates.
24-
f : float
24+
frac : float
2525
Fraction of distance from `p1` to `p3` over p2` at which
2626
to find coordinates of `p`.
2727
2828
Returns
2929
-------
30-
p : tuple
31-
Containing x, y, z cartesian coordinates.
30+
point : tuple
31+
Containing x, y, z cartesian coordinates.
3232
3333
Notes
3434
-----
@@ -45,13 +45,16 @@ def find_point_at_fraction(p1, p2, p3, f):
4545
4646
Examples
4747
--------
48-
>>> find_point_at_fraction((1., 0., 0.), (0., 0., 1.), (-1., 0., 0.), f=0.)
48+
>>> p1 = (1., 0., 0.)
49+
>>> p2 = (0., 0., 1.)
50+
>>> p3 = (-1., 0., 0.)
51+
>>> find_point_at_fraction(p1, p2, p3, frac=0.)
4952
(1., 0., 0.)
50-
>>> find_point_at_fraction((1., 0., 0.), (0., 0., 1.), (-1., 0., 0.), f=.5)
53+
>>> find_point_at_fraction(p1, p2, p3, frac=.5)
5154
(0., 0., 1.)
52-
>>> find_point_at_fraction((1., 0., 0.), (0., 0., 1.), (-1., 0., 0.), f=1.)
55+
>>> find_point_at_fraction(p1, p2, p3, frac=1.)
5356
(-1., 0., 0.)
54-
>>> find_point_at_fraction((1., 0., 0.), (0., 0., 1.), (-1., 0., 0.), f=.3)
57+
>>> find_point_at_fraction(p1, p2, p3, frac=.3)
5558
(0.5878, 0.0, 0.809)
5659
5760
"""
@@ -126,14 +129,14 @@ def find_point_at_fraction(p1, p2, p3, f):
126129
theta = theta + 2*np.pi
127130

128131
# Now calculate coordinates at fraction
129-
x = xc + xu * np.cos(f*theta) + xv*np.sin(f*theta)
130-
y = yc + yu * np.cos(f*theta) + yv*np.sin(f*theta)
131-
z = zc + zu * np.cos(f*theta) + zv*np.sin(f*theta)
132+
x = xc + xu * np.cos(frac*theta) + xv*np.sin(frac*theta)
133+
y = yc + yu * np.cos(frac*theta) + yv*np.sin(frac*theta)
134+
z = zc + zu * np.cos(frac*theta) + zv*np.sin(frac*theta)
132135

133136
# Round to 4 decimals and collect the points in tuple
134-
p = np.asarray((x, y, z))
135-
p = tuple(p.round(decimals=4))
136-
return p
137+
point = np.asarray((x, y, z))
138+
point = tuple(point.round(decimals=4))
139+
return point
137140

138141

139142
# Convenient helper function to access xyz coordinates from df

0 commit comments

Comments
 (0)