From f919c62f31deac322734d9bd6d31506b752415f8 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Tue, 11 Aug 2026 01:45:38 -0700 Subject: [PATCH 01/39] pyproject: create dep group for typechecking --- pyproject.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6acd93aad..69e049b32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dynamic = ["authors", "license", "readme", "version", "classifiers", "dependenci [dependency-groups] dev = [ {include-group = "test"}, - "mypy", + {include-group = "types"}, ] test = [ "pycortex[headless]", @@ -22,6 +22,11 @@ test = [ "pytest-cov", "pytest-timeout", ] +types = [ + "mypy", + "scipy-stubs", +] + [project.optional-dependencies] headless = [ From 1775cfeeb247f9b4b122d880dd740934892b2dc7 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Tue, 11 Aug 2026 02:01:31 -0700 Subject: [PATCH 02/39] pyproject: ignore packages without type annotations --- pyproject.toml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 69e049b32..655cb7a7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,4 +45,15 @@ ignore-words-list = 'nd,acount,anormal,fpt,coo,transpart,FO,lins' [tool.mypy] allow_redefinition = true -disable_error_code = "import-untyped" +disable_error_code = "import-untyped" # TODO: narrow to specific packages + +[[tool.mypy.overrides]] +module = [ + "bpy", + "bpy.ops", + "mayavi", + "progressbar", + "scikits.sparse.cholmod", + "tvtk.api", +] +ignore_missing_imports = true From 82fceca72a6950bfd6317bff878591c461c49bb2 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Sat, 28 Feb 2026 00:22:29 -0800 Subject: [PATCH 03/39] CI automatically save coverage with pytest --- .gitignore | 1 + pytest.ini | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 613aa48c5..21a752d97 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ pip-log.txt # Unit test / coverage reports .coverage +coverage.* .tox nosetests.xml diff --git a/pytest.ini b/pytest.ini index 25191cf6d..f34750789 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,6 +4,8 @@ testpaths = addopts = -r a -v + --cov=. + --cov-report xml # Per-test timeout (in seconds) so a single hung headless browser session # does not consume the entire CI budget. Individual tests can override with # @pytest.mark.timeout(N). Requires the optional ``pytest-timeout`` From cc6e1af60d5e7cf2cb15b939f420294e2b8bfa7b Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Sat, 28 Feb 2026 00:23:48 -0800 Subject: [PATCH 04/39] Typing: Transform --- cortex/xfm.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cortex/xfm.py b/cortex/xfm.py index de023ce58..d51fd911c 100644 --- a/cortex/xfm.py +++ b/cortex/xfm.py @@ -1,7 +1,9 @@ """Affine transformation class """ import os +from typing import Union import numpy as np +import numpy.typing as npt import subprocess class Transform: @@ -9,7 +11,7 @@ class Transform: A standard affine transform. Typically holds a transform from anatomical magnet space to epi file space. ''' - def __init__(self, xfm, reference): + def __init__(self, xfm: npt.NDArray, reference: Union[str, tuple, npt.NDArray]): self.xfm = xfm self.reference = None From efeb4ca1629e324a886d5fbed1e76063330d3e11 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 4 Mar 2026 02:20:43 -0800 Subject: [PATCH 05/39] Transform: more specific annotation --- cortex/xfm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cortex/xfm.py b/cortex/xfm.py index d51fd911c..86be53f59 100644 --- a/cortex/xfm.py +++ b/cortex/xfm.py @@ -11,7 +11,7 @@ class Transform: A standard affine transform. Typically holds a transform from anatomical magnet space to epi file space. ''' - def __init__(self, xfm: npt.NDArray, reference: Union[str, tuple, npt.NDArray]): + def __init__(self, xfm: npt.NDArray, reference: Union[str, tuple[int, int, int], npt.NDArray]): self.xfm = xfm self.reference = None From f7f8b7566be8bcd9dd8ade248a5895ce3e721aa3 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Fri, 6 Mar 2026 15:22:48 -0800 Subject: [PATCH 06/39] Typing: cortex/xfm.py --- cortex/xfm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cortex/xfm.py b/cortex/xfm.py index 86be53f59..9009ef085 100644 --- a/cortex/xfm.py +++ b/cortex/xfm.py @@ -1,7 +1,7 @@ """Affine transformation class """ import os -from typing import Union +from typing import Union, Any import numpy as np import numpy.typing as npt import subprocess @@ -28,11 +28,11 @@ def __init__(self, xfm: npt.NDArray, reference: Union[str, tuple[int, int, int], self.reference = reference self.shape = self.reference.shape[:3][::-1] - def __call__(self, pts): + def __call__(self, pts: npt.NDArray) -> npt.NDArray: return np.dot(self.xfm, np.hstack([pts, np.ones((len(pts),1))]).T)[:3].T @property - def inv(self): + def inv(self) -> "Transform": ref = self.reference if ref is None: ref = self.shape @@ -342,7 +342,7 @@ def to_freesurfer(self, fs_register, subject, freesurfer_subject_dir=None): return fs_anat2func -def isstr(obj): +def isstr(obj: Any) -> bool: """Check for stringy-ness in python 2.7 or 3""" try: return isinstance(obj, basestring) From 8aaea827761aba184e8cb58b4dc6c343cae16d0f Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 4 Mar 2026 20:33:35 -0800 Subject: [PATCH 07/39] Monkeytype for cortex/volume.py --- cortex/volume.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cortex/volume.py b/cortex/volume.py index ca8c78eb0..df83dcd0f 100644 --- a/cortex/volume.py +++ b/cortex/volume.py @@ -1,13 +1,15 @@ """Contains functions for working with volume data """ import os +from typing import Optional, Union import numpy as np +import numpy.typing as npt from . import dataset from .database import db from .xfm import Transform -def unmask(mask, data): +def unmask(mask: npt.NDArray, data: npt.NDArray) -> Union[np.ma.MaskedArray, npt.NDArray]: """unmask(mask, data) Unmask the data, assuming it's been masked. Creates a volume @@ -54,15 +56,15 @@ def unmask(mask, data): return output.squeeze() -def detrend_median(data, kernel=15): +def detrend_median(data: npt.NDArray, kernel: int=15) -> npt.NDArray: from scipy.signal import medfilt lowfreq = medfilt(data, [1, kernel, kernel]) return data - lowfreq -def detrend_gradient(data, diff=3): +def detrend_gradient(data: npt.ArrayLike, diff: int=3) -> npt.NDArray: return (np.array(np.gradient(data, 1, diff, diff))**2).sum(0) -def detrend_poly(data, polyorder = 10, mask=None): +def detrend_poly(data: npt.NDArray, polyorder: int = 10, mask: Optional[npt.NDArray] = None) -> npt.NDArray: from scipy.special import legendre polys = [legendre(i) for i in range(polyorder)] s = data.shape @@ -84,7 +86,7 @@ def detrend_poly(data, polyorder = 10, mask=None): else: return detrended.reshape(*s) -def mosaic(data, dim=0, show=True, **kwargs): +def mosaic(data: npt.NDArray, dim: int=0, show: bool=True, **kwargs) -> tuple[npt.NDArray, tuple[int, int]]: """ Turns volume data into a mosaic, useful for quickly viewing volumetric data with radiological convention (left side of figure is right side of subject). From 57fabffa98b350a2554c9563b07558457103a623 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 12 Aug 2026 22:45:44 -0700 Subject: [PATCH 08/39] xfm.py: better type Extracted from 6a914786; the dataset/view2D.py and dataset/viewRGB.py hunks from that same original commit belong with the Dataset hierarchy PR instead. --- cortex/xfm.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cortex/xfm.py b/cortex/xfm.py index 9009ef085..48395282a 100644 --- a/cortex/xfm.py +++ b/cortex/xfm.py @@ -11,6 +11,8 @@ class Transform: A standard affine transform. Typically holds a transform from anatomical magnet space to epi file space. ''' + shape: tuple[int, int, int] + def __init__(self, xfm: npt.NDArray, reference: Union[str, tuple[int, int, int], npt.NDArray]): self.xfm = xfm self.reference = None @@ -26,7 +28,7 @@ def __init__(self, xfm: npt.NDArray, reference: Union[str, tuple[int, int, int], self.shape = reference else: self.reference = reference - self.shape = self.reference.shape[:3][::-1] + self.shape = self.reference.shape[:3][::-1] # type: ignore def __call__(self, pts: npt.NDArray) -> npt.NDArray: return np.dot(self.xfm, np.hstack([pts, np.ones((len(pts),1))]).T)[:3].T From 3a1f7b012ac377f2edcb50c87269885f33980ae6 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Sat, 14 Mar 2026 01:41:07 -0700 Subject: [PATCH 09/39] Some types for align --- cortex/align.py | 14 ++++++++++++-- cortex/xfm.py | 4 ++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/cortex/align.py b/cortex/align.py index 7a236db0c..8fc8f9455 100644 --- a/cortex/align.py +++ b/cortex/align.py @@ -5,6 +5,7 @@ import subprocess as sp import tempfile import warnings +from typing import Optional import numpy as np @@ -107,8 +108,17 @@ def fs_manual(subject, xfmname, **kwargs): return manual(subject, xfmname, **kwargs) -def manual(subject, xfmname, output_name="register.lta", wm_color="yellow", - pial_color="blue", wm_surface='white', noclean=False, reference=None, inspect_only=False): +def manual( + subject: str, + xfmname: str, + output_name: str = "register.lta", + wm_color: str = "yellow", + pial_color: str = "blue", + wm_surface: str = "white", + noclean: bool = False, + reference: Optional[str] = None, + inspect_only: bool = False, +) -> Optional[str]: """Open Freesurfer FreeView GUI for manually aligning/adjusting a functional volume to the cortical surface for `subject`. This creates a new transform called `xfmname`. The name of a nibabel-readable file (e.g. NIfTI) should be diff --git a/cortex/xfm.py b/cortex/xfm.py index 48395282a..209c68712 100644 --- a/cortex/xfm.py +++ b/cortex/xfm.py @@ -1,7 +1,7 @@ """Affine transformation class """ import os -from typing import Union, Any +from typing import Optional, Union, Any import numpy as np import numpy.typing as npt import subprocess @@ -287,7 +287,7 @@ def from_freesurfer(cls, fs_register, func_nii, subject, freesurfer_subject_dir= return cls(coord, refIm) - def to_freesurfer(self, fs_register, subject, freesurfer_subject_dir=None): + def to_freesurfer(self, fs_register: str, subject: str, freesurfer_subject_dir: Optional[str]=None): """Converts a pycortex transform to a FreeSurfer transform. Converts a transform stored in pycortex xfm object to the FreeSurfer format From 60647f166219791c257b4c0b853593513c883e19 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 12 Aug 2026 22:46:17 -0700 Subject: [PATCH 10/39] xfm.py: remove some Python 2 logic Extracted from e2bc6512; the dataset/braindata.py hunk from that same original commit belongs with the Dataset hierarchy PR instead. --- cortex/xfm.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/cortex/xfm.py b/cortex/xfm.py index 209c68712..b73f7db98 100644 --- a/cortex/xfm.py +++ b/cortex/xfm.py @@ -264,7 +264,7 @@ def from_freesurfer(cls, fs_register, func_nii, subject, freesurfer_subject_dir= # Read vox2ras transform for the anatomical volume try: cmd = ('mri_info', '--vox2ras', anat_mgz) - L = decode(subprocess.check_output(cmd)).splitlines() + L = subprocess.check_output(cmd).decode().splitlines() anat_vox2ras = np.array([[np.float64(s) for s in ll.split() if s] for ll in L]) except OSError: print ("Error occurred while executing:\n{}".format(' '.join(cmd))) @@ -344,13 +344,6 @@ def to_freesurfer(self, fs_register: str, subject: str, freesurfer_subject_dir: return fs_anat2func -def isstr(obj: Any) -> bool: - """Check for stringy-ness in python 2.7 or 3""" - try: - return isinstance(obj, basestring) - except NameError: - return isinstance(obj, str) - def decode(obj): if isinstance(obj, bytes): obj = obj.decode() @@ -369,7 +362,7 @@ def _vox2ras_tkr(image): output affine""" try: cmd = ('mri_info', '--vox2ras-tkr', image) - L = decode(subprocess.check_output(cmd)).splitlines() + L = subprocess.check_output(cmd).decode().splitlines() # Skip headers/additional information. Example output of # mri_info --vox2ras-tkr # From b23fbb31b1f171cee3a15c048a4574dd05de5c2b Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Sat, 14 Mar 2026 02:27:04 -0700 Subject: [PATCH 11/39] nibabel types --- cortex/align.py | 20 ++++++++++---------- cortex/volume.py | 2 +- cortex/xfm.py | 7 ++++--- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/cortex/align.py b/cortex/align.py index 8fc8f9455..db770b7f8 100644 --- a/cortex/align.py +++ b/cortex/align.py @@ -5,7 +5,7 @@ import subprocess as sp import tempfile import warnings -from typing import Optional +from typing import Literal, Optional import numpy as np @@ -357,15 +357,15 @@ def automatic_fsl( def automatic( - subject, - xfmname, - reference, - init="coreg", - epi_mask=False, - intermediate=None, - reference_contrast="t2", - noclean=False, -): + subject: str, + xfmname: str, + reference: str, + init: str = "coreg", + epi_mask: bool = False, + intermediate: Optional[str] = None, + reference_contrast: Literal['t1', 't2'] = "t2", + noclean: bool = False, +) -> Optional[str]: """Perform automatic alignment using Freesurfer's boundary-based registration. The `reference` image and resulting transform called `xfmname` will be automatically stored in the database. diff --git a/cortex/volume.py b/cortex/volume.py index df83dcd0f..7110e25f7 100644 --- a/cortex/volume.py +++ b/cortex/volume.py @@ -241,7 +241,7 @@ def epi2anatspace(volumedata, order=1): offset=transpart, output_shape=anat.shape[::-1], cval=np.nan, order=order).T -def anat2epispace(anatdata, subject, xfmname, order=1): +def anat2epispace(anatdata: npt.NDArray, subject: str, xfmname: str, order: int=1) -> npt.NDArray: """Resamples data from anatomical space into epi space Parameters diff --git a/cortex/xfm.py b/cortex/xfm.py index b73f7db98..549dfcd7d 100644 --- a/cortex/xfm.py +++ b/cortex/xfm.py @@ -1,7 +1,7 @@ """Affine transformation class """ import os -from typing import Optional, Union, Any +from typing import Optional, Union, cast import numpy as np import numpy.typing as npt import subprocess @@ -20,8 +20,8 @@ def __init__(self, xfm: npt.NDArray, reference: Union[str, tuple[int, int, int], if isinstance(reference, str): import nibabel try: - self.reference = nibabel.load(reference) - self.shape = self.reference.shape[:3][::-1] + self.reference = cast(nibabel.Nifti1Image, nibabel.load(reference)) + self.shape = self.reference.shape[:3][::-1] # type: ignore except IOError: self.reference = reference elif isinstance(reference, tuple): @@ -176,6 +176,7 @@ def to_fsl(self, anat_nii, direction='func>anat'): # transforms. Thus the anatomical file is the "infile" in FSL-speak. infile = anat_nii + inIm: nibabel.Nifti1Image try: inIm = nibabel.load(infile) except AttributeError: From 699b6dbf11c9c806ab99253f9f308b88aa82da4d Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 15 Apr 2026 19:14:13 -0700 Subject: [PATCH 12/39] cortex.volume: partial type annots. Could be expanded more with dimensions --- cortex/volume.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cortex/volume.py b/cortex/volume.py index 7110e25f7..582dd5f64 100644 --- a/cortex/volume.py +++ b/cortex/volume.py @@ -1,7 +1,7 @@ """Contains functions for working with volume data """ import os -from typing import Optional, Union +from typing import Optional, TypeVar, Union import numpy as np import numpy.typing as npt @@ -9,7 +9,8 @@ from .database import db from .xfm import Transform -def unmask(mask: npt.NDArray, data: npt.NDArray) -> Union[np.ma.MaskedArray, npt.NDArray]: +DType = TypeVar('DType', bound=np.generic) +def unmask(mask: np.ndarray[tuple[int, int, int], np.dtype[np.bool_]], data: npt.NDArray[DType]) -> Union[np.ma.MaskedArray, npt.NDArray[DType], npt.NDArray[np.uint8]]: """unmask(mask, data) Unmask the data, assuming it's been masked. Creates a volume @@ -86,7 +87,7 @@ def detrend_poly(data: npt.NDArray, polyorder: int = 10, mask: Optional[npt.NDAr else: return detrended.reshape(*s) -def mosaic(data: npt.NDArray, dim: int=0, show: bool=True, **kwargs) -> tuple[npt.NDArray, tuple[int, int]]: +def mosaic(data: npt.NDArray[DType], dim: int=0, show: bool=True, **kwargs) -> tuple[Union[npt.NDArray[DType], npt.NDArray[np.uint8]], tuple[int, int]]: """ Turns volume data into a mosaic, useful for quickly viewing volumetric data with radiological convention (left side of figure is right side of subject). From 047fe5762f7fed06ec905a137ada41b980095db3 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 12 Aug 2026 22:46:33 -0700 Subject: [PATCH 13/39] volume.py: more specific types for unmask's MaskedArray return Extracted from 7a1fb693; the dataset/braindata.py, dataset/viewRGB.py, and webgl/data.py hunks from that same original commit belong with the Dataset hierarchy and webgl PRs instead. --- cortex/volume.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cortex/volume.py b/cortex/volume.py index 582dd5f64..bb1698c2d 100644 --- a/cortex/volume.py +++ b/cortex/volume.py @@ -10,7 +10,8 @@ from .xfm import Transform DType = TypeVar('DType', bound=np.generic) -def unmask(mask: np.ndarray[tuple[int, int, int], np.dtype[np.bool_]], data: npt.NDArray[DType]) -> Union[np.ma.MaskedArray, npt.NDArray[DType], npt.NDArray[np.uint8]]: +# TODO: MaskedArray typing might require newer numpy versions. If so, drop the generic typing. +def unmask(mask: np.ndarray[tuple[int, int, int], np.dtype[np.bool_]], data: npt.NDArray[DType]) -> Union[np.ma.MaskedArray[tuple[int, ...], np.dtype[DType]], npt.NDArray[DType], npt.NDArray[np.uint8]]: """unmask(mask, data) Unmask the data, assuming it's been masked. Creates a volume From 3e0c421c3215a727a5fe60cf0a6117de3df565f4 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Tue, 11 Aug 2026 16:08:54 -0700 Subject: [PATCH 14/39] volume: use scipy.ndimage instead of deprecated interpolation shim (mypy Wave 3) scipy.ndimage.interpolation.affine_transform is a deprecated shim typed as returning `object`, which mypy then can't follow through `.T`. Import from scipy.ndimage instead, and narrow anat2epispace's `order` param to the Literal[0..5] scipy actually accepts. --- cortex/volume.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cortex/volume.py b/cortex/volume.py index bb1698c2d..e4e0677d0 100644 --- a/cortex/volume.py +++ b/cortex/volume.py @@ -1,7 +1,7 @@ """Contains functions for working with volume data """ import os -from typing import Optional, TypeVar, Union +from typing import Literal, Optional, TypeVar, Union import numpy as np import numpy.typing as npt @@ -118,7 +118,7 @@ def mosaic(data: npt.NDArray[DType], dim: int=0, show: bool=True, **kwargs) -> t else: output = (np.nan*np.ones(shape)).astype(data.dtype) - sl = [slice(None), slice(None), slice(None)] + sl: list[Union[int, slice]] = [slice(None), slice(None), slice(None)] for h in range(ntall): for w in range(nwide): sl[dim] = h*nwide+w @@ -227,7 +227,7 @@ def epi2anatspace(volumedata, order=1): anatspace : ndarray The ND array of the anatomy space data """ - from scipy.ndimage.interpolation import affine_transform + from scipy.ndimage import affine_transform ds = dataset.normalize(volumedata) volumedata = ds#.data @@ -243,7 +243,7 @@ def epi2anatspace(volumedata, order=1): offset=transpart, output_shape=anat.shape[::-1], cval=np.nan, order=order).T -def anat2epispace(anatdata: npt.NDArray, subject: str, xfmname: str, order: int=1) -> npt.NDArray: +def anat2epispace(anatdata: npt.NDArray, subject: str, xfmname: str, order: Literal[0, 1, 2, 3, 4, 5]=1) -> npt.NDArray: """Resamples data from anatomical space into epi space Parameters @@ -262,7 +262,7 @@ def anat2epispace(anatdata: npt.NDArray, subject: str, xfmname: str, order: int= epidata : ndarray data in EPI space """ - from scipy.ndimage.interpolation import affine_transform + from scipy.ndimage import affine_transform anatref = db.get_anat(subject) target = db.get_xfm(subject, xfmname, "coord") From b26114a3a415d2bfb2cb73664982fe71937349af Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 12 Aug 2026 22:47:14 -0700 Subject: [PATCH 15/39] xfm/align: apply Transform.reference / reference_nifti changes from fd711d8b Extracted from fd711d8b; the dataset/braindata.py and utils.py hunks from that same original commit belong with the Dataset hierarchy and utils PRs instead. The database.py hunk is deferred to later in this branch's history, at the point in PR 4's own commit sequence where it originally landed (item 17 of 22). --- cortex/align.py | 4 +++- cortex/xfm.py | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/cortex/align.py b/cortex/align.py index db770b7f8..1940221fd 100644 --- a/cortex/align.py +++ b/cortex/align.py @@ -201,7 +201,9 @@ def manual( if reference is None: # Load load extant transform-relevant things - reference = sub_xfm.reference.get_filename() + if sub_xfm.reference is None: + raise ValueError('Cannot inspect reference-free transform') + reference = sub_xfm.reference_nifti.get_filename() _ = sub_xfm.to_freesurfer(os.path.join(cache, "register.dat"), subject) # Transform in freesurfer .dat format # Command for FreeView and run cmd = ("freeview -v $SUBJECTS_DIR/{sub}/mri/orig.mgz " diff --git a/cortex/xfm.py b/cortex/xfm.py index 549dfcd7d..f00480a87 100644 --- a/cortex/xfm.py +++ b/cortex/xfm.py @@ -1,19 +1,23 @@ """Affine transformation class """ import os -from typing import Optional, Union, cast +from typing import Optional, Union, cast, TYPE_CHECKING import numpy as np import numpy.typing as npt import subprocess +if TYPE_CHECKING: + import nibabel + class Transform: ''' A standard affine transform. Typically holds a transform from anatomical magnet space to epi file space. ''' shape: tuple[int, int, int] + reference: Optional[Union[str, "nibabel.Nifti1Image", npt.NDArray]] - def __init__(self, xfm: npt.NDArray, reference: Union[str, tuple[int, int, int], npt.NDArray]): + def __init__(self, xfm: npt.NDArray, reference: Union[str, tuple[int, int, int], npt.NDArray, "nibabel.Nifti1Image"]): self.xfm = xfm self.reference = None @@ -30,6 +34,15 @@ def __init__(self, xfm: npt.NDArray, reference: Union[str, tuple[int, int, int], self.reference = reference self.shape = self.reference.shape[:3][::-1] # type: ignore + @property + def reference_nifti(self) -> "nibabel.Nifti1Image": + """The reference as a loaded nifti image, for callers that need its + affine/header. Raises if the reference is absent or never loaded.""" + import nibabel + if not isinstance(self.reference, nibabel.Nifti1Image): + raise ValueError('Transform has no loaded reference image') + return self.reference + def __call__(self, pts: npt.NDArray) -> npt.NDArray: return np.dot(self.xfm, np.hstack([pts, np.ones((len(pts),1))]).T)[:3].T @@ -324,10 +337,10 @@ def to_freesurfer(self, fs_register: str, subject: str, freesurfer_subject_dir: anat_tkrvox2ras = _vox2ras_tkr(anat.get_filename()) # Read tkvox2ras transform for the functional volume - func_tkrvox2ras = _vox2ras_tkr(self.reference.get_filename()) + func_tkrvox2ras = _vox2ras_tkr(self.reference_nifti.get_filename()) # Read voxel resolution of the functional volume - func_voxres = self.reference.header.get_zooms() + func_voxres = self.reference_nifti.header.get_zooms() # Calculate FreeSurfer transform fs_anat2func = np.dot(func_tkrvox2ras, np.dot(self.xfm, np.dot(anat_vox2ras, inv(anat_tkrvox2ras)))) From e64b6f5ff952ee646d54b00031be3d8f8f5bfb90 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Sat, 28 Feb 2026 00:59:49 -0800 Subject: [PATCH 16/39] Typing: formats.pyx --- cortex/formats.pyx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cortex/formats.pyx b/cortex/formats.pyx index 7fad11700..ca84b13ae 100644 --- a/cortex/formats.pyx +++ b/cortex/formats.pyx @@ -8,6 +8,8 @@ from collections import OrderedDict cimport cython cimport numpy as np +import numpy as _py_np +import numpy.typing as npt from libc.string cimport strtok from libc.stdlib cimport atoi, atof @@ -16,7 +18,7 @@ np.import_array() PY3 = sys.version_info[0] > 3 -def read(globname): +def read(globname: str) -> tuple[npt.NDArray[_py_np.floating], npt.NDArray[_py_np.integer]]: readers = OrderedDict([('gii', read_gii), ('npz', read_npz), ('vtk', read_vtk), ('off', read_off), ('stl', read_stl)]) for ext, func in readers.items(): try: From 559bd980541af79dadf84ebd2d451893084a0cc2 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 4 Mar 2026 01:26:25 -0800 Subject: [PATCH 17/39] Add type stub for formats.pyx . Unsure if the right approach --- cortex/formats.pyi | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 cortex/formats.pyi diff --git a/cortex/formats.pyi b/cortex/formats.pyi new file mode 100644 index 000000000..82614c9ad --- /dev/null +++ b/cortex/formats.pyi @@ -0,0 +1,49 @@ +from __future__ import annotations + +import sys + +import cython +import numpy as _py_np +import numpy.typing as npt + +PY3 = sys.version_info[0] > 3 + +def read(globname: str) -> tuple[npt.NDArray[_py_np.floating], npt.NDArray[_py_np.integer]]: + ... + +def read_off(filename): + ... + +def read_npz(filename): + ... + +def read_gii(filename): + ... + +@cython.boundscheck(False) +def read_stl(filename): + ... + +def read_obj(filename, norm=False, uv=False): + ... + +@cython.boundscheck(False) +def read_vtk(filename): + ... + +def write_vtk(filename, pts: object, polys: object, norms: object=None): + ... + +def write_off(filename, pts: object, polys: object): + ... + +def write_stl(filename, pts: object, polys: object): + ... + +def write_gii(filename, pts: object, polys: object): + ... + +def write_obj(filename, pts: object, polys: object, colors: object=None): + ... + +# This file was generated by stubgen-pyx v0.2.2 from formats.pyx \ No newline at end of file From f5b8d168bedfa04544b28d6d81521368de71aba2 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Sat, 4 Apr 2026 02:07:17 -0700 Subject: [PATCH 18/39] formats.pyi read_obj types --- cortex/formats.pyi | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/cortex/formats.pyi b/cortex/formats.pyi index 82614c9ad..24991e0f9 100644 --- a/cortex/formats.pyi +++ b/cortex/formats.pyi @@ -1,5 +1,6 @@ from __future__ import annotations +from typing import overload, Literal import sys import cython @@ -24,7 +25,16 @@ def read_gii(filename): def read_stl(filename): ... -def read_obj(filename, norm=False, uv=False): +@overload +def read_obj(filename: str, norm: Literal[False]=False, uv: Literal[False]=False) -> tuple[npt.NDArray[_py_np.floating], npt.NDArray[_py_np.integer]]: + ... + +# TODO: add overloads for all cases +@overload +def read_obj(filename: str, norm: Literal[True], uv: Literal[True]) -> tuple[npt.NDArray[_py_np.floating], npt.NDArray[_py_np.integer], list[list[float]], list[list[float]]]: + ... + +def read_obj(filename: str, norm: bool=False, uv: bool=False) -> tuple[npt.NDArray[_py_np.floating], npt.NDArray[_py_np.integer], list[list[float]] | None, list[list[float]] | None]: ... @cython.boundscheck(False) From 5638cc094390dc71a612c8600c0412e0ff095347 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Sat, 4 Apr 2026 02:07:17 -0700 Subject: [PATCH 19/39] Finish formats.pyi stub --- cortex/formats.pyi | 92 ++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 48 deletions(-) diff --git a/cortex/formats.pyi b/cortex/formats.pyi index 24991e0f9..1c69cdffd 100644 --- a/cortex/formats.pyi +++ b/cortex/formats.pyi @@ -1,59 +1,55 @@ -from __future__ import annotations +import os +from typing import Any, Literal, overload -from typing import overload, Literal -import sys - -import cython import numpy as _py_np import numpy.typing as npt -PY3 = sys.version_info[0] > 3 - -def read(globname: str) -> tuple[npt.NDArray[_py_np.floating], npt.NDArray[_py_np.integer]]: - ... +_Path = str | os.PathLike[str] -def read_off(filename): - ... +PY3: bool -def read_npz(filename): - ... +def read(globname: str) -> tuple[npt.NDArray[_py_np.floating], npt.NDArray[_py_np.integer]]: ... -def read_gii(filename): - ... +# `polys` is float64 rather than integer for a file declaring zero faces, since +# `np.array([])` is float64. Same caveat on `read_obj` below. +def read_off(filename: _Path) -> tuple[npt.NDArray[_py_np.floating], npt.NDArray[_py_np.integer]]: ... -@cython.boundscheck(False) -def read_stl(filename): - ... +# dtypes are whatever was stored in the archive. +def read_npz(filename: _Path) -> tuple[npt.NDArray[Any], npt.NDArray[Any]]: ... +def read_gii(filename: _Path) -> tuple[npt.NDArray[_py_np.floating], npt.NDArray[_py_np.integer]]: ... +def read_stl(filename: _Path) -> tuple[npt.NDArray[_py_np.floating], npt.NDArray[_py_np.uint32]]: ... +# 2-tuple only when both flags are literal `False`; unsound for non-literal `False, False`. @overload -def read_obj(filename: str, norm: Literal[False]=False, uv: Literal[False]=False) -> tuple[npt.NDArray[_py_np.floating], npt.NDArray[_py_np.integer]]: - ... - -# TODO: add overloads for all cases +def read_obj( # type: ignore[overload-overlap] # overlaps the bool fallback below + filename: _Path, norm: Literal[False] = False, uv: Literal[False] = False +) -> tuple[npt.NDArray[_py_np.floating], npt.NDArray[_py_np.integer]]: ... @overload -def read_obj(filename: str, norm: Literal[True], uv: Literal[True]) -> tuple[npt.NDArray[_py_np.floating], npt.NDArray[_py_np.integer], list[list[float]], list[list[float]]]: - ... - -def read_obj(filename: str, norm: bool=False, uv: bool=False) -> tuple[npt.NDArray[_py_np.floating], npt.NDArray[_py_np.integer], list[list[float]] | None, list[list[float]] | None]: - ... - -@cython.boundscheck(False) -def read_vtk(filename): - ... - -def write_vtk(filename, pts: object, polys: object, norms: object=None): - ... - -def write_off(filename, pts: object, polys: object): - ... - -def write_stl(filename, pts: object, polys: object): - ... - -def write_gii(filename, pts: object, polys: object): - ... - -def write_obj(filename, pts: object, polys: object, colors: object=None): - ... - -# This file was generated by stubgen-pyx v0.2.2 from formats.pyx \ No newline at end of file +def read_obj( + filename: _Path, norm: bool = False, uv: bool = False +) -> tuple[ + npt.NDArray[_py_np.floating], + npt.NDArray[_py_np.integer], + list[list[float]] | None, + list[list[float]] | None, +]: ... + +def read_vtk(filename: _Path) -> tuple[npt.NDArray[_py_np.float64], npt.NDArray[_py_np.uint32]]: ... + +# The writers require real arrays, not just ArrayLike: they read `polys.dtype` +# and `pts.astype`, and index with `pts[polys]`. +def write_vtk( + filename: _Path, + pts: npt.NDArray[Any], + polys: npt.NDArray[Any], + norms: npt.NDArray[Any] | None = None, +) -> None: ... +def write_off(filename: _Path, pts: npt.NDArray[Any], polys: npt.NDArray[Any]) -> None: ... +def write_stl(filename: _Path, pts: npt.NDArray[Any], polys: npt.NDArray[Any]) -> None: ... +def write_gii(filename: _Path, pts: npt.NDArray[Any], polys: npt.NDArray[Any]) -> None: ... +def write_obj( + filename: _Path, + pts: npt.NDArray[Any], + polys: npt.NDArray[Any], + colors: npt.NDArray[Any] | None = None, +) -> None: ... From 19d8a944fb99c0e9f230883bcc0dcbf0f7f1ac2e Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 2 Apr 2025 00:09:06 -0500 Subject: [PATCH 20/39] Add types to Database --- cortex/database.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/cortex/database.py b/cortex/database.py index 336471b10..601319d6c 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -1,6 +1,7 @@ """ Contains a singleton object `db` of type `Database` which allows easy access to surface files, anatomical images, and transforms that are stored in the pycortex filestore. """ +from __future__ import annotations import copy import functools import glob @@ -11,6 +12,7 @@ import tempfile import warnings from hashlib import sha1 +from typing import Optional, TypedDict import numpy as np @@ -18,6 +20,18 @@ default_filestore = options.config.get('basic', 'filestore') +class PathsType(TypedDict): + surfs: dict[str, dict[str, str]] + xfms: list[str] + xfmdir: str + anats: str + surfinfo: str + masks: str + rois: str + overlays: str + views: list[str] + surf2surf: str + def _memo(fn): @functools.wraps(fn) @@ -150,10 +164,10 @@ class Database: ---------- This database object dynamically generates handles to all subjects within the filestore. """ - def __init__(self, filestore=default_filestore): + def __init__(self, filestore: str=default_filestore): self.filestore = filestore - self._subjects = None - self.auxfile = None + self._subjects: Optional[dict[str, SubjectDB]] = None + self.auxfile: Optional[Database] = None def __repr__(self): subjs = "\n ".join(sorted(self.subjects.keys())) @@ -434,7 +448,7 @@ def save_xfm(self, subject, name, xfm, xfmtype="magnet", reference=None): with open(fname, "w") as fp: json.dump(jsdict, fp, sort_keys=True, indent=4) - def get_xfm(self, subject, name, xfmtype="coord"): + def get_xfm(self, subject, name, xfmtype="coord") -> 'Transform': """Retrieves a transform from the filestore Parameters @@ -464,7 +478,7 @@ def get_xfm(self, subject, name, xfmtype="coord"): return Transform(xfmdict[xfmtype], reference) @_memo - def get_surf(self, subject, type, hemisphere="both", merge=False, nudge=False): + def get_surf(self, subject, type: str, hemisphere="both", merge=False, nudge=False): '''Return the surface pair for the given subject, surface type, and hemisphere. Parameters @@ -553,6 +567,7 @@ def get_mask(self, subject, xfmname, type='thick'): try: import nibabel nib = nibabel.load(fname) + #reveal_type(nib.get_fdata().T != 0) return nib.get_fdata().T != 0 except IOError: print('Mask not found, generating...') @@ -667,7 +682,7 @@ def get_paths(self, subject): if self.subjects[subject]._warning is not None: warnings.warn(self.subjects[subject]._warning) - surfs = dict() + surfs: dict[str, dict[str, str]] = dict() for surf in os.listdir(surfpath): ssurf = os.path.splitext(surf)[0].split('_') name = '_'.join(ssurf[:-1]) @@ -682,7 +697,7 @@ def get_paths(self, subject): os.makedirs(viewsdir) views = os.listdir(viewsdir) - filenames = dict( + filenames = PathsType( surfs=surfs, xfms=sorted(os.listdir(os.path.join(self.filestore, subject, "transforms"))), xfmdir=os.path.join(self.filestore, subject, "transforms", "{xfmname}", "matrices.xfm"), From 75585b1c7e24f344587db421f8c6246d8c6f981d Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 12 Aug 2026 22:47:42 -0700 Subject: [PATCH 21/39] database.py: add types to lots of things Extracted from fffcb009; the svgoverlay.py hunk from that same original commit belongs with the svgoverlay PR instead. --- cortex/database.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/cortex/database.py b/cortex/database.py index 601319d6c..f8539e0fc 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -12,11 +12,14 @@ import tempfile import warnings from hashlib import sha1 -from typing import Optional, TypedDict +from typing import Dict, List, Tuple, Union, Optional, TypedDict, TYPE_CHECKING import numpy as np from . import options +if TYPE_CHECKING: + from cortex.dataset.views import Vertex + from cortex.svgoverlay import SVGOverlay default_filestore = options.config.get('basic', 'filestore') @@ -169,11 +172,11 @@ def __init__(self, filestore: str=default_filestore): self._subjects: Optional[dict[str, SubjectDB]] = None self.auxfile: Optional[Database] = None - def __repr__(self): + def __repr__(self) -> str: subjs = "\n ".join(sorted(self.subjects.keys())) return """Pycortex database\n Subjects:\n {subjs}""".format(subjs=subjs) - def __getattr__(self, attr): + def __getattr__(self, attr: str): if attr in self.subjects: if self.subjects[attr]._warning is not None: warnings.warn(self.subjects[attr]._warning) @@ -187,7 +190,7 @@ def __dir__(self): 'get_mri_surf2surf_matrix'] + list(self.subjects.keys()) @property - def subjects(self): + def subjects(self) -> Dict[str, SubjectDB]: if self._subjects is not None: return self._subjects subjs = os.listdir(os.path.join(self.filestore)) @@ -240,7 +243,7 @@ def get_anat(self, subject, type='raw', xfmname=None, recache=False, order=1, ** from . import volume return volume.anat2epispace(anatnib.get_fdata().T.astype(float), subject, xfmname, order=order) - def get_surfinfo(self, subject, type="curvature", recache=False, **kwargs): + def get_surfinfo(self, subject: str, type: str="curvature", recache: bool=False, **kwargs) -> Vertex: """Return auxiliary surface information from the filestore. Surface info is defined as anatomical information specific to a subject in surface space. A Vertex class will be returned as necessary. Info not found in the filestore will be automatically generated. @@ -359,7 +362,7 @@ def get_mri_surf2surf_matrix(self, subject, surface_type, hemi='both', save_sparse_array(fpath, tmp, h, mode='a') return mats - def get_overlay(self, subject, overlay_file=None, **kwargs): + def get_overlay(self, subject: str, overlay_file: Optional[str]=None, **kwargs) -> SVGOverlay: from . import svgoverlay pts, polys = self.get_surf(subject, "flat", merge=True, nudge=True) @@ -448,7 +451,7 @@ def save_xfm(self, subject, name, xfm, xfmtype="magnet", reference=None): with open(fname, "w") as fp: json.dump(jsdict, fp, sort_keys=True, indent=4) - def get_xfm(self, subject, name, xfmtype="coord") -> 'Transform': + def get_xfm(self, subject: str, name: str, xfmtype: str="coord") -> 'Transform': """Retrieves a transform from the filestore Parameters @@ -478,7 +481,7 @@ def get_xfm(self, subject, name, xfmtype="coord") -> 'Transform': return Transform(xfmdict[xfmtype], reference) @_memo - def get_surf(self, subject, type: str, hemisphere="both", merge=False, nudge=False): + def get_surf(self, subject: str, type: str, hemisphere: str="both", merge: bool=False, nudge: bool=False) -> Union[Tuple[Tuple[ndarray, ndarray], Tuple[ndarray, ndarray]], Tuple[ndarray, ndarray]]: '''Return the surface pair for the given subject, surface type, and hemisphere. Parameters @@ -554,7 +557,7 @@ def save_mask(self, subject, xfmname, type, mask): nib = nibabel.Nifti1Image(mask.astype(np.uint8).T, affine) nib.to_filename(fname) - def get_mask(self, subject, xfmname, type='thick'): + def get_mask(self, subject: str, xfmname: str, type: str='thick') -> ndarray: if hasattr(type, 'decode'): type = type.decode('utf8') @@ -633,7 +636,7 @@ def get_coords(self, subject, xfmname, hemisphere="both", magnet=None): return coords - def get_cache(self, subject): + def get_cache(self, subject: str) -> str: try: self.auxfile.get_surf(subject, "fiducial") #generate the hashed name of the filename and subject as the directory name @@ -674,7 +677,7 @@ def clear_cache(self, subject, clear_all_caches=True): shutil.rmtree(default_cachedir) os.makedirs(default_cachedir) - def get_paths(self, subject): + def get_paths(self, subject: str): """Get a dictionary with a list of all candidate filenames for associated data, such as roi overlays, flatmap caches, and ctm caches. """ surfpath = os.path.join(self.filestore, subject, "surfaces") From ea45894d1909613ea0b69f8970ec4f2a51ed2bcc Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 1 Oct 2025 01:39:46 -0700 Subject: [PATCH 22/39] Cleanup type annotations --- cortex/database.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cortex/database.py b/cortex/database.py index f8539e0fc..08f5e734f 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -12,11 +12,13 @@ import tempfile import warnings from hashlib import sha1 -from typing import Dict, List, Tuple, Union, Optional, TypedDict, TYPE_CHECKING +from typing import Tuple, Union, Optional, TypedDict, TYPE_CHECKING import numpy as np +import numpy.typing as npt from . import options +from .xfm import Transform if TYPE_CHECKING: from cortex.dataset.views import Vertex from cortex.svgoverlay import SVGOverlay @@ -190,7 +192,7 @@ def __dir__(self): 'get_mri_surf2surf_matrix'] + list(self.subjects.keys()) @property - def subjects(self) -> Dict[str, SubjectDB]: + def subjects(self) -> dict[str, SubjectDB]: if self._subjects is not None: return self._subjects subjs = os.listdir(os.path.join(self.filestore)) @@ -451,7 +453,7 @@ def save_xfm(self, subject, name, xfm, xfmtype="magnet", reference=None): with open(fname, "w") as fp: json.dump(jsdict, fp, sort_keys=True, indent=4) - def get_xfm(self, subject: str, name: str, xfmtype: str="coord") -> 'Transform': + def get_xfm(self, subject: str, name: str, xfmtype: str="coord") -> Transform: """Retrieves a transform from the filestore Parameters @@ -463,7 +465,6 @@ def get_xfm(self, subject: str, name: str, xfmtype: str="coord") -> 'Transform': xfmtype : str, optional Type of transform to return. Defaults to coord. """ - from .xfm import Transform if xfmtype == 'coord': try: return self.auxfile.get_xfm(subject, name) @@ -481,7 +482,7 @@ def get_xfm(self, subject: str, name: str, xfmtype: str="coord") -> 'Transform': return Transform(xfmdict[xfmtype], reference) @_memo - def get_surf(self, subject: str, type: str, hemisphere: str="both", merge: bool=False, nudge: bool=False) -> Union[Tuple[Tuple[ndarray, ndarray], Tuple[ndarray, ndarray]], Tuple[ndarray, ndarray]]: + def get_surf(self, subject: str, type: str, hemisphere: str="both", merge: bool=False, nudge: bool=False) -> Union[Tuple[Tuple[npt.NDArray, npt.NDArray], Tuple[npt.NDArray, npt.NDArray]], Tuple[npt.NDArray, npt.NDArray]]: '''Return the surface pair for the given subject, surface type, and hemisphere. Parameters @@ -557,7 +558,7 @@ def save_mask(self, subject, xfmname, type, mask): nib = nibabel.Nifti1Image(mask.astype(np.uint8).T, affine) nib.to_filename(fname) - def get_mask(self, subject: str, xfmname: str, type: str='thick') -> ndarray: + def get_mask(self, subject: str, xfmname: str, type: str='thick') -> npt.NDArray: if hasattr(type, 'decode'): type = type.decode('utf8') From 1e901635fcbda65c06e0b14f1c10619ae2ea8fa4 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 1 Oct 2025 17:38:57 -0700 Subject: [PATCH 23/39] Add surface-specific overloads for get_surf --- cortex/database.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/cortex/database.py b/cortex/database.py index 08f5e734f..3c31495a8 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -12,7 +12,7 @@ import tempfile import warnings from hashlib import sha1 -from typing import Tuple, Union, Optional, TypedDict, TYPE_CHECKING +from typing import Literal, Tuple, Union, Optional, TypedDict, TYPE_CHECKING, overload import numpy as np import numpy.typing as npt @@ -481,8 +481,20 @@ def get_xfm(self, subject: str, name: str, xfmtype: str="coord") -> Transform: xfmdict = json.load(f) return Transform(xfmdict[xfmtype], reference) + @overload + def get_surf(self, subject: str, type: str, hemisphere: Literal['both']='both', merge: Literal[True], nudge: bool=False) -> Tuple[npt.NDArray, npt.NDArray]: + ... + + @overload + def get_surf(self, subject: str, type: str, hemisphere: Literal['both']='both', merge: Literal[False]=False, nudge: bool=False) -> Tuple[Tuple[npt.NDArray, npt.NDArray], Tuple[npt.NDArray, npt.NDArray]]: + ... + + @overload + def get_surf(self, subject: str, type: str, hemisphere: Literal['lh', 'rh'], merge: bool=False, nudge: bool=False) -> Tuple[npt.NDArray, npt.NDArray]: + ... + @_memo - def get_surf(self, subject: str, type: str, hemisphere: str="both", merge: bool=False, nudge: bool=False) -> Union[Tuple[Tuple[npt.NDArray, npt.NDArray], Tuple[npt.NDArray, npt.NDArray]], Tuple[npt.NDArray, npt.NDArray]]: + def get_surf(self, subject: str, type: str, hemisphere: Literal['lh', 'rh', 'both']="both", merge: bool=False, nudge: bool=False) -> Union[Tuple[Tuple[npt.NDArray, npt.NDArray], Tuple[npt.NDArray, npt.NDArray]], Tuple[npt.NDArray, npt.NDArray]]: '''Return the surface pair for the given subject, surface type, and hemisphere. Parameters From 71a3fa7540c0765c26a3734dffc0aa5a97006ee6 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Sun, 14 Dec 2025 19:52:44 -0800 Subject: [PATCH 24/39] Another attempt at get_surf --- cortex/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cortex/database.py b/cortex/database.py index 3c31495a8..d0392de56 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -482,7 +482,7 @@ def get_xfm(self, subject: str, name: str, xfmtype: str="coord") -> Transform: return Transform(xfmdict[xfmtype], reference) @overload - def get_surf(self, subject: str, type: str, hemisphere: Literal['both']='both', merge: Literal[True], nudge: bool=False) -> Tuple[npt.NDArray, npt.NDArray]: + def get_surf(self, subject: str, type: str, hemisphere: Literal['lh', 'rh', 'both'], merge: Literal[True], nudge: bool=False) -> Tuple[npt.NDArray, npt.NDArray]: ... @overload From 63de07071de349db1ac76eadc42c33efa5954d12 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Sat, 28 Feb 2026 00:21:36 -0800 Subject: [PATCH 25/39] Fix types in database --- cortex/database.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cortex/database.py b/cortex/database.py index d0392de56..6e42ee845 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -12,7 +12,7 @@ import tempfile import warnings from hashlib import sha1 -from typing import Literal, Tuple, Union, Optional, TypedDict, TYPE_CHECKING, overload +from typing import Literal, Tuple, Union, Optional, TypedDict, TYPE_CHECKING, overload, cast import numpy as np import numpy.typing as npt @@ -297,8 +297,8 @@ def get_surfinfo(self, subject: str, type: str="curvature", recache: bool=False, return Vertex(verts, subject) return npz - def get_mri_surf2surf_matrix(self, subject, surface_type, hemi='both', - fs_subj=None, target_subj='fsaverage', + def get_mri_surf2surf_matrix(self, subject: str, surface_type: str, hemi: Literal['lh', 'rh', 'both']='both', + fs_subj: Optional[str]=None, target_subj: str='fsaverage', **kwargs): """Get matrix generated by surf2surf to map one subject's surface to another's @@ -527,7 +527,7 @@ def get_surf(self, subject: str, type: str, hemisphere: Literal['lh', 'rh', 'bot files = self.get_paths(subject)['surfs'] if hemisphere.lower() == "both": - left, right = [ self.get_surf(subject, type, hemisphere=h) for h in ["lh", "rh"]] + left, right = [ self.get_surf(subject, type, hemisphere=cast(Literal['lh', 'rh'], h)) for h in ["lh", "rh"]] if type != "fiducial" and nudge: left[0][:,0] -= left[0].max(0)[0] right[0][:,0] -= right[0].min(0)[0] From 1fe5de3c99959b119e1b5e8ad7739a5ac39cfc78 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 4 Mar 2026 01:29:35 -0800 Subject: [PATCH 26/39] database: specify numpy types --- cortex/database.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cortex/database.py b/cortex/database.py index 6e42ee845..2ef95bbd2 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -482,19 +482,19 @@ def get_xfm(self, subject: str, name: str, xfmtype: str="coord") -> Transform: return Transform(xfmdict[xfmtype], reference) @overload - def get_surf(self, subject: str, type: str, hemisphere: Literal['lh', 'rh', 'both'], merge: Literal[True], nudge: bool=False) -> Tuple[npt.NDArray, npt.NDArray]: + def get_surf(self, subject: str, type: str, hemisphere: Literal['both'], merge: Literal[True], nudge: bool=False) -> Tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]]: ... @overload - def get_surf(self, subject: str, type: str, hemisphere: Literal['both']='both', merge: Literal[False]=False, nudge: bool=False) -> Tuple[Tuple[npt.NDArray, npt.NDArray], Tuple[npt.NDArray, npt.NDArray]]: + def get_surf(self, subject: str, type: str, hemisphere: Literal['both']='both', merge: Literal[False]=False, nudge: bool=False) -> Tuple[Tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]], Tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]]]: ... @overload - def get_surf(self, subject: str, type: str, hemisphere: Literal['lh', 'rh'], merge: bool=False, nudge: bool=False) -> Tuple[npt.NDArray, npt.NDArray]: + def get_surf(self, subject: str, type: str, hemisphere: Literal['lh', 'rh'], merge: bool=False, nudge: bool=False) -> Tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]]: ... @_memo - def get_surf(self, subject: str, type: str, hemisphere: Literal['lh', 'rh', 'both']="both", merge: bool=False, nudge: bool=False) -> Union[Tuple[Tuple[npt.NDArray, npt.NDArray], Tuple[npt.NDArray, npt.NDArray]], Tuple[npt.NDArray, npt.NDArray]]: + def get_surf(self, subject: str, type: str, hemisphere: Literal['lh', 'rh', 'both']="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]]]: '''Return the surface pair for the given subject, surface type, and hemisphere. Parameters From 59f1545397674aa62cad6c4cb4a20ddf8cf349f1 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 4 Mar 2026 03:05:08 -0800 Subject: [PATCH 27/39] Hail mary to get Database.get_surf working --- cortex/database.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cortex/database.py b/cortex/database.py index 2ef95bbd2..32bbb57bb 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -481,8 +481,9 @@ def get_xfm(self, subject: str, name: str, xfmtype: str="coord") -> Transform: xfmdict = json.load(f) return Transform(xfmdict[xfmtype], reference) + # TODO: forcing '*' WILL cause issues. Look for all instances of merge=True ! @overload - def get_surf(self, subject: str, type: str, hemisphere: Literal['both'], merge: Literal[True], nudge: bool=False) -> Tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]]: + def get_surf(self, subject: str, type: str, hemisphere: Literal['both']='both', *, merge: Literal[True], nudge: bool=False) -> Tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]]: ... @overload From e1eed8b043dae6882f114bc72b872eb851f5ca68 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Fri, 6 Mar 2026 15:31:22 -0800 Subject: [PATCH 28/39] Narrower type for Database.get_mask --- cortex/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cortex/database.py b/cortex/database.py index 32bbb57bb..434867b31 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -571,7 +571,7 @@ def save_mask(self, subject, xfmname, type, mask): nib = nibabel.Nifti1Image(mask.astype(np.uint8).T, affine) nib.to_filename(fname) - def get_mask(self, subject: str, xfmname: str, type: str='thick') -> npt.NDArray: + def get_mask(self, subject: str, xfmname: str, type: str='thick') -> npt.NDArray[np.bool]: if hasattr(type, 'decode'): type = type.decode('utf8') From fef261d6f63ef618427c96fc0437fcd21a4feaf7 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Fri, 6 Mar 2026 18:53:54 -0800 Subject: [PATCH 29/39] More cortex/database.py types --- cortex/database.py | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/cortex/database.py b/cortex/database.py index 434867b31..104ecce54 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -12,7 +12,7 @@ import tempfile import warnings from hashlib import sha1 -from typing import Literal, Tuple, Union, Optional, TypedDict, TYPE_CHECKING, overload, cast +from typing import Literal, Union, Optional, TypedDict, TYPE_CHECKING, overload, cast import numpy as np import numpy.typing as npt @@ -52,7 +52,7 @@ def memofn(self, *args, **kwargs): return memofn class SubjectDB: - def __init__(self, subj, filestore=default_filestore): + def __init__(self, subj: str, filestore: str = default_filestore): self.subject = subj self._warning = None self._transforms = None @@ -80,7 +80,7 @@ def surfaces(self): return self._surfaces class SurfaceDB: - def __init__(self, subj, filestore=default_filestore): + def __init__(self, subj: str, filestore: str = default_filestore): self.subject = subj self.types = {} db = Database(filestore) @@ -93,31 +93,31 @@ def __repr__(self): def __dir__(self): return list(self.types.keys()) - def __getattr__(self, attr): + def __getattr__(self, attr: str): if attr in self.types: return self.types[attr] raise AttributeError(attr) class Surf: - def __init__(self, subject, surftype, filestore=default_filestore): + def __init__(self, subject: str, surftype: str, filestore: str = default_filestore): self.subject, self.surftype = subject, surftype self.db = Database(filestore) - def get(self, hemisphere="both"): + def get(self, hemisphere: Literal['lh', 'rh', 'both'] = "both"): return self.db.get_surf(self.subject, self.surftype, hemisphere) - def show(self, hemisphere="both"): + def show(self, hemisphere: Literal['lh', 'rh', 'both'] = "both"): from mayavi import mlab pts, polys = self.db.get_surf(self.subject, self.surftype, hemisphere, merge=True, nudge=True) return mlab.triangular_mesh(pts[:,0], pts[:,1], pts[:,2], polys) class XfmDB: - def __init__(self, subj, filestore=default_filestore): + def __init__(self, subj: str, filestore: str = default_filestore): self.subject = subj self.filestore = filestore - self.xfms = Database(self.filestore).get_paths(subj)['xfms'] + self.xfms: list[str] = Database(self.filestore).get_paths(subj)['xfms'] - def __getitem__(self, name): + def __getitem__(self, name: str) -> 'XfmSet': if name in self.xfms: return XfmSet(self.subject, name, filestore=self.filestore) raise AttributeError @@ -127,7 +127,7 @@ def __repr__(self): return f"Available transforms for {self.subject}:\n{xfms}" class XfmSet: - def __init__(self, subj, name, filestore=default_filestore): + def __init__(self, subj: str, name: str, filestore: str = default_filestore): self.subject = subj self.name = name jspath = os.path.join(filestore, subj, 'transforms', name, 'matrices.xfm') @@ -136,7 +136,7 @@ def __init__(self, subj, name, filestore=default_filestore): self.masks = MaskSet(subj, name, filestore=filestore) self.db = Database(filestore) - def __getattr__(self, attr): + def __getattr__(self, attr: str) -> Transform: if attr in self._jsdat: return self.db.get_xfm(self.subject, self.name, attr) raise AttributeError @@ -145,14 +145,14 @@ def __repr__(self): return "Types: {types}".format(types=", ".join(self._jsdat.keys())) class MaskSet: - def __init__(self, subj, name, filestore=default_filestore): + def __init__(self, subj: str, name: str, filestore: str = default_filestore): self.subject = subj self.xfmname = name maskform = Database(filestore).get_paths(subj)['masks'] maskpath = maskform.format(xfmname=name, type='*') - self._masks = {os.path.split(path)[1][5:-7]: path for path in glob.glob(maskpath)} + self._masks: dict[str, str] = {os.path.split(path)[1][5:-7]: path for path in glob.glob(maskpath)} - def __getitem__(self, item): + def __getitem__(self, item: str) -> npt.NDArray: import nibabel return nibabel.load(self._masks[item]).get_fdata().T @@ -483,19 +483,24 @@ def get_xfm(self, subject: str, name: str, xfmtype: str="coord") -> Transform: # TODO: forcing '*' WILL cause issues. Look for all instances of merge=True ! @overload - def get_surf(self, subject: str, type: str, hemisphere: Literal['both']='both', *, merge: Literal[True], nudge: bool=False) -> Tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]]: + def get_surf(self, subject: str, type: str, hemisphere: Literal['both']='both', *, merge: Literal[True], nudge: bool=False) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]]: + ... + + @overload + def get_surf(self, subject: str, type: str, hemisphere: Literal['both']='both', merge: Literal[False]=False, nudge: bool=False) -> tuple[tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]], tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]]]: ... @overload - def get_surf(self, subject: str, type: str, hemisphere: Literal['both']='both', merge: Literal[False]=False, nudge: bool=False) -> Tuple[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, hemisphere: Literal['lh', 'rh'], merge: bool=False, nudge: bool=False) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]]: ... + # Fallthrough case for the recursive call @overload - def get_surf(self, subject: str, type: str, hemisphere: Literal['lh', 'rh'], merge: bool=False, nudge: bool=False) -> Tuple[npt.NDArray[np.floating], npt.NDArray[np.integer]]: + def get_surf(self, subject: str, type: str, hemisphere: Literal['lh', 'rh', 'both']="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]]]: ... @_memo - def get_surf(self, subject: str, type: str, hemisphere: Literal['lh', 'rh', 'both']="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, hemisphere: Literal['lh', 'rh', 'both']="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]]]: '''Return the surface pair for the given subject, surface type, and hemisphere. Parameters From 7637b93c91f3ea92e87a9bd9cec6aed623305cc4 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 12 Aug 2026 22:48:43 -0700 Subject: [PATCH 30/39] database.py: fix some errors Extracted from 4b97b45d; the dataset/views.py hunk from that same original commit belongs with the Dataset hierarchy PR instead. --- cortex/database.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cortex/database.py b/cortex/database.py index 104ecce54..6ef2db991 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -180,8 +180,9 @@ def __repr__(self) -> str: def __getattr__(self, attr: str): if attr in self.subjects: - if self.subjects[attr]._warning is not None: - warnings.warn(self.subjects[attr]._warning) + _warning = self.subjects[attr]._warning + if _warning is not None: + warnings.warn(_warning) return self.subjects[attr] else: raise AttributeError From 9add77a723fcce38542dec573b07bf0ad909640d Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Mon, 9 Mar 2026 21:03:41 -0700 Subject: [PATCH 31/39] cortex/database.py: reduce typing errors --- cortex/database.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/cortex/database.py b/cortex/database.py index 6ef2db991..40e6d60b0 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -207,7 +207,7 @@ def reload_subjects(self): self._subjects = None self.subjects - def get_anat(self, subject, type='raw', xfmname=None, recache=False, order=1, **kwargs): + def get_anat(self, subject: str, type: Literal['raw', 'brainmask', 'whitematter', 'voxelize'] ='raw', xfmname: Optional[str]=None, recache: bool=False, order: int=1, **kwargs): """Return anatomical information from the filestore. Anatomical information is defined as any volume-space anatomical information pertaining to the subject, such as T1 image, white matter masks, etc. Volumes not found in the database will be automatically generated. @@ -233,6 +233,7 @@ def get_anat(self, subject, type='raw', xfmname=None, recache=False, order=1, ** anatfile = anatform.format(type=type, opts=opts, ext="nii.gz") if not os.path.exists(anatfile) or recache: + # TODO: does `raw` enter this block? print("Generating %s anatomical..."%type) from . import anat getattr(anat, type)(anatfile, subject, **kwargs) @@ -350,6 +351,7 @@ def get_mri_surf2surf_matrix(self, subject: str, surface_type: str, hemi: Litera if not os.path.exists(fdir): print("Creating surf2surf directory for subject %s"%(subject)) os.makedirs(fdir) + hemis: list[Literal['lh', 'rh']] if hemi == 'both': hemis = ['lh', 'rh'] else: @@ -697,13 +699,14 @@ def clear_cache(self, subject, clear_all_caches=True): shutil.rmtree(default_cachedir) os.makedirs(default_cachedir) - def get_paths(self, subject: str): + def get_paths(self, subject: str) -> PathsType: """Get a dictionary with a list of all candidate filenames for associated data, such as roi overlays, flatmap caches, and ctm caches. """ surfpath = os.path.join(self.filestore, subject, "surfaces") - if self.subjects[subject]._warning is not None: - warnings.warn(self.subjects[subject]._warning) + _warn = self.subjects[subject]._warning + if _warn is not None: + warnings.warn(_warn) surfs: dict[str, dict[str, str]] = dict() for surf in os.listdir(surfpath): @@ -735,7 +738,7 @@ def get_paths(self, subject: str): return filenames - def make_subj(self, subject): + def make_subj(self, subject: str): if os.path.exists(os.path.join(self.filestore, subject)): if input("Are you sure you want to overwrite this existing subject?\n" "This will delete all files for this subject in the filestore, " From 169785553282438f4bfdeb9be9dbcc2fa0cb8c5f Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Sat, 4 Apr 2026 02:21:12 -0700 Subject: [PATCH 32/39] more Database types --- cortex/database.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cortex/database.py b/cortex/database.py index 40e6d60b0..e84aa2e5e 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -395,7 +395,7 @@ def get_overlay(self, subject: str, overlay_file: Optional[str]=None, **kwargs) overlay_file = paths['overlays'] return svgoverlay.get_overlay(subject, overlay_file, pts, polys, **kwargs) - def save_xfm(self, subject, name, xfm, xfmtype="magnet", reference=None): + def save_xfm(self, subject: str, name: str, xfm: npt.NDArray[np.floating], xfmtype: str="magnet", reference: Optional[str]=None): """ Load a transform into the surface database. If the transform exists already, update it If it does not exist, copy the reference epi into the filestore and insert. @@ -566,7 +566,7 @@ def get_surf(self, subject: str, type: str, hemisphere: Literal['lh', 'rh', 'bot except KeyError: raise IOError(f"Surface type '{type}' not found for {hemi} hemisphere of subject '{subject}'") - def save_mask(self, subject, xfmname, type, mask): + def save_mask(self, subject: str, xfmname: str, type: str, mask: npt.NDArray[np.bool]) -> None: fname = self.get_paths(subject)['masks'].format(xfmname=xfmname, type=type) if os.path.exists(fname): raise IOError('Refusing to overwrite existing mask') @@ -601,7 +601,7 @@ def get_mask(self, subject: str, xfmname: str, type: str='thick') -> npt.NDArray self.save_mask(subject, xfmname, type, mask) return mask - def get_shared_voxels(self, subject, xfmname, hemi="both", merge=True, use_astar=True, recache=False): + def get_shared_voxels(self, subject: str, xfmname: str, hemi: Literal['lh', 'rh', 'both']="both", merge: bool=True, use_astar: bool=True, recache: bool=False): """Get an array indicating which vertices are inappropriately mapped to the same voxel. For a given transform and surface, returns an array containing a list of vertices which @@ -678,7 +678,7 @@ def get_cache(self, subject: str) -> str: os.makedirs(cachedir) return cachedir - def clear_cache(self, subject, clear_all_caches=True): + def clear_cache(self, subject: str, clear_all_caches: bool=True) -> None: """Clears config-specified and default file caches for a subject. """ @@ -738,7 +738,7 @@ def get_paths(self, subject: str) -> PathsType: return filenames - def make_subj(self, subject: str): + def make_subj(self, subject: str) -> None: if os.path.exists(os.path.join(self.filestore, subject)): if input("Are you sure you want to overwrite this existing subject?\n" "This will delete all files for this subject in the filestore, " @@ -754,7 +754,7 @@ def make_subj(self, subject: str): except OSError: print("Error making directory %s"%path) - def save_view(self,vw,subject,name,is_overwrite=False): + def save_view(self,vw,subject: str,name: str,is_overwrite: bool=False) -> None: """Set the view for an open webshow instance from a saved view Sets the view in a currently-open cortex.webshow instance (with handle `vw`) @@ -806,7 +806,7 @@ def get_view(self,vw,subject,name): view = json.load(fp) vw._set_view(**view) - def get_mnixfm(self, subject, xfm, template=None): + def get_mnixfm(self, subject: str, xfm: str, template: Optional[str]=None) -> npt.NDArray[np.floating]: """Get transform from the space specified by `xfm` to MNI space. Parameters From 05f326434e5c51ea692cc168ece68a368dd89758 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 12 Aug 2026 22:49:11 -0700 Subject: [PATCH 33/39] database.py: annotate from movie-rendering pass Extracted from 137d9e73; the export/save_views.py, mapper/patch.py, mapper/point.py, polyutils/misc.py, webgl/serve.py, and webgl/view.py hunks from that same original commit belong with their respective PRs instead. --- cortex/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cortex/database.py b/cortex/database.py index e84aa2e5e..d6e59bed1 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -627,7 +627,7 @@ def get_shared_voxels(self, subject: str, xfmname: str, hemi: Literal['lh', 'rh' voxels = np.load(shared_voxel_file) return voxels - def get_coords(self, subject, xfmname, hemisphere="both", magnet=None): + def get_coords(self, subject: str, xfmname: str, hemisphere: Literal['lh', 'rh', 'both']="both", magnet: Optional[npt.NDArray]=None): """Calculate the coordinates of each vertex in the epi space by transforming the fiducial to the coordinate space Parameters From 1205de1f86f55c5c7baf98995af3688e49bcb7ea Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 12 Aug 2026 22:49:22 -0700 Subject: [PATCH 34/39] database.py: more types shared with brainctm Extracted from 410454df; the brainctm.py hunk from that same original commit belongs with the CTM/webgl PR instead. --- cortex/database.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/cortex/database.py b/cortex/database.py index d6e59bed1..fa51b3401 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -55,8 +55,8 @@ class SubjectDB: def __init__(self, subj: str, filestore: str = default_filestore): self.subject = subj self._warning = None - self._transforms = None - self._surfaces = None + self._transforms: Optional[XfmDB] = None + self._surfaces: Optional[SurfaceDB] = None self.filestore = filestore try: @@ -66,14 +66,14 @@ def __init__(self, subj: str, filestore: str = default_filestore): pass @property - def transforms(self): + def transforms(self) -> XfmDB: if self._transforms is not None: return self._transforms self._transforms = XfmDB(self.subject, filestore=self.filestore) return self._transforms @property - def surfaces(self): + def surfaces(self) -> SurfaceDB: if self._surfaces is not None: return self._surfaces self._surfaces = SurfaceDB(self.subject, filestore=self.filestore) @@ -627,7 +627,7 @@ def get_shared_voxels(self, subject: str, xfmname: str, hemi: Literal['lh', 'rh' voxels = np.load(shared_voxel_file) return voxels - def get_coords(self, subject: str, xfmname: str, hemisphere: Literal['lh', 'rh', 'both']="both", magnet: Optional[npt.NDArray]=None): + def get_coords(self, subject: str, xfmname: str, hemisphere: Literal['lh', 'rh', 'both']="both", magnet: Optional[npt.NDArray]=None) -> list[npt.NDArray[np.floating]]: """Calculate the coordinates of each vertex in the epi space by transforming the fiducial to the coordinate space Parameters @@ -648,10 +648,12 @@ def get_coords(self, subject: str, xfmname: str, hemisphere: Literal['lh', 'rh', xfm = self.get_xfm(subject, xfmname, xfmtype="magnet") xfm = np.linalg.inv(magnet) * xfm - coords = [] + coords: list[npt.NDArray[np.floating]] = [] vtkTmp = self.get_surf(subject, "fiducial", hemisphere=hemisphere, nudge=False) if not isinstance(vtkTmp,(tuple,list)): vtkTmp = [vtkTmp] + pts: npt.NDArray[np.floating] + polys: npt.NDArray[np.integer] for pts, polys in vtkTmp: wpts = np.vstack([pts.T, np.ones(len(pts))]) coords.append(np.dot(xfm.xfm, wpts)[:3].round().astype(int).T) From 874e36061a7d004154ce8eab99a01f96ef389cd9 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Fri, 5 Jun 2026 17:01:41 -0700 Subject: [PATCH 35/39] Cast nibabel to NifTI images to reduce type errors --- cortex/database.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cortex/database.py b/cortex/database.py index fa51b3401..b1363afab 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -154,7 +154,7 @@ def __init__(self, subj: str, name: str, filestore: str = default_filestore): def __getitem__(self, item: str) -> npt.NDArray: import nibabel - return nibabel.load(self._masks[item]).get_fdata().T + return cast(nibabel.Nifti1Image, nibabel.load(self._masks[item])).get_fdata().T def __repr__(self): return "Masks: [{types}]".format(types=', '.join(self._masks.keys())) @@ -239,7 +239,7 @@ def get_anat(self, subject: str, type: Literal['raw', 'brainmask', 'whitematter' getattr(anat, type)(anatfile, subject, **kwargs) import nibabel - anatnib = nibabel.load(anatfile) + anatnib = cast(nibabel.Nifti1Image, nibabel.load(anatfile)) if xfmname is None: return anatnib @@ -430,7 +430,7 @@ def save_xfm(self, subject: str, name: str, xfm: npt.NDArray[np.floating], xfmty if reference is None: raise ValueError("Please specify a reference") fpath = os.path.join(path, "reference.nii.gz") - nib = nibabel.load(reference) + nib = cast(nibabel.Nifti1Image, nibabel.load(reference)) data = nib.get_fdata() if len(data.shape) > 3: import warnings @@ -591,8 +591,7 @@ def get_mask(self, subject: str, xfmname: str, type: str='thick') -> npt.NDArray fname = self.get_paths(subject)['masks'].format(xfmname=xfmname, type=type) try: import nibabel - nib = nibabel.load(fname) - #reveal_type(nib.get_fdata().T != 0) + nib = cast(nibabel.Nifti1Image, nibabel.load(fname)) return nib.get_fdata().T != 0 except IOError: print('Mask not found, generating...') From eb46c13cf6fa4fde7735588595173ffe926aac1e Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 12 Aug 2026 22:49:52 -0700 Subject: [PATCH 36/39] database.py: apply Transform.reference_nifti change from fd711d8b Extracted from fd711d8b (see also the earlier xfm/align commit in this branch, which added Transform.reference_nifti itself); the dataset/braindata.py and utils.py hunks from that same original commit belong with the Dataset hierarchy and utils PRs instead. --- cortex/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cortex/database.py b/cortex/database.py index b1363afab..1f3855fea 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -575,7 +575,7 @@ def save_mask(self, subject: str, xfmname: str, type: str, mask: npt.NDArray[np. xfm = self.get_xfm(subject, xfmname) if xfm.shape != mask.shape: raise ValueError("Invalid mask shape: must match shape of reference image") - affine = xfm.reference.affine + affine = xfm.reference_nifti.affine nib = nibabel.Nifti1Image(mask.astype(np.uint8).T, affine) nib.to_filename(fname) From 061b8cbdf935fe6b7e4ebfebc4997871e3866595 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Wed, 12 Aug 2026 22:50:41 -0700 Subject: [PATCH 37/39] database: narrow literals, type auxfile fallback Extracted from 245d95ff; the sha1(str) crash fix from this same original commit moved to PR 0 and is deliberately not included here. Keeps the Literal narrowing on get_anat's order / get_surf's hemi, the cast() to Nifti1Image, and the targeted type: ignore comments on the self.auxfile fallback pattern. --- cortex/database.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/cortex/database.py b/cortex/database.py index 1f3855fea..1e67eb82b 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -207,7 +207,7 @@ def reload_subjects(self): self._subjects = None self.subjects - def get_anat(self, subject: str, type: Literal['raw', 'brainmask', 'whitematter', 'voxelize'] ='raw', xfmname: Optional[str]=None, recache: bool=False, order: int=1, **kwargs): + def get_anat(self, subject: str, type: Literal['raw', 'brainmask', 'whitematter', 'voxelize'] ='raw', xfmname: Optional[str]=None, recache: bool=False, order: Literal[0, 1, 2, 3, 4, 5]=1, **kwargs): """Return anatomical information from the filestore. Anatomical information is defined as any volume-space anatomical information pertaining to the subject, such as T1 image, white matter masks, etc. Volumes not found in the database will be automatically generated. @@ -277,7 +277,7 @@ def get_surfinfo(self, subject: str, type: str="curvature", recache: bool=False, if len(kwargs) > 0: opts = "[%s]"%','.join(["%s=%s"%i for i in kwargs.items()]) try: - self.auxfile.get_surf(subject, "fiducial") + self.auxfile.get_surf(subject, "fiducial") # type: ignore[union-attr] surfifile = os.path.join(self.get_cache(subject),"%s%s.npz"%(type, opts)) except (AttributeError, IOError): surfiform = self.get_paths(subject)['surfinfo'] @@ -441,7 +441,7 @@ def save_xfm(self, subject: str, name: str, xfm: npt.NDArray[np.floating], xfmty jsdict = dict() - nib = nibabel.load(os.path.join(path, "reference.nii.gz")) + nib = cast(nibabel.Nifti1Image, nibabel.load(os.path.join(path, "reference.nii.gz"))) if xfmtype == "magnet": jsdict['magnet'] = np.array(xfm).tolist() jsdict['coord'] = np.dot(np.linalg.inv(nib.affine), xfm).tolist() @@ -470,7 +470,7 @@ def get_xfm(self, subject: str, name: str, xfmtype: str="coord") -> Transform: """ if xfmtype == 'coord': try: - return self.auxfile.get_xfm(subject, name) + return self.auxfile.get_xfm(subject, name) # type: ignore[union-attr] except (AttributeError, IOError): pass @@ -529,7 +529,7 @@ def get_surf(self, subject: str, type: str, hemisphere: Literal['lh', 'rh', 'bot For single hemisphere ''' try: - return self.auxfile.get_surf(subject, type, hemisphere, merge=merge, nudge=nudge) + return self.auxfile.get_surf(subject, type, hemisphere, merge=merge, nudge=nudge) # type: ignore[union-attr, arg-type] except (AttributeError, IOError): pass @@ -547,7 +547,8 @@ def get_surf(self, subject: str, type: str, hemisphere: Literal['lh', 'rh', 'bot return pts, polys return left, right - elif hemisphere.lower() in ("lh", "left"): + hemi: Literal['lh', 'rh'] + if hemisphere.lower() in ("lh", "left"): hemi = "lh" elif hemisphere.lower() in ("rh", "right"): hemi = "rh" @@ -584,7 +585,7 @@ def get_mask(self, subject: str, xfmname: str, type: str='thick') -> npt.NDArray type = type.decode('utf8') try: - self.auxfile.get_mask(subject, xfmname, type) + self.auxfile.get_mask(subject, xfmname, type) # type: ignore[union-attr] except (AttributeError, IOError): pass @@ -610,7 +611,7 @@ def get_shared_voxels(self, subject: str, xfmname: str, hemi: Literal['lh', 'rh' """ # Test for packed subjects try: - voxels = self.auxfile.get_shared_voxels(subject, xfmname, hemi=hemi, merge=merge, use_astar=use_astar) + voxels = self.auxfile.get_shared_voxels(subject, xfmname, hemi=hemi, merge=merge, use_astar=use_astar) # type: ignore[union-attr] return voxels except (AttributeError, IOError): pass @@ -619,7 +620,7 @@ def get_shared_voxels(self, subject: str, xfmname: str, hemi: Literal['lh', 'rh' if not os.path.exists(shared_voxel_file) or recache: print('Shared voxel array not found, generating...') from .utils import get_shared_voxels as _get_shared_voxels - voxels = _get_shared_voxels(subject, xfmname, hemi=hemi, merge=merge, use_astar=use_astar) + voxels = _get_shared_voxels(subject, xfmname, hemi=hemi, merge=merge, use_astar=use_astar) # type: ignore[call-overload] np.save(shared_voxel_file, voxels) return voxels else: @@ -661,10 +662,10 @@ def get_coords(self, subject: str, xfmname: str, hemisphere: Literal['lh', 'rh', def get_cache(self, subject: str) -> str: try: - self.auxfile.get_surf(subject, "fiducial") + self.auxfile.get_surf(subject, "fiducial") # type: ignore[union-attr] #generate the hashed name of the filename and subject as the directory name import hashlib - hashname = "pycx_%s"%hashlib.md5(self.auxfile.h5.filename).hexdigest()[-8:] + hashname = "pycx_%s"%hashlib.md5(self.auxfile.h5.filename).hexdigest()[-8:] # type: ignore[union-attr] cachedir = os.path.join(tempfile.gettempdir(), hashname, subject) except (AttributeError, IOError): try: From 53d22f98813e45e8f5ec9ab6e3f1bee840857866 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Tue, 11 Aug 2026 23:08:05 -0700 Subject: [PATCH 38/39] database.py: remove unused ignores --- cortex/database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cortex/database.py b/cortex/database.py index 1e67eb82b..9f6b7f055 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -529,7 +529,7 @@ def get_surf(self, subject: str, type: str, hemisphere: Literal['lh', 'rh', 'bot For single hemisphere ''' try: - return self.auxfile.get_surf(subject, type, hemisphere, merge=merge, nudge=nudge) # type: ignore[union-attr, arg-type] + return self.auxfile.get_surf(subject, type, hemisphere, merge=merge, nudge=nudge) # type: ignore[union-attr] except (AttributeError, IOError): pass @@ -620,7 +620,7 @@ def get_shared_voxels(self, subject: str, xfmname: str, hemi: Literal['lh', 'rh' if not os.path.exists(shared_voxel_file) or recache: print('Shared voxel array not found, generating...') from .utils import get_shared_voxels as _get_shared_voxels - voxels = _get_shared_voxels(subject, xfmname, hemi=hemi, merge=merge, use_astar=use_astar) # type: ignore[call-overload] + voxels = _get_shared_voxels(subject, xfmname, hemi=hemi, merge=merge, use_astar=use_astar) np.save(shared_voxel_file, voxels) return voxels else: From ff87f4cf8e0eba887f87528c792f231e8382a557 Mon Sep 17 00:00:00 2001 From: Aditya Vaidya Date: Mon, 17 Aug 2026 18:31:14 -0700 Subject: [PATCH 39/39] database: correct auxfile annotation to be Dataset Also fixes a string type bug, and type overloading for Dataset.get_surf Ported from 9abb0802 (types-data branch) / bb81c50a (types-easy). --- cortex/database.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cortex/database.py b/cortex/database.py index 9f6b7f055..cbcbc60d1 100644 --- a/cortex/database.py +++ b/cortex/database.py @@ -20,6 +20,7 @@ from . import options from .xfm import Transform if TYPE_CHECKING: + from cortex.dataset.dataset import Dataset from cortex.dataset.views import Vertex from cortex.svgoverlay import SVGOverlay @@ -172,7 +173,10 @@ class Database: def __init__(self, filestore: str=default_filestore): self.filestore = filestore self._subjects: Optional[dict[str, SubjectDB]] = None - self.auxfile: Optional[Database] = None + # Side channel set by Dataset.from_file and cortex.webgl.show: a Dataset + # standing in for the filestore, so views can resolve surfaces and + # transforms out of the .hdf they were loaded from. + self.auxfile: Optional["Dataset"] = None def __repr__(self) -> str: subjs = "\n ".join(sorted(self.subjects.keys())) @@ -665,7 +669,10 @@ def get_cache(self, subject: str) -> str: self.auxfile.get_surf(subject, "fiducial") # type: ignore[union-attr] #generate the hashed name of the filename and subject as the directory name import hashlib - hashname = "pycx_%s"%hashlib.md5(self.auxfile.h5.filename).hexdigest()[-8:] # type: ignore[union-attr] + # md5 needs bytes: passing the str filename raised an uncaught TypeError, + # so this path could never have completed. + filename = self.auxfile.h5.filename # type: ignore[union-attr] + hashname = "pycx_%s"%hashlib.md5(filename.encode()).hexdigest()[-8:] cachedir = os.path.join(tempfile.gettempdir(), hashname, subject) except (AttributeError, IOError): try: