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"  # Paper: Pasta is a multi-platform, multi-tissue transcriptomic aging clock.
model.metadata["species"] = "Mus musculus"  # Paper: Pasta was extended for application to mouse samples.
model.metadata["year"] = 2025
model.metadata["approved_by_author"] = "✅"
model.metadata["citation"] = "Salignon, J. et al. Pasta, a versatile transcriptomic clock, maps the chemical and genetic determinants of aging and rejuvenation. bioRxiv 2025.06.04.657785 (2025)."
model.metadata["doi"] = "https://doi.org/10.1101/2025.06.04.657785"
model.metadata["notes"] = "Mouse application of the human Pasta model after mapping one-to-one orthologues, rank transformation, and median imputation of missing model genes."
model.metadata["research_only"] = None
model.metadata["tissue"] = ["multi-tissue"]  # Paper: Mouse validation used diverse tissues and datasets.
model.metadata["predicts"] = ["transcriptomic age"]  # Paper: Mouse expression was mapped and the Pasta age model was applied.
model.metadata["training_target"] = ["age ordering"]  # Paper: The transferred model retains the human age-shift classifier.
model.metadata["unit"] = ["years"]  # Paper: Pasta classifier scores are converted into age differences.
model.metadata["model_type"] = "orthologue-transferred ridge logistic regression"  # Paper: Ridge-regularized generalized linear models used 10-fold cross-validation.
model.metadata["platform"] = ["RNA-seq", "gene expression microarray"]  # Paper: Training studies included bulk RNA-seq and microarray.
model.metadata["population"] = "human, age unspecified"  # Paper: Mouse genes were restricted to one-to-one human orthologues.
model.metadata["journal"] = "bioRxiv"
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
[ ]: