scFM-dataselection / tools /_native_scimilarity.py
lcrawford's picture
Copilot
Add study-trained SCimilarity checkpoints
e697bd2 verified
Raw History Blame Contribute Delete
2.72 kB
"""Load the study's native SCimilarity encoder and decoder with PyTorch."""
import json
from pathlib import Path
import torch
from _scimilarity_networks import Decoder, Encoder
class SCimilarity(torch.nn.Module):
def __init__(self, gene_order, hidden_dim, latent_dim):
super().__init__()
self.gene_order = tuple(gene_order)
self.n_genes = len(self.gene_order)
self.latent_dim = latent_dim
self.encoder = Encoder(self.n_genes, latent_dim=latent_dim, hidden_dim=hidden_dim)
self.decoder = Decoder(self.n_genes, latent_dim=latent_dim, hidden_dim=hidden_dim)
def forward(self, expression):
embedding = self.encoder(expression)
return embedding, self.decoder(embedding)
def _load_state(path):
checkpoint = torch.load(path, map_location="cpu", weights_only=True)
if not isinstance(checkpoint, dict) or set(checkpoint) != {"state_dict"}:
raise ValueError(f"Expected only a state_dict in {path}")
state = checkpoint["state_dict"]
if not isinstance(state, dict) or not state:
raise ValueError(f"Missing model tensors in {path}")
if any(not isinstance(value, torch.Tensor) or not torch.isfinite(value).all()
for value in state.values()):
raise ValueError(f"Non-tensor or nonfinite model state in {path}")
return state
def load_scimilarity(model_directory):
directory = Path(model_directory)
gene_order = [
line.strip() for line in (directory / "gene_order.tsv").read_text().splitlines()
]
if not gene_order or any(not gene for gene in gene_order) or len(set(gene_order)) != len(gene_order):
raise ValueError("SCimilarity gene order must contain unique, nonempty gene symbols")
encoder_state = _load_state(directory / "encoder.ckpt")
layer_sizes = json.loads((directory / "layer_sizes.json").read_text())
if {name: list(value.shape) for name, value in encoder_state.items()} != layer_sizes:
raise ValueError("SCimilarity encoder tensors disagree with layer_sizes.json")
layers = [
shape for name, shape in sorted(
layer_sizes.items(), key=lambda item: int(item[0].split(".")[1])
)
if name.endswith(".weight") and len(shape) == 2
]
if len(layers) < 2 or layers[0][1] != len(gene_order):
raise ValueError("SCimilarity network dimensions disagree with the gene order")
model = SCimilarity(
gene_order, hidden_dim=[shape[0] for shape in layers[:-1]],
latent_dim=layers[-1][0],
)
model.encoder.load_state_dict(encoder_state, strict=True)
model.decoder.load_state_dict(_load_state(directory / "decoder.ckpt"), strict=True)
model.eval()
return model