Pasta Mouse#
Index#
Let’s first import some packages:
[1]:
import os
import inspect
import shutil
import json
import subprocess
import torch
import pandas as pd
import pyaging as pya
Instantiate model class#
[2]:
def print_entire_class(cls):
source = inspect.getsource(cls)
print(source)
print_entire_class(pya.models.PastaMouse)
class PastaMouse(Pasta):
def __init__(self):
super().__init__()
self.base_model_features = None
self.mouse_feature_indices = None
self.full_reference_values = None
def set_mouse_features(self, full_features, full_reference_values=None, mouse_prefix="ENSMUSG"):
"""
Configure the mouse-only interface while keeping the full feature space for the base model.
"""
self.base_model_features = list(full_features)
self.full_reference_values = full_reference_values
self.mouse_feature_indices = [
i
for i, feature in enumerate(self.base_model_features)
if isinstance(feature, str) and feature.startswith(mouse_prefix)
]
if len(self.mouse_feature_indices) == 0:
raise ValueError("No mouse features were identified when configuring PastaMouse.")
self.features = [self.base_model_features[i] for i in self.mouse_feature_indices]
if self.full_reference_values is None:
self.reference_values = None
elif isinstance(self.full_reference_values, torch.Tensor):
self.reference_values = self.full_reference_values[self.mouse_feature_indices].detach().clone()
else:
self.reference_values = [self.full_reference_values[i] for i in self.mouse_feature_indices]
def _expand_with_reference(self, x):
"""
Reconstruct the full 8113-length input expected by the base model by
inserting reference values for human-only genes.
"""
if self.base_model_features is None or self.mouse_feature_indices is None:
raise ValueError("PastaMouse must be configured with set_mouse_features before inference.")
if self.full_reference_values is None:
ref_full = torch.zeros(len(self.base_model_features), device=x.device, dtype=x.dtype)
elif isinstance(self.full_reference_values, torch.Tensor):
ref_full = self.full_reference_values.to(device=x.device, dtype=x.dtype)
else:
ref_full = torch.tensor(self.full_reference_values, device=x.device, dtype=x.dtype)
full_x = ref_full.unsqueeze(0).repeat(x.size(0), 1)
full_x[:, self.mouse_feature_indices] = x
return full_x
def forward(self, x):
# Build the full feature vector (mouse data + human reference values) before preprocessing.
x_full = self._expand_with_reference(x)
x_full = self.preprocess(x_full)
x_full = self.base_model(x_full)
x_full = self.postprocess(x_full)
return x_full
[3]:
model = pya.models.PastaMouse()
Define clock metadata#
[4]:
model.metadata["clock_name"] = 'pastamouse'
model.metadata["data_type"] = 'transcriptomics'
model.metadata["species"] = 'Mus musculus'
model.metadata["year"] = 2025
model.metadata["approved_by_author"] = '✅'
model.metadata["citation"] = 'Salignon, Jerome, et al. "Pasta, an age-shift transcriptomic clock, maps the chemical and genetic determinants of aging and rejuvenation." bioRxiv (2025): 2025-06.'
model.metadata["doi"] = "https://doi.org/10.1101/2025.06.04.657785"
model.metadata["research_only"] = None
model.metadata["notes"] = "Mouse implementation of the age-shift transcriptomic clock, predicting relative cellular age from rank-transformed expression restricted to mouse one-to-one orthologs of the human age-associated genes."
model.metadata["tissue"] = 'Multi-tissue (trained on paired samples from same tissue/study, human transcriptomic data across studies; mouse-ortholog variant applies same model to mouse tissues)'
model.metadata["predicts"] = 'Relative age-shift (biological age difference between paired samples), not absolute chronological age'
model.metadata["unit"] = 'years'
model.metadata["model_type"] = 'Ridge regression'
model.metadata["platform"] = 'RNA-seq'
model.metadata["population"] = 'Primarily human (pan-tissue, adult), with a mouse-ortholog adaptation (pastamouse) enabling application to mouse transcriptomic data'
model.metadata["journal"] = 'bioRxiv (Cold Spring Harbor Laboratory)'
model.metadata["last_author"] = 'Christian G. Riedel'
model.metadata["n_features"] = 1600
model.metadata["citations"] = 1
model.metadata["citations_date"] = '2026-07-05'
Download clock dependencies#
Download coefficient file#
[5]:
coeff_url = "https://raw.githubusercontent.com/bio-learn/biolearn/master/biolearn/data/Pasta.csv"
os.system(f"curl -L {coeff_url} -o Pasta.csv")
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 322k 100 322k 0 0 1477k 0 --:--:-- --:--:-- --:--:-- 1478k
[5]:
0
Download ortholog mapping#
[6]:
ortholog_url = "https://raw.githubusercontent.com/jsalignon/pasta/main/data/v_human_mouse_one2one.rda"
os.system(f"curl -L {ortholog_url} -o v_human_mouse_one2one.rda")
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 24796 100 24796 0 0 129k 0 --:--:-- --:--:-- --:--:-- 129k
[6]:
0
Load features#
From CSV file#
[7]:
coeffs = pd.read_csv('Pasta.csv')
coeffs['feature'] = coeffs['GeneID']
coeffs['coefficient'] = coeffs['CoefficientTraining']
model.features = coeffs['feature'].tolist()
Map to mouse orthologs#
[8]:
r_cmd = (
"load('v_human_mouse_one2one.rda'); "
"df <- data.frame(mouse=names(v_human_mouse_one2one), human=as.character(v_human_mouse_one2one)); "
"write.csv(df, 'v_human_mouse_one2one.csv', row.names=FALSE)"
)
os.system(f"Rscript -e \"{r_cmd}\"")
ortholog_df = pd.read_csv('v_human_mouse_one2one.csv')
human_to_mouse = dict(zip(ortholog_df['human'], ortholog_df['mouse']))
mapped_features = [human_to_mouse.get(gene, gene) for gene in model.features]
mapped_count = sum(gene in human_to_mouse for gene in model.features)
print(f"Mapped {mapped_count} of {len(model.features)} features to mouse orthologs.")
model.features = mapped_features
Mapped 1600 of 8113 features to mouse orthologs.
[9]:
import numpy as np
len(np.intersect1d(list(human_to_mouse.keys()), list(coeffs['feature'])))
[9]:
1600
[10]:
len(np.unique(list(coeffs['feature'])))
[10]:
8113
Load weights into base model#
From CSV file#
[11]:
weights = torch.tensor(coeffs['coefficient'].tolist()).unsqueeze(0)
intercept = torch.tensor([0.0])
Linear model#
[12]:
base_model = pya.models.LinearModel(input_dim=len(model.features))
base_model.linear.weight.data = weights.float()
base_model.linear.bias.data = intercept.float()
model.base_model = base_model
Load reference values#
[13]:
full_features = list(model.features)
full_reference_values = [float('nan')] * len(full_features)
model.reference_values = full_reference_values
model.set_mouse_features(full_features, full_reference_values)
Load preprocess and postprocess objects#
[14]:
model.preprocess_name = "median_fill_and_rank_normalization"
model.preprocess_dependencies = None
[15]:
model.postprocess_name = "scale_and_shift"
model.postprocess_dependencies = [-4.76348378687217, -0.0502893445253186]
Check all clock parameters#
[16]:
pya.utils.print_model_details(model)
%==================================== Model Details ====================================%
Model Attributes:
training: True
metadata: {'approved_by_author': '✅',
'citation': 'Salignon, Jerome, et al. "Pasta, an age-shift transcriptomic '
'clock, maps the chemical and genetic determinants of aging and '
'rejuvenation." bioRxiv (2025): 2025-06.',
'clock_name': 'pastamouse',
'data_type': 'transcriptomics',
'doi': 'https://doi.org/10.1101/2025.06.04.657785',
'notes': 'Rank-normalized Pasta clock using mouse one-to-one ortholog genes '
'when available.',
'research_only': None,
'species': 'Mus musculus',
'version': None,
'year': 2025}
reference_values: [nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan]... [Total elements: 1600]
preprocess_name: 'median_fill_and_rank_normalization'
preprocess_dependencies: None
postprocess_name: 'scale_and_shift'
postprocess_dependencies: [-4.76348378687217, -0.0502893445253186]
features: ['ENSMUSG00000017307', 'ENSMUSG00000064289', 'ENSMUSG00000032783', 'ENSMUSG00000039047', 'ENSMUSG00000043448', 'ENSMUSG00000052997', 'ENSMUSG00000052833', 'ENSMUSG00000000244', 'ENSMUSG00000024566', 'ENSMUSG00000006304', 'ENSMUSG00000070733', 'ENSMUSG00000020572', 'ENSMUSG00000022634', 'ENSMUSG00000024785', 'ENSMUSG00000024873', 'ENSMUSG00000022607', 'ENSMUSG00000058407', 'ENSMUSG00000097485', 'ENSMUSG00000030521', 'ENSMUSG00000052593', 'ENSMUSG00000028969', 'ENSMUSG00000031843', 'ENSMUSG00000038481', 'ENSMUSG00000022323', 'ENSMUSG00000009555', 'ENSMUSG00000078154', 'ENSMUSG00000022816', 'ENSMUSG00000021963', 'ENSMUSG00000025024', 'ENSMUSG00000006941']... [Total elements: 1600]
base_model_features: ['ENSG00000196839', 'ENSG00000170558', 'ENSG00000133997', 'ENSG00000168060', 'ENSMUSG00000017307', 'ENSG00000136754', 'ENSG00000113552', 'ENSG00000177485', 'ENSMUSG00000064289', 'ENSG00000094631', 'ENSG00000108840', 'ENSG00000170248', 'ENSG00000153094', 'ENSG00000159921', 'ENSG00000165879', 'ENSMUSG00000032783', 'ENSMUSG00000039047', 'ENSG00000179776', 'ENSG00000167670', 'ENSG00000129484', 'ENSG00000041880', 'ENSG00000113361', 'ENSG00000141198', 'ENSG00000100284', 'ENSG00000013619', 'ENSG00000010017', 'ENSG00000105993', 'ENSG00000113810', 'ENSMUSG00000043448', 'ENSMUSG00000052997']... [Total elements: 8113]
mouse_feature_indices: [4, 8, 15, 16, 28, 29, 30, 42, 44, 63, 77, 82, 83, 105, 107, 111, 112, 117, 118, 119, 120, 121, 147, 153, 155, 164, 171, 179, 180, 183]... [Total elements: 1600]
full_reference_values: [nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan]... [Total elements: 8113]
%==================================== Model Details ====================================%
Model Structure:
base_model: LinearModel(
(linear): Linear(in_features=8113, out_features=1, bias=True)
)
%==================================== Model Details ====================================%
Model Parameters and Weights:
base_model.linear.weight: [-2.4399256290053017e-05, -1.774273368937429e-05, 1.554851587570738e-05, 1.1031659596483223e-05, 1.6993128156173043e-05, 3.9308954001171514e-05, -0.00012627331307157874, 2.8949250463483622e-06, -6.271281017689034e-05, 2.9893646569689736e-05, 3.6174697015667334e-05, 6.864466558909044e-05, -2.3814825908630155e-05, 3.11008479911834e-05, 1.0880126865231432e-05, 9.605172635929193e-06, 1.1990639904979616e-05, 9.29949510464212e-06, 6.331568147288635e-05, -3.362866482348181e-05, -0.00022874546993989497, -2.7509766368893906e-05, 6.674586074950639e-06, 1.986255301744677e-05, -3.5506527638062835e-05, 2.922421663242858e-05, -4.5067787141306326e-05, 5.991863872623071e-05, 3.728850060724653e-05, 4.235586311551742e-05]... [Tensor of shape torch.Size([1, 8113])]
base_model.linear.bias: tensor([0.])
%==================================== Model Details ====================================%
Basic test#
[17]:
torch.manual_seed(42)
input = torch.randn(10, len(model.features), dtype=float)
model.eval()
model.to(float)
pred = model(input)
pred
[17]:
tensor([[ 4.9918],
[ -8.4652],
[ 15.1181],
[-31.3271],
[ 27.5393],
[-10.9938],
[ -3.4235],
[-14.1880],
[-24.5564],
[ -7.9826]], dtype=torch.float64, grad_fn=<AddBackward0>)
Save torch model#
[18]:
torch.save(model, f"../weights/{model.metadata['clock_name']}.pt")
Clear directory#
[19]:
# Function to remove a folder and all its contents
def remove_folder(path):
try:
shutil.rmtree(path)
print(f"Deleted folder: {path}")
except Exception as e:
print(f"Error deleting folder {path}: {e}")
# Get a list of all files and folders in the current directory
all_items = os.listdir('.')
# Loop through the items
for item in all_items:
# Check if it's a file and does not end with .ipynb
if os.path.isfile(item) and not item.endswith('.ipynb'):
os.remove(item)
print(f"Deleted file: {item}")
# Check if it's a folder
elif os.path.isdir(item):
remove_folder(item)
Deleted file: v_human_mouse_one2one.rda
Deleted file: Pasta.csv
Deleted file: v_human_mouse_one2one.csv
[ ]: