Skip to content

Commit 44b91a4

Browse files
committed
Add type annotations to all public methods and functions
- from __future__ import annotations for forward references - Annotated all CrossCoder methods, MvNorm, module-level functions - Pure annotation changes, no runtime behavior modified
1 parent 60c3924 commit 44b91a4

1 file changed

Lines changed: 44 additions & 42 deletions

File tree

vbjax/crosscoder.py

Lines changed: 44 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
normal, which helps when training data pools heterogeneous cohorts.
99
"""
1010

11+
from __future__ import annotations
12+
1113
import pickle
1214
import functools
1315
import numpy
@@ -34,7 +36,7 @@ class TrainedArch:
3436
variational: bool = False
3537

3638

37-
def triu_to_mat(triu):
39+
def triu_to_mat(triu: Any) -> Any:
3840
"Fold flat upper-triangular vectors into symmetric square matrices."
3941
squeeze = triu.ndim == 1
4042
if squeeze:
@@ -47,7 +49,7 @@ def triu_to_mat(triu):
4749
return mat[0] if squeeze else mat
4850

4951

50-
def triu_to_mat_np(triu):
52+
def triu_to_mat_np(triu: Any) -> Any:
5153
"NumPy equivalent of :func:`triu_to_mat` for post-training paths."
5254
squeeze = triu.ndim == 1
5355
if squeeze:
@@ -61,25 +63,25 @@ def triu_to_mat_np(triu):
6163
return mat[0] if squeeze else mat
6264

6365

64-
def _xavier(key, shape, scale=1.0):
66+
def _xavier(key: Any, shape: tuple[int, ...], scale: float = 1.0) -> Any:
6567
return jax.random.normal(key, shape) * np.sqrt(scale / shape[0])
6668

6769

6870
class MvNorm:
6971
"Multivariate normal with persistent PRNG key for sampling."
7072

71-
def __init__(self, us, mean, cov, key=None):
73+
def __init__(self, us: Any, mean: Any, cov: Any, key: Any | None = None) -> None:
7274
self.us = us
7375
self.mean = mean
7476
self.cov = cov
7577
self.key = key if key is not None else jax.random.PRNGKey(SEED)
7678

77-
def sample(self, n):
79+
def sample(self, n: int) -> Any:
7880
self.key, k = jax.random.split(self.key)
7981
return jax.random.multivariate_normal(k, self.mean, self.cov, shape=(n,))
8082

8183

82-
def _denorm(flat, ntype, mean, std, scale, nonneg):
84+
def _denorm(flat: Any, ntype: str, mean: Any, std: float, scale: float, nonneg: bool) -> Any:
8385
"Invert the per-view normalization applied by :meth:`CrossCoder.add_view`."
8486
if ntype == 'logit':
8587
probs = jax.nn.sigmoid(flat * std + mean)
@@ -106,7 +108,7 @@ class CrossCoder:
106108
and returns to Python only for logging. Disable for step-wise debug.
107109
"""
108110

109-
def __init__(self, variational=True, chunked_training=True):
111+
def __init__(self, variational: bool = True, chunked_training: bool = True) -> None:
110112
self.variational = variational
111113
self.chunked_training = chunked_training
112114
self.conns = []
@@ -125,7 +127,7 @@ def __init__(self, variational=True, chunked_training=True):
125127
'scales', 'parcs', 'tts', 'wbs', 'history', 'norm_types', 'nonneg',
126128
'archs')
127129

128-
def to_pkl(self, fname):
130+
def to_pkl(self, fname: str) -> None:
129131
data = {k: getattr(self, k) for k in self._pkl_keys}
130132
# Convert TrainedArch dataclasses to dicts for pickling
131133
if 'archs' in data:
@@ -135,7 +137,7 @@ def to_pkl(self, fname):
135137
pickle.dump(data, fd)
136138

137139
@classmethod
138-
def from_pkl(cls, fname):
140+
def from_pkl(cls, fname: str) -> CrossCoder:
139141
with open(fname, 'rb') as fd:
140142
data = pickle.load(fd)
141143
self = cls(variational=data.get('variational', False),
@@ -172,8 +174,8 @@ def from_pkl(cls, fname):
172174
return self
173175

174176
@classmethod
175-
def from_numpy_array(cls, weights, tts=None, parc='Schaefer-17Networks',
176-
variational=False, chunked_training=True, normalize='center'):
177+
def from_numpy_array(cls, weights: numpy.ndarray, tts: int | None = None, parc: str = 'Schaefer-17Networks',
178+
variational: bool = False, chunked_training: bool = True, normalize: str = 'center') -> CrossCoder:
177179
"Build a single-view CrossCoder from an ``(ns, nn, nn)`` connectome stack."
178180
weights = numpy.maximum(weights, 0.0)
179181
ns, nn, _ = weights.shape
@@ -183,7 +185,7 @@ def from_numpy_array(cls, weights, tts=None, parc='Schaefer-17Networks',
183185
self.add_view(weights[:, i, j], f'{nn}-{parc}', normalize=normalize, nonneg=True)
184186
return self
185187

186-
def add_view(self, data, parc_name, normalize='zscore', nonneg=False):
188+
def add_view(self, data: Any, parc_name: str, normalize: str = 'zscore', nonneg: bool = False) -> None:
187189
"Register a view, normalizing its flat upper-tri connectomes."
188190
data = np.asarray(data)
189191
scale = 1.0
@@ -217,15 +219,15 @@ def add_view(self, data, parc_name, normalize='zscore', nonneg=False):
217219
self.norm_types.append(normalize)
218220
self.nonneg.append(nonneg)
219221

220-
def shuffle(self, seed=None):
222+
def shuffle(self, seed: int | None = None) -> numpy.ndarray:
221223
"Shuffle all views with a common permutation and return it."
222224
n = self.conns[0].shape[0]
223225
key = jax.random.PRNGKey(SEED if seed is None else seed)
224226
perm = jax.random.permutation(key, np.arange(n))
225227
self.conns = [c[perm] for c in self.conns]
226228
return numpy.asarray(perm)
227229

228-
def make_wbs(self, nlat, key=None):
230+
def make_wbs(self, nlat: int, key: Any | None = None) -> list:
229231
"Initialize weights/biases for all views at given latent size."
230232
if key is None:
231233
key = jax.random.PRNGKey(SEED)
@@ -247,7 +249,7 @@ def make_wbs(self, nlat, key=None):
247249
return wbs
248250

249251

250-
def make_loss(self):
252+
def make_loss(self) -> tuple:
251253
"Build the cross-prediction loss and its gradient."
252254
if self.variational:
253255
@jax.jit
@@ -281,8 +283,8 @@ def loss(wbs, conns):
281283
return loss, grad
282284

283285

284-
def train(self, nlat, lr=3e-4, niter=2000, tts=None, mb=64,
285-
beta_start=0.0, beta_end=1e-3, anneal_steps=1500, key=None):
286+
def train(self, nlat: int, lr: float = 3e-4, niter: int = 2000, tts: int | None = None, mb: int = 64,
287+
beta_start: float = 0.0, beta_end: float = 1e-3, anneal_steps: int = 1500, key: Any | None = None) -> tuple:
286288
"""
287289
Fit a single architecture. Appends the learned weights and trace
288290
into ``self.wbs`` / ``self.history`` and returns
@@ -320,9 +322,9 @@ def train(self, nlat, lr=3e-4, niter=2000, tts=None, mb=64,
320322
cr = self.calc_confusion_rate(nlat, tts=tts)
321323
return trace, wbs, cr
322324

323-
def _train_var(self, loss_fn, grad_fn, opt_update, get_params, opt_state,
324-
mbkey, train_c, test_c, tts, mb, niter,
325-
beta_start, beta_end, anneal_steps):
325+
def _train_var(self, loss_fn: Any, grad_fn: Any, opt_update: Any, get_params: Any, opt_state: Any,
326+
mbkey: Any, train_c: Any, test_c: Any, tts: int, mb: int, niter: int,
327+
beta_start: float, beta_end: float, anneal_steps: int) -> tuple:
326328
def beta_at(it):
327329
return np.minimum(
328330
beta_start + (beta_end - beta_start) * (it / anneal_steps), beta_end)
@@ -385,8 +387,8 @@ def body(carry, i):
385387

386388
return trace, get_params(opt_state), log_freq
387389

388-
def _train_det(self, loss_fn, grad_fn, opt_update, get_params, opt_state,
389-
mbkey, train_c, test_c, tts, mb, niter):
390+
def _train_det(self, loss_fn: Any, grad_fn: Any, opt_update: Any, get_params: Any, opt_state: Any,
391+
mbkey: Any, train_c: Any, test_c: Any, tts: int, mb: int, niter: int) -> tuple:
390392
def step(i, opt_state, mbkey):
391393
mbkey, kb = jax.random.split(mbkey)
392394
idx = jax.random.randint(kb, (mb,), 0, tts)
@@ -444,7 +446,7 @@ def body(carry, i):
444446
return trace, get_params(opt_state), log_freq
445447

446448

447-
def _get_arch(self, nlat):
449+
def _get_arch(self, nlat: int) -> TrainedArch:
448450
"""Get TrainedArch by nlat value."""
449451
for a in self.archs:
450452
if a.nlat == nlat:
@@ -463,7 +465,7 @@ def arch(self):
463465
return [wb[0][0][0][1].size if self.variational else wb[0][0][1].size
464466
for wb in self.wbs]
465467

466-
def _conf_rates_det(self, wbs, conns):
468+
def _conf_rates_det(self, wbs: Any, conns: Any) -> numpy.ndarray:
467469
@jax.jit
468470
def dist(a, b):
469471
return np.sum((a[:, None] - b) ** 2, axis=-1)
@@ -476,7 +478,7 @@ def dist(a, b):
476478
crs[i, j] = 1 - ok.mean()
477479
return crs
478480

479-
def _conf_rates_var(self, wbs, conns, n_samples=0):
481+
def _conf_rates_var(self, wbs: Any, conns: Any, n_samples: int = 0) -> numpy.ndarray:
480482
"""Compute (n_views, n_views) confusion rate matrix for variational mode."""
481483
@jax.jit
482484
def dist(a, b):
@@ -508,7 +510,7 @@ def dist(a, b):
508510

509511
return crs
510512

511-
def calc_confusion_rate(self, arch, tts=None, self_recon_only=True, n_samples=0):
513+
def calc_confusion_rate(self, arch: int, tts: int | None = None, self_recon_only: bool = True, n_samples: int = 0) -> float:
512514
"Fraction of test subjects not fingerprinted correctly."
513515
tts = tts or self.tts
514516
wbs = self._get_arch(arch).wbs
@@ -522,7 +524,7 @@ def calc_confusion_rate(self, arch, tts=None, self_recon_only=True, n_samples=0)
522524
return float(numpy.diag(crs).mean())
523525
return float(crs.mean())
524526

525-
def confusion_matrix(self, arch, tts=None, n_samples=0):
527+
def confusion_matrix(self, arch: int, tts: int | None = None, n_samples: int = 0) -> numpy.ndarray:
526528
"""Return (n_views, n_views) confusion rate matrix.
527529
528530
Entry (i, j) is the fraction of test subjects from view i whose
@@ -549,7 +551,7 @@ def confusion_matrix(self, arch, tts=None, n_samples=0):
549551
return self._conf_rates_var(wbs, test_c, n_samples=n_samples)
550552

551553

552-
def encode(self, arch, parc, tts=None, sample=False, key=None):
554+
def encode(self, arch: int, parc: str, tts: int | None = None, sample: bool = False, key: Any | None = None) -> Any:
553555
"Encode the normalized connectomes of a view into latent space."
554556
ta = self._get_arch(arch)
555557
iparc = self.parcs.index(parc)
@@ -565,7 +567,7 @@ def encode(self, arch, parc, tts=None, sample=False, key=None):
565567
(ew, eb), _ = ta.wbs[iparc]
566568
return c @ ew + eb
567569

568-
def encode_all(self, arch, tts=None, sample=False, key=None):
570+
def encode_all(self, arch: int, tts: int | None = None, sample: bool = False, key: Any | None = None) -> dict[str, Any]:
569571
"""Encode all views into latent space.
570572
571573
Returns
@@ -576,7 +578,7 @@ def encode_all(self, arch, tts=None, sample=False, key=None):
576578
return {parc: self.encode(arch, parc, tts=tts, sample=sample, key=key)
577579
for parc in self.parcs}
578580

579-
def decode(self, arch, parc, z, raw=False):
581+
def decode(self, arch: int, parc: str, z: Any, raw: bool = False) -> Any:
580582
"Decode latent vectors into flat upper-tri connectomes."
581583
iparc = self.parcs.index(parc)
582584
_, (w_dec, b_dec) = self._get_arch(arch).wbs[iparc]
@@ -586,19 +588,19 @@ def decode(self, arch, parc, z, raw=False):
586588
return _denorm(rec, self.norm_types[iparc], self.means[iparc],
587589
self.stds[iparc], self.scales[iparc], self.nonneg[iparc])
588590

589-
def decode_conn(self, arch, parc, z, clip_positive=None):
591+
def decode_conn(self, arch: int, parc: str, z: Any, clip_positive: bool | None = None) -> Any:
590592
"Decode latents into full symmetric connectomes ``(ns, nn, nn)``."
591593
flat = self.decode(arch, parc, z, raw=False)
592594
if clip_positive is True:
593595
flat = np.maximum(flat, 0.0)
594596
return triu_to_mat(flat)
595597

596-
def get_triu(self, parc, tts=None):
598+
def get_triu(self, parc: str, tts: int | None = None) -> Any:
597599
"Return normalized flat upper-tri connectomes for a view."
598600
return self.conns[self.parcs.index(parc)][
599601
self.tts if tts is None else tts:]
600602

601-
def get_conn(self, parc, tts=None):
603+
def get_conn(self, parc: str, tts: int | None = None) -> Any:
602604
"Return empirical connectomes ``(ns, nn, nn)`` for a view."
603605
iparc = self.parcs.index(parc)
604606
flat = _denorm(self.get_triu(parc, tts), self.norm_types[iparc],
@@ -607,7 +609,7 @@ def get_conn(self, parc, tts=None):
607609
return triu_to_mat(flat)
608610

609611

610-
def calc_mvn(self, arch, tts=None):
612+
def calc_mvn(self, arch: int, tts: int | None = None) -> MvNorm:
611613
"Total-variance multivariate normal over the cohort latents."
612614
ta = self._get_arch(arch)
613615
tts = tts or self.tts
@@ -628,7 +630,7 @@ def calc_mvn(self, arch, tts=None):
628630
cov = cov + np.diag(np.mean(np.concatenate(var, axis=0), axis=0))
629631
return MvNorm(us, mean, cov)
630632

631-
def decompose_latent(self, arch, tts=None):
633+
def decompose_latent(self, arch: int, tts: int | None = None) -> dict:
632634
"SVD of the centered cohort latents for a given architecture."
633635
mvn = self.calc_mvn(arch, tts)
634636
X = mvn.us - mvn.mean
@@ -640,7 +642,7 @@ def decompose_latent(self, arch, tts=None):
640642

641643

642644
@classmethod
643-
def combine(cls, cc1, cc2, shuffle=True):
645+
def combine(cls, cc1: CrossCoder, cc2: CrossCoder, shuffle: bool = True) -> CrossCoder:
644646
"Concatenate two CrossCoders with identical views and normalizations."
645647
assert cc1.variational == cc2.variational
646648
assert cc1.parcs == cc2.parcs
@@ -660,12 +662,12 @@ def combine(cls, cc1, cc2, shuffle=True):
660662
return cc
661663

662664

663-
def sweep_crosscoder(model, dims, n_trials=20, seed=42, keep_best=False,
664-
lr_range=(1e-5, 1e-2), niter_range=(500, 5000),
665-
mb_choices=(32, 64, 128),
666-
beta_end_range=(1e-6, 1e-3),
667-
anneal_range=(500, 3000),
668-
score_fn=None):
665+
def sweep_crosscoder(model: CrossCoder, dims: list[int], n_trials: int = 20, seed: int = 42, keep_best: bool = False,
666+
lr_range: tuple[float, float] = (1e-5, 1e-2), niter_range: tuple[int, int] = (500, 5000),
667+
mb_choices: tuple[int, ...] = (32, 64, 128),
668+
beta_end_range: tuple[float, float] = (1e-6, 1e-3),
669+
anneal_range: tuple[int, int] = (500, 3000),
670+
score_fn: Any | None = None) -> tuple[list[dict], dict | None]:
669671
"""
670672
Random hyperparameter sweep over ``CrossCoder.train``.
671673

0 commit comments

Comments
 (0)