pyaging 0.5.3__tar.gz → 0.5.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyaging-0.5.3 → pyaging-0.5.4}/.gitignore +5 -1
- {pyaging-0.5.3 → pyaging-0.5.4}/PKG-INFO +2 -2
- {pyaging-0.5.3 → pyaging-0.5.4}/README.md +1 -1
- {pyaging-0.5.3 → pyaging-0.5.4}/pyproject.toml +1 -1
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/__init__.py +1 -1
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/data/feature_ranges.json +1 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/logger/_logger.py +1 -1
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/models/__init__.py +29 -0
- pyaging-0.5.4/src/pyaging/models/_author_extensions.py +28 -0
- pyaging-0.5.4/src/pyaging/models/_organage.py +33 -0
- pyaging-0.5.4/src/pyaging/models/_pc_extensions.py +67 -0
- pyaging-0.5.4/src/pyaging/models/_replication.py +88 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/predict/_pred.py +0 -10
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/predict/_pred_utils.py +1 -23
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/preprocess/_preprocess.py +0 -9
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/utils/_hf.py +4 -5
- {pyaging-0.5.3 → pyaging-0.5.4}/LICENSE +0 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/data/__init__.py +0 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/data/_data.py +0 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/logger/__init__.py +0 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/logger/_live.py +0 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/models/_base_models.py +0 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/models/_models.py +0 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/predict/__init__.py +0 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/predict/_cache.py +0 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/predict/_transforms.py +0 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/preprocess/__init__.py +0 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/preprocess/_preprocess_utils.py +0 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/preprocess/_tage.py +0 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/py.typed +0 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/utils/__init__.py +0 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/utils/_feature_ranges.py +0 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/utils/_files.py +0 -0
- {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/utils/_utils.py +0 -0
|
@@ -10,6 +10,10 @@ __pycache__/
|
|
|
10
10
|
# Tool caches
|
|
11
11
|
.pytest_cache/
|
|
12
12
|
.ruff_cache/
|
|
13
|
+
.coverage
|
|
14
|
+
.coverage.*
|
|
15
|
+
coverage.xml
|
|
16
|
+
htmlcov/
|
|
13
17
|
|
|
14
18
|
# Packaging artifacts
|
|
15
19
|
build/
|
|
@@ -35,7 +39,7 @@ example_data/
|
|
|
35
39
|
|
|
36
40
|
# Generated clock weights and aggregate metadata (published to Hugging Face)
|
|
37
41
|
clocks/weights/
|
|
38
|
-
clocks/metadata
|
|
42
|
+
clocks/metadata/*.pt
|
|
39
43
|
clocks/blood_chemistry_example.pkl
|
|
40
44
|
clocks/blood_chemistry_example.csv
|
|
41
45
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyaging
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: A Python-based compendium of GPU-optimized aging clocks.
|
|
5
5
|
Project-URL: homepage, https://github.com/lucascamillomd/pyaging
|
|
6
6
|
Project-URL: repository, https://github.com/lucascamillomd/pyaging
|
|
@@ -81,7 +81,7 @@ pya.pred.predict_age(adata, ["Horvath2013", "AltumAge", "DunedinPACE"])
|
|
|
81
81
|
adata.obs.head()
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
Clock weights are downloaded on demand from per-clock repositories under the [`pyaging` Hugging Face organization](https://huggingface.co/pyaging) (example data comes from [`lucascamillomd/pyaging-data`](https://huggingface.co/lucascamillomd/pyaging-data)). Set `PYAGING_DATA_REVISION` to an existing data release tag, such as `v0.5.
|
|
84
|
+
Clock weights are downloaded on demand from per-clock repositories under the [`pyaging` Hugging Face organization](https://huggingface.co/pyaging) (example data comes from [`lucascamillomd/pyaging-data`](https://huggingface.co/lucascamillomd/pyaging-data)). Set `PYAGING_DATA_REVISION` to an existing data release tag, such as `v0.5.4`, to pin downloads; it defaults to `main`. Version 0.5.4 adds CellDRIFT, MiAge, PCBrainAge, IntrinClock370, PhenoAge partitions, standalone PCGrimAge proxies, and 90 proteomic OrganAge outputs. See the [input and output guide](https://pyaging.readthedocs.io/en/latest/clocks_0_5_4.html). Model weights retain the original authors’ licensing terms.
|
|
85
85
|
|
|
86
86
|
### Run the same clocks across datasets
|
|
87
87
|
|
|
@@ -48,7 +48,7 @@ pya.pred.predict_age(adata, ["Horvath2013", "AltumAge", "DunedinPACE"])
|
|
|
48
48
|
adata.obs.head()
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
Clock weights are downloaded on demand from per-clock repositories under the [`pyaging` Hugging Face organization](https://huggingface.co/pyaging) (example data comes from [`lucascamillomd/pyaging-data`](https://huggingface.co/lucascamillomd/pyaging-data)). Set `PYAGING_DATA_REVISION` to an existing data release tag, such as `v0.5.
|
|
51
|
+
Clock weights are downloaded on demand from per-clock repositories under the [`pyaging` Hugging Face organization](https://huggingface.co/pyaging) (example data comes from [`lucascamillomd/pyaging-data`](https://huggingface.co/lucascamillomd/pyaging-data)). Set `PYAGING_DATA_REVISION` to an existing data release tag, such as `v0.5.4`, to pin downloads; it defaults to `main`. Version 0.5.4 adds CellDRIFT, MiAge, PCBrainAge, IntrinClock370, PhenoAge partitions, standalone PCGrimAge proxies, and 90 proteomic OrganAge outputs. See the [input and output guide](https://pyaging.readthedocs.io/en/latest/clocks_0_5_4.html). Model weights retain the original authors’ licensing terms.
|
|
52
52
|
|
|
53
53
|
### Run the same clocks across datasets
|
|
54
54
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"modality_defaults": {
|
|
4
4
|
"DNA methylation": {"unit": "beta value", "low": 0.0, "high": 1.0},
|
|
5
|
+
"proteomics": {"unit": "NPX", "low": null, "high": null},
|
|
5
6
|
"transcriptomics": {"unit": null, "low": 0.0, "high": null},
|
|
6
7
|
"transcriptomics (relative)": {"unit": null, "low": null, "high": null},
|
|
7
8
|
"histone modification": {"unit": null, "low": 0.0, "high": null},
|
|
@@ -46,7 +46,7 @@ def format_logging_message(msg, logging_level, indent_level=1, indent_space_num=
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
class Logger:
|
|
49
|
-
"""Logger
|
|
49
|
+
"""Logger for pyaging's text output and file logging."""
|
|
50
50
|
|
|
51
51
|
FORMAT = "%(message)s"
|
|
52
52
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# pyaging/models/__init__.py
|
|
2
2
|
|
|
3
|
+
from ._author_extensions import DNAmPhenoAgeNonPRC, DNAmPhenoAgePRC, IntrinClock370
|
|
3
4
|
from ._base_models import (
|
|
4
5
|
AltumAgeNeuralNetwork,
|
|
5
6
|
LinearModel,
|
|
@@ -173,8 +174,36 @@ from ._models import (
|
|
|
173
174
|
epiTOC2,
|
|
174
175
|
stemTOC,
|
|
175
176
|
)
|
|
177
|
+
from ._organage import OrganAge
|
|
178
|
+
from ._pc_extensions import (
|
|
179
|
+
PCBrainAge,
|
|
180
|
+
PCGrimAgeADM,
|
|
181
|
+
PCGrimAgeB2M,
|
|
182
|
+
PCGrimAgeCystatinC,
|
|
183
|
+
PCGrimAgeGDF15,
|
|
184
|
+
PCGrimAgeLeptin,
|
|
185
|
+
PCGrimAgePackYrs,
|
|
186
|
+
PCGrimAgePAI1,
|
|
187
|
+
PCGrimAgeTIMP1,
|
|
188
|
+
)
|
|
189
|
+
from ._replication import CellDRIFT, MiAge
|
|
176
190
|
|
|
177
191
|
__all__ = [
|
|
192
|
+
"CellDRIFT",
|
|
193
|
+
"DNAmPhenoAgeNonPRC",
|
|
194
|
+
"DNAmPhenoAgePRC",
|
|
195
|
+
"IntrinClock370",
|
|
196
|
+
"MiAge",
|
|
197
|
+
"OrganAge",
|
|
198
|
+
"PCBrainAge",
|
|
199
|
+
"PCGrimAgeADM",
|
|
200
|
+
"PCGrimAgeB2M",
|
|
201
|
+
"PCGrimAgeCystatinC",
|
|
202
|
+
"PCGrimAgeGDF15",
|
|
203
|
+
"PCGrimAgeLeptin",
|
|
204
|
+
"PCGrimAgePackYrs",
|
|
205
|
+
"PCGrimAgePAI1",
|
|
206
|
+
"PCGrimAgeTIMP1",
|
|
178
207
|
"ABEC",
|
|
179
208
|
"ADBahadoSingh",
|
|
180
209
|
"AltumAge",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Author-defined IntrinClock and PhenoAge variants."""
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
|
|
5
|
+
from ._base_models import pyagingModel
|
|
6
|
+
from ._models import IntrinClock
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IntrinClock370(IntrinClock):
|
|
10
|
+
"""The original cv.glmnet model at lambda.1se, as used by the author demo."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DNAmPhenoAgePRC(pyagingModel):
|
|
14
|
+
"""Polycomb-related PhenoAge contribution, with no age intercept.
|
|
15
|
+
|
|
16
|
+
Matches the author's ``calcPRCPhenoAge(..., imputation = FALSE)``:
|
|
17
|
+
missing CpGs and supplied NA beta values make zero contribution.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def preprocess(self, x):
|
|
21
|
+
return torch.where(torch.isnan(x), torch.zeros_like(x), x)
|
|
22
|
+
|
|
23
|
+
def postprocess(self, x):
|
|
24
|
+
return x
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DNAmPhenoAgeNonPRC(DNAmPhenoAgePRC):
|
|
28
|
+
"""Complementary non-polycomb PhenoAge contribution, without intercept."""
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Goeminne et al. plasma-protein OrganAge models.
|
|
2
|
+
|
|
3
|
+
Original author code and coefficients:
|
|
4
|
+
https://github.com/ludgergoeminne/organAging
|
|
5
|
+
DOI: 10.1016/j.cmet.2024.10.005
|
|
6
|
+
|
|
7
|
+
The released coefficients are subject to the authors' Academic / Non-Commercial
|
|
8
|
+
License. Each converted artifact carries that license and its pinned provenance.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from ._base_models import pyagingModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OrganAge(pyagingModel):
|
|
15
|
+
"""Score Olink NPX values with a released OrganAge coefficient vector.
|
|
16
|
+
|
|
17
|
+
Protein names are the authors' case-sensitive symbols, including hyphens.
|
|
18
|
+
NPX values are already normalized; no cohort scaling is applied here. The
|
|
19
|
+
authors recommend fold one for new data. Chronological models include an
|
|
20
|
+
intercept and return years; mortality models have no intercept and return
|
|
21
|
+
relative natural-log mortality hazards.
|
|
22
|
+
|
|
23
|
+
The authors' example omits absent protein columns and propagates NaN values
|
|
24
|
+
in present columns. Zero-filled absent features preserve that omission in
|
|
25
|
+
pyaging, while present NaNs remain NaNs. Any imputation performed before
|
|
26
|
+
scoring is the caller's separate data-preparation decision.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def preprocess(self, x):
|
|
30
|
+
return x
|
|
31
|
+
|
|
32
|
+
def postprocess(self, x):
|
|
33
|
+
return x
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""PC clocks evaluated with algebraically composed PCA/regression weights.
|
|
2
|
+
|
|
3
|
+
The builders preserve the original authors' double-precision coefficients.
|
|
4
|
+
Composing rotation and regression once avoids storing the large PCA rotation
|
|
5
|
+
again for each standalone PCGrimAge proxy. This does not retrain the models.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import torch
|
|
9
|
+
|
|
10
|
+
from ._models import LinearReferenceClock
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PCBrainAge(LinearReferenceClock):
|
|
14
|
+
"""Thrush et al. brain chronological-age predictor, in years."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _PCGrimAgeProxy(LinearReferenceClock):
|
|
18
|
+
"""Keep the selected original demographic covariates mandatory."""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
super().__init__()
|
|
22
|
+
self.required_covariates = []
|
|
23
|
+
self.register_buffer("required_covariate_indices", torch.empty(0, dtype=torch.long))
|
|
24
|
+
|
|
25
|
+
def preprocess(self, x):
|
|
26
|
+
selected = x[:, self.required_covariate_indices]
|
|
27
|
+
invalid = ~torch.isfinite(selected)
|
|
28
|
+
if invalid.any():
|
|
29
|
+
bad_columns = invalid.any(dim=0).nonzero().flatten().tolist()
|
|
30
|
+
names = ", ".join(self.required_covariates[i] for i in bad_columns)
|
|
31
|
+
raise ValueError(
|
|
32
|
+
f"{self.metadata.get('clock_name') or type(self).__name__} requires "
|
|
33
|
+
f"supplied finite demographic inputs for every sample: {names}."
|
|
34
|
+
)
|
|
35
|
+
return super().preprocess(x)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PCGrimAgePackYrs(_PCGrimAgeProxy):
|
|
39
|
+
"""Higgins-Chen et al. PC-based DNAm smoking pack-years proxy."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PCGrimAgeADM(_PCGrimAgeProxy):
|
|
43
|
+
"""Higgins-Chen et al. PC-based DNAm adrenomedullin proxy."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PCGrimAgeB2M(_PCGrimAgeProxy):
|
|
47
|
+
"""Higgins-Chen et al. PC-based DNAm beta-2-microglobulin proxy."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PCGrimAgeCystatinC(_PCGrimAgeProxy):
|
|
51
|
+
"""Higgins-Chen et al. PC-based DNAm cystatin C proxy."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PCGrimAgeGDF15(_PCGrimAgeProxy):
|
|
55
|
+
"""Higgins-Chen et al. PC-based DNAm GDF15 proxy."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PCGrimAgeLeptin(_PCGrimAgeProxy):
|
|
59
|
+
"""Higgins-Chen et al. PC-based DNAm leptin proxy."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PCGrimAgePAI1(_PCGrimAgeProxy):
|
|
63
|
+
"""Higgins-Chen et al. PC-based DNAm PAI-1 proxy."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PCGrimAgeTIMP1(_PCGrimAgeProxy):
|
|
67
|
+
"""Higgins-Chen et al. PC-based DNAm TIMP-1 proxy."""
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""DNA-methylation clocks of cellular replication from the authors' releases."""
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
|
|
5
|
+
from ._base_models import pyagingModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CellDRIFT(pyagingModel):
|
|
9
|
+
"""CellDRIFT with its two PCA projections folded into the linear weights.
|
|
10
|
+
|
|
11
|
+
The original authors set supplied NaNs to zero. Entirely absent CpGs use
|
|
12
|
+
the released module means, carried in ``reference_values`` for alignment.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def preprocess(self, x):
|
|
16
|
+
return torch.where(torch.isnan(x), torch.zeros_like(x), x)
|
|
17
|
+
|
|
18
|
+
def postprocess(self, x):
|
|
19
|
+
return x
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MiAge(pyagingModel):
|
|
23
|
+
"""The authors' five-start bounded nonlinear MiAge least-squares fit.
|
|
24
|
+
|
|
25
|
+
L-BFGS-B is evaluated on CPU in float64 and its result returns to the input
|
|
26
|
+
tensor's dtype/device. Missing beta values are omitted from the objective,
|
|
27
|
+
exactly as ``na.rm=TRUE`` in the original R functions. An all-NaN sample
|
|
28
|
+
therefore returns the first tied start (2008), which is not a valid
|
|
29
|
+
biological estimate; callers should ensure measured CpGs are available.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.register_buffer("b", torch.empty(0, dtype=torch.float64))
|
|
35
|
+
self.register_buffer("c", torch.empty(0, dtype=torch.float64))
|
|
36
|
+
self.register_buffer("d", torch.empty(0, dtype=torch.float64))
|
|
37
|
+
|
|
38
|
+
def set_parameters(self, b, c, d):
|
|
39
|
+
self.b = torch.tensor(b, dtype=torch.float64)
|
|
40
|
+
self.c = torch.tensor(c, dtype=torch.float64)
|
|
41
|
+
self.d = torch.tensor(d, dtype=torch.float64)
|
|
42
|
+
|
|
43
|
+
def preprocess(self, x):
|
|
44
|
+
return x
|
|
45
|
+
|
|
46
|
+
def postprocess(self, x):
|
|
47
|
+
return x
|
|
48
|
+
|
|
49
|
+
def forward(self, x):
|
|
50
|
+
import numpy as np
|
|
51
|
+
from scipy.optimize import fmin_l_bfgs_b
|
|
52
|
+
|
|
53
|
+
values = x.detach().to(device="cpu", dtype=torch.float64).numpy()
|
|
54
|
+
if np.isinf(values).any():
|
|
55
|
+
raise ValueError("MiAge beta values must be finite or NaN.")
|
|
56
|
+
b, c, d = (value.detach().to(device="cpu", dtype=torch.float64).numpy() for value in (self.b, self.c, self.d))
|
|
57
|
+
log_b = np.log(b)
|
|
58
|
+
result = np.empty((len(values), 1), dtype=np.float64)
|
|
59
|
+
for index, row in enumerate(values):
|
|
60
|
+
supplied = ~np.isnan(row)
|
|
61
|
+
observed, local_c, local_d = row[supplied], c[supplied], d[supplied]
|
|
62
|
+
local_b, local_log = b[supplied], log_b[supplied]
|
|
63
|
+
|
|
64
|
+
def objective(n, local_b=local_b, local_c=local_c, local_d=local_d, observed=observed, local_log=local_log):
|
|
65
|
+
power = np.power(local_b, n[0] - 1)
|
|
66
|
+
residual = local_c + power * local_d - observed
|
|
67
|
+
value = np.sum(residual * residual)
|
|
68
|
+
gradient = 2 * np.sum(residual * power * local_log * local_d)
|
|
69
|
+
return value, np.array([gradient])
|
|
70
|
+
|
|
71
|
+
best_value, best_n = np.inf, 2008.0
|
|
72
|
+
# R optim defaults: lmm=5, pgtol=0, maxit=100; author factr=1.
|
|
73
|
+
for start in (2008.0, 4006.0, 6004.0, 8002.0, 500.0):
|
|
74
|
+
n, value, _ = fmin_l_bfgs_b(
|
|
75
|
+
objective,
|
|
76
|
+
np.array([start]),
|
|
77
|
+
bounds=[(10.0, 10000.0)],
|
|
78
|
+
m=5,
|
|
79
|
+
factr=1.0,
|
|
80
|
+
pgtol=0.0,
|
|
81
|
+
maxiter=100,
|
|
82
|
+
)
|
|
83
|
+
if value < best_value:
|
|
84
|
+
best_value, best_n = value, n[0]
|
|
85
|
+
if not np.isfinite(best_value):
|
|
86
|
+
raise ValueError("MiAge could not obtain a finite least-squares objective for this sample.")
|
|
87
|
+
result[index, 0] = best_n
|
|
88
|
+
return torch.as_tensor(result, dtype=x.dtype, device=x.device)
|
|
@@ -114,12 +114,10 @@ def predict_age(
|
|
|
114
114
|
if adata.n_obs == 0:
|
|
115
115
|
raise ValueError("Prediction requires at least one sample.")
|
|
116
116
|
|
|
117
|
-
# Ensure clock_names is a list with lowercase names
|
|
118
117
|
if isinstance(clock_names, str):
|
|
119
118
|
clock_names = [clock_names]
|
|
120
119
|
clock_names = [clock_name.lower() for clock_name in clock_names]
|
|
121
120
|
|
|
122
|
-
# Set device for PyTorch operations
|
|
123
121
|
device = set_torch_device()
|
|
124
122
|
|
|
125
123
|
enabled = display_enabled(verbose)
|
|
@@ -131,10 +129,8 @@ def predict_age(
|
|
|
131
129
|
with quiet_hf_bars(verbose), display:
|
|
132
130
|
for clock_name in clock_names:
|
|
133
131
|
display.start_clock(clock_name, "loading weights")
|
|
134
|
-
# Pipeline warnings surface on the display
|
|
135
132
|
pipeline_logger = DisplayLogger(lambda m, name=clock_name: display.warn(name, m))
|
|
136
133
|
|
|
137
|
-
# Load and prepare the clock
|
|
138
134
|
if clock_cache is None:
|
|
139
135
|
model = load_clock(clock_name, device, dir, pipeline_logger)
|
|
140
136
|
else:
|
|
@@ -154,11 +150,9 @@ def predict_age(
|
|
|
154
150
|
f"Clock '{clock_name}' needs preprocessed input (adata.uns['{required_flag}'] is missing)."
|
|
155
151
|
)
|
|
156
152
|
|
|
157
|
-
# Disclaimer for commercial clocks
|
|
158
153
|
if model.metadata.get("research_only", False):
|
|
159
154
|
display.warn(clock_name, "research use only")
|
|
160
155
|
|
|
161
|
-
# Check and update adata for missing features
|
|
162
156
|
if transform_name is not None:
|
|
163
157
|
# Cohort-relative clock: its features live in the space the
|
|
164
158
|
# transform produces, not in adata.X.
|
|
@@ -171,11 +165,9 @@ def predict_age(
|
|
|
171
165
|
display.stage(clock_name, "matching features")
|
|
172
166
|
check_features_in_adata(adata, model, pipeline_logger)
|
|
173
167
|
|
|
174
|
-
# Warn if the input values look implausible for these features
|
|
175
168
|
display.stage(clock_name, "checking feature ranges")
|
|
176
169
|
check_feature_ranges(adata, model, pipeline_logger)
|
|
177
170
|
|
|
178
|
-
# Perform age prediction applying preprocessing and postprocessing steps
|
|
179
171
|
display.stage(clock_name, "predicting")
|
|
180
172
|
|
|
181
173
|
def progress_callback(completed, total, name=clock_name):
|
|
@@ -185,11 +177,9 @@ def predict_age(
|
|
|
185
177
|
adata, model, device, batch_size, pipeline_logger, progress_callback=progress_callback
|
|
186
178
|
)
|
|
187
179
|
|
|
188
|
-
# Add predicted ages and clock metadata to adata
|
|
189
180
|
display.stage(clock_name, "writing results")
|
|
190
181
|
add_pred_ages_and_clock_metadata_adata(adata, model, predicted_ages_tensor, dir, pipeline_logger)
|
|
191
182
|
|
|
192
|
-
# Delete the clock matrix object
|
|
193
183
|
if clean:
|
|
194
184
|
del adata.obsm[f"X_{clock_name}"]
|
|
195
185
|
|
|
@@ -16,7 +16,7 @@ except Exception:
|
|
|
16
16
|
|
|
17
17
|
import gc
|
|
18
18
|
|
|
19
|
-
from ..models import pyagingModel
|
|
19
|
+
from ..models._base_models import pyagingModel
|
|
20
20
|
from ..preprocess._tage import _prepare_tage
|
|
21
21
|
from ..utils._feature_ranges import resolve_feature_bounds
|
|
22
22
|
from ..utils._hf import PyAgingResourceNotFoundError, download_clock_weights
|
|
@@ -137,12 +137,10 @@ def _load_clock_impl(clock_name: str, device: str, dir: str, logger, indent_leve
|
|
|
137
137
|
logger.error(message, indent_level=indent_level + 1)
|
|
138
138
|
raise NameError(message) from exc
|
|
139
139
|
|
|
140
|
-
# Load the clock from the file
|
|
141
140
|
# Restore on CPU even when the weights were saved from a CUDA device. The
|
|
142
141
|
# explicit transfer below then respects the caller's selected device.
|
|
143
142
|
clock = torch.load(weights_path, weights_only=False, map_location="cpu")
|
|
144
143
|
|
|
145
|
-
# Prepare clock for inference
|
|
146
144
|
clock.to(torch.float64)
|
|
147
145
|
clock.to(device)
|
|
148
146
|
clock.eval()
|
|
@@ -301,11 +299,9 @@ def _align_features_into_obsm(adata, model, source_values, source_features, logg
|
|
|
301
299
|
# instead of rebuilding a Python dictionary over every input feature.
|
|
302
300
|
model_feature_indices = source_features.get_indexer(model.features)
|
|
303
301
|
|
|
304
|
-
# Identify missing features
|
|
305
302
|
missing_features_mask = model_feature_indices == -1
|
|
306
303
|
missing_features = np.array(model.features)[missing_features_mask].tolist()
|
|
307
304
|
|
|
308
|
-
# Assign values for existing features
|
|
309
305
|
existing_features_mask = ~missing_features_mask
|
|
310
306
|
existing_features_indices = model_feature_indices[existing_features_mask]
|
|
311
307
|
supplied_values = source_values[:, existing_features_indices]
|
|
@@ -315,23 +311,19 @@ def _align_features_into_obsm(adata, model, source_values, source_features, logg
|
|
|
315
311
|
supplied_values = cp.asnumpy(supplied_values)
|
|
316
312
|
matrix[:, existing_features_mask] = supplied_values
|
|
317
313
|
|
|
318
|
-
# Handle missing features
|
|
319
314
|
matrix[:, missing_features_mask] = (
|
|
320
315
|
np.array(model.reference_values)[missing_features_mask] if model.reference_values is not None else 0
|
|
321
316
|
)
|
|
322
317
|
adata.obsm[f"X_{model.metadata['clock_name']}"] = matrix
|
|
323
318
|
|
|
324
|
-
# Calculate missing features statistics
|
|
325
319
|
num_missing_features = len(missing_features)
|
|
326
320
|
percent_missing = 100 * num_missing_features / len(model.features)
|
|
327
321
|
|
|
328
|
-
# Add missing features and percent missing values to the clock
|
|
329
322
|
adata.uns[f"{model.metadata['clock_name']}_percent_na"] = percent_missing
|
|
330
323
|
adata.uns[f"{model.metadata['clock_name']}_missing_features"] = missing_features
|
|
331
324
|
# Columns the input carried; check_feature_ranges judges only these.
|
|
332
325
|
adata.uns[f"{model.metadata['clock_name']}_supplied_features_mask"] = existing_features_mask
|
|
333
326
|
|
|
334
|
-
# Raises error if there are no features in the data
|
|
335
327
|
if percent_missing == 100:
|
|
336
328
|
message = (
|
|
337
329
|
f"Every single feature out of {len(model.features)} features "
|
|
@@ -342,14 +334,12 @@ def _align_features_into_obsm(adata, model, source_values, source_features, logg
|
|
|
342
334
|
logger.error(message, indent_level=3)
|
|
343
335
|
raise NameError(message)
|
|
344
336
|
|
|
345
|
-
# Log and add missing features if any
|
|
346
337
|
if len(missing_features) > 0:
|
|
347
338
|
logger.warning(
|
|
348
339
|
f"{num_missing_features} out of {len(model.features)} features "
|
|
349
340
|
f"({percent_missing:.2f}%) are missing: {missing_features[: np.min([3, num_missing_features])]}, etc.",
|
|
350
341
|
indent_level=indent_level + 1,
|
|
351
342
|
)
|
|
352
|
-
# If there are reference values provided
|
|
353
343
|
if model.reference_values is not None:
|
|
354
344
|
logger.info(
|
|
355
345
|
f"Using reference feature values for {model.metadata['clock_name']}",
|
|
@@ -577,7 +567,6 @@ def predict_ages_with_model(
|
|
|
577
567
|
if adata.n_obs == 0:
|
|
578
568
|
raise ValueError("Prediction requires at least one sample.")
|
|
579
569
|
|
|
580
|
-
# If there is a preprocessing step
|
|
581
570
|
if model.preprocess_name is not None:
|
|
582
571
|
logger.info(
|
|
583
572
|
f"The preprocessing method is {model.preprocess_name}",
|
|
@@ -586,7 +575,6 @@ def predict_ages_with_model(
|
|
|
586
575
|
else:
|
|
587
576
|
logger.info("There is no preprocessing necessary", indent_level=indent_level + 1)
|
|
588
577
|
|
|
589
|
-
# If there is a postprocessing step
|
|
590
578
|
if model.postprocess_name is not None:
|
|
591
579
|
logger.info(
|
|
592
580
|
f"The postprocessing method is {model.postprocess_name}",
|
|
@@ -595,7 +583,6 @@ def predict_ages_with_model(
|
|
|
595
583
|
else:
|
|
596
584
|
logger.info("There is no postprocessing necessary", indent_level=indent_level + 1)
|
|
597
585
|
|
|
598
|
-
# Batched prediction over the clock's feature matrix on the model's device
|
|
599
586
|
matrix = adata.obsm[f"X_{model.metadata['clock_name']}"]
|
|
600
587
|
starts = range(0, matrix.shape[0], batch_size)
|
|
601
588
|
predictions = []
|
|
@@ -616,7 +603,6 @@ def predict_ages_with_model(
|
|
|
616
603
|
predictions.append(prediction)
|
|
617
604
|
if progress_callback is not None:
|
|
618
605
|
progress_callback(index + 1, len(starts))
|
|
619
|
-
# Concatenate all batch predictions
|
|
620
606
|
predictions = torch.cat(predictions)
|
|
621
607
|
|
|
622
608
|
return predictions
|
|
@@ -691,13 +677,10 @@ def add_pred_ages_and_clock_metadata_adata(
|
|
|
691
677
|
{'species': 'Homo sapiens', 'data_type': 'methylation', 'citation': 'Horvath, S. (2013)'}
|
|
692
678
|
|
|
693
679
|
"""
|
|
694
|
-
# Convert from a torch tensor to a flat numpy array
|
|
695
680
|
predicted_ages = predicted_ages.cpu().detach().numpy().flatten()
|
|
696
681
|
|
|
697
|
-
# Add predicted ages to adata.obs
|
|
698
682
|
adata.obs[model.metadata["clock_name"]] = predicted_ages
|
|
699
683
|
|
|
700
|
-
# Add clock metadata to adata.uns
|
|
701
684
|
adata.uns[f"{model.metadata['clock_name']}_metadata"] = deepcopy(model.metadata)
|
|
702
685
|
|
|
703
686
|
|
|
@@ -786,16 +769,13 @@ def cleanup_clock_memory(model=None, clock_name=None, dir=None, **kwargs) -> Non
|
|
|
786
769
|
>>> # ... use model ...
|
|
787
770
|
>>> cleanup_clock_memory(model=model, clock_name="horvath2013", dir="pyaging_data")
|
|
788
771
|
"""
|
|
789
|
-
# Delete the model if provided
|
|
790
772
|
if model is not None:
|
|
791
773
|
del model
|
|
792
774
|
|
|
793
|
-
# Delete any additional objects passed via kwargs
|
|
794
775
|
for obj in kwargs.values():
|
|
795
776
|
if obj is not None:
|
|
796
777
|
del obj
|
|
797
778
|
|
|
798
|
-
# Delete the .pt file from disk if specified
|
|
799
779
|
if clock_name is not None and dir is not None:
|
|
800
780
|
weights_path = os.path.join(dir, f"{clock_name}.pt")
|
|
801
781
|
try:
|
|
@@ -805,8 +785,6 @@ def cleanup_clock_memory(model=None, clock_name=None, dir=None, **kwargs) -> Non
|
|
|
805
785
|
# Silently ignore file deletion errors to avoid disrupting tests
|
|
806
786
|
pass
|
|
807
787
|
|
|
808
|
-
# Force garbage collection
|
|
809
788
|
gc.collect()
|
|
810
789
|
|
|
811
|
-
# Clear PyTorch CUDA cache
|
|
812
790
|
torch.cuda.empty_cache()
|
|
@@ -71,14 +71,12 @@ def bigwig_to_df(bw_files: str | list[str], dir: str = "pyaging_data", verbose:
|
|
|
71
71
|
if not PYBIGWIG_AVAILABLE:
|
|
72
72
|
raise ImportError("pyBigWig is not installed. To use this function, please install it.")
|
|
73
73
|
|
|
74
|
-
# Ensure bws is a list
|
|
75
74
|
if isinstance(bw_files, str):
|
|
76
75
|
bw_files = [bw_files]
|
|
77
76
|
if not bw_files:
|
|
78
77
|
raise ValueError("bw_files must contain at least one bigWig file.")
|
|
79
78
|
|
|
80
79
|
with live_step("processing bigWig files", verbose) as (step, pipeline_logger):
|
|
81
|
-
# Get genomic annotation data
|
|
82
80
|
genes = load_ensembl_metadata(dir, pipeline_logger, indent_level=1)
|
|
83
81
|
|
|
84
82
|
regions = list(zip(genes["chr"], genes["start"], genes["end"], strict=True))
|
|
@@ -86,7 +84,6 @@ def bigwig_to_df(bw_files: str | list[str], dir: str = "pyaging_data", verbose:
|
|
|
86
84
|
for index, bw_file in enumerate(bw_files):
|
|
87
85
|
step.update(f"processing {os.path.basename(bw_file)} ({index + 1}/{len(bw_files)})")
|
|
88
86
|
|
|
89
|
-
# Open bigWig file
|
|
90
87
|
with open_bw(bw_file) as bw:
|
|
91
88
|
for gene_index, (chromosome, start, end) in enumerate(regions):
|
|
92
89
|
try:
|
|
@@ -171,7 +168,6 @@ def df_to_adata(
|
|
|
171
168
|
raise TypeError("Input df must be a pandas DataFrame.")
|
|
172
169
|
|
|
173
170
|
with live_step("building AnnData object", verbose) as (step, pipeline_logger):
|
|
174
|
-
# Split data and metadata
|
|
175
171
|
if metadata_cols is None:
|
|
176
172
|
metadata_cols = []
|
|
177
173
|
if len(metadata_cols) > 0:
|
|
@@ -180,20 +176,15 @@ def df_to_adata(
|
|
|
180
176
|
else:
|
|
181
177
|
metadata = None
|
|
182
178
|
|
|
183
|
-
# Create an AnnData object
|
|
184
179
|
adata = create_anndata_object(df, pipeline_logger)
|
|
185
180
|
|
|
186
|
-
# Add metadata
|
|
187
181
|
add_metadata_to_anndata(adata, metadata, pipeline_logger)
|
|
188
182
|
|
|
189
|
-
# Log statistics
|
|
190
183
|
missing_pct = log_data_statistics(adata.X, pipeline_logger)
|
|
191
184
|
|
|
192
|
-
# Impute missing values
|
|
193
185
|
step.update(f"imputing missing values ({imputer_strategy})")
|
|
194
186
|
impute_missing_values(adata, imputer_strategy, pipeline_logger)
|
|
195
187
|
|
|
196
|
-
# Add unstructured data
|
|
197
188
|
if "X_imputed" in adata.layers:
|
|
198
189
|
add_unstructured_data(adata, imputer_strategy, pipeline_logger)
|
|
199
190
|
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Clock weights live in one repository per clock under the ``pyaging``
|
|
4
4
|
organization (``pyaging/<clock_name>``); shared assets (example data, the
|
|
5
|
-
aggregate metadata file) live in the
|
|
5
|
+
aggregate metadata file) live in the shared data repository, which is also
|
|
6
6
|
the fallback whenever a clock's own repository cannot be read.
|
|
7
7
|
|
|
8
8
|
Set the ``PYAGING_DATA_REVISION`` environment variable to pin downloads to a
|
|
9
|
-
specific revision of the data repositories
|
|
10
|
-
``v0.3.1``) for reproducibility. Defaults to ``main``, the live data release.
|
|
9
|
+
specific existing revision of the data repositories for reproducibility. Defaults to ``main``, the live data release.
|
|
11
10
|
"""
|
|
12
11
|
|
|
13
12
|
import os
|
|
@@ -120,7 +119,7 @@ def download_hf_file(
|
|
|
120
119
|
def download_clock_weights(clock_name: str, dir: str = "pyaging_data", logger=None, indent_level: int = 1) -> str:
|
|
121
120
|
"""Download a clock's weight file, preferring its dedicated repository.
|
|
122
121
|
|
|
123
|
-
Each clock lives in ``pyaging/<clock_name>``; the
|
|
122
|
+
Each clock lives in ``pyaging/<clock_name>``; the shared data
|
|
124
123
|
repository is the fallback when the per-clock repository cannot be read,
|
|
125
124
|
including when it lacks the requested revision. Both use the same revision.
|
|
126
125
|
The repo's ``config.json`` is fetched alongside the weights - it carries
|
|
@@ -138,7 +137,7 @@ def download_clock_weights(clock_name: str, dir: str = "pyaging_data", logger=No
|
|
|
138
137
|
):
|
|
139
138
|
# The Hub answers 401 (not 404) for nonexistent repos when the caller
|
|
140
139
|
# has no token, which maps to the authentication error - for anonymous
|
|
141
|
-
# users a typoed clock name must still reach the
|
|
140
|
+
# users a typoed clock name must still reach the shared-repository fallback and
|
|
142
141
|
# its clock-not-available message.
|
|
143
142
|
return download_hf_file(filename, dir, logger, indent_level=indent_level)
|
|
144
143
|
with suppress(PyAgingHubError):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|