Skip to content

Commit 29137b9

Browse files
committed
mapper: volume<->surface projection typing (PR 6)
Types cortex/mapper/{__init__,mapper,line,patch,point,samplers}.py. These files are exclusively owned by PR 6 (no commit before this one in main..types-easy touches cortex/mapper/), so checked out directly from types-easy rather than replayed commit-by-commit. Verified: zero diff against types-easy for all six mapper files.
1 parent 1a61725 commit 29137b9

6 files changed

Lines changed: 71 additions & 23 deletions

File tree

cortex/mapper/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations # needed for `type`?
2+
13
import os
24

35
import numpy as np
@@ -7,11 +9,11 @@
79
from .utils import nanproject, vol2surf
810

911

10-
def get_mapper(subject, xfmname, type='nearest', recache=False, **kwargs):
12+
def get_mapper(subject: str, xfmname: str, maptype: str='nearest', recache: bool=False, **kwargs) -> Mapper:
1113
from ..database import db
1214
from . import point, patch, line
1315

14-
mapcls = dict(
16+
mapcls: dict[str, type[Mapper]] = dict(
1517
nearest=point.PointNN,
1618
trilinear=point.PointTrilin,
1719
gaussian=point.PointGauss,
@@ -22,7 +24,7 @@ def get_mapper(subject, xfmname, type='nearest', recache=False, **kwargs):
2224
line_nearest=line.LineNN,
2325
line_trilinear=line.LineTrilin,
2426
line_lanczos=line.LineLanczos)
25-
Map = mapcls[type]
27+
Map = mapcls[maptype]
2628
ptype = Map.__name__.lower()
2729
kwds ='_'.join(['%s%s'%(k,str(v)) for k, v in list(kwargs.items())])
2830
if len(kwds) > 0:

cortex/mapper/line.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def _getmask(cls, pia, wm, polys, shape, npts=64, mp=True, **kwargs):
2626
#vidx = np.nonzero(valid)[0]
2727
mapper = sparse.csr_matrix((len(pia), np.prod(shape)))
2828
for t in np.linspace(0, 1, npts+2)[1:-1]:
29-
i, j, data = cls.sampler(pia*t + wm*(1-t), shape)
29+
i, j, data = cls.sampler(pia*t + wm*(1-t), shape, **kwargs)
3030
mapper = mapper + sparse.csr_matrix((data / npts, (i, j)), shape=mapper.shape)
3131
return mapper
3232

cortex/mapper/mapper.py

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
1+
import abc
2+
from typing import Union, overload
3+
14
import numpy as np
5+
import numpy.typing as npt
26
from scipy import sparse
37

48
from .. import dataset
59

10+
import sys
11+
if sys.version_info < (3, 11):
12+
from typing_extensions import Self
13+
else:
14+
from typing import Self
15+
616
import warnings
17+
718
warnings.simplefilter('ignore', sparse.SparseEfficiencyWarning)
819

9-
class Mapper:
20+
MapperShape = Union[npt.NDArray[np.integer], tuple[int, int, int]]
21+
22+
class Mapper(abc.ABC):
1023
'''Maps data from epi volume onto surface using various projections'''
11-
def __init__(self, left, right, shape, subject, xfmname):
24+
def __init__(self, left: sparse.csr_matrix, right: sparse.csr_matrix, shape: MapperShape, subject: str, xfmname: str):
1225
self.idxmap = None
1326
self.masks = [left, right]
1427
self.nverts = left.shape[0] + right.shape[0]
@@ -17,7 +30,7 @@ def __init__(self, left, right, shape, subject, xfmname):
1730
self.xfmname = xfmname
1831

1932
@classmethod
20-
def from_cache(cls, cachefile, subject, xfmname):
33+
def from_cache(cls, cachefile: str, subject: str, xfmname: str) -> Self:
2134
npz = np.load(cachefile)
2235
left = (npz['left_data'], npz['left_indices'], npz['left_indptr'])
2336
right = (npz['right_data'], npz['right_indices'], npz['right_indptr'])
@@ -26,20 +39,26 @@ def from_cache(cls, cachefile, subject, xfmname):
2639
return cls(lsparse, rsparse, npz['shape'], subject, xfmname)
2740

2841
@property
29-
def mask(self):
42+
def mask(self) -> npt.NDArray[np.bool_]:
3043
mask = np.array(self.masks[0].sum(0) + self.masks[1].sum(0))
3144
return (mask.squeeze() != 0).reshape(self.shape)
3245

3346
@property
34-
def hemimasks(self):
47+
def hemimasks(self) -> list[npt.NDArray[np.bool_]]:
3548
func = lambda m: (np.array(m.sum(0)).squeeze() != 0).reshape(self.shape)
3649
return [func(x) for x in self.masks]
3750

3851
def __repr__(self):
3952
ptype = self.__class__.__name__
4053
return '<%s mapper with %d vertices>'%(ptype, self.nverts)
4154

42-
def __call__(self, data):
55+
@overload
56+
def __call__(self, data: Union[dataset.Volume, tuple]) -> dataset.Vertex: ...
57+
58+
@overload
59+
def __call__(self, data: dataset.Vertex) -> tuple[npt.NDArray, npt.NDArray]: ...
60+
61+
def __call__(self, data: Union[dataset.Vertex, dataset.Volume, tuple]) -> Union[tuple[npt.NDArray, npt.NDArray], dataset.Vertex]:
4362
if isinstance(data, tuple):
4463
data = dataset.Volume(*data)
4564

@@ -61,17 +80,23 @@ def __call__(self, data):
6180
volume.shape = len(volume), -1
6281
volume = volume.T
6382

64-
mapped = []
83+
mapped: list[npt.NDArray] = []
6584
for mask in self.masks:
66-
mapped.append(np.array(mask * volume).T)
85+
mapped.append(np.array(mask * volume).T) # change to @ matmul
6786

6887
if self.idxmap is not None:
6988
mapped[0] = mapped[0][:, self.idxmap[0]]
7089
mapped[1] = mapped[1][:, self.idxmap[1]]
7190

7291
return dataset.Vertex(np.hstack(mapped).squeeze(), data.subject)
7392

74-
def backwards(self, vertexdata):
93+
@overload
94+
def backwards(self, vertexdata: dataset.Vertex) -> dataset.Volume: ...
95+
96+
@overload
97+
def backwards(self, vertexdata: npt.NDArray) -> npt.NDArray: ...
98+
99+
def backwards(self, vertexdata: Union[dataset.Vertex, npt.NDArray]) -> Union[dataset.Volume, npt.NDArray]:
75100
'''Projects vertex data back into volume space.
76101
77102
Parameters
@@ -81,8 +106,7 @@ def backwards(self, vertexdata):
81106
If Vertex object is provided, a Volume object is returned
82107
If an array is provided, an array is returned
83108
'''
84-
Vert2Vol = isinstance(vertexdata, dataset.Vertex)
85-
if Vert2Vol:
109+
if isinstance(vertexdata, dataset.Vertex):
86110
to_map = vertexdata.data
87111
else:
88112
to_map = vertexdata
@@ -91,8 +115,8 @@ def backwards(self, vertexdata):
91115
# dot the vertex data with the stacked mappers
92116
partial_vertex = bothmappers.T.dot(to_map)
93117
# solve the inverse mapping problem
94-
voxeldata = self._get_backmapper().solve(partial_vertex).reshape(self.shape)
95-
if Vert2Vol:
118+
voxeldata: npt.NDArray = self._get_backmapper().solve(partial_vertex).reshape(self.shape)
119+
if isinstance(vertexdata, dataset.Vertex):
96120
# construct a volume object with the new data
97121
return dataset.Volume(voxeldata, self.subject, self.xfmname)
98122
else:
@@ -112,10 +136,10 @@ def _get_backmapper(self):
112136
return self._backmapper
113137

114138
@classmethod
115-
def _cache(cls, filename, subject, xfmname, **kwargs):
139+
def _cache(cls, filename: str, subject: str, xfmname: str, **kwargs) -> Self:
116140
print('Caching mapper...')
117141
from ..database import db
118-
masks = []
142+
masks: list[sparse.csr_matrix] = []
119143
xfm = db.get_xfm(subject, xfmname, xfmtype='coord')
120144
fid = db.get_surf(subject, 'fiducial', merge=False, nudge=False)
121145

@@ -130,7 +154,19 @@ def _cache(cls, filename, subject, xfmname, **kwargs):
130154
_savecache(filename, masks[0], masks[1], xfm.shape)
131155
return cls(masks[0], masks[1], xfm.shape, subject, xfmname)
132156

133-
def _savecache(filename, left, right, shape):
157+
@classmethod
158+
@abc.abstractmethod
159+
def _getmask(cls, coords: npt.NDArray[np.floating], polys: npt.NDArray[np.integer], shape: tuple[int, int, int], **kwargs) -> sparse.csr_matrix:
160+
'''Generates a sparse matrix mapping from volume to surface vertices'''
161+
pass
162+
163+
@staticmethod
164+
@abc.abstractmethod
165+
def sampler(coords: npt.NDArray[np.floating], shape: tuple[int, int, int], **kwargs) -> tuple[npt.NDArray[np.intp], npt.NDArray[np.intp], npt.NDArray[np.floating]]:
166+
'''Generates a sparse matrix mapping from volume to surface vertices'''
167+
pass
168+
169+
def _savecache(filename: str, left: sparse.csr_matrix, right: sparse.csr_matrix, shape: MapperShape) -> None:
134170
np.savez(filename,
135171
left_data=left.data,
136172
left_indices=left.indices,

cortex/mapper/patch.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from .. import polyutils
77

88
class PatchMapper(Mapper):
9+
patchsize: int
10+
911
@classmethod
1012
def _getmask(cls, pts, polys, shape, npts=64, mp=True, **kwargs):
1113
rand = np.random.rand(2, npts)

cortex/mapper/point.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import numpy as np
2+
import numpy.typing as npt
23
from scipy import sparse
34

45
from . import Mapper
56
from . import samplers
67

78
class PointMapper(Mapper):
89
@classmethod
9-
def _getmask(cls, coords, polys, shape, **kwargs):
10+
def _getmask(cls, coords: npt.NDArray[np.floating], polys: npt.NDArray[np.integer], shape: tuple[int, int, int], **kwargs) -> sparse.csr_matrix:
1011
valid = np.unique(polys)
1112
mcoords = np.nan * np.ones_like(coords)
1213
mcoords[valid] = coords[valid]

cortex/mapper/samplers.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import numpy as np
2+
import numpy.typing as npt
23

34
def collapse(j, data):
45
"""Collapses samples into a single row"""
56
uniques = np.unique(j)
67
return uniques, np.array([data[j == u].sum() for u in uniques])
78

8-
def nearest(coords, shape, **kwargs):
9+
def nearest(coords: npt.NDArray[np.floating], shape: tuple[int, int, int], **kwargs) -> tuple[npt.NDArray[np.intp], npt.NDArray[np.intp], npt.NDArray[np.float64]]:
10+
if len(kwargs) > 0:
11+
raise ValueError('nearest sampler does not take any kwargs')
12+
913
valid = ~(np.isnan(coords).all(1))
1014
valid = np.logical_and(valid, np.logical_and(coords[:,0] > -.5, coords[:,0] < shape[2]+.5))
1115
valid = np.logical_and(valid, np.logical_and(coords[:,1] > -.5, coords[:,1] < shape[1]+.5))
@@ -16,7 +20,10 @@ def nearest(coords, shape, **kwargs):
1620
#return np.nonzero(valid)[0], j, (rcoords > 0).all(1) #np.ones((valid.sum(),))
1721
return np.nonzero(valid)[0], j, np.ones((valid.sum(),))
1822

19-
def trilinear(coords, shape, **kwargs):
23+
def trilinear(coords: npt.NDArray[np.floating], shape: tuple[int, int, int], **kwargs) -> tuple[npt.NDArray[np.intp], npt.NDArray[np.intp], npt.NDArray[np.floating]]:
24+
if len(kwargs) > 0:
25+
raise ValueError('trilinear sampler does not take any kwargs')
26+
2027
#trilinear interpolation equation from http://paulbourke.net/miscellaneous/interpolation/
2128
valid = ~(np.isnan(coords).all(1))
2229
(x, y, z), floor = np.modf(coords[valid].T)

0 commit comments

Comments
 (0)