Pasta Mouse#

Index#

  1. Instantiate model class

  2. Define clock metadata

  3. Download clock dependencies

  4. Load features

  5. Map to mouse orthologs

  6. Load weights into base model

  7. Load reference values

  8. Load preprocess and postprocess objects

  9. Check all clock parameters

  10. Basic test

  11. Save torch model

  12. Clear directory

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
[ ]: