pyaging 0.5.2__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.2 → pyaging-0.5.4}/.gitignore +5 -1
- {pyaging-0.5.2 → pyaging-0.5.4}/PKG-INFO +18 -2
- {pyaging-0.5.2 → pyaging-0.5.4}/README.md +15 -1
- {pyaging-0.5.2 → pyaging-0.5.4}/pyproject.toml +3 -1
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/__init__.py +1 -1
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/data/_data.py +5 -2
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/data/feature_ranges.json +3 -2
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/logger/_live.py +9 -10
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/logger/_logger.py +24 -26
- {pyaging-0.5.2 → 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.2 → pyaging-0.5.4}/src/pyaging/models/_models.py +66 -49
- 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.2 → pyaging-0.5.4}/src/pyaging/predict/__init__.py +2 -0
- pyaging-0.5.4/src/pyaging/predict/_cache.py +56 -0
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/predict/_pred.py +29 -14
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/predict/_pred_utils.py +51 -41
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/predict/_transforms.py +8 -14
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/preprocess/_preprocess.py +25 -65
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/preprocess/_preprocess_utils.py +38 -21
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/preprocess/_tage.py +19 -17
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/utils/_feature_ranges.py +1 -0
- pyaging-0.5.4/src/pyaging/utils/_files.py +16 -0
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/utils/_hf.py +39 -8
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/utils/_utils.py +24 -23
- {pyaging-0.5.2 → pyaging-0.5.4}/LICENSE +0 -0
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/data/__init__.py +0 -0
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/logger/__init__.py +0 -0
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/models/_base_models.py +0 -0
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/preprocess/__init__.py +0 -0
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/py.typed +0 -0
- {pyaging-0.5.2 → pyaging-0.5.4}/src/pyaging/utils/__init__.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
|
|
@@ -19,11 +19,13 @@ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
|
19
19
|
Classifier: Typing :: Typed
|
|
20
20
|
Requires-Python: >=3.11
|
|
21
21
|
Requires-Dist: anndata>=0.10.3
|
|
22
|
+
Requires-Dist: httpx<1,>=0.23
|
|
22
23
|
Requires-Dist: huggingface-hub<2,>=1.3
|
|
23
24
|
Requires-Dist: numpy>=2.0.0
|
|
24
25
|
Requires-Dist: pandas>=2.2.0
|
|
25
26
|
Requires-Dist: rich>=13.0
|
|
26
27
|
Requires-Dist: scikit-learn>=1.3.2
|
|
28
|
+
Requires-Dist: scipy>=1.11
|
|
27
29
|
Requires-Dist: torch>=2.2.0
|
|
28
30
|
Provides-Extra: histone
|
|
29
31
|
Requires-Dist: pybigwig>=0.3.23; extra == 'histone'
|
|
@@ -79,7 +81,21 @@ pya.pred.predict_age(adata, ["Horvath2013", "AltumAge", "DunedinPACE"])
|
|
|
79
81
|
adata.obs.head()
|
|
80
82
|
```
|
|
81
83
|
|
|
82
|
-
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
|
|
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
|
+
|
|
86
|
+
### Run the same clocks across datasets
|
|
87
|
+
|
|
88
|
+
Reuse loaded models with a bounded cache:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
clocks = ["Horvath2013", "AltumAge"]
|
|
92
|
+
cache = pya.pred.ClockCache(maxsize=len(clocks))
|
|
93
|
+
for adata in datasets: # Each dataset is a separate AnnData object.
|
|
94
|
+
pya.pred.predict_age(adata, clocks, clock_cache=cache)
|
|
95
|
+
cache.clear()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The cache avoids repeated download checks, deserialization, and device transfers. It holds at most `maxsize` models and keeps separate entries for each device and data revision. It is optional; ordinary calls retain no model cache. Clear it to refresh weights from a moving `main` revision. See the [prediction guide](https://pyaging.readthedocs.io/en/latest/pyaging.predict.html) for memory and cohort guidance.
|
|
83
99
|
|
|
84
100
|
## ❓ Can't find an aging clock?
|
|
85
101
|
|
|
@@ -48,7 +48,21 @@ 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
|
|
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
|
+
|
|
53
|
+
### Run the same clocks across datasets
|
|
54
|
+
|
|
55
|
+
Reuse loaded models with a bounded cache:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
clocks = ["Horvath2013", "AltumAge"]
|
|
59
|
+
cache = pya.pred.ClockCache(maxsize=len(clocks))
|
|
60
|
+
for adata in datasets: # Each dataset is a separate AnnData object.
|
|
61
|
+
pya.pred.predict_age(adata, clocks, clock_cache=cache)
|
|
62
|
+
cache.clear()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The cache avoids repeated download checks, deserialization, and device transfers. It holds at most `maxsize` models and keeps separate entries for each device and data revision. It is optional; ordinary calls retain no model cache. Clear it to refresh weights from a moving `main` revision. See the [prediction guide](https://pyaging.readthedocs.io/en/latest/pyaging.predict.html) for memory and cohort guidance.
|
|
52
66
|
|
|
53
67
|
## ❓ Can't find an aging clock?
|
|
54
68
|
|
|
@@ -34,8 +34,10 @@ dependencies = [
|
|
|
34
34
|
"pandas>=2.2.0",
|
|
35
35
|
"torch>=2.2.0",
|
|
36
36
|
"numpy>=2.0.0",
|
|
37
|
+
"scipy>=1.11",
|
|
37
38
|
"anndata>=0.10.3",
|
|
38
39
|
"huggingface-hub>=1.3,<2",
|
|
40
|
+
"httpx>=0.23,<1",
|
|
39
41
|
"rich>=13.0",
|
|
40
42
|
]
|
|
41
43
|
|
|
@@ -100,7 +102,7 @@ only-include = ["src/pyaging", "README.md", "LICENSE", "pyproject.toml"]
|
|
|
100
102
|
|
|
101
103
|
[tool.ruff]
|
|
102
104
|
line-length = 120
|
|
103
|
-
extend-exclude = ["*.ipynb"
|
|
105
|
+
extend-exclude = ["*.ipynb"]
|
|
104
106
|
|
|
105
107
|
[tool.ruff.lint]
|
|
106
108
|
select = [
|
|
@@ -2,6 +2,7 @@ import shutil
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
4
|
from ..logger._live import SimpleStep, display_enabled, live_step, quiet_hf_bars
|
|
5
|
+
from ..utils._files import atomic_output_path
|
|
5
6
|
from ..utils._hf import download_hf_file
|
|
6
7
|
|
|
7
8
|
_EXAMPLE_DATA_FILENAMES = {
|
|
@@ -63,13 +64,15 @@ def download_example_data(data_type: str, dir: str = "pyaging_data", verbose: bo
|
|
|
63
64
|
enabled = display_enabled(verbose)
|
|
64
65
|
filename = _EXAMPLE_DATA_FILENAMES[data_type]
|
|
65
66
|
destination = Path(dir) / filename
|
|
67
|
+
if destination.is_dir():
|
|
68
|
+
raise IsADirectoryError(destination)
|
|
66
69
|
if destination.exists():
|
|
67
70
|
SimpleStep(filename, enabled=enabled).done(f"example data already at {destination}")
|
|
68
71
|
return str(destination)
|
|
69
72
|
|
|
70
73
|
with quiet_hf_bars(verbose), live_step(f"downloading {filename}", verbose) as (step, pipeline_logger):
|
|
71
74
|
cache_path = download_hf_file(filename, dir, pipeline_logger, indent_level=1)
|
|
72
|
-
destination
|
|
73
|
-
|
|
75
|
+
with atomic_output_path(destination) as temporary_path:
|
|
76
|
+
shutil.copy(cache_path, temporary_path)
|
|
74
77
|
step.done(f"example data at {destination}")
|
|
75
78
|
return str(destination)
|
|
@@ -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},
|
|
@@ -80,8 +81,8 @@
|
|
|
80
81
|
"grimage2timp1": {"unit": "pg/mL", "low": 1000.0, "high": 5000000.0},
|
|
81
82
|
"hdl_cholesterol": {"unit": "mmol/L", "low": 0.1, "high": 5.0},
|
|
82
83
|
"health_compared_to_one_year_ago": {"unit": "NHANES change code (1 = better, 2 = worse, 3 = same, 7 = refused, 9 = don't know)", "low": 1.0, "high": 9.0},
|
|
83
|
-
"healthcare_use_index": {"unit": "NHANES visit-count code (0 = none,
|
|
84
|
-
"healthcare_visits_past_year": {"unit": "NHANES visit-count code (0 = none,
|
|
84
|
+
"healthcare_use_index": {"unit": "NHANES 1999-2000 HUQ050 visit-count code (0 = none, 5 = 13 or more)", "low": 0.0, "high": 5.0},
|
|
85
|
+
"healthcare_visits_past_year": {"unit": "NHANES 1999-2000 HUQ050 visit-count code (0 = none, 1 = 1, 2 = 2 to 3, 3 = 4 to 9, 4 = 10 to 12, 5 = 13 or more, 77 = refused, 99 = don't know); collapse the 0 to 8 HUQ051 scale of later cycles onto it first", "low": 0.0, "high": 99.0},
|
|
85
86
|
"hematocrit": {"unit": "%", "low": 5.0, "high": 75.0},
|
|
86
87
|
"hemoglobin": {"unit": "g/dL", "low": 2.0, "high": 25.0},
|
|
87
88
|
"hemoglobin_a1c": {"unit": "%", "low": 2.0, "high": 20.0},
|
|
@@ -21,6 +21,7 @@ import contextlib
|
|
|
21
21
|
import sys
|
|
22
22
|
import threading
|
|
23
23
|
import time
|
|
24
|
+
from contextvars import ContextVar
|
|
24
25
|
|
|
25
26
|
from rich.console import Console, Group
|
|
26
27
|
|
|
@@ -52,6 +53,7 @@ DOT = "⏺" if sys.platform == "darwin" else "●"
|
|
|
52
53
|
ELBOW = "⎿"
|
|
53
54
|
|
|
54
55
|
_console = Console()
|
|
56
|
+
_hf_bars_disabled = ContextVar("pyaging_hf_bars_disabled", default=False)
|
|
55
57
|
|
|
56
58
|
|
|
57
59
|
class Sparkle:
|
|
@@ -107,20 +109,17 @@ def display_enabled(verbose) -> bool:
|
|
|
107
109
|
|
|
108
110
|
@contextlib.contextmanager
|
|
109
111
|
def quiet_hf_bars(verbose=True):
|
|
110
|
-
"""Suppress
|
|
111
|
-
own progress, and always at verbose=False. Non-interactive verbose runs
|
|
112
|
-
keep the bars: they are the only live progress signal there."""
|
|
113
|
-
from huggingface_hub.utils import are_progress_bars_disabled, disable_progress_bars, enable_progress_bars
|
|
112
|
+
"""Suppress bars for this run without changing global Hub settings.
|
|
114
113
|
|
|
114
|
+
Non-interactive verbose runs keep the bars: they are the only live
|
|
115
|
+
progress signal there. Nested quiet contexts remain quiet.
|
|
116
|
+
"""
|
|
115
117
|
animated = _console.is_jupyter or _console.is_interactive
|
|
116
|
-
|
|
117
|
-
if were_enabled:
|
|
118
|
-
disable_progress_bars()
|
|
118
|
+
token = _hf_bars_disabled.set(_hf_bars_disabled.get() or not verbose or animated)
|
|
119
119
|
try:
|
|
120
120
|
yield
|
|
121
121
|
finally:
|
|
122
|
-
|
|
123
|
-
enable_progress_bars()
|
|
122
|
+
_hf_bars_disabled.reset(token)
|
|
124
123
|
|
|
125
124
|
|
|
126
125
|
def _bar(completed: float, total: float | None, width: int = 24, pulse: bool = False):
|
|
@@ -143,7 +142,7 @@ def live_step(label: str, verbose):
|
|
|
143
142
|
The step renders animated when interactive, prints final lines when
|
|
144
143
|
piped, and is fully silent at verbose=False - callers keep one code path.
|
|
145
144
|
"""
|
|
146
|
-
with SimpleStep(label, enabled=display_enabled(verbose)) as step:
|
|
145
|
+
with quiet_hf_bars(verbose), SimpleStep(label, enabled=display_enabled(verbose)) as step:
|
|
147
146
|
yield step, DisplayLogger(step.warn)
|
|
148
147
|
|
|
149
148
|
|
|
@@ -4,6 +4,7 @@ import functools
|
|
|
4
4
|
import logging
|
|
5
5
|
import sys
|
|
6
6
|
import time
|
|
7
|
+
from collections.abc import Sized
|
|
7
8
|
from contextlib import contextmanager
|
|
8
9
|
|
|
9
10
|
|
|
@@ -45,7 +46,7 @@ def format_logging_message(msg, logging_level, indent_level=1, indent_space_num=
|
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
class Logger:
|
|
48
|
-
"""
|
|
49
|
+
"""Logger for pyaging's text output and file logging."""
|
|
49
50
|
|
|
50
51
|
FORMAT = "%(message)s"
|
|
51
52
|
|
|
@@ -69,13 +70,6 @@ class Logger:
|
|
|
69
70
|
self.logger.propagate = False
|
|
70
71
|
self.log_time()
|
|
71
72
|
|
|
72
|
-
# Other global initialization
|
|
73
|
-
silence_logger("anndata")
|
|
74
|
-
silence_logger("h5py")
|
|
75
|
-
silence_logger("numba")
|
|
76
|
-
silence_logger("pysam")
|
|
77
|
-
silence_logger("pystan")
|
|
78
|
-
|
|
79
73
|
if level is not None:
|
|
80
74
|
self.logger.setLevel(level)
|
|
81
75
|
else:
|
|
@@ -112,8 +106,10 @@ class Logger:
|
|
|
112
106
|
"""
|
|
113
107
|
previous = self.namespace
|
|
114
108
|
self.namespace = namespace
|
|
115
|
-
|
|
116
|
-
|
|
109
|
+
try:
|
|
110
|
+
yield
|
|
111
|
+
finally:
|
|
112
|
+
self.namespace = previous
|
|
117
113
|
|
|
118
114
|
def namespace_message(self, message):
|
|
119
115
|
"""Add namespace information at the beginning of the logging message.
|
|
@@ -296,16 +292,17 @@ class Logger:
|
|
|
296
292
|
ts :
|
|
297
293
|
total size
|
|
298
294
|
"""
|
|
299
|
-
if
|
|
300
|
-
self.report_hook_percent_state =
|
|
301
|
-
|
|
302
|
-
if ts == -1:
|
|
295
|
+
if ts <= 0:
|
|
296
|
+
self.report_hook_percent_state = None
|
|
303
297
|
return
|
|
304
298
|
|
|
305
|
-
|
|
299
|
+
if bn == 0 or self.report_hook_percent_state is None:
|
|
300
|
+
self.report_hook_percent_state = 0
|
|
301
|
+
|
|
302
|
+
cur_percent = min(rs * bn / ts, 1)
|
|
306
303
|
|
|
307
304
|
if cur_percent - self.report_hook_percent_state > 0.01:
|
|
308
|
-
self.report_progress(
|
|
305
|
+
self.report_progress(percent=cur_percent * 100, indent_level=self.indent_level)
|
|
309
306
|
self.report_hook_percent_state = cur_percent
|
|
310
307
|
if rs * bn >= ts:
|
|
311
308
|
self.report_hook_percent_state = None
|
|
@@ -318,8 +315,8 @@ class LoggerManager:
|
|
|
318
315
|
CRITICAL = logging.CRITICAL
|
|
319
316
|
EXCEPTION = logging.ERROR
|
|
320
317
|
|
|
321
|
-
main_logger = Logger("
|
|
322
|
-
temp_timer_logger = Logger("
|
|
318
|
+
main_logger = Logger("pyaging")
|
|
319
|
+
temp_timer_logger = Logger("pyaging.temp-timer")
|
|
323
320
|
|
|
324
321
|
@staticmethod
|
|
325
322
|
def get_main_logger():
|
|
@@ -337,23 +334,24 @@ class LoggerManager:
|
|
|
337
334
|
def progress_logger(generator, logger=None, progress_name="", indent_level=1):
|
|
338
335
|
if logger is None:
|
|
339
336
|
logger = LoggerManager.get_temp_timer_logger()
|
|
340
|
-
iterator = iter(generator)
|
|
341
337
|
logger.log_time()
|
|
342
|
-
|
|
338
|
+
total = len(generator) if isinstance(generator, Sized) else None
|
|
343
339
|
prev_progress_percent = 0
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
340
|
+
for i, item in enumerate(generator, start=1):
|
|
341
|
+
if total is None or total == 0:
|
|
342
|
+
yield item
|
|
343
|
+
continue
|
|
344
|
+
new_progress_percent = i / total * 100
|
|
347
345
|
# report every `interval` percent
|
|
348
346
|
if new_progress_percent - prev_progress_percent > 1 or new_progress_percent >= 100:
|
|
349
347
|
logger.report_progress(
|
|
350
348
|
count=i,
|
|
351
|
-
total=
|
|
349
|
+
total=total,
|
|
352
350
|
progress_name=progress_name,
|
|
353
351
|
indent_level=indent_level,
|
|
354
352
|
)
|
|
355
353
|
prev_progress_percent = new_progress_percent
|
|
356
|
-
yield
|
|
354
|
+
yield item
|
|
357
355
|
|
|
358
356
|
|
|
359
357
|
def main_info(message, indent_level=1):
|
|
@@ -435,4 +433,4 @@ def main_info_verbose_timeit(msg):
|
|
|
435
433
|
|
|
436
434
|
|
|
437
435
|
def main_set_level(level):
|
|
438
|
-
|
|
436
|
+
LoggerManager.main_logger.setLevel(level)
|
|
@@ -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."""
|
|
@@ -42,6 +42,24 @@ LINAGE2_COMORBIDITY_ITEMS = (
|
|
|
42
42
|
)
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def _global_median(x):
|
|
46
|
+
"""Keep the rank clocks' lower median and all-missing zero fallback."""
|
|
47
|
+
median = torch.nanmedian(x)
|
|
48
|
+
return torch.where(torch.isnan(median), torch.zeros_like(median), median)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _average_ranks(values):
|
|
52
|
+
"""Assign average one-based ranks without scalar tensor comparisons."""
|
|
53
|
+
sorted_values, order = torch.sort(values)
|
|
54
|
+
_, counts = torch.unique_consecutive(sorted_values, return_counts=True)
|
|
55
|
+
ends = counts.cumsum(0)
|
|
56
|
+
starts = ends - counts
|
|
57
|
+
averages = (starts + ends + 1).to(values.dtype) / 2
|
|
58
|
+
ranks = torch.empty_like(values)
|
|
59
|
+
ranks[order] = torch.repeat_interleave(averages, counts, output_size=values.numel())
|
|
60
|
+
return ranks
|
|
61
|
+
|
|
62
|
+
|
|
45
63
|
class AltumAge(pyagingModel):
|
|
46
64
|
def __init__(self):
|
|
47
65
|
super().__init__()
|
|
@@ -1138,6 +1156,7 @@ class PCGrimAge(pyagingModel):
|
|
|
1138
1156
|
self.features_PCTIMP1 = None
|
|
1139
1157
|
|
|
1140
1158
|
def forward(self, x):
|
|
1159
|
+
x = self.preprocess(x)
|
|
1141
1160
|
CpGs = x[:, :-2]
|
|
1142
1161
|
Female = x[:, -2].unsqueeze(1)
|
|
1143
1162
|
Age = x[:, -1].unsqueeze(1)
|
|
@@ -1278,28 +1297,21 @@ class Pasta(pyagingModel):
|
|
|
1278
1297
|
"""
|
|
1279
1298
|
Assign average ranks (1-based) per vector, handling ties.
|
|
1280
1299
|
"""
|
|
1281
|
-
|
|
1282
|
-
ranks = torch.empty_like(sorted_vals, dtype=values.dtype)
|
|
1300
|
+
return _average_ranks(values)
|
|
1283
1301
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
end = start + 1
|
|
1288
|
-
while end < n and sorted_vals[end] == sorted_vals[start]:
|
|
1289
|
-
end += 1
|
|
1290
|
-
avg_rank = (start + end - 1) / 2.0 + 1.0
|
|
1291
|
-
ranks[sorted_idx[start:end]] = avg_rank
|
|
1292
|
-
start = end
|
|
1302
|
+
def prepare_cohort_context(self, x):
|
|
1303
|
+
"""Compute the missing-value fill once across the complete cohort."""
|
|
1304
|
+
return _global_median(x)
|
|
1293
1305
|
|
|
1294
|
-
|
|
1306
|
+
def predict_with_cohort_context(self, x, context):
|
|
1307
|
+
"""Score a batch with its cohort's median without storing run state."""
|
|
1308
|
+
return self.postprocess(self.base_model(self.preprocess(x, context)))
|
|
1295
1309
|
|
|
1296
|
-
def preprocess(self, x):
|
|
1310
|
+
def preprocess(self, x, median=None):
|
|
1297
1311
|
"""
|
|
1298
1312
|
Fill missing values with the global median then rank-normalize per sample.
|
|
1299
1313
|
"""
|
|
1300
|
-
median =
|
|
1301
|
-
if torch.isnan(median):
|
|
1302
|
-
median = torch.tensor(0.0, device=x.device, dtype=x.dtype)
|
|
1314
|
+
median = _global_median(x) if median is None else median.to(device=x.device, dtype=x.dtype)
|
|
1303
1315
|
x = torch.where(torch.isnan(x), median, x)
|
|
1304
1316
|
|
|
1305
1317
|
ranked = torch.empty_like(x, dtype=x.dtype)
|
|
@@ -1349,21 +1361,33 @@ class PastaMouse(Pasta):
|
|
|
1349
1361
|
else:
|
|
1350
1362
|
self.reference_values = [self.full_reference_values[i] for i in self.mouse_feature_indices]
|
|
1351
1363
|
|
|
1352
|
-
def
|
|
1353
|
-
"""
|
|
1354
|
-
Reconstruct the full 8113-length input expected by the base model by
|
|
1355
|
-
inserting reference values for human-only genes.
|
|
1356
|
-
"""
|
|
1364
|
+
def _full_reference_tensor(self, x):
|
|
1357
1365
|
if self.base_model_features is None or self.mouse_feature_indices is None:
|
|
1358
1366
|
raise ValueError("PastaMouse must be configured with set_mouse_features before inference.")
|
|
1359
1367
|
|
|
1360
1368
|
if self.full_reference_values is None:
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1369
|
+
return torch.zeros(len(self.base_model_features), device=x.device, dtype=x.dtype)
|
|
1370
|
+
return torch.as_tensor(self.full_reference_values, device=x.device, dtype=x.dtype)
|
|
1371
|
+
|
|
1372
|
+
def prepare_cohort_context(self, x):
|
|
1373
|
+
"""Include human reference values without expanding the full cohort."""
|
|
1374
|
+
reference = self._full_reference_tensor(x)
|
|
1375
|
+
inserted = torch.ones(reference.shape, dtype=torch.bool, device=x.device)
|
|
1376
|
+
inserted[self.mouse_feature_indices] = False
|
|
1377
|
+
# Missing references do not affect nanmedian, which is the common case.
|
|
1378
|
+
reference = reference[inserted & ~torch.isnan(reference)]
|
|
1379
|
+
values = torch.cat([x.reshape(-1), reference.repeat(x.size(0))]) if reference.numel() else x
|
|
1380
|
+
return _global_median(values)
|
|
1381
|
+
|
|
1382
|
+
def predict_with_cohort_context(self, x, context):
|
|
1383
|
+
return super().predict_with_cohort_context(self._expand_with_reference(x), context)
|
|
1366
1384
|
|
|
1385
|
+
def _expand_with_reference(self, x):
|
|
1386
|
+
"""
|
|
1387
|
+
Reconstruct the full 8113-length input expected by the base model by
|
|
1388
|
+
inserting reference values for human-only genes.
|
|
1389
|
+
"""
|
|
1390
|
+
ref_full = self._full_reference_tensor(x)
|
|
1367
1391
|
full_x = ref_full.unsqueeze(0).repeat(x.size(0), 1)
|
|
1368
1392
|
full_x[:, self.mouse_feature_indices] = x
|
|
1369
1393
|
return full_x
|
|
@@ -1386,28 +1410,21 @@ class Reg(pyagingModel):
|
|
|
1386
1410
|
"""
|
|
1387
1411
|
Assign average ranks (1-based) per vector, handling ties.
|
|
1388
1412
|
"""
|
|
1389
|
-
|
|
1390
|
-
ranks = torch.empty_like(sorted_vals, dtype=values.dtype)
|
|
1413
|
+
return _average_ranks(values)
|
|
1391
1414
|
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
end = start + 1
|
|
1396
|
-
while end < n and sorted_vals[end] == sorted_vals[start]:
|
|
1397
|
-
end += 1
|
|
1398
|
-
avg_rank = (start + end - 1) / 2.0 + 1.0
|
|
1399
|
-
ranks[sorted_idx[start:end]] = avg_rank
|
|
1400
|
-
start = end
|
|
1415
|
+
def prepare_cohort_context(self, x):
|
|
1416
|
+
"""Compute the missing-value fill once across the complete cohort."""
|
|
1417
|
+
return _global_median(x)
|
|
1401
1418
|
|
|
1402
|
-
|
|
1419
|
+
def predict_with_cohort_context(self, x, context):
|
|
1420
|
+
"""Score a batch with its cohort's median without storing run state."""
|
|
1421
|
+
return self.postprocess(self.base_model(self.preprocess(x, context)))
|
|
1403
1422
|
|
|
1404
|
-
def preprocess(self, x):
|
|
1423
|
+
def preprocess(self, x, median=None):
|
|
1405
1424
|
"""
|
|
1406
1425
|
Fill missing values with the global median then rank-normalize per sample.
|
|
1407
1426
|
"""
|
|
1408
|
-
median =
|
|
1409
|
-
if torch.isnan(median):
|
|
1410
|
-
median = torch.tensor(0.0, device=x.device, dtype=x.dtype)
|
|
1427
|
+
median = _global_median(x) if median is None else median.to(device=x.device, dtype=x.dtype)
|
|
1411
1428
|
x = torch.where(torch.isnan(x), median, x)
|
|
1412
1429
|
|
|
1413
1430
|
ranked = torch.empty_like(x, dtype=x.dtype)
|
|
@@ -1708,11 +1725,11 @@ class PhenoAge(pyagingModel):
|
|
|
1708
1725
|
Applies a convertion from a CDF of the mortality score from a Gompertz
|
|
1709
1726
|
distribution to phenotypic age.
|
|
1710
1727
|
"""
|
|
1711
|
-
#
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
return
|
|
1728
|
+
# log(1 - mortality) equals the negative Gompertz hazard exactly.
|
|
1729
|
+
# Cancel the nested exp/log terms before finite precision rounds the
|
|
1730
|
+
# mortality to zero or one, preserving the published constants.
|
|
1731
|
+
log_scale = math.log(0.00553 * math.expm1(120 * 0.0192) / 0.0192)
|
|
1732
|
+
return 141.50225 + (x + log_scale) / 0.090165
|
|
1716
1733
|
|
|
1717
1734
|
|
|
1718
1735
|
class PhenoAgeSaoPaulo(pyagingModel):
|
|
@@ -2273,7 +2290,7 @@ class stemTOC(pyagingModel):
|
|
|
2273
2290
|
quantiles = []
|
|
2274
2291
|
for row in x:
|
|
2275
2292
|
filtered_row = row[row != -1]
|
|
2276
|
-
quantile_95 = torch.quantile(filtered_row, 0.95) if len(filtered_row) > 0 else
|
|
2293
|
+
quantile_95 = torch.quantile(filtered_row, 0.95) if len(filtered_row) > 0 else x.new_tensor(float("nan"))
|
|
2277
2294
|
quantiles.append(quantile_95)
|
|
2278
2295
|
return torch.vstack(quantiles)
|
|
2279
2296
|
|
|
@@ -2290,7 +2307,7 @@ class epiTOC1(pyagingModel):
|
|
|
2290
2307
|
means = []
|
|
2291
2308
|
for row in x:
|
|
2292
2309
|
filtered_row = row[row != -1]
|
|
2293
|
-
mean = torch.mean(filtered_row) if len(filtered_row) > 0 else
|
|
2310
|
+
mean = torch.mean(filtered_row) if len(filtered_row) > 0 else x.new_tensor(float("nan"))
|
|
2294
2311
|
means.append(mean)
|
|
2295
2312
|
return torch.vstack(means)
|
|
2296
2313
|
|
|
@@ -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
|