Skip to content

Commit

Permalink
[ENH] Add generic rectangle implant and remove codecov (#631)
Browse files Browse the repository at this point in the history
* add rectangle implant
;

* add to init and tests

* test w new codecov version

* test w new codecov parent

* test w new codecov parent

* remove codecov
  • Loading branch information
jgranley committed Jun 15, 2024
1 parent e5289c3 commit 3f8d5af
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 14 deletions.
22 changes: 12 additions & 10 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@ jobs:
mkdir test_dir
cd test_dir
pytest --pyargs pulse2percept --cov-report=xml --cov=pulse2percept --doctest-modules
- name: Upload coveragei report to codecov.io
uses: codecov/codecov-action@v1
# Cannot yet post coverage report as comments on the PR, but see:
# https://github.com/codecov/codecov-python/pull/214
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./test_dir/coverage.xml
flags: unittests
name: codecov-umbrella
yml: ./codecov.yml
# removed for now since it fails to compare to recent parent
# - name: Upload coveragei report to codecov.io
# uses: codecov/codecov-action@v4
# # Cannot yet post coverage report as comments on the PR, but see:
# # https://github.com/codecov/codecov-python/pull/214
# with:
# token: ${{ secrets.CODECOV_TOKEN }}
# file: ./test_dir/coverage.xml
# flags: unittests
# name: codecov-umbrella
# yml: ./codecov.yml
# commit_parent: e5289c3
3 changes: 2 additions & 1 deletion pulse2percept/implants/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* :ref:`Basic Concepts > Visual Prostheses <topics-implants>`
"""
from .base import ProsthesisSystem
from .base import ProsthesisSystem, RectangleImplant
from .electrodes import (Electrode, PointSource, DiskElectrode,
SquareElectrode, HexElectrode)
from .electrode_arrays import ElectrodeArray, ElectrodeGrid
Expand Down Expand Up @@ -56,6 +56,7 @@
'PRIMA55',
'PRIMA40',
'ProsthesisSystem',
'RectangleImplant',
'SquareElectrode',
'IMIE'
]
71 changes: 69 additions & 2 deletions pulse2percept/implants/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
from functools import reduce
from scipy.spatial import cKDTree
from skimage.transform import SimilarityTransform
from collections import OrderedDict

from .electrodes import Electrode
from .electrode_arrays import ElectrodeArray
from .electrodes import Electrode, DiskElectrode
from .electrode_arrays import ElectrodeArray, ElectrodeGrid
from ..stimuli import Stimulus, ImageStimulus, VideoStimulus
from ..utils import PrettyPrint
from ..utils._fast_math import c_gcd
Expand Down Expand Up @@ -390,3 +391,69 @@ def electrode_names(self):
def electrode_objects(self):
"""Return a list of all electrode objects in the array"""
return self.earray.electrode_objects



class RectangleImplant(ProsthesisSystem):
""" A generic rectangular implant
Parameters
----------
x, y, z : float, optional
The x, y, z coordinates of the center of the implant
rot : float, optional
The rotation of the implant in degrees
shape : tuple, optional
The number of rows and columns in the implant
r : float, optional
The radius of the implant
spacing : float, optional
The distance between electrodes in the implant
eye : str, optional
The eye in which the implant is implanted
stim : :py:class:`~pulse2percept.stimuli.Stimulus` source type
A valid source type for a stimulus
preprocess : bool, optional
Whether to preprocess the stimulus
safe_mode : bool, optional
Whether to enforce charge balance
"""
def __init__(self, x=0, y=0, z=0, rot=0, shape=(15, 15), r=150./2, spacing=400., eye='RE', stim=None,
preprocess=True, safe_mode=False):
self.safe_mode = safe_mode
self.preprocess = preprocess
self.shape = shape
names = ('A', '1')
self.earray = ElectrodeGrid(self.shape, spacing, x=x, y=y, z=z, r=r,
rot=rot, names=names, etype=DiskElectrode)
self.stim = stim

# Set left/right eye:
if not isinstance(eye, str):
raise TypeError("'eye' must be a string, either 'LE' or 'RE'.")
if eye != 'LE' and eye != 'RE':
raise ValueError("'eye' must be either 'LE' or 'RE'.")
self.eye = eye
# Unfortunately, in the left eye the labeling of columns is reversed...
if eye == 'LE':
# TODO: Would be better to have more flexibility in the naming
# convention. This is a quick-and-dirty fix:
names = self.earray.electrode_names
objects = self.earray.electrode_objects
names = np.array(names).reshape(self.earray.shape)
# Reverse column names:
for row in range(self.earray.shape[0]):
names[row] = names[row][::-1]
# Build a new ordered dict:
electrodes = OrderedDict()
for name, obj in zip(names.ravel(), objects):
electrodes.update({name: obj})
# Assign the new ordered dict to earray:
self.earray._electrodes = electrodes
def _pprint_params(self):
"""Return dict of class attributes to pretty-print"""
params = super()._pprint_params()
params.update({'shape': self.shape, 'safe_mode': self.safe_mode,
'preprocess': self.preprocess})
return params
81 changes: 80 additions & 1 deletion pulse2percept/implants/tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from skimage.measure import label, regionprops

from pulse2percept.implants import (PointSource, ElectrodeArray, ElectrodeGrid,
ProsthesisSystem)
ProsthesisSystem, RectangleImplant)
from pulse2percept.stimuli import Stimulus, ImageStimulus, VideoStimulus
from pulse2percept.models import ScoreboardModel

Expand Down Expand Up @@ -145,3 +145,82 @@ def test_ProsthesisSystem_deactivate():
implant.deactivate(electrode)
npt.assert_equal(implant[electrode].activated, False)
npt.assert_equal(electrode in implant.stim.electrodes, False)

@pytest.mark.parametrize('ztype', ('float', 'list'))
@pytest.mark.parametrize('x', (-100, 200))
@pytest.mark.parametrize('y', (-200, 400))
@pytest.mark.parametrize('rot', (-45, 60))
def test_rectangle_implant(ztype, x, y, rot):
# Create an argus like implant and make sure location is correct
z = 100 if ztype == 'float' else np.ones(60) * 20
implant = RectangleImplant(x=x, y=y, z=z, rot=rot, shape=(6, 10), r=112.5, spacing=575.0)

# Slots:
npt.assert_equal(hasattr(implant, '__slots__'), True)

# Coordinates of first electrode
xy = np.array([-2587.5, -1437.5]).T

# Rotate
rot_rad = np.deg2rad(rot)
R = np.array([np.cos(rot_rad), -np.sin(rot_rad),
np.sin(rot_rad), np.cos(rot_rad)]).reshape((2, 2))
xy = np.matmul(R, xy)

# Then off-set: Make sure first electrode is placed
# correctly
npt.assert_almost_equal(implant['A1'].x, xy[0] + x)
npt.assert_almost_equal(implant['A1'].y, xy[1] + y)

# Make sure array center is still (x,y)
y_center = implant['F1'].y + (implant['A10'].y - implant['F1'].y) / 2
npt.assert_almost_equal(y_center, y)
x_center = implant['A1'].x + (implant['F10'].x - implant['A1'].x) / 2
npt.assert_almost_equal(x_center, x)

# Make sure radius is correct
for e in ['A1', 'B3', 'C5', 'D7', 'E9', 'F10']:
npt.assert_almost_equal(implant[e].r, 112.5)

# Indexing must work for both integers and electrode names
for idx, (name, electrode) in enumerate(implant.electrodes.items()):
npt.assert_equal(electrode, implant[idx])
npt.assert_equal(electrode, implant[name])
npt.assert_equal(implant["unlikely name for an electrode"], None)

# Right-eye implant:
xc, yc = 500, -500
implant = RectangleImplant(eye='RE', x=xc, y=yc)
npt.assert_equal(implant['A10'].x > implant['A1'].x, True)
npt.assert_almost_equal(implant['A10'].y, implant['A1'].y)

# Left-eye implant:
implant = RectangleImplant(eye='LE', x=xc, y=yc)
npt.assert_equal(implant['A1'].x > implant['A10'].x, True)
npt.assert_almost_equal(implant['A10'].y, implant['A1'].y)

# In both left and right eyes, rotation with positive angle should be
# counter-clock-wise (CCW): for (x>0,y>0), decreasing x and increasing y
for eye, el in zip(['LE', 'RE'], ['O1', 'O15']):
# By default, electrode 'F1' in a left eye has the same coordinates as
# 'F10' in a right eye (because the columns are reversed). Thus both
# cases are testing an electrode with x>0, y>0:
before = RectangleImplant(eye=eye)
after = RectangleImplant(eye=eye, rot=20)
npt.assert_equal(after[el].x < before[el].x, True)
npt.assert_equal(after[el].y > before[el].y, True)

# Set a stimulus via dict:
implant = RectangleImplant(stim={'B7': 13})
npt.assert_equal(implant.stim.shape, (1, 1))
npt.assert_equal(implant.stim.electrodes, ['B7'])

# Set a stimulus via array:
implant = RectangleImplant(stim=np.ones(225))
npt.assert_equal(implant.stim.shape, (225, 1))
npt.assert_almost_equal(implant.stim.data, 1)

# test different shapes
for shape in [(6, 10), (5, 12), (15, 15)]:
implant = RectangleImplant(shape=shape)
npt.assert_equal(implant.earray.shape, shape)

0 comments on commit 3f8d5af

Please sign in to comment.