Skip to content

Vision

DNN vision representation module.

VisionModel

Unified interface for extracting DNN activations from images.

Composes a :class:~vneurotk.vision.model.backend.base.BaseBackend and a :class:~vneurotk.vision.model.selector.ModuleSelector. Activations are returned as-is; any further processing (pooling, embedding, etc.) is left to the user.

Parameters:

Name Type Description Default
model_id str

Model identifier passed directly to the backend, e.g. "facebook/dinov2-base" (transformers) or "resnet50" (timm).

required
backend str

Backend to use: "transformers" (default), "timm", or "thingsvision".

'transformers'
selector ModuleSelector or None

Layer selection strategy. Defaults to :class:~vneurotk.vision.model.selector.BlockLevelSelector.

None
device str

Inference device (default "cpu").

'cpu'
pretrained bool

Load pretrained weights (default True).

True
Source code in src/vneurotk/vision/model/base.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
class VisionModel:
    """Unified interface for extracting DNN activations from images.

    Composes a :class:`~vneurotk.vision.model.backend.base.BaseBackend`
    and a :class:`~vneurotk.vision.model.selector.ModuleSelector`.
    Activations are returned as-is; any further processing (pooling,
    embedding, etc.) is left to the user.

    Parameters
    ----------
    model_id : str
        Model identifier passed directly to the backend, e.g.
        ``"facebook/dinov2-base"`` (transformers) or ``"resnet50"`` (timm).
    backend : str
        Backend to use: ``"transformers"`` (default), ``"timm"``, or
        ``"thingsvision"``.
    selector : ModuleSelector or None
        Layer selection strategy.  Defaults to
        :class:`~vneurotk.vision.model.selector.BlockLevelSelector`.
    device : str
        Inference device (default ``"cpu"``).
    pretrained : bool
        Load pretrained weights (default ``True``).
    """

    def __init__(
        self,
        model_id: str,
        backend: str = "transformers",
        selector: ModuleSelector | None = None,
        device: str = "cpu",
        pretrained: bool = True,
    ) -> None:
        self._selector = selector or BlockLevelSelector()

        self._backend = self._build_backend(backend, device)
        self._backend.load(model_id, pretrained=pretrained)

        self._bind_selector()

        logger.info(
            "VisionModel ready | model={} | backend={} | modules={}",
            model_id,
            backend,
            len(self._module_names),
        )

    # ------------------------------------------------------------------
    # Class-method constructor
    # ------------------------------------------------------------------

    @classmethod
    def from_model(
        cls,
        model: Any,
        backend: BaseBackend,
        selector: ModuleSelector | None = None,
    ) -> VisionModel:
        """Build a VisionModel from an already-loaded model.

        Parameters
        ----------
        model : nn.Module
            Pre-loaded PyTorch model.
        backend : BaseBackend
            Backend instance with *model* already assigned.
        selector : ModuleSelector or None
            Defaults to :class:`BlockLevelSelector`.

        Returns
        -------
        VisionModel
        """
        inst = object.__new__(cls)
        inst._selector = selector or BlockLevelSelector()
        inst._backend = backend
        inst._backend.model = model

        inst._bind_selector()
        return inst

    # ------------------------------------------------------------------
    # Internal setup
    # ------------------------------------------------------------------

    def _bind_selector(self) -> None:
        """Select modules, register hooks, and build the module-type map.

        Called by ``__init__``, ``from_model``, and ``set_selector`` so the
        wiring logic lives in exactly one place.
        """
        module_names = self._selector.select(self._backend.enumerate_modules())
        self._backend.register_hooks(module_names)
        self._module_names = module_names
        self._module_type_map: dict[str, str] = {m.name: m.module_type for m in self._backend.enumerate_modules()}

    # ------------------------------------------------------------------
    # Extraction
    # ------------------------------------------------------------------

    @staticmethod
    def _normalize_images(image: Any) -> dict:
        """Normalise any supported input type to a ``{stim_id: image}`` dict.

        Parameters
        ----------
        image : dict or single image
            ``dict`` → returned as-is.
            Single image (PIL, ndarray, str, Path) → ``{0: image}``.

        Returns
        -------
        dict

        Raises
        ------
        TypeError
            If *image* is a ``list``.  Use ``{0: img0, 1: img1, …}`` or any
            mapping with meaningful stim IDs instead — a list would silently
            assign integer stim IDs that won't align with
            ``BaseData.trial_stim_ids``.
        """
        if isinstance(image, list):
            raise TypeError(
                "batch input requires a dict mapping stim_id → image; "
                "got a list.  Use {0: img0, 1: img1, …} for integer stim IDs, "
                "or {name: img, …} for named stimuli."
            )
        if isinstance(image, dict):
            return image
        return {0: image}

    def extract(
        self,
        image: Any,
        batch_size: int = DEFAULT_BATCH_SIZE,
        show_progress: bool = True,
    ) -> VisualRepresentations:
        """Extract DNN activations for one image or a collection of stimuli.

        Parameters
        ----------
        image : PIL.Image.Image or np.ndarray or str or Path or dict
            Single image → ``n_sample=1``, ``stim_id=0``.
            ``dict`` mapping stim_ids to images → ``n_sample=len(dict)``.
            String / ``pathlib.Path`` values are opened automatically.
            ``list`` is **not** accepted — use a ``dict`` with explicit
            stim IDs to preserve alignment with ``BaseData.trial_stim_ids``.
        batch_size : int
            Number of images per GPU forward pass.  Default 32.
            Ignored for single-image input.
        show_progress : bool
            Display a tqdm progress bar over batches.  Automatically
            suppressed for single-image input.  Default ``True``.

        Returns
        -------
        VisualRepresentations
        """
        if batch_size <= 0:
            raise ValueError(f"batch_size must be positive, got {batch_size}")

        images = self._normalize_images(image)
        is_single = not isinstance(image, dict)
        return self.extract_for_modules(
            images,
            self._module_names,
            batch_size=1 if is_single else batch_size,
            show_progress=False if is_single else show_progress,
        )

    # ------------------------------------------------------------------
    # Properties
    # ------------------------------------------------------------------

    @property
    def model_id(self) -> str:
        """Model identifier (e.g. ``'facebook/dinov2-base'``, ``'resnet50'``)."""
        return self._backend.get_model_meta().model_id

    @property
    def module_names(self) -> list[str]:
        """Names of all currently hooked modules."""
        return list(self._module_names)

    @property
    def module_list(self) -> list:
        """All modules available in the loaded model.

        Returns
        -------
        list[ModuleInfo]
            One entry per named module, ordered as ``model.named_modules()``.
            Each entry exposes ``.name``, ``.module_type``, ``.depth``,
            and ``.n_params``.
        """
        return self._backend.enumerate_modules()

    # ------------------------------------------------------------------
    # Inspection & reconfiguration
    # ------------------------------------------------------------------

    def print_modules(self, max_depth: int | None = None, console: Any = None) -> None:
        """Print a tree-style summary of all model modules.

        Parameters
        ----------
        max_depth : int or None
            Maximum nesting depth to display.  ``None`` shows all levels.
        console : rich.console.Console or None
            Rich console to use for output.  Pass ``Console(record=True)`` to
            capture the output for SVG / HTML export.
        """
        _print_modules(self.module_list, max_depth=max_depth, console=console)

    @staticmethod
    def _filter_modules(
        module_list: list,
        types: set[str],
        names: set[str],
    ) -> list:
        """Filter *module_list* by module type and/or exact name.

        Returns all modules whose ``module_type`` is in *types* OR whose
        ``name`` is in *names*.  Both sets may be empty; an entry matches if
        it satisfies at least one non-empty criterion.

        Parameters
        ----------
        module_list : list[ModuleInfo]
            Full module list from :attr:`module_list`.
        types : set[str]
            ``module_type`` values to include.
        names : set[str]
            Exact ``name`` values to include.

        Returns
        -------
        list[ModuleInfo]
            Ordered subset of *module_list* (preserves original order).
        """
        return [m for m in module_list if (types and m.module_type in types) or (names and m.name in names)]

    def set_selector(
        self,
        selector: ModuleSelector | list | None = None,
        *,
        module_type: str | list[str] | None = None,
        module_name: str | list[str] | None = None,
    ) -> None:
        """Replace the layer selector and re-register hooks.

        Accepts the following forms (combinable):

        - ``set_selector(BlockLevelSelector())`` — explicit selector object
        - ``set_selector(["layer.0", "layer.1"])`` — list of module names / ModuleInfo
        - ``set_selector(module_type="Dinov2Layer")`` — all modules of that type
        - ``set_selector(module_type=["Dinov2Layer", "LayerNorm"])`` — multiple types
        - ``set_selector(module_name="encoder.layer.3")`` — single module by name
        - ``set_selector(module_name=["enc.0", "enc.6"])`` — multiple names
        - ``set_selector(module_type="Dinov2Layer", module_name="layernorm")``
          — union of both filters

        *selector* and (*module_type* / *module_name*) are mutually exclusive.

        Parameters
        ----------
        selector : ModuleSelector, list, or None
            Explicit selector object or list of module names / ModuleInfo objects.
        module_type : str, list[str], or None
            Hook all modules whose ``module_type`` is in this set.
        module_name : str, list[str], or None
            Hook modules whose ``name`` is in this set (exact match).

        Raises
        ------
        ValueError
            If no arguments are supplied, *selector* is combined with filters,
            or the resulting module list is empty.
        """
        using_filters = module_type is not None or module_name is not None
        if selector is None and not using_filters:
            raise ValueError("Provide selector, module_type, or module_name.")
        if selector is not None and using_filters:
            raise ValueError("selector is mutually exclusive with module_type/module_name.")

        if using_filters:
            types = {module_type} if isinstance(module_type, str) else set(module_type or [])
            names = {module_name} if isinstance(module_name, str) else set(module_name or [])
            matched = self._filter_modules(self.module_list, types, names)
            if not matched:
                raise ValueError(
                    f"No modules matched module_type={module_type!r}, "
                    f"module_name={module_name!r}. "
                    f"Available types: {sorted({m.module_type for m in self.module_list})}"
                )
            selector = CustomSelector(matched)
        elif isinstance(selector, list):
            selector = CustomSelector(selector)

        assert selector is not None
        self._selector = selector
        self._bind_selector()
        logger.info("Selector updated | modules={}", len(self._module_names))

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    def extract_for_modules(
        self,
        images: dict,
        module_names: list[str],
        batch_size: int,
        show_progress: bool = True,
    ) -> VisualRepresentations:
        """Extract activations for a subset of modules without altering state.

        Temporarily re-registers hooks for *module_names*, runs extraction,
        then restores the original hook configuration.

        Parameters
        ----------
        images : dict
            ``{stim_id: image}`` mapping.
        module_names : list[str]
            Subset of module names to extract.
        batch_size : int
            Images per forward pass.
        show_progress : bool
            Show tqdm progress bar.

        Returns
        -------
        VisualRepresentations
        """
        with self._hooked_for(module_names):
            return self._extract_batch(images, batch_size=batch_size, show_progress=show_progress)

    @contextlib.contextmanager
    def _hooked_for(self, module_names: list[str]):
        """Temporarily re-register hooks for *module_names*, then restore."""
        original = self._module_names
        self._backend.register_hooks(module_names)
        self._module_names = module_names
        try:
            yield
        finally:
            self._backend.register_hooks(original)
            self._module_names = original

    def _build_vr_list(
        self,
        stim_ids: list,
        features: dict[str, np.ndarray],
    ) -> list[VisualRepresentation]:
        """Assemble VisualRepresentation objects from batched features.

        Pairs each layer's activation array with *stim_ids*, using the
        current model metadata and module-type map.  The caller guarantees
        that rows in every array are aligned to *stim_ids* in the same order.

        Parameters
        ----------
        stim_ids : list
            Ordered stimulus IDs produced by :meth:`_prepare_images`.
        features : dict[str, np.ndarray]
            Layer name → activation array of shape ``(n_stim, ...)``.

        Returns
        -------
        list[VisualRepresentation]
        """
        model_meta = self._backend.get_model_meta()
        return [
            VisualRepresentation(
                model=model_meta.model_id,
                module_name=layer,
                module_type=self._module_type_map.get(layer, ""),
                stim_ids=stim_ids,
                array=arr,
            )
            for layer, arr in features.items()
        ]

    def _extract_batch(self, images: dict, batch_size: int, show_progress: bool = False) -> VisualRepresentations:
        stim_ids, loaded = self._prepare_images(images)
        features = self._run_batches(loaded, batch_size, show_progress)
        vr_list = self._build_vr_list(stim_ids, features)
        logger.info(
            "Extracted | n={} | batch_size={} | modules={}",
            len(stim_ids),
            batch_size,
            len(vr_list),
        )
        return VisualRepresentations(vr_list)

    def _run_batches(
        self,
        loaded: list,
        batch_size: int,
        show_progress: bool = False,
    ) -> dict[str, np.ndarray]:
        """Run batched forward passes and concatenate activations per layer.

        Parameters
        ----------
        loaded : list
            Pre-loaded images (PIL / ndarray / Tensor).
        batch_size : int
            Images per forward pass.
        show_progress : bool
            Show tqdm progress bar.

        Returns
        -------
        dict[str, np.ndarray]
            Layer name → concatenated activation array of shape ``(n_images, ...)``.
        """
        all_features: dict[str, list[np.ndarray]] = {}
        n_chunks = (len(loaded) + batch_size - 1) // batch_size
        iterator = tqdm(
            range(0, len(loaded), batch_size),
            total=n_chunks,
            desc="VisionModel",
            unit="batch",
            disable=not show_progress or n_chunks <= 1,
        )
        for start in iterator:
            chunk = loaded[start : start + batch_size]
            chunk_feats = self._forward_chunk(chunk)
            for layer, arr in chunk_feats.items():
                all_features.setdefault(layer, []).append(arr)
        return {layer: np.concatenate(arrs, axis=0) for layer, arrs in all_features.items()}

    @staticmethod
    def _prepare_images(images: dict) -> tuple[list[Any], list[Any]]:
        """Resolve image sources and return (stim_ids, loaded_images).

        Handles string and Path entries by opening them with PIL.
        Already-loaded images (PIL, ndarray, Tensor) pass through unchanged.

        Parameters
        ----------
        images : dict
            ``{stim_id: image_or_path}`` mapping.

        Returns
        -------
        tuple[list, list]
            ``(stim_ids, loaded_images)`` in the same order as *images*.
        """
        from pathlib import Path

        from PIL import Image as PILImage

        stim_ids = list(images.keys())
        loaded: list[Any] = []
        for sid in stim_ids:
            img = images[sid]
            if isinstance(img, (str, Path)):
                img = PILImage.open(img).convert("RGB")
            loaded.append(img)
        return stim_ids, loaded

    def _forward_chunk(self, images: list[Any]) -> dict[str, np.ndarray]:
        """Run one forward pass and return batched activations per layer.

        Parameters
        ----------
        images : list
            Batch of images (PIL / ndarray / str / Path).

        Returns
        -------
        dict[str, np.ndarray]
            Layer name → array of shape ``(B, ...)``.
        """
        inputs = self._backend.preprocess(images)
        with self._backend.collecting() as collect:
            self._backend.forward(inputs)
            activations = collect()
        return {name: act.numpy() for name, act in activations.items()}

    @classmethod
    def register_backend(cls, name: str, backend_cls: type[BaseBackend]) -> None:
        """Register a custom backend class under *name*.

        Parameters
        ----------
        name : str
            Key used in the ``backend=`` argument of :class:`VisionModel`.
        backend_cls : type[BaseBackend]
            Backend class to register.  Must be a concrete subclass of
            :class:`~vneurotk.vision.model.backend.base.BaseBackend`.
        """
        _BACKEND_REGISTRY[name] = backend_cls

    @staticmethod
    def _build_backend(backend: str, device: str) -> BaseBackend:
        if backend not in _BACKEND_REGISTRY:
            raise ValueError(f"Unknown backend {backend!r}. Available: {sorted(_BACKEND_REGISTRY)}")
        entry = _BACKEND_REGISTRY[backend]
        if isinstance(entry, str):
            module_path, class_name = entry.rsplit(":", 1)
            BackendClass: type[BaseBackend] = getattr(importlib.import_module(module_path), class_name)
        else:
            BackendClass = entry
        return BackendClass(device=device)

model_id property

Model identifier (e.g. 'facebook/dinov2-base', 'resnet50').

module_list property

All modules available in the loaded model.

Returns:

Type Description
list[ModuleInfo]

One entry per named module, ordered as model.named_modules(). Each entry exposes .name, .module_type, .depth, and .n_params.

module_names property

Names of all currently hooked modules.

extract(image, batch_size=DEFAULT_BATCH_SIZE, show_progress=True)

Extract DNN activations for one image or a collection of stimuli.

Parameters:

Name Type Description Default
image Image or ndarray or str or Path or dict

Single image → n_sample=1, stim_id=0. dict mapping stim_ids to images → n_sample=len(dict). String / pathlib.Path values are opened automatically. list is not accepted — use a dict with explicit stim IDs to preserve alignment with BaseData.trial_stim_ids.

required
batch_size int

Number of images per GPU forward pass. Default 32. Ignored for single-image input.

DEFAULT_BATCH_SIZE
show_progress bool

Display a tqdm progress bar over batches. Automatically suppressed for single-image input. Default True.

True

Returns:

Type Description
VisualRepresentations
Source code in src/vneurotk/vision/model/base.py
def extract(
    self,
    image: Any,
    batch_size: int = DEFAULT_BATCH_SIZE,
    show_progress: bool = True,
) -> VisualRepresentations:
    """Extract DNN activations for one image or a collection of stimuli.

    Parameters
    ----------
    image : PIL.Image.Image or np.ndarray or str or Path or dict
        Single image → ``n_sample=1``, ``stim_id=0``.
        ``dict`` mapping stim_ids to images → ``n_sample=len(dict)``.
        String / ``pathlib.Path`` values are opened automatically.
        ``list`` is **not** accepted — use a ``dict`` with explicit
        stim IDs to preserve alignment with ``BaseData.trial_stim_ids``.
    batch_size : int
        Number of images per GPU forward pass.  Default 32.
        Ignored for single-image input.
    show_progress : bool
        Display a tqdm progress bar over batches.  Automatically
        suppressed for single-image input.  Default ``True``.

    Returns
    -------
    VisualRepresentations
    """
    if batch_size <= 0:
        raise ValueError(f"batch_size must be positive, got {batch_size}")

    images = self._normalize_images(image)
    is_single = not isinstance(image, dict)
    return self.extract_for_modules(
        images,
        self._module_names,
        batch_size=1 if is_single else batch_size,
        show_progress=False if is_single else show_progress,
    )

extract_for_modules(images, module_names, batch_size, show_progress=True)

Extract activations for a subset of modules without altering state.

Temporarily re-registers hooks for module_names, runs extraction, then restores the original hook configuration.

Parameters:

Name Type Description Default
images dict

{stim_id: image} mapping.

required
module_names list[str]

Subset of module names to extract.

required
batch_size int

Images per forward pass.

required
show_progress bool

Show tqdm progress bar.

True

Returns:

Type Description
VisualRepresentations
Source code in src/vneurotk/vision/model/base.py
def extract_for_modules(
    self,
    images: dict,
    module_names: list[str],
    batch_size: int,
    show_progress: bool = True,
) -> VisualRepresentations:
    """Extract activations for a subset of modules without altering state.

    Temporarily re-registers hooks for *module_names*, runs extraction,
    then restores the original hook configuration.

    Parameters
    ----------
    images : dict
        ``{stim_id: image}`` mapping.
    module_names : list[str]
        Subset of module names to extract.
    batch_size : int
        Images per forward pass.
    show_progress : bool
        Show tqdm progress bar.

    Returns
    -------
    VisualRepresentations
    """
    with self._hooked_for(module_names):
        return self._extract_batch(images, batch_size=batch_size, show_progress=show_progress)

from_model(model, backend, selector=None) classmethod

Build a VisionModel from an already-loaded model.

Parameters:

Name Type Description Default
model Module

Pre-loaded PyTorch model.

required
backend BaseBackend

Backend instance with model already assigned.

required
selector ModuleSelector or None

Defaults to :class:BlockLevelSelector.

None

Returns:

Type Description
VisionModel
Source code in src/vneurotk/vision/model/base.py
@classmethod
def from_model(
    cls,
    model: Any,
    backend: BaseBackend,
    selector: ModuleSelector | None = None,
) -> VisionModel:
    """Build a VisionModel from an already-loaded model.

    Parameters
    ----------
    model : nn.Module
        Pre-loaded PyTorch model.
    backend : BaseBackend
        Backend instance with *model* already assigned.
    selector : ModuleSelector or None
        Defaults to :class:`BlockLevelSelector`.

    Returns
    -------
    VisionModel
    """
    inst = object.__new__(cls)
    inst._selector = selector or BlockLevelSelector()
    inst._backend = backend
    inst._backend.model = model

    inst._bind_selector()
    return inst

print_modules(max_depth=None, console=None)

Print a tree-style summary of all model modules.

Parameters:

Name Type Description Default
max_depth int or None

Maximum nesting depth to display. None shows all levels.

None
console Console or None

Rich console to use for output. Pass Console(record=True) to capture the output for SVG / HTML export.

None
Source code in src/vneurotk/vision/model/base.py
def print_modules(self, max_depth: int | None = None, console: Any = None) -> None:
    """Print a tree-style summary of all model modules.

    Parameters
    ----------
    max_depth : int or None
        Maximum nesting depth to display.  ``None`` shows all levels.
    console : rich.console.Console or None
        Rich console to use for output.  Pass ``Console(record=True)`` to
        capture the output for SVG / HTML export.
    """
    _print_modules(self.module_list, max_depth=max_depth, console=console)

register_backend(name, backend_cls) classmethod

Register a custom backend class under name.

Parameters:

Name Type Description Default
name str

Key used in the backend= argument of :class:VisionModel.

required
backend_cls type[BaseBackend]

Backend class to register. Must be a concrete subclass of :class:~vneurotk.vision.model.backend.base.BaseBackend.

required
Source code in src/vneurotk/vision/model/base.py
@classmethod
def register_backend(cls, name: str, backend_cls: type[BaseBackend]) -> None:
    """Register a custom backend class under *name*.

    Parameters
    ----------
    name : str
        Key used in the ``backend=`` argument of :class:`VisionModel`.
    backend_cls : type[BaseBackend]
        Backend class to register.  Must be a concrete subclass of
        :class:`~vneurotk.vision.model.backend.base.BaseBackend`.
    """
    _BACKEND_REGISTRY[name] = backend_cls

set_selector(selector=None, *, module_type=None, module_name=None)

Replace the layer selector and re-register hooks.

Accepts the following forms (combinable):

  • set_selector(BlockLevelSelector()) — explicit selector object
  • set_selector(["layer.0", "layer.1"]) — list of module names / ModuleInfo
  • set_selector(module_type="Dinov2Layer") — all modules of that type
  • set_selector(module_type=["Dinov2Layer", "LayerNorm"]) — multiple types
  • set_selector(module_name="encoder.layer.3") — single module by name
  • set_selector(module_name=["enc.0", "enc.6"]) — multiple names
  • set_selector(module_type="Dinov2Layer", module_name="layernorm") — union of both filters

selector and (module_type / module_name) are mutually exclusive.

Parameters:

Name Type Description Default
selector ModuleSelector, list, or None

Explicit selector object or list of module names / ModuleInfo objects.

None
module_type str, list[str], or None

Hook all modules whose module_type is in this set.

None
module_name str, list[str], or None

Hook modules whose name is in this set (exact match).

None

Raises:

Type Description
ValueError

If no arguments are supplied, selector is combined with filters, or the resulting module list is empty.

Source code in src/vneurotk/vision/model/base.py
def set_selector(
    self,
    selector: ModuleSelector | list | None = None,
    *,
    module_type: str | list[str] | None = None,
    module_name: str | list[str] | None = None,
) -> None:
    """Replace the layer selector and re-register hooks.

    Accepts the following forms (combinable):

    - ``set_selector(BlockLevelSelector())`` — explicit selector object
    - ``set_selector(["layer.0", "layer.1"])`` — list of module names / ModuleInfo
    - ``set_selector(module_type="Dinov2Layer")`` — all modules of that type
    - ``set_selector(module_type=["Dinov2Layer", "LayerNorm"])`` — multiple types
    - ``set_selector(module_name="encoder.layer.3")`` — single module by name
    - ``set_selector(module_name=["enc.0", "enc.6"])`` — multiple names
    - ``set_selector(module_type="Dinov2Layer", module_name="layernorm")``
      — union of both filters

    *selector* and (*module_type* / *module_name*) are mutually exclusive.

    Parameters
    ----------
    selector : ModuleSelector, list, or None
        Explicit selector object or list of module names / ModuleInfo objects.
    module_type : str, list[str], or None
        Hook all modules whose ``module_type`` is in this set.
    module_name : str, list[str], or None
        Hook modules whose ``name`` is in this set (exact match).

    Raises
    ------
    ValueError
        If no arguments are supplied, *selector* is combined with filters,
        or the resulting module list is empty.
    """
    using_filters = module_type is not None or module_name is not None
    if selector is None and not using_filters:
        raise ValueError("Provide selector, module_type, or module_name.")
    if selector is not None and using_filters:
        raise ValueError("selector is mutually exclusive with module_type/module_name.")

    if using_filters:
        types = {module_type} if isinstance(module_type, str) else set(module_type or [])
        names = {module_name} if isinstance(module_name, str) else set(module_name or [])
        matched = self._filter_modules(self.module_list, types, names)
        if not matched:
            raise ValueError(
                f"No modules matched module_type={module_type!r}, "
                f"module_name={module_name!r}. "
                f"Available types: {sorted({m.module_type for m in self.module_list})}"
            )
        selector = CustomSelector(matched)
    elif isinstance(selector, list):
        selector = CustomSelector(selector)

    assert selector is not None
    self._selector = selector
    self._bind_selector()
    logger.info("Selector updated | modules={}", len(self._module_names))

Module Selectors

Bases: ABC

Abstract base class for layer selection strategies.

Subclasses implement :meth:select, which receives a list of :class:~vneurotk.vision.meta.ModuleInfo objects and returns an ordered list of module name strings to hook.

Source code in src/vneurotk/vision/model/selector.py
class ModuleSelector(ABC):
    """Abstract base class for layer selection strategies.

    Subclasses implement :meth:`select`, which receives a list of
    :class:`~vneurotk.vision.meta.ModuleInfo` objects and returns an
    ordered list of module name strings to hook.
    """

    @abstractmethod
    def select(self, modules: list[ModuleInfo]) -> list[str]:
        """Return layer names to hook.

        Parameters
        ----------
        modules : list[ModuleInfo]
            All named modules enumerated by the backend, as returned by
            :meth:`~vneurotk.vision.model.backend.base.BaseBackend.enumerate_modules`.

        Returns
        -------
        list[str]
            Ordered module names to register hooks on.
        """

select(modules) abstractmethod

Return layer names to hook.

Parameters:

Name Type Description Default
modules list[ModuleInfo]

All named modules enumerated by the backend, as returned by :meth:~vneurotk.vision.model.backend.base.BaseBackend.enumerate_modules.

required

Returns:

Type Description
list[str]

Ordered module names to register hooks on.

Source code in src/vneurotk/vision/model/selector.py
@abstractmethod
def select(self, modules: list[ModuleInfo]) -> list[str]:
    """Return layer names to hook.

    Parameters
    ----------
    modules : list[ModuleInfo]
        All named modules enumerated by the backend, as returned by
        :meth:`~vneurotk.vision.model.backend.base.BaseBackend.enumerate_modules`.

    Returns
    -------
    list[str]
        Ordered module names to register hooks on.
    """

Bases: ModuleSelector

Select major block-level modules appropriate for the architecture.

Uses regex patterns matched against module names. Architecture patterns are tried in order; the first match wins. Falls back to top-level children (depth == 1) if no pattern matches.

Parameters:

Name Type Description Default
max_depth int

Maximum nesting depth to include (default 2). Controls how deeply nested sub-blocks are included.

2
include_patterns list[str] or None

Additional regex patterns to include alongside defaults.

None
Source code in src/vneurotk/vision/model/selector.py
class BlockLevelSelector(ModuleSelector):
    """Select major block-level modules appropriate for the architecture.

    Uses regex patterns matched against module names.  Architecture
    patterns are tried in order; the first match wins.  Falls back to
    top-level children (depth == 1) if no pattern matches.

    Parameters
    ----------
    max_depth : int
        Maximum nesting depth to include (default 2).  Controls how
        deeply nested sub-blocks are included.
    include_patterns : list[str] or None
        Additional regex patterns to include alongside defaults.
    """

    _ARCH_PATTERNS: list[tuple[str, int]] = [
        (r"^blocks\.\d+$", 2),  # timm ViT
        (r"^encoder\.layers\.\d+$", 3),  # HF ViT (plural)
        (r"^encoder\.layer\.\d+$", 3),  # HF DINOv2 (singular)
        (r"^model\.layer\.\d+$", 3),  # HF DINOv3
        (r"^layer\d+\.\d+$", 3),  # ResNet
        (r"^features\.\d+$", 2),  # VGG / EfficientNet
        (r"^stages\.\d+$", 2),  # ConvNeXt
        (r"^layers\.\d+$", 2),  # Swin / generic
        (r"^vision_model\.encoder\.layers\.\d+$", 4),  # SigLIP / SigLIP2
    ]

    def __init__(
        self,
        max_depth: int = 2,
        include_patterns: list[str] | None = None,
        arch_patterns: list[tuple[str, int]] | None = None,
    ) -> None:
        self.max_depth = max_depth
        self._extra = [re.compile(p) for p in (include_patterns or [])]
        raw = arch_patterns if arch_patterns is not None else self._ARCH_PATTERNS
        self._compiled_patterns: list[tuple[re.Pattern, int]] = [(re.compile(p), d) for p, d in raw]

    @classmethod
    def default_patterns(cls) -> list[tuple[str, int]]:
        """Return a copy of the built-in architecture patterns.

        Returns
        -------
        list[tuple[str, int]]
            Each element is ``(regex_pattern, max_depth)``.
            Mutating the returned list does not affect the class default.
        """
        return list(cls._ARCH_PATTERNS)

    @staticmethod
    def _module_depth(name: str) -> int:
        """Return the nesting depth of a module given its dotted name.

        Parameters
        ----------
        name : str
            Module name as produced by ``model.named_modules()``,
            e.g. ``"encoder.layer.3"`` → depth 3.

        Returns
        -------
        int
            ``name.count(".") + 1``.  Empty string returns ``0``.
        """
        return name.count(".") + 1 if name else 0

    def select(self, modules: list[ModuleInfo]) -> list[str]:
        """Select block-level layers from *modules*.

        Parameters
        ----------
        modules : list[ModuleInfo]

        Returns
        -------
        list[str]
        """
        selected: list[str] = []

        for m in modules:
            matched = any(pat.match(m.name) and m.depth <= max_d for pat, max_d in self._compiled_patterns)
            if not matched:
                matched = any(pat.search(m.name) for pat in self._extra)

            if matched:
                selected.append(m.name)

        if not selected:
            selected = [m.name for m in modules if m.depth == 1]

        return selected

default_patterns() classmethod

Return a copy of the built-in architecture patterns.

Returns:

Type Description
list[tuple[str, int]]

Each element is (regex_pattern, max_depth). Mutating the returned list does not affect the class default.

Source code in src/vneurotk/vision/model/selector.py
@classmethod
def default_patterns(cls) -> list[tuple[str, int]]:
    """Return a copy of the built-in architecture patterns.

    Returns
    -------
    list[tuple[str, int]]
        Each element is ``(regex_pattern, max_depth)``.
        Mutating the returned list does not affect the class default.
    """
    return list(cls._ARCH_PATTERNS)

select(modules)

Select block-level layers from modules.

Parameters:

Name Type Description Default
modules list[ModuleInfo]
required

Returns:

Type Description
list[str]
Source code in src/vneurotk/vision/model/selector.py
def select(self, modules: list[ModuleInfo]) -> list[str]:
    """Select block-level layers from *modules*.

    Parameters
    ----------
    modules : list[ModuleInfo]

    Returns
    -------
    list[str]
    """
    selected: list[str] = []

    for m in modules:
        matched = any(pat.match(m.name) and m.depth <= max_d for pat, max_d in self._compiled_patterns)
        if not matched:
            matched = any(pat.search(m.name) for pat in self._extra)

        if matched:
            selected.append(m.name)

    if not selected:
        selected = [m.name for m in modules if m.depth == 1]

    return selected

Bases: ModuleSelector

Select all leaf modules (modules with no children).

Parameters:

Name Type Description Default
exclude_types tuple[type, ...] or None

Module types to skip. Defaults to activation and regularization layers that carry no representational content.

None
Source code in src/vneurotk/vision/model/selector.py
class AllLeafSelector(ModuleSelector):
    """Select all leaf modules (modules with no children).

    Parameters
    ----------
    exclude_types : tuple[type, ...] or None
        Module types to skip.  Defaults to activation and regularization
        layers that carry no representational content.
    """

    _DEFAULT_EXCLUDE = (
        nn.Dropout,
        nn.Identity,
        nn.ReLU,
        nn.GELU,
        nn.SiLU,
        nn.Sigmoid,
        nn.Softmax,
    )

    def __init__(self, exclude_types: tuple | None = None) -> None:
        raw = exclude_types if exclude_types is not None else self._DEFAULT_EXCLUDE
        self._exclude_names: frozenset[str] = frozenset(t.__name__ for t in raw)

    def select(self, modules: list[ModuleInfo]) -> list[str]:
        """Return names of all non-excluded leaf modules.

        Parameters
        ----------
        modules : list[ModuleInfo]

        Returns
        -------
        list[str]
        """
        return [m.name for m in modules if m.is_leaf and m.module_type not in self._exclude_names]

select(modules)

Return names of all non-excluded leaf modules.

Parameters:

Name Type Description Default
modules list[ModuleInfo]
required

Returns:

Type Description
list[str]
Source code in src/vneurotk/vision/model/selector.py
def select(self, modules: list[ModuleInfo]) -> list[str]:
    """Return names of all non-excluded leaf modules.

    Parameters
    ----------
    modules : list[ModuleInfo]

    Returns
    -------
    list[str]
    """
    return [m.name for m in modules if m.is_leaf and m.module_type not in self._exclude_names]

Bases: ModuleSelector

Use an explicit user-supplied list of layer names.

Parameters:

Name Type Description Default
layer_names list[str] or list[ModuleInfo]

Exact module names as they appear in model.named_modules(), or :class:~vneurotk.vision.meta.ModuleInfo objects as returned by :attr:VisionModel.module_list.

required

Raises:

Type Description
ValueError

During :meth:select if any name is not found in the module list.

Source code in src/vneurotk/vision/model/selector.py
class CustomSelector(ModuleSelector):
    """Use an explicit user-supplied list of layer names.

    Parameters
    ----------
    layer_names : list[str] or list[ModuleInfo]
        Exact module names as they appear in ``model.named_modules()``,
        or :class:`~vneurotk.vision.meta.ModuleInfo`
        objects as returned by :attr:`VisionModel.module_list`.

    Raises
    ------
    ValueError
        During :meth:`select` if any name is not found in the module list.
    """

    def __init__(self, layer_names: list) -> None:
        self.layer_names = [item.name if hasattr(item, "name") else item for item in layer_names]

    def select(self, modules: list[ModuleInfo]) -> list[str]:
        """Validate and return the configured layer names.

        Parameters
        ----------
        modules : list[ModuleInfo]

        Returns
        -------
        list[str]

        Raises
        ------
        ValueError
            If any layer name is absent from *modules*.
        """
        available = {m.name for m in modules}
        missing = [n for n in self.layer_names if n not in available]
        if missing:
            raise ValueError(
                f"Layer(s) not found in model: {missing}. Inspect available names with model.named_modules()."
            )
        return list(self.layer_names)

select(modules)

Validate and return the configured layer names.

Parameters:

Name Type Description Default
modules list[ModuleInfo]
required

Returns:

Type Description
list[str]

Raises:

Type Description
ValueError

If any layer name is absent from modules.

Source code in src/vneurotk/vision/model/selector.py
def select(self, modules: list[ModuleInfo]) -> list[str]:
    """Validate and return the configured layer names.

    Parameters
    ----------
    modules : list[ModuleInfo]

    Returns
    -------
    list[str]

    Raises
    ------
    ValueError
        If any layer name is absent from *modules*.
    """
    available = {m.name for m in modules}
    missing = [n for n in self.layer_names if n not in available]
    if missing:
        raise ValueError(
            f"Layer(s) not found in model: {missing}. Inspect available names with model.named_modules()."
        )
    return list(self.layer_names)

Representations

Collection of :class:VisualRepresentation objects.

Returned by :meth:~vneurotk.vision.model.base.VisionModel.extract. Supports DataFrame-style filtering via boolean masks on :attr:meta.

Parameters:

Name Type Description Default
representations list[VisualRepresentation]

Ordered list of atomic activation records.

required

Examples:

>>> visual_representations = model.extract(images)
>>> meta = visual_representations.meta
>>> subset = visual_representations[meta["module_type"] == "Dinov2Layer"]
Source code in src/vneurotk/vision/representation/visual_representations.py
class VisualRepresentations:
    """Collection of :class:`VisualRepresentation` objects.

    Returned by :meth:`~vneurotk.vision.model.base.VisionModel.extract`.
    Supports DataFrame-style filtering via boolean masks on :attr:`meta`.

    Parameters
    ----------
    representations : list[VisualRepresentation]
        Ordered list of atomic activation records.

    Examples
    --------
    >>> visual_representations = model.extract(images)
    >>> meta = visual_representations.meta
    >>> subset = visual_representations[meta["module_type"] == "Dinov2Layer"]
    """

    def __init__(self, representations: list[VisualRepresentation]) -> None:
        self._visual_representations: list[VisualRepresentation] = list(representations)
        self._assert_shared_stim_ids(self._visual_representations)
        self._meta: pd.DataFrame = self._build_meta(self._visual_representations)

    # ------------------------------------------------------------------
    # Meta
    # ------------------------------------------------------------------

    @staticmethod
    def _assert_shared_stim_ids(visual_representations: list[VisualRepresentation]) -> None:
        if len(visual_representations) > 1:
            ref = visual_representations[0].stim_ids
            for vr in visual_representations[1:]:
                if vr.stim_ids != ref:
                    raise ValueError(
                        f"All VisualRepresentations must share the same stim_ids. "
                        f"Mismatch between '{visual_representations[0].module_name}' and '{vr.module_name}'."
                    )

    @staticmethod
    def _build_meta(visual_representations: list[VisualRepresentation]) -> pd.DataFrame:
        if not visual_representations:
            return pd.DataFrame(columns=["model", "module_type", "module_name", "shape"])
        return pd.DataFrame(
            [
                {
                    "model": vr.model,
                    "module_type": vr.module_type,
                    "module_name": vr.module_name,
                    "shape": vr.shape,
                }
                for vr in visual_representations
            ]
        )

    @property
    def meta(self) -> pd.DataFrame:
        """DataFrame with columns ``model``, ``module_type``, ``module_name``, ``shape``."""
        return self._meta

    # ------------------------------------------------------------------
    # Collection interface
    # ------------------------------------------------------------------

    def __len__(self) -> int:
        return len(self._visual_representations)

    def __iter__(self):
        return iter(self._visual_representations)

    @overload
    def __getitem__(self, key: str) -> VisualRepresentation: ...

    @overload
    def __getitem__(self, key: int | np.integer) -> VisualRepresentation: ...

    @overload
    def __getitem__(self, key: pd.Series | np.ndarray) -> VisualRepresentations: ...

    def __getitem__(
        self, key: pd.Series | np.ndarray | int | np.integer | str
    ) -> VisualRepresentations | VisualRepresentation:
        """Filter or index into the collection.

        Parameters
        ----------
        key : pd.Series or np.ndarray of bool, int, or str
            - ``str`` → look up by module name, returns :class:`VisualRepresentation`.
            - ``int`` → positional index, returns :class:`VisualRepresentation`.
            - 1-D bool array/Series aligned to :attr:`meta` → returns filtered
              :class:`VisualRepresentations`.

        Raises
        ------
        TypeError
            If *key* is a 0-d boolean (e.g. the result of a scalar comparison like
            ``'module_type' == 'Dinov2Layer'``).
        """
        if isinstance(key, str):
            return self.by_module(key)
        if isinstance(key, (int, np.integer)):
            return self._visual_representations[int(key)]
        arr = np.asarray(key)
        if arr.ndim == 0:
            raise TypeError(
                "Boolean index must be a 1-D array aligned to meta rows. "
                "Got a scalar — did you mean meta['col'] == value instead of 'col' == value?"
            )
        return self.filter(arr.astype(bool))

    # ------------------------------------------------------------------
    # Properties
    # ------------------------------------------------------------------

    @property
    def n_stim(self) -> int:
        """Number of stimuli (from first record, or 0 if empty)."""
        return self._visual_representations[0].n_stim if self._visual_representations else 0

    @property
    def stim_ids(self) -> tuple:
        """Stimulus IDs shared by all records."""
        return self._visual_representations[0].stim_ids if self._visual_representations else ()

    @property
    def module_names(self) -> list[str]:
        """Module names of all contained records."""
        return [vr.module_name for vr in self._visual_representations]

    # ------------------------------------------------------------------
    # Access helpers
    # ------------------------------------------------------------------

    def numpy(self, layer: str) -> np.ndarray:
        """Return activation array for *layer*, shape ``(n_stim, ...)``.

        Parameters
        ----------
        layer : str
            Module name.
        """
        return self.by_module(layer).array

    def to_tensor(self, layer: str) -> Any:
        """Return activations for *layer* as a PyTorch tensor.

        Parameters
        ----------
        layer : str
            Module name.
        """
        try:
            import torch  # type: ignore

            return torch.from_numpy(np.asarray(self.by_module(layer).array))
        except ImportError as exc:
            raise ImportError("torch is required for to_tensor()") from exc

    def select(self, ids: list | np.ndarray) -> VisualRepresentations:
        """Return a subset of stimuli by their IDs across all records.

        Parameters
        ----------
        ids : list or np.ndarray
            Stimulus IDs to keep.
        """
        return VisualRepresentations([vr.select(ids) for vr in self._visual_representations])

    def select_by_index(self, indices: list | np.ndarray) -> VisualRepresentations:
        """Return a subset of stimuli by positional index across all records.

        Parameters
        ----------
        indices : list or np.ndarray
            Integer indices.
        """
        ids = [self.stim_ids[i] for i in indices]
        return self.select(ids)

    def by_module(self, name: str, model: str | None = None) -> VisualRepresentation:
        """Return the :class:`VisualRepresentation` for *name*.

        Parameters
        ----------
        name : str
            Module name.
        model : str or None
            Model identifier to disambiguate when multiple records share the same
            module name.

        Raises
        ------
        KeyError
            If *name* is not found, or is ambiguous and *model* was not given.
        """
        if model is not None:
            for vr in self._visual_representations:
                if vr.model == model and vr.module_name == name:
                    return vr
            raise KeyError(f"Module {name!r} for model {model!r} not found.")

        matches = [vr for vr in self._visual_representations if vr.module_name == name]
        if not matches:
            raise KeyError(f"Layer {name!r} not found in VisualRepresentations.")
        if len(matches) > 1:
            models = [vr.model for vr in matches]
            raise KeyError(f"Module {name!r} found in {len(matches)} models: {models}. Specify model= to disambiguate.")
        return matches[0]

    def filter(self, mask: pd.Series | np.ndarray) -> VisualRepresentations:
        """Return a subset filtered by a 1-D boolean mask over :attr:`meta` rows.

        Parameters
        ----------
        mask : pd.Series or np.ndarray of bool
            Aligned to :attr:`meta` rows.
        """
        bool_mask = np.asarray(mask, dtype=bool)
        return VisualRepresentations(
            [vr for vr, keep in zip(self._visual_representations, bool_mask, strict=True) if keep]
        )

    # ------------------------------------------------------------------
    # Representation
    # ------------------------------------------------------------------

    def __repr__(self) -> str:
        return (
            f"VisualRepresentations("
            f"{self.n_stim} stimuli x {len(self._visual_representations)} modules, "
            f"models={list({vr.model for vr in self._visual_representations})})"
        )

meta property

DataFrame with columns model, module_type, module_name, shape.

module_names property

Module names of all contained records.

n_stim property

Number of stimuli (from first record, or 0 if empty).

stim_ids property

Stimulus IDs shared by all records.

__getitem__(key)

__getitem__(key: str) -> VisualRepresentation
__getitem__(key: int | np.integer) -> VisualRepresentation
__getitem__(key: pd.Series | np.ndarray) -> VisualRepresentations

Filter or index into the collection.

Parameters:

Name Type Description Default
key pd.Series or np.ndarray of bool, int, or str
  • str → look up by module name, returns :class:VisualRepresentation.
  • int → positional index, returns :class:VisualRepresentation.
  • 1-D bool array/Series aligned to :attr:meta → returns filtered :class:VisualRepresentations.
required

Raises:

Type Description
TypeError

If key is a 0-d boolean (e.g. the result of a scalar comparison like 'module_type' == 'Dinov2Layer').

Source code in src/vneurotk/vision/representation/visual_representations.py
def __getitem__(
    self, key: pd.Series | np.ndarray | int | np.integer | str
) -> VisualRepresentations | VisualRepresentation:
    """Filter or index into the collection.

    Parameters
    ----------
    key : pd.Series or np.ndarray of bool, int, or str
        - ``str`` → look up by module name, returns :class:`VisualRepresentation`.
        - ``int`` → positional index, returns :class:`VisualRepresentation`.
        - 1-D bool array/Series aligned to :attr:`meta` → returns filtered
          :class:`VisualRepresentations`.

    Raises
    ------
    TypeError
        If *key* is a 0-d boolean (e.g. the result of a scalar comparison like
        ``'module_type' == 'Dinov2Layer'``).
    """
    if isinstance(key, str):
        return self.by_module(key)
    if isinstance(key, (int, np.integer)):
        return self._visual_representations[int(key)]
    arr = np.asarray(key)
    if arr.ndim == 0:
        raise TypeError(
            "Boolean index must be a 1-D array aligned to meta rows. "
            "Got a scalar — did you mean meta['col'] == value instead of 'col' == value?"
        )
    return self.filter(arr.astype(bool))

by_module(name, model=None)

Return the :class:VisualRepresentation for name.

Parameters:

Name Type Description Default
name str

Module name.

required
model str or None

Model identifier to disambiguate when multiple records share the same module name.

None

Raises:

Type Description
KeyError

If name is not found, or is ambiguous and model was not given.

Source code in src/vneurotk/vision/representation/visual_representations.py
def by_module(self, name: str, model: str | None = None) -> VisualRepresentation:
    """Return the :class:`VisualRepresentation` for *name*.

    Parameters
    ----------
    name : str
        Module name.
    model : str or None
        Model identifier to disambiguate when multiple records share the same
        module name.

    Raises
    ------
    KeyError
        If *name* is not found, or is ambiguous and *model* was not given.
    """
    if model is not None:
        for vr in self._visual_representations:
            if vr.model == model and vr.module_name == name:
                return vr
        raise KeyError(f"Module {name!r} for model {model!r} not found.")

    matches = [vr for vr in self._visual_representations if vr.module_name == name]
    if not matches:
        raise KeyError(f"Layer {name!r} not found in VisualRepresentations.")
    if len(matches) > 1:
        models = [vr.model for vr in matches]
        raise KeyError(f"Module {name!r} found in {len(matches)} models: {models}. Specify model= to disambiguate.")
    return matches[0]

filter(mask)

Return a subset filtered by a 1-D boolean mask over :attr:meta rows.

Parameters:

Name Type Description Default
mask pd.Series or np.ndarray of bool

Aligned to :attr:meta rows.

required
Source code in src/vneurotk/vision/representation/visual_representations.py
def filter(self, mask: pd.Series | np.ndarray) -> VisualRepresentations:
    """Return a subset filtered by a 1-D boolean mask over :attr:`meta` rows.

    Parameters
    ----------
    mask : pd.Series or np.ndarray of bool
        Aligned to :attr:`meta` rows.
    """
    bool_mask = np.asarray(mask, dtype=bool)
    return VisualRepresentations(
        [vr for vr, keep in zip(self._visual_representations, bool_mask, strict=True) if keep]
    )

numpy(layer)

Return activation array for layer, shape (n_stim, ...).

Parameters:

Name Type Description Default
layer str

Module name.

required
Source code in src/vneurotk/vision/representation/visual_representations.py
def numpy(self, layer: str) -> np.ndarray:
    """Return activation array for *layer*, shape ``(n_stim, ...)``.

    Parameters
    ----------
    layer : str
        Module name.
    """
    return self.by_module(layer).array

select(ids)

Return a subset of stimuli by their IDs across all records.

Parameters:

Name Type Description Default
ids list or ndarray

Stimulus IDs to keep.

required
Source code in src/vneurotk/vision/representation/visual_representations.py
def select(self, ids: list | np.ndarray) -> VisualRepresentations:
    """Return a subset of stimuli by their IDs across all records.

    Parameters
    ----------
    ids : list or np.ndarray
        Stimulus IDs to keep.
    """
    return VisualRepresentations([vr.select(ids) for vr in self._visual_representations])

select_by_index(indices)

Return a subset of stimuli by positional index across all records.

Parameters:

Name Type Description Default
indices list or ndarray

Integer indices.

required
Source code in src/vneurotk/vision/representation/visual_representations.py
def select_by_index(self, indices: list | np.ndarray) -> VisualRepresentations:
    """Return a subset of stimuli by positional index across all records.

    Parameters
    ----------
    indices : list or np.ndarray
        Integer indices.
    """
    ids = [self.stim_ids[i] for i in indices]
    return self.select(ids)

to_tensor(layer)

Return activations for layer as a PyTorch tensor.

Parameters:

Name Type Description Default
layer str

Module name.

required
Source code in src/vneurotk/vision/representation/visual_representations.py
def to_tensor(self, layer: str) -> Any:
    """Return activations for *layer* as a PyTorch tensor.

    Parameters
    ----------
    layer : str
        Module name.
    """
    try:
        import torch  # type: ignore

        return torch.from_numpy(np.asarray(self.by_module(layer).array))
    except ImportError as exc:
        raise ImportError("torch is required for to_tensor()") from exc

Atomic activation record: one model × one module.

Parameters:

Name Type Description Default
model str

Model identifier, e.g. "facebook/dinov2-base".

required
module_name str

Module name as from named_modules(), e.g. "encoder.layer.11".

required
module_type str

Class name of the module, e.g. "Dinov2Layer".

required
stim_ids list

Ordered stimulus identifiers corresponding to the first axis of array.

required
array ndarray or None

Activation array of shape (n_stim, ...). Mutually exclusive with array_loader; one of the two must be provided.

None
array_loader callable or None

Zero-argument callable that returns the activation array on first access. Used for lazy loading from HDF5. Mutually exclusive with array.

None
shape tuple or None

Pre-computed shape to return from :attr:shape without triggering array loading. Required when array_loader is given; ignored otherwise.

None
Source code in src/vneurotk/vision/representation/visual_representations.py
class VisualRepresentation:
    """Atomic activation record: one model × one module.

    Parameters
    ----------
    model : str
        Model identifier, e.g. ``"facebook/dinov2-base"``.
    module_name : str
        Module name as from ``named_modules()``, e.g. ``"encoder.layer.11"``.
    module_type : str
        Class name of the module, e.g. ``"Dinov2Layer"``.
    stim_ids : list
        Ordered stimulus identifiers corresponding to the first axis of *array*.
    array : np.ndarray or None
        Activation array of shape ``(n_stim, ...)``.  Mutually exclusive with
        *array_loader*; one of the two must be provided.
    array_loader : callable or None
        Zero-argument callable that returns the activation array on first access.
        Used for lazy loading from HDF5.  Mutually exclusive with *array*.
    shape : tuple or None
        Pre-computed shape to return from :attr:`shape` without triggering array
        loading.  Required when *array_loader* is given; ignored otherwise.
    """

    def __init__(
        self,
        model: str,
        module_name: str,
        module_type: str,
        stim_ids: list,
        array: np.ndarray | None = None,
        *,
        array_loader: Callable[[], np.ndarray] | None = None,
        shape: tuple | None = None,
    ) -> None:
        if array is None and array_loader is None:
            raise ValueError("Either array or array_loader must be provided.")
        self.model = model
        self.module_name = module_name
        self.module_type = module_type
        self.stim_ids: tuple = tuple(stim_ids)
        self._array: np.ndarray | None = np.asarray(array) if array is not None else None
        self._array_loader: Callable[[], np.ndarray] | None = array_loader
        self._shape: tuple | None = shape if self._array is None else None
        self._id_to_idx: dict[Any, int] = {sid: i for i, sid in enumerate(self.stim_ids)}

    @property
    def array(self) -> np.ndarray:
        """Activation array, loaded lazily from HDF5 if constructed with *array_loader*."""
        if self._array is None:
            self._array = self._array_loader()  # ty: ignore[call-non-callable]
            self._array_loader = None
            self._shape = None
        return self._array

    @array.setter
    def array(self, value: np.ndarray) -> None:
        self._array = np.asarray(value)
        self._shape = None

    @property
    def n_stim(self) -> int:
        """Number of stimuli."""
        return len(self.stim_ids)

    @property
    def shape(self) -> tuple:
        """Shape of the activation array ``(n_stim, ...)``."""
        if self._shape is not None:
            return self._shape
        return self.array.shape

    def select(self, ids: list | np.ndarray) -> VisualRepresentation:
        """Return a subset of stimuli by their IDs.

        Parameters
        ----------
        ids : list or np.ndarray
            Stimulus IDs to select.

        Returns
        -------
        VisualRepresentation
        """
        ids_list = list(ids)
        indices = np.array([self._id_to_idx[sid] for sid in ids_list])
        return VisualRepresentation(
            model=self.model,
            module_name=self.module_name,
            module_type=self.module_type,
            stim_ids=ids_list,
            array=self.array[indices],
        )

    def __repr__(self) -> str:
        return f"VisualRepresentation(model={self.model!r}, module={self.module_name!r}, shape={self.shape})"

array property writable

Activation array, loaded lazily from HDF5 if constructed with array_loader.

n_stim property

Number of stimuli.

shape property

Shape of the activation array (n_stim, ...).

select(ids)

Return a subset of stimuli by their IDs.

Parameters:

Name Type Description Default
ids list or ndarray

Stimulus IDs to select.

required

Returns:

Type Description
VisualRepresentation
Source code in src/vneurotk/vision/representation/visual_representations.py
def select(self, ids: list | np.ndarray) -> VisualRepresentation:
    """Return a subset of stimuli by their IDs.

    Parameters
    ----------
    ids : list or np.ndarray
        Stimulus IDs to select.

    Returns
    -------
    VisualRepresentation
    """
    ids_list = list(ids)
    indices = np.array([self._id_to_idx[sid] for sid in ids_list])
    return VisualRepresentation(
        model=self.model,
        module_name=self.module_name,
        module_type=self.module_type,
        stim_ids=ids_list,
        array=self.array[indices],
    )

Image Source

Bases: Protocol

Protocol for any mapping from stimulus ID to image data.

Both :class:~vneurotk.neuro.base.StimulusSet and :class:~vneurotk.io.loader.LazyH5Dict satisfy this protocol, as does a plain :class:dict. Callers that accept stimulus images should annotate their parameter as ImageSource rather than enumerating concrete types.

Source code in src/vneurotk/vision/image_source.py
@runtime_checkable
class ImageSource(Protocol):
    """Protocol for any mapping from stimulus ID to image data.

    Both :class:`~vneurotk.neuro.base.StimulusSet` and
    :class:`~vneurotk.io.loader.LazyH5Dict` satisfy this protocol, as does a
    plain :class:`dict`.  Callers that accept stimulus images should annotate
    their parameter as ``ImageSource`` rather than enumerating concrete types.
    """

    def __getitem__(self, stim_id: Any) -> Any: ...

    def __contains__(self, stim_id: Any) -> bool: ...

    def __len__(self) -> int: ...

Metadata

Provenance record for a feature extraction run.

Parameters:

Name Type Description Default
model_id str

Model identifier passed to the backend, e.g. "facebook/dinov2-base" or "resnet50".

required
backend str

Backend used: "timm", "transformers", or "thingsvision".

required
Source code in src/vneurotk/vision/meta.py
@dataclass
class ModelInfo:
    """Provenance record for a feature extraction run.

    Parameters
    ----------
    model_id : str
        Model identifier passed to the backend, e.g. ``"facebook/dinov2-base"``
        or ``"resnet50"``.
    backend : str
        Backend used: ``"timm"``, ``"transformers"``, or ``"thingsvision"``.
    """

    model_id: str
    backend: str

Metadata for an enumerated module.

Parameters:

Name Type Description Default
name str

Module name as from named_modules().

required
module_type str

Class name of the module.

required
depth int

Nesting depth in the module tree.

required
n_params int

Total number of parameters in this module (including children).

0
is_leaf bool

True if the module has no child modules (suitable for direct hooking).

False
param_shapes dict[str, tuple]

Shape of each directly-owned parameter (empty for container modules). E.g. {"weight": (768, 768), "bias": (768,)}.

dict()
Source code in src/vneurotk/vision/meta.py
@dataclass
class ModuleInfo:
    """Metadata for an enumerated module.

    Parameters
    ----------
    name : str
        Module name as from ``named_modules()``.
    module_type : str
        Class name of the module.
    depth : int
        Nesting depth in the module tree.
    n_params : int
        Total number of parameters in this module (including children).
    is_leaf : bool
        True if the module has no child modules (suitable for direct hooking).
    param_shapes : dict[str, tuple]
        Shape of each directly-owned parameter (empty for container modules).
        E.g. ``{"weight": (768, 768), "bias": (768,)}``.
    """

    name: str
    module_type: str
    depth: int
    n_params: int = 0
    is_leaf: bool = False
    param_shapes: dict[str, tuple] = field(default_factory=dict)