Skip to content

Commit

Permalink
[ENH] Add ICVP Cortical Implant + Test (#542)
Browse files Browse the repository at this point in the history
* [MNT] Update requirements.txt (#507)

* [DOC] Fix gallery thumbnail images (#510)

* [FIX] Add check for empty stimulus (#522)

* add check for empty stimulus in stim setter for implants

* add check for empty np.ndarray

* [FIX] Fix electrode numbering annotation in implant.plot() (#523)

* fix implant annotation

* used zorder

* [ENH][REF] Modify Grid2D and VisualFieldMap to support multiple visual areas (#509)

* grid interface changes

* changed valueerror to runtime error in percept save test

* Fix unique time point bug

* temp commit to store grid stuff

* update requirements

* update base.py for new grid class

* Grid class now supports multiple layers

* refactor layer to be region

* Add region_mappings, RetinalMap

* Fixed overwriting static attributes

* Base class for cortical models

* [MNT] Update requirements.txt (#507)

* Add tests, made inv transforms optional to overwrite

* update with named tuple coordinate grids, and ret_to_dva etc
"

* refactor everything to ret_to_dva

* Update static ret2dva references

* removed backwards compatibility for ret2dva

* update doc

* [REF] Add topography, implants.cortex, and models.cortex submodules (#518)

* [MNT] Update requirements.txt (#507)

* [DOC] Fix gallery thumbnail images (#510)

* add topography module

* fix imports

* refactor tests for topography module

* doc

* add epmty submodules for implants.cortex and models.cortex

* add orion implant and test

* update cortex __init__.py

* add orion implant and test

* add icvp model and tests, fix implant plotting

---------

Co-authored-by: Jacob Granley <jgranley@ucsb.edu>
Co-authored-by: isaac hoffman <trsileneh@gmail.com>
  • Loading branch information
3 people committed Apr 18, 2023
1 parent d2f0346 commit c6185f6
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 3 deletions.
9 changes: 7 additions & 2 deletions pulse2percept/implants/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,13 @@ def __init__(self, earray, stim=None, eye='RE', preprocess=False,

def _pprint_params(self):
"""Return dict of class attributes to pretty-print"""
return {'earray': self.earray, 'stim': self.stim, 'eye': self.eye,
'safe_mode': self.safe_mode, 'preprocess': self.preprocess}
params = {
'earray': self.earray, 'stim': self.stim, 'safe_mode': self.safe_mode,
'preprocess': self.preprocess
}
if hasattr(self, "eye"):
params['eye'] = self.eye
return params

@staticmethod
def _require_charge_balanced(stim):
Expand Down
4 changes: 3 additions & 1 deletion pulse2percept/implants/cortex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@

from .orion import Orion
from .cortivis import Cortivis
from .icvp import ICVP

__all__ = [
"Orion",
"Cortivis"
"Cortivis",
"ICVP",
]
80 changes: 80 additions & 0 deletions pulse2percept/implants/cortex/icvp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""`ICVP`"""
import numpy as np

from ..base import ProsthesisSystem
from ..electrodes import DiskElectrode
from ..electrode_arrays import ElectrodeGrid


class ICVP(ProsthesisSystem):
"""Create an ICVP array.
This function creates a ICVP array and places it on the visual cortex
such that the center of the base of the array is at 3D location (x,y,z) given
in microns, and the array is rotated by angle ``rot``, given in degrees.
ICVP (Intracortical Visual Prosthesis Project) is an electrode array containing
16 Parylene-insulated (and 2 uninsulated reference and counter) iridium shaft
electrodes in a 4 column array with 400 um spacing. The electrodes have
a diameter of 15 um at the laser cut. They are inserted either 650 um
or 850 um into the cortex.
"""
# Frozen class: User cannot add more class attributes
__slots__ = ('shape',)

# 100um diameter at base
# (https://iopscience.iop.org/article/10.1088/1741-2552/abb9bf/pdf)

# 400um spacing, 4x4 + reference (R) and count (C)
# (https://iopscience.iop.org/article/10.1088/1741-2552/ac2bb8)

# depth of shanks: 650 or 850 um
# (https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=9175335)

def __init__(self, x=15000, y=0, z=0, rot=0, stim=None,
preprocess=False, safe_mode=False):
if not np.isclose(z, 0):
raise NotImplementedError
self.preprocess = preprocess
self.safe_mode = safe_mode
self.shape = (5, 4)
spacing = 400
names = np.array(
[
[i for i in range(1, 5)] + ['R'],
[i for i in range(5, 9)] + ['t1'],
[i for i in range(9, 14)],
['C'] + [i for i in range(14, 17)] + ['t2']
]
)
names = np.rot90(names).flatten()

if not isinstance(z, (list, np.ndarray)):
z = np.full(20, z, dtype=float)

# These electrodes have a shaft length of 650 microns, the rest are 650 microns
length_650 = {'9', '2', '6', '11', '15', '4', '8', '13'}

# account for depth of shanks
z_offset = [650 if name in length_650 else 850 for name in names]
z -= z_offset

self.earray = ElectrodeGrid(
self.shape, spacing, x=x, y=y, z=z, rot=rot, names=names,
type='hex', orientation='vertical', r=50, etype=DiskElectrode
)
for e in ['t1', 't2']:
self.earray.remove_electrode(e)

self.earray.deactivate(['R', 'C'])

# Beware of race condition: Stim must be set last, because it requires
# indexing into self.electrodes:
self.stim = stim

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
68 changes: 68 additions & 0 deletions pulse2percept/implants/cortex/tests/test_icvp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import numpy as np
import pytest
import numpy.testing as npt

from pulse2percept.implants.cortex.icvp import ICVP

@pytest.mark.parametrize('x', (-100, 200))
@pytest.mark.parametrize('y', (-200, 400))
@pytest.mark.parametrize('rot', (-45, 60))
def test_icvp(x, y, rot):
icvp = ICVP(x, y, rot=rot)
non_rot_icvp = ICVP(0)

n_elec = 18
spacing = 400
radius = 50
length_650 = {'9', '2', '6', '11', '15', '4', '8', '13'}
deactivated_electrodes = {'R', 'C'}

# Slots:
npt.assert_equal(hasattr(icvp, '__slots__'), True)
npt.assert_equal(hasattr(icvp, '__dict__'), False)

# Make sure number of electrodes is correct
npt.assert_equal(icvp.n_electrodes, n_elec)
npt.assert_equal(len(icvp.earray.electrodes), n_elec)

# Coordinates of 11 when device is not rotated:
xy = np.array([non_rot_icvp['11'].x, non_rot_icvp['11'].y])
# 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 = R @ xy
# Then off-set: Make sure first electrode is placed
# correctly
npt.assert_almost_equal(icvp['11'].x, xy[0] + x, decimal=2)
npt.assert_almost_equal(icvp['11'].y, xy[1] + y, decimal=2)

for electrode in icvp.earray.electrode_objects:
npt.assert_almost_equal(electrode.r, radius)

if electrode.name in deactivated_electrodes:
npt.assert_equal(electrode.activated, False)
else:
npt.assert_equal(electrode.activated, True)

if electrode.name in length_650:
npt.assert_equal(electrode.z, -650)
else:
npt.assert_equal(electrode.z, -850)

# Make sure center to center spacing is correct
npt.assert_almost_equal(np.sqrt(
(icvp['11'].x - icvp['7'].x) ** 2 +
(icvp['11'].y - icvp['7'].y) ** 2),
spacing
)
npt.assert_almost_equal(np.sqrt(
(icvp['11'].x - icvp['10'].x) ** 2 +
(icvp['11'].y - icvp['10'].y) ** 2),
spacing
)
npt.assert_almost_equal(np.sqrt(
(icvp['11'].x - icvp['15'].x) ** 2 +
(icvp['11'].y - icvp['15'].y) ** 2),
spacing
)

0 comments on commit c6185f6

Please sign in to comment.