Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

multigeom

multigeom

Multi-scale information geometry of neural population codes, in Python.

multigeom computes the Fisher information metric J(x) of any fitted PyTorch model of neural responses, and the multi-scale Fisher metric

G(x) = ∫₀^∞ [φ_t ∗ J_t](x) dt

the Fisher information of the stimulus-diffused likelihood integrated over all diffusion scales. G(x) is directly related to the mutual information between stimulus and response, and for natural images it is estimated with a conditional diffusion model. The package also reproduces the macaque V1/V4 analyses of the paper.

What the pipeline does

  1. Encoders (NeuralEncoder.fit_readout; the paper's recipe is multigeom.fit.train_encoder). A frozen backbone (InceptionV3 truncated at Conv2d_4a_3x3, ResNet-50 truncated at layer1.0, or the ON-OFF front-end of Berardino et al.) followed by a per-neuron Gaussian spatial-attention readout, fitted to the trial-averaged responses of the 50 most reliable V1 or V4 channels of the THINGS Ventral-stream Spiking Dataset (Papale et al., 2025).
  2. Synthetic responses (multigeom.diffusion.responses). The encoder f_φ is evaluated on every TinyImageNet training image; these 50-d vectors condition the diffusion model.
  3. Conditional diffusion model (multigeom.diffusion.train). A UNet2DConditionModel (64×64 grayscale, ~68M parameters) conditioned on the responses through a 3-layer MLP and cross-attention. Training follows the Gaussian observation model p(r | x) = N(f_φ(x), σ² I) with σ = 0.22, and uses classifier-free-guidance dropout (rate 0.1) so that the same network also gives the unconditional denoiser. 200 epochs.
  4. G(x) and J(x) (multigeom.pipeline.per_image). The top-50 eigenpairs of G(x) from the conditional-pair estimator (Algorithm 1 of the paper, multigeom.geometry.estimator), and of J(x) = σ⁻² J_fᵀ J_f by autograd (multigeom.geometry.fisher), on 200 held-out TinyImageNet validation images.
  5. Cross-model comparison (multigeom.pipeline.alignment). Subspace alignment A_k = (1/k) ‖V_Aᵀ V_B‖²_F and Bures-Wasserstein distance between every pair of (encoder, area) configurations, for k = 1, …, 50, averaged over images.
  6. Local metric field (multigeom.pipeline.metric_field). The 2×2 projection of G and J onto the leading two eigenvectors at a reference image, on a grid of images perturbed along those eigenvectors (Fig. 5).

Figures and the encoder table are drawn from these outputs by the modules in multigeom.viz. Supporting analyses quoted in the paper's appendices (calibration of σ, Monte-Carlo variance of the conditional-pair versus the unconditional-conditional estimator, backbone layer sweep) are in multigeom.fit and multigeom.pipeline.estimator_variance.

Installation

The package is multigeom. The core install gives encoders, the Fisher metric J(x), the diffusion model, the G(x) estimator, and the information-geometric analyses; two extras add the rest.

pip install multigeom                 # core: encoders, J(x), the diffusion model, the G(x) estimator, the analyses
pip install "multigeom[paper]"        # + what reproducing the paper needs: the ON-OFF encoder (plenoptic), the TVSD loaders (h5py)
pip install "multigeom[all]"          # + Jupyter and Modal (serverless GPUs)

From a clone, for development and to run the paper's pipeline:

git clone https://github.com/multigeom/multigeom.git
cd multigeom
python -m venv .venv && source .venv/bin/activate
pip install -e ".[all,dev,docs]"

pyproject.toml declares the supported dependency ranges. The pipeline and tests were run with Python 3.11 and CUDA 12.6.

import multigeom

images = ...                                                # [N, 1, 64, 64] tensor, values in [-1, 1]
encoder = multigeom.load_encoder("inception", area="V1")    # an encoder of the paper, fitted to macaque V1 (downloaded)
responses = encoder.predict(images)                         # [N, 50] predicted neural responses
multigeom.validate_encoder(encoder)                         # checks the encoder contract

image = images[:1]                                          # one image, [1, 1, 64, 64]
J = multigeom.compute_fisher(image, encoder, sigma=0.22)    # eigenpairs of the Fisher metric J(x)

# G(x) also needs the diffusion model `unet` trained for this encoder and its noise schedule `alphas_cumprod`:
G = multigeom.compute_g(image=image, encoder=encoder, unet=unet, alphas_cumprod=a, cfg=cfg, device=device)

The command line multigeom runs the pipeline: multigeom jacobian | compute-g | train-diffusion | align (multigeom --help). The notebooks are in the companion repository multigeom-tutorials; 0_bring_your_own_encoder.ipynb computes J(x) for a model of your own.

Data

The three datasets (TVSD, THINGS images, TinyImageNet) and their expected layout are described in multigeom-tutorials, where the notebooks read them. Pretrained backbone weights are downloaded automatically.

Running

One (encoder, area) configuration end to end (stages 1-4). ENCODER is inception, resnet50 or onoff; AREA is V1 or V4:

ENCODER=inception; AREA=V1; RUN=runs; DEVICE=cuda:0

python -m multigeom.fit.train_encoder --config $ENCODER --area $AREA --device $DEVICE \
    --out-dir $RUN/encoders/$ENCODER/$AREA
python -m multigeom.diffusion.responses --config $ENCODER --area $AREA --device $DEVICE \
    --encoder-ckpt $RUN/encoders/$ENCODER/$AREA/best_model.pt --out-dir $RUN/responses/$ENCODER/$AREA
python -m multigeom.diffusion.train --config diffusion --device $DEVICE \
    --responses $RUN/responses/$ENCODER/$AREA/train_responses.pt --out-dir $RUN/diffusion/$ENCODER/$AREA
python -m multigeom.pipeline.per_image --config estimator --encoder-cfg $ENCODER --area $AREA --device $DEVICE \
    --encoder-ckpt $RUN/encoders/$ENCODER/$AREA/best_model.pt \
    --diffusion-ckpt $RUN/diffusion/$ENCODER/$AREA/final_model.pt --out-dir $RUN/per_image/$ENCODER/$AREA

Run these for all six configurations of the paper (about 30 A100-hours each, almost all of it diffusion training), then the comparisons and figures:

python -m multigeom.pipeline.alignment --runs-dir runs --out-dir runs/metrics
python -m multigeom.pipeline.metric_field --img-idx 0 --out-dir runs/metric_field/img0000
python -m multigeom.viz.table1    --runs-dir runs --out-dir figures/out
python -m multigeom.viz.fig3      --runs-dir runs --out-dir figures/out --img-idx 0
python -m multigeom.viz.fig4      --runs-dir runs --out-dir figures/out --img-idx 0
python -m multigeom.viz.fig5      --runs-dir runs --out-dir figures/out --img-idx 0
python -m multigeom.viz.fig_app_bw --runs-dir runs --out-dir figures/out

multigeom.pipeline.per_image accepts --start/--end to split the 200 test images across GPUs. Each image is seeded independently (seed + image index), so the split does not change the results. Every checkpoint and output file stores a provenance record (git commit, command line, seed, config files, package versions).

Where each result comes from

Paper item Script Inputs
Table 1 (encoder performance) multigeom.viz.table1 runs/encoders/*/*/best_model.pt
Fig. 3B-C (measured vs predicted responses) multigeom.viz.fig3 stage 1
Fig. 3D (receptive fields) multigeom.viz.fig3 stage 1 readout parameters
Fig. 3E-F (eigenvectors of G and J) multigeom.viz.fig3 stage 4
Fig. 4A (performance of the three encoders) multigeom.viz.fig4 stage 1
Fig. 4B-C (eigenvectors per encoder) multigeom.viz.fig4 stage 4
Fig. 4D-E (subspace alignment vs k) multigeom.viz.fig4 stage 5
Fig. 5 (local metric field) multigeom.viz.fig5 stage 6
Bures-Wasserstein figure (appendix) multigeom.viz.fig_app_bw stage 5
σ = 0.22 calibration (appendix) multigeom.fit.calibrate_sigma TVSD
CC vs UC estimator variance (appendix) multigeom.pipeline.estimator_variance stages 1, 3
Backbone layer choice (appendix) multigeom.fit.layer_sweep TVSD, THINGS

Fig. 3A (model schematic) and the illustrative one- and two-dimensional examples of Figs. 1 and 2 are not produced by this code.

Compute

Measured on NVIDIA A100-SXM4-40GB GPUs, per (encoder, area) configuration:

Stage Time
1. encoder readout (features cached once) minutes
2. TinyImageNet responses minutes
3. diffusion training, 200 epochs ~30 A100-hours (~15 h on 2 GPUs)
4. G(x) and J(x), 200 images ~2 A100-hours
5. metrics minutes (CPU)

Bring your own encoder

Any fitted PyTorch model can be used as the encoder f(x), if it follows these rules. multigeom.encoder checks them (multigeom.validate_encoder(encoder); multigeom.compute_fisher does it automatically for models that do not use the reference-encoder internals, and multigeom.compute_g(..., validate=True) on request) and raises EncoderContractError naming the rule.

Rule Requirement
R1 an nn.Module
R2 input [B, 1, 64, 64], values in [-1, 1]; the model does any rescaling or channel repeat itself
R3 output [B, N], floating point and finite, the same N for every batch size (and n_neurons == N if set)
R4 deterministic in eval mode (no dropout or noise at inference)
R5 differentiable w.r.t. the input through forward (no torch.no_grad() or detach()): needed for J(x)
R6 samples independent within a batch (no batch statistics in train mode)
R7 does not modify its input in place
R8 responses vary across images (warning)
R9 responses of order 1 (for example z-scored), as the noise sigma is 0.22 (warning)

Training the diffusion model on the encoder's responses needs R1-R4, R6 and R9; estimating G(x) and J(x) needs all of them. The fit itself, and the 64x64 grayscale natural-image domain, are not checked. The diffusion model's conditioning layer is sized by N, so each encoder needs its own trained diffusion model.

Subspace alignment (Fig. 4D-E) on several GPUs

2_subspace_alignment.ipynb (in multigeom-tutorials) estimates G(x) and J(x) for many images and configurations and plots Fig. 4D-E. The images are independent, so they are split over workers (local GPUs, or serverless GPUs on Modal) with identical results; no DDP/FSDP is involved. All supporting functions are in multigeom.pipeline.distribute. The notebook sets five parameters, CONFIGS (one or more (encoder, area) pairs), N_IMAGES, BACKEND ("local" or "modal"), DEVICE ("gpu" or "cpu") and NUM_WORKERS, and a table in the notebook gives example values for four typical cases. The Modal route needs pip install modal and modal token new, and costs money: the notebook prints an estimate and refuses to launch above MAX_COST_USD.

Local metric field (Fig. 5)

3_local_metric_field.ipynb (in multigeom-tutorials) plots the local metric field projected onto the leading eigenvectors of G(x): a reference image is perturbed along the two leading eigenvectors of the Inception model's G(x), and the metrics G(x) and J(x) of the Inception and ResNet-50 models are drawn as ellipses on that 2-D grid, for V1 and V4. One GPU is enough: every grid point of every model needs one G(x) estimate (about 15 s on an RTX 5090), so the 5 x 5 grid with both areas takes about 26 minutes and a 3 x 3 grid with one area about 5 minutes. The computation is multigeom.pipeline.metric_field, run as a background process by multigeom.pipeline.metric_field_job, and it can be stopped and resumed. The notebook perturbs by 0.5*sqrt(lambda) for G(x) (about 0.3) but by a fixed 0.3 for J(x): J has far larger eigenvalues, and with the same rule its perturbed images are clipped and the ellipses of the two models swap sizes, unlike the paper's panel B. The paper does not state its step sizes, so this is a choice (the metric_field module itself uses 0.5*sqrt(lambda) for both metrics, and the notebook runs it once per metric). The paper does not state its reference image either; the notebook uses validation image 64, the one that best reproduces the pattern of the paper's panel B among the 200 test images (a scan of the 200 test images with six perturbation settings), whereas the metric_field module defaults to image 0. The elongated V1 ellipses of the paper's panel A are not reproduced (the leading eigenvalues of G(x) are close for every image).

Supporting analyses

Three modules reproduce numbers quoted in the appendices. They are not part of the main pipeline and write JSON files with a provenance record. Each module's docstring gives the method.

Module Paper statement Inputs Cost
multigeom.fit.calibrate_sigma σ = 0.22 (V1), 0.25 (V4) TVSD files seconds, CPU
multigeom.pipeline.estimator_variance the conditional-pair estimator has 80-95% lower Monte-Carlo variance than the unconditional-conditional form encoder and diffusion checkpoints, test images minutes per image, GPU
multigeom.fit.layer_sweep Conv2d_4a_3x3 maximises the test correlation in V1 and V4 TVSD files, THINGS images one encoder fit per layer, GPU
python -m multigeom.fit.calibrate_sigma --out runs/analysis/sigma_calibration.json
python -m multigeom.pipeline.estimator_variance --encoder-cfg inception \
    --encoder-ckpt runs/encoders/inception/V1/best_model.pt \
    --diffusion-ckpt runs/diffusion/inception/V1/final_model.pt --n-images 6 --n-runs 6 --n-mc 100
python -m multigeom.fit.layer_sweep --backbone inception --area V1

On the released TVSD file, the calibration gives 0.2275 (V1) and 0.2451 (V4) on the top-50 channels, and 0.2517 (V1) and 0.2591 (V4) on all channels of the area.

Tests

pytest tests/unit -m "not slow and not gpu and not pretrained"   # unit tests: synthetic data, no checkpoints (what GitHub runs)
pytest tests/unit                                                # all unit tests, including the ones that need checkpoints

The tests that run the notebooks and compare their figures with stored references are in multigeom-tutorials.

Checkpoints

Trained encoder and diffusion checkpoints, and the per-image G(x)/J(x) eigendecompositions behind the figures, will be released with a persistent identifier; this section will link to them.

Repository layout

multigeom/
    __init__.py     public API: load_encoder, NeuralEncoder, compute_fisher, compute_g, subspace_alignment, ...
    encoder.py      the encoder contract (Protocol and validate_encoder)
    encoders/       core: NeuralEncoder, load_encoder, validate_encoder, readouts, the reference encoders (ON-OFF needs the [paper] extra)
    geometry/       core: fisher (compute_fisher), estimator (compute_g), metric_field
    analysis/       core: subspace_alignment, bures_wasserstein
    diffusion/      core: model, config, responses, train
    pipeline/       core: per_image, alignment, metric_field, distribute (local and Modal workers)
    data/           TinyImageNet loader and the canonical image transform
    fit/            extra [paper]: fitting encoders to the TVSD recordings, layer sweep, noise calibration
    viz/            the figures of the paper
    configs/        packaged YAML settings (encoders, diffusion, estimator)
    cli.py  config.py  utils.py
tests/unit/   unit tests (synthetic data)

Citation

See CITATION.cff.

@inproceedings{azeglio2026multiscale,
  title     = {A multi-scale information geometry reveals the structure of mutual information in neural populations},
  author    = {Azeglio, Simone and Laquitaine, Steeve and Ferrari, Ulisse and Chalk, Matthew},
  booktitle = {Advances in Neural Information Processing Systems},
  year      = {2026}
}

License

Code: MIT (see LICENSE). The datasets keep their own licenses: TVSD (Papale et al.), THINGS images (Hebart et al.), TinyImageNet. The ON-OFF front-end and its pretrained parameters come from plenoptic; the conditional UNet from Hugging Face diffusers.

Metadata

Release files for multigeom 0.1.0.dev1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for multigeom 0.1.0.dev1
File Size Uploaded Provenance Actions
multigeom-0.1.0.dev1.tar.gz 156.1 kB With provenance Details for multigeom-0.1.0.dev1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for multigeom 0.1.0.dev1
File Interpreter ABI Platform Actions
multigeom-0.1.0.dev1-py3-none-any.whl Python 3 none any Details for multigeom-0.1.0.dev1-py3-none-any.whl

Total release size: 291.8 kB

Release files / multigeom-0.1.0.dev1.tar.gz

Download URL multigeom-0.1.0.dev1.tar.gz
Size 156.1 kB
Tags Source
SHA-256 checksum
How to use checksums
ac1c430bbb959b3ac31d99d661ef1dacf2aee0a85694aeb523487256fdf8ceb6
BLAKE2b-256 checksum
How to use checksums
9c9541b9429f3c38f29d3392ada56b4ef2452eba1768920fc5262fa38203e489
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / multigeom-0.1.0.dev1-py3-none-any.whl

Download URL multigeom-0.1.0.dev1-py3-none-any.whl
Size 135.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f957ca7dcd93cce3d21514059025edf2e19e025513e6bc92778df459919fac7a
BLAKE2b-256 checksum
How to use checksums
c89521be94dad2fbabf4a6a2acc1e389432c4815e728d247b21eb017278828f5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Security for multigeom 0.1.0.dev1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for multigeom-0.1.0.dev1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page