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.
Files changed (34) hide show
  1. {pyaging-0.5.3 → pyaging-0.5.4}/.gitignore +5 -1
  2. {pyaging-0.5.3 → pyaging-0.5.4}/PKG-INFO +2 -2
  3. {pyaging-0.5.3 → pyaging-0.5.4}/README.md +1 -1
  4. {pyaging-0.5.3 → pyaging-0.5.4}/pyproject.toml +1 -1
  5. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/__init__.py +1 -1
  6. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/data/feature_ranges.json +1 -0
  7. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/logger/_logger.py +1 -1
  8. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/models/__init__.py +29 -0
  9. pyaging-0.5.4/src/pyaging/models/_author_extensions.py +28 -0
  10. pyaging-0.5.4/src/pyaging/models/_organage.py +33 -0
  11. pyaging-0.5.4/src/pyaging/models/_pc_extensions.py +67 -0
  12. pyaging-0.5.4/src/pyaging/models/_replication.py +88 -0
  13. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/predict/_pred.py +0 -10
  14. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/predict/_pred_utils.py +1 -23
  15. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/preprocess/_preprocess.py +0 -9
  16. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/utils/_hf.py +4 -5
  17. {pyaging-0.5.3 → pyaging-0.5.4}/LICENSE +0 -0
  18. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/data/__init__.py +0 -0
  19. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/data/_data.py +0 -0
  20. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/logger/__init__.py +0 -0
  21. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/logger/_live.py +0 -0
  22. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/models/_base_models.py +0 -0
  23. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/models/_models.py +0 -0
  24. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/predict/__init__.py +0 -0
  25. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/predict/_cache.py +0 -0
  26. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/predict/_transforms.py +0 -0
  27. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/preprocess/__init__.py +0 -0
  28. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/preprocess/_preprocess_utils.py +0 -0
  29. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/preprocess/_tage.py +0 -0
  30. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/py.typed +0 -0
  31. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/utils/__init__.py +0 -0
  32. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/utils/_feature_ranges.py +0 -0
  33. {pyaging-0.5.3 → pyaging-0.5.4}/src/pyaging/utils/_files.py +0 -0
  34. {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/all_clock_metadata.pt
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
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.2`, to pin downloads; it defaults to `main`. Package-only releases do not create new HF tags. Version 0.5.3 uses the existing model artifacts.
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.2`, to pin downloads; it defaults to `main`. Package-only releases do not create new HF tags. Version 0.5.3 uses the existing model artifacts.
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
 
@@ -102,7 +102,7 @@ only-include = ["src/pyaging", "README.md", "LICENSE", "pyproject.toml"]
102
102
 
103
103
  [tool.ruff]
104
104
  line-length = 120
105
- extend-exclude = ["*.ipynb", "dev", "docs/superpowers"]
105
+ extend-exclude = ["*.ipynb"]
106
106
 
107
107
  [tool.ruff.lint]
108
108
  select = [
@@ -4,4 +4,4 @@ from . import data, logger, models, utils
4
4
  from . import predict as pred
5
5
  from . import preprocess as pp
6
6
 
7
- __version__ = "0.5.3"
7
+ __version__ = "0.5.4"
@@ -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 that sets up pyaging's legacy text progress output."""
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 legacy data repository, which is also
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 (e.g. a release tag such as
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 legacy shared data
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 legacy fallback and
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