diff --git a/cortex/dataset/braindata.py b/cortex/dataset/braindata.py index 4ee81c899..2a15f0970 100644 --- a/cortex/dataset/braindata.py +++ b/cortex/dataset/braindata.py @@ -61,7 +61,22 @@ def exp(self): return self.copy(np.exp(self.data)) def uniques(self, collapse=False): - """TODO: WHAT IS THIS + """Yield the underlying BrainData object(s) this view is built from. + + For a plain Volume/Vertex, that's just itself; composite views + (VolumeRGB, VertexRGB, Volume2D, Vertex2D) override this to yield + their individual channels instead. + + Parameters + ---------- + collapse : bool, optional + Unused here; accepted for interface compatibility with the + composite-view overrides of this method. + + Returns + ------- + generator + Yields `BrainData` """ yield self @@ -509,9 +524,23 @@ def __getitem__(self, idx): #return VertexData(self.data[idx], self.subject, **self.attrs) return self.copy(self.data[idx]) - - # TODO: simple + def to_json(self, simple: bool = False) -> dict[str, list[str]]: + """Serialize this vertex data to a JSON-compatible dict, for the + webgl viewer / HDF5 export. + + Parameters + ---------- + simple : bool, optional + If True, return an abbreviated summary (hemisphere split point + and frame count) rather than the full webgl payload. Default + False. + + Returns + ------- + dict + Serialized data. + """ if simple: sdict = dict(split=self.llen, frames=self.vertices.shape[0]) sdict.update(super().to_json(simple=simple)) diff --git a/cortex/dataset/dataset.py b/cortex/dataset/dataset.py index 45abd2dc1..574023e8a 100644 --- a/cortex/dataset/dataset.py +++ b/cortex/dataset/dataset.py @@ -15,12 +15,16 @@ class Dataset: """ - Wrapper for multiple data objects. This often does not need to be used - explicitly--for example, if a dictionary of data objects is passed to - `cortex.webshow`, it will automatically be converted into a `Dataset`. + Wrapper for a named collection of Dataview objects (Volume, Vertex, VolumeRGB, + etc.). Provides a standard way to save/load that collection as an HDF5 file, + and lets a single Dataview be treated the same as a full Dataset elsewhere in + pycortex (e.g. `cortex.webshow`). + + Each keyword argument names one view; its value can be a Dataview, a tuple + (implicitly converted to a Volume or Vertex), a filename string (loaded from + an .hdf file), a dict (converted to a nested Dataset), or another Dataset + (merged in). - # TODO: should be BrainData & Dataview, or just Dataview - All kwargs should be `BrainData` or `Dataset` objects. """ def __init__(self, **kwargs: Union[Dataview, dict, str, tuple, Dataset]) -> None: self.h5: Optional[h5py.File] = None @@ -29,8 +33,16 @@ def __init__(self, **kwargs: Union[Dataview, dict, str, tuple, Dataset]) -> None self.append(**kwargs) def append(self, **kwargs: Union[Dataview, dict, str, tuple, Dataset]) -> Dataset: - """Add the `BrainData` or `Dataset` objects in `kwargs` into this - dataset. + """Add the views in `kwargs` into this dataset. Each keyword names + one view; its value can be a Dataview, a tuple (implicitly converted + to a Volume or Vertex), a filename string (loaded from an .hdf + file), a dict (converted to a nested Dataset), or another Dataset + (merged in). See the `Dataset` class docstring for details. + + Returns + ------- + Dataset + This dataset, with the new views added, for chaining. """ for name, data in kwargs.items(): norm = normalize(data) @@ -118,7 +130,19 @@ def from_file(cls, filename: str, subject: Optional[str]=None) -> Dataset: return ds def uniques(self, collapse: bool=False) -> set[Dataview]: - """Return the set of unique BrainData objects contained by this dataset""" + """Return the set of unique BrainData objects contained by this dataset + + Parameters + ---------- + collapse : bool, optional + Passed through to each view's own `uniques()`. Default False. + + Returns + ------- + uniques : set + The distinct BrainData objects (e.g. Volume/Vertex channels) + referenced across all views in this dataset. + """ uniques = set() for name, view in self: # .uniques() is provided by BrainData @@ -128,6 +152,24 @@ def uniques(self, collapse: bool=False) -> set[Dataview]: return uniques def save(self, filename: Optional[str]=None, pack: bool=False) -> None: + """Write this dataset's views out to an HDF5 file. + + Parameters + ---------- + filename : str, optional + Path to write to. If omitted, writes to the file this dataset + was already opened from (raises if there is none). + pack : bool, optional + If True, also bundle each view's subject surfaces, transforms, + and masks into the file, so it can be opened without access to + the pycortex filestore/database. Default False. + + Returns + ------- + None + Writes `filename` (or the already-open file) as a side effect; + has no return value. + """ if filename is not None: self.h5 = h5py.File(filename, 'a') elif self.h5 is None: @@ -172,6 +214,32 @@ def get_surf(self, subject: str, type: str, hemi: Literal['lh', 'rh'], *, merge: def get_surf(self, subject: str, type: str, hemi: Literal['both', 'lh', 'rh']='both', *, merge: bool=False, nudge: bool=False) -> Union[tuple[tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]], tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]]], tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]]]: ... def get_surf(self, subject: str, type: str, hemi: Literal['both', 'lh', 'rh']='both', *, merge: bool=False, nudge: bool=False) -> Union[tuple[tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]], tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]]], tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]]]: + """Retrieve a subject's surface geometry from this (packed) dataset file. + + Parameters + ---------- + subject : str + Subject identifier, as packed into this dataset. + type : str + Surface type, e.g. 'wm', 'pia', or 'fiducial' (averaged from + 'wm' and 'pia'). + hemi : str, optional + 'lh', 'rh', or 'both'. Default 'both'. + merge : bool, optional + If True (and `hemi='both'`), stack both hemispheres into a + single (pts, polys) pair with right-hemisphere face indices + offset. Default False. + nudge : bool, optional + If True, shift each hemisphere so they don't overlap along the + x-axis. Default False. + + Returns + ------- + (pts, polys) or ((lpts, lpolys), (rpts, rpolys)) + Vertex coordinates and triangle faces for the requested + surface. If `hemi='both'` and `merge=False`, a pair of + (pts, polys) tuples, one per hemisphere, is returned instead. + """ pts: npt.NDArray[np.floating] polys: npt.NDArray[np.integer] if hemi == 'both': @@ -201,6 +269,20 @@ def get_surf(self, subject: str, type: str, hemi: Literal['both', 'lh', 'rh']='b raise IOError('Subject not found in package') def get_xfm(self, subject: str, xfmname: str) -> Transform: + """Retrieve a subject's transform from this (packed) dataset file. + + Parameters + ---------- + subject : str + Subject identifier, as packed into this dataset. + xfmname : str + Transform name, as packed into this dataset. + + Returns + ------- + Transform + The requested transform. + """ try: group: h5py.Group = self.h5['subjects'][subject]['transforms'][xfmname] return Transform(group['xfm'][:], tuple(group['xfm'].attrs['shape'])) @@ -208,6 +290,22 @@ def get_xfm(self, subject: str, xfmname: str) -> Transform: raise IOError('Transform not found in package') def get_mask(self, subject: str, xfmname: str, maskname: str): + """Retrieve a subject's voxel mask from this (packed) dataset file. + + Parameters + ---------- + subject : str + Subject identifier, as packed into this dataset. + xfmname : str + Transform name the mask belongs to, as packed into this dataset. + maskname : str + Name of the mask to retrieve. + + Returns + ------- + h5py.Dataset + The requested mask array, as stored in the HDF5 file. + """ try: group: h5py.Group = self.h5['subjects'][subject]['transforms'][xfmname]['masks'] return group[maskname] @@ -215,6 +313,23 @@ def get_mask(self, subject: str, xfmname: str, maskname: str): raise IOError('Mask not found in package') def get_overlay(self, subject: str, type: str='rois', **kwargs) -> tempfile._TemporaryFileWrapper: + """Retrieve a subject's overlay (currently only ROIs) from this + (packed) dataset file. + + Parameters + ---------- + subject : str + Subject identifier, as packed into this dataset. + type : str, optional + Overlay type to retrieve. Only 'rois' is currently supported. + Default 'rois'. + + Returns + ------- + tempfile.NamedTemporaryFile + A temporary file containing the overlay's SVG data, seeked to + the start. + """ try: group: h5py.Group = self.h5['subjects'][subject] if type == "rois": diff --git a/cortex/dataset/view2D.py b/cortex/dataset/view2D.py index 089deff95..ed2dd1129 100644 --- a/cortex/dataset/view2D.py +++ b/cortex/dataset/view2D.py @@ -34,6 +34,20 @@ def __init__(self, description: str="", cmap: Optional[str]=None, self.description = description def uniques(self, collapse=False): + """Yield the two underlying Dataview objects (dim1, dim2) that make + up this composite 2D view. + + Parameters + ---------- + collapse : bool, optional + Unused here; accepted for interface compatibility with + `Dataview.uniques`. + + Returns + ------- + generator + Yields `self.dim1`, then `self.dim2`. + """ yield self.dim1 yield self.dim2 @@ -48,6 +62,22 @@ def _write_hdf(self, h5, name="data"): return viewnode def to_json(self, simple=False): + """Serialize this 2D dataview to a JSON-compatible dict, for the + webgl viewer / HDF5 export. + + Parameters + ---------- + simple : bool, optional + Unused here; accepted for interface compatibility with + `Dataview.to_json`. + + Returns + ------- + dict + Serialized view data, including both dims' names, cmap, + vmin/vmax pairs, state, attrs, description, and (if the + underlying dims are Volumes) the shared xfm. + """ sdict = dict(data=[[self.dim1.name, self.dim2.name]], state=self.state, attrs=self.attrs, @@ -143,7 +173,20 @@ class Volume2D(Dataview2D): Maximum value in colormap for dim2. If not given defaults to TODO:WHAT **kwargs All additional arguments in kwargs are passed to the VolumeData and Dataview - + state : untyped + role unclear + priority : int (default = 1) + controls the order in which datasets are viewed in webgl + stim : str + path to stimulus file + rate : numeric (default 1) + frame rate for movie/time series playback in webgl + delay : numeric (default 0) + delay for movie/time series playback in webgl + filter : str (default = "nearest") + interpolation filter for volumetric rendering in webgl (nearest/trilinear/nearlin/debug) + alpha : ndarray or Volume + overwrites the computed alpha channel from a cmap, but currently commented out in Dataview2D """ _cls = VolumeData dim1: Volume @@ -238,7 +281,20 @@ class Vertex2D(Dataview2D): Maximum value in colormap for dim2. If not given defaults to TODO:WHAT **kwargs All additional arguments in kwargs are passed to the VolumeData and Dataview - + state : untyped + role unclear + priority : int (default = 1) + controls the order in which datasets are viewed in webgl + stim : str + path to stimulus file + rate : numeric (default 1) + frame rate for movie/time series playback in webgl + delay : numeric (default 0) + delay for movie/time series playback in webgl + filter : str (default = "nearest") + interpolation filter for volumetric rendering in webgl (nearest/trilinear/nearlin/debug) + alpha : ndarray or Volume + overwrites the computed alpha channel from a cmap, but currently commented out in Dataview2D """ _cls = VertexData blend_curvature = _cls.blend_curvature # hacky inheritance diff --git a/cortex/dataset/viewRGB.py b/cortex/dataset/viewRGB.py index 8d2c9d4ea..0456e36c9 100644 --- a/cortex/dataset/viewRGB.py +++ b/cortex/dataset/viewRGB.py @@ -103,6 +103,20 @@ def __init__( ) def uniques(self, collapse=False): + """Yield the underlying Dataview channels that make up this RGB view. + + Parameters + ---------- + collapse : bool, optional + If True, yield this RGB view itself as a single unit instead of + its individual channels. Default False. + + Returns + ------- + generator + Yields `self` if `collapse`, otherwise `self.red`, `self.green`, + `self.blue`, and `self.alpha` (if set). + """ if collapse: yield self else: @@ -591,6 +605,21 @@ def alpha(self, alpha: Optional[Union[npt.NDArray, Volume]]): self._alpha = alpha def to_json(self, simple=False): + """Serialize this RGB volume to a JSON-compatible dict, for the + webgl viewer / HDF5 export. + + Parameters + ---------- + simple : bool, optional + If True, return an abbreviated summary suitable for a quick + listing rather than the full webgl payload. Default False. + + Returns + ------- + dict + Serialized view data, including the transform matrix (when not + `simple`) needed to place the RGB volume in the correct space. + """ sdict = super().to_json(simple=simple) if simple: sdict["shape"] = self.red.shape @@ -894,6 +923,21 @@ def vertices(self) -> npt.NDArray[np.uint8]: return np.array(verts).transpose([1, 2, 0]) def to_json(self, simple=False): + """Serialize this RGB vertex view to a JSON-compatible dict, for the + webgl viewer / HDF5 export. + + Parameters + ---------- + simple : bool, optional + If True, return an abbreviated summary (hemisphere split point + and frame count) rather than the full webgl payload. Default + False. + + Returns + ------- + dict + Serialized view data. + """ sdict = super().to_json(simple=simple) if simple: diff --git a/cortex/dataset/views.py b/cortex/dataset/views.py index 6402cdea0..f07d07ec7 100644 --- a/cortex/dataset/views.py +++ b/cortex/dataset/views.py @@ -207,6 +207,23 @@ def __init__( self.description = description def copy(self, *args, **kwargs): + """Create a new instance of this Dataview's class, reusing its + display settings (cmap, vmin, vmax, description, state, attrs). + + Parameters + ---------- + *args + Positional arguments passed to the subclass constructor (e.g. + new `red`/`green`/`blue` data for a VolumeRGB). + **kwargs + Additional keyword arguments; merged with (and overriding) this + Dataview's own `attrs`. + + Returns + ------- + Dataview + A new instance of `self.__class__`. + """ kwargs.update(self.attrs) return self.__class__( *args, @@ -256,6 +273,23 @@ def to_json(self, simple: bool=False) -> DataviewJSON: @staticmethod def from_hdf(node, subject=None): + """Reconstruct a Dataview from a `/views/` node in an HDF5 + file previously written by `Dataview._write_hdf`. + + Parameters + ---------- + node : h5py.Dataset + The view node to decode. + subject : str, optional + Subject to use instead of the one stored in the file (e.g. if + the subject has been renamed since the file was written). + + Returns + ------- + Dataview + The decoded Volume, Vertex, VolumeRGB, VertexRGB, Volume2D, or + Vertex2D object. + """ data = json.loads(u(node[0])) desc = node[1] try: @@ -411,7 +445,21 @@ class Volume(VolumeData, Dataview): description : str, optional String describing this dataset. Displayed in webgl viewer. **kwargs - All additional arguments in kwargs are passed to the VolumeData and Dataview + All additional arguments in kwargs are passed to the VolumeData and Dataview. + state : untyped + role unclear + priority : int (default = 1) + controls the order in which datasets are viewed in webgl + stim : str + path to stimulus file + rate : numeric (default 1) + frame rate for movie/time series playback in webgl + delay : numeric (default 0) + delay for movie/time series playback in webgl + filter : str + interpolation filter for volumetric rendering in webgl (nearest/trilinear/nearlin/debug) + + """ @@ -459,6 +507,14 @@ def _write_hdf(self, h5, name="data"): @property def raw(self) -> VolumeRGB: + """Colormap this Volume's data into an RGBA VolumeRGB. + + Returns + ------- + VolumeRGB + This Volume's data, mapped through `cmap`/`vmin`/`vmax` into + per-voxel RGB colors, with alpha set to 0 for NaN voxels. + """ (r, g, b, a), nan_mask = super().raw result = VolumeRGB( r, @@ -502,6 +558,18 @@ class Vertex(VertexData, Dataview): String describing this dataset. Displayed in webgl viewer. **kwargs All additional arguments in kwargs are passed to the VolumeData and Dataview + state : untyped + role unclear + priority : int (default = 1) + controls the order in which datasets are viewed in webgl + stim : str + path to stimulus file + rate : numeric (default 1) + frame rate for movie/time series playback in webgl + delay : numeric (default 0) + delay for movie/time series playback in webgl + filter : str + interpolation filter for volumetric rendering in webgl (nearest/trilinear/nearlin/debug) """ @@ -543,6 +611,14 @@ def _write_hdf(self, h5, name="data"): @property def raw(self) -> VertexRGB: + """Colormap this Vertex's data into an RGBA VertexRGB. + + Returns + ------- + VertexRGB + This Vertex's data, mapped through `cmap`/`vmin`/`vmax` into + per-vertex RGB colors, with alpha set to 0 for NaN vertices. + """ (r, g, b, a), nan_mask = super().raw result = VertexRGB( r, @@ -567,12 +643,15 @@ def map( ) -> Vertex: """Map this data from this surface to another surface - Calls `cortex.freesurfer.vertex_to_vertex()` with this - vertex object as the first argument. + Builds a source-to-target vertex mapping matrix via + `cortex.freesurfer.get_mri_surf2surf_matrix()` and applies it to + this Vertex's data. - NOTE: Requires either previous computation of mapping matrices - (with `cortex.db.get_mri_surf2surf_matrix`) or active - freesurfer environment. + NOTE: Requires the source and target subjects' registered sphere + surfaces (`?h.sphere.reg`), produced by Freesurfer's `recon-all` + pipeline. No active Freesurfer installation is needed at call + time -- the mapping is computed directly from those files in + pure Python. Parameters ---------- @@ -581,7 +660,13 @@ def map( Other Parameters ---------------- - kwargs map to `cortex.freesurfer.vertex_to_vertex()` + kwargs map to `cortex.freesurfer.get_mri_surf2surf_matrix()` + + Returns + ------- + Vertex + This data, resampled onto `target_subj`'s vertices, with the + same `cmap`, `vmin`, and `vmax` as this Vertex. """ # Input check if hemi not in ["lh", "rh", "both"]: