Skip to content

feat: VIVS - #32

Open
ori-kron-wis wants to merge 36 commits into
mainfrom
Ori-add-VIVS
Open

feat: VIVS#32
ori-kron-wis wants to merge 36 commits into
mainfrom
Ori-add-VIVS

Conversation

@ori-kron-wis

Copy link
Copy Markdown
Member

Description

Brief description of changes and motivation.

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Refactoring

Testing

  • Tests pass locally (pytest tests/ -v)
  • New tests added for new functionality

Checklist

  • Code follows project style (ruff check src/ tests/)
  • Documentation updated if needed
  • CHANGELOG.md updated

ori-kron-wis and others added 26 commits July 23, 2026 12:45
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds VIVSModule (BaseModuleClass) composing a fresh scvi.module.VAE
(self.x_module) with an ImportanceScoreNet (self.xy_module). loss()
switches on self._phase: "x" delegates to x_module's VAE loss, "xy"
computes the CRT importance-score loss directly and leaves x_module
untouched (frozen when pretrained, unused for grad in this phase).

_get_inference_input/_get_generative_input/inference/generative all
delegate to x_module, which is what lets VAEMixin.get_latent_representation
work later without extra code.

Deviations from brief needed to make the exact test code pass against
the installed scvi-tools 1.5.0.post1:
- test tensors dicts needed a "labels" key: VAE._get_generative_input
  unconditionally reads tensors[REGISTRY_KEYS.LABELS_KEY], with no
  .get() fallback, regardless of n_labels.
- loss()'s xy-phase LossOutput(...) needed n_obs_minibatch=x.shape[0]
  explicit: reconstruction_loss=xy_out["loss"] is a 0-dim mean, and
  LossOutput.__post_init__ otherwise tries reconstruction_loss.shape[0]
  to infer it, which IndexErrors on a scalar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires VIVSModule into the DestVI/SCVIVA-style model idiom: VAEMixin +
UnsupervisedTrainingMixin + SpatialBaseModel, with setup_anndata
registering X, batch, and the Y obsm field (n_Y drives n_responses).
x_model plumbing is present but only the fresh-VAE constructor path is
exercised here; pretrained reuse lands in a later task.
Trains the generative VAE over X first, freezes it, then trains the
importance-score net for Y|X. Sequential order (not joint) is required
for CRT validity: the knockoff sampler must not see Y.

Also registers a dummy LABELS_KEY field in setup_anndata, needed
because VAE._get_generative_input unconditionally reads it and the
field was missing (unit tests masked this by hand-building tensors).
… delegation

Adds test for Task 7 that validates VAEMixin.get_latent_representation
works on VIVS instances purely through the delegation wiring from Task 4
(VIVSModule._get_inference_input and inference delegate to x_module).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_sample_knockoffs previously re-ran the encoder (module.inference) on
every Monte Carlo iteration, folding extra encoder-uncertainty variance
into each null draw and diverging from the reference JAX implementation
(vivs/_vivs.py:227-228,302), which samples z once per batch and only
resamples the decoder's px noise across MC draws.

Split _sample_knockoffs(x, batch_index) into _encode_for_knockoffs(x,
batch_index) -> (z, library), run once per batch, and
_sample_knockoffs(z, library, batch_index), called once per MC draw
reusing the same z/library.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements _get_importance_vmap, vectorizing the per-gene knockoff
substitution + statistic recomputation over the gene axis via
torch.vmap(randomness="different"). Uses non-mutating torch.where
gene substitution (verified in-place index_put_ breaks vmap once a
dropout/BatchNorm submodule runs downstream).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ance

Wrap the second (in-loop) `_get_importance_vmap` call site with the same
try/except RuntimeError -> friendly OOM message used at the first call
site, since it fires on the majority of MC samples for realistic
n_mc_samples. Also pass device=x.device when building gene_ids in
_get_importance_vmap so CUDA tensors don't hit a device-mismatch error
that would otherwise be misreported as an OOM by the new guard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per-cell (unsummed) importance scores for a specific set of genes,
substituted simultaneously with fresh decoder-noise draws per MC
iteration (encode once per batch, resample px per iteration).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Multi-resolution CRT: clusters genes via get_gene_groupings at each
requested resolution (plus an always-appended full-resolution single-gene
pass), then reruns the CRT with group-level knockoff substitution using a
soft additive mask (x*(1-mask) + x_tilde*mask), reusing
_encode_for_knockoffs/_sample_knockoffs/_crt_pvalue. Assembles per-gene
pval/padj/cluster_assignment across resolutions into an xarray.Dataset.

Fixes a brief bug found during TDD: torch.nn.functional.one_hot requires
an int64 tensor, but hierarchy.fcluster's output and the appended
np.arange(...).astype(np.int32) full-resolution grouping are int32 -
cast to .long() before one_hot.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rtance

The .min(axis=1) cross-feature aggregation used to build genes_to_plot
disagreed with the unchanged .all() reduction in is_cluster_detected,
which still reduced over the feature dim too -- requiring near-unanimous
significance across every response feature before a cluster counted as
detected. Add a feature: int | str = 0 parameter and slice gene_results
to a single feature (isel/sel) at the top of the function instead, matching
how the original JAX reference implementation was actually used. Removes
the .min(axis=1) hack entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docs/api/reference/ is gitignored and Sphinx-autosummary-generated for
every other model; VIVS's .rst was force-added in the prior commit,
making it the sole tracked file in that directory. Untrack it to match
convention -- the file stays on disk, just no longer force-committed.
Adds docs/tutorials/VIVS_niche_gene_selection.ipynb, demonstrating VIVS
paired with a pretrained scVIVA model (reused as VIVS's x_model knockoff
sampler) to test gene dependence on scVIVA's niche_composition. Loads the
same Xenium breast-cancer dataset as scVIVA_tutorial.ipynb (data-loading
cell copied verbatim). Notebook is written but not executed (no GPU here);
registered in docs/tutorials/index.md's toctree.
…n test

obs_t was averaged across responses to (batch_n, 1) while tilde_t_mean
kept the full (batch_n, n_responses_selected) shape, silently breaking
the intended `tilde_t_mean - obs_t` comparison whenever more than one
response is selected. Drop the mean reduction so obs_t matches
tilde_t_mean's shape, matching the JAX reference semantics.

Also add a save/load round-trip regression test for VIVS, which was
planned in the design doc but never landed in the implementation plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e033878ced

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/scviva/model/utils/_vivs_utils.py Outdated
Comment thread src/scviva/model/_vivs.py
Comment on lines +283 to +285
obs_all_loss = self.module.xy_module(self.module.xy_input(x, batch_index), y)[
"all_loss"
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move CRT input batches onto the model device

When the model has been transferred to CUDA, the AnnData loader still yields CPU tensors and this direct xy_module call is not covered by auto_move_data, so the first linear layer fails with a CPU/CUDA device mismatch. The same direct-call pattern affects predict_t, get_cell_scores, and hierarchical importance; move each batch to the module device before invoking xy_module and create accumulators on that device.

Useful? React with 👍 / 👎.

Comment thread src/scviva/model/_vivs.py
"`x_model` must already be trained (call `.train()` on it) before "
"being passed to VIVS."
)
x_module = x_model.module

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Adapt pretrained models before reusing their modules

When a documented DestVI or ResolVI instance is supplied as x_model, assigning its module directly does not provide the VAE interface that the VIVS wrapper assumes. DestVI's MRDeconv inference requires augmented expression and its generative method requires ind_x, while ResolVI's Pyro module does not expose the delegated VAE inference/generative API, so training or the first knockoff request fails. Restrict accepted models to compatible VAE modules or add model-specific adapters.

Useful? React with 👍 / 👎.

Comment thread src/scviva/model/_vivs.py Outdated
"xmax": xmax + 0.5,
"ymin": resolution_idx,
"ymax": resolution_idx + 1,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle plots with no significant genes

When no gene passes the threshold at base_resolution, plot_df is constructed from an empty list and consequently has no xmin, xmax, ymin, or ymax columns. The subsequent geom_rect mapping then fails instead of returning an empty plot, which is a normal outcome for a CRT result; initialize the expected columns or explicitly handle the no-discovery case.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant