altar-enformer 0.3.0__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.
@@ -0,0 +1,33 @@
1
+ *.pyc
2
+ *~
3
+ **/__pycache__/*
4
+ *.swp
5
+ .vscode/
6
+ .idea/
7
+ .DS_Store
8
+ .env*
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .pytest_cache/
12
+ .coverage
13
+ htmlcov/
14
+ coverage.xml
15
+ pytest.xml
16
+ .hypothesis/
17
+ .python-version
18
+ .venv
19
+ .venv-*/
20
+ *.egg-info/
21
+ build/
22
+ dist/
23
+ site/
24
+ *.sqlite3
25
+ *.log
26
+
27
+ # Generated variant indexes and datasets. The small canonical gene table is tracked.
28
+ altar/altar/variants/data/ccres.dnatree
29
+ altar/altar/variants/data/region_annotations.parquet
30
+ altar/altar/variants/data/variants.pkl.gz
31
+ altar/altar/variants/data/region_provenance.json
32
+ altar/altar/variants/data/raw/
33
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Riya Sinha
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.5
2
+ Name: altar-enformer
3
+ Version: 0.3.0
4
+ Summary: Enformer model binding for large-scale Altar variant scoring
5
+ Project-URL: Documentation, https://kundajelab.github.io/altar/
6
+ Author: Riya Sinha
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: altar<0.4,>=0.3.0
19
+ Provides-Extra: prepare
20
+ Requires-Dist: kagglehub<2,>=1; extra == 'prepare'
21
+ Provides-Extra: test
22
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'test'
23
+ Requires-Dist: pytest>=8; extra == 'test'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Altar Enformer binding
27
+
28
+ `altar-enformer` is the lightweight control-plane binding for high-throughput Enformer variant scoring. It
29
+ registers `ENFORMER` in Altar's canonical model-plugin group and has no TensorFlow dependency.
30
+
31
+ One Altar model instance contains immutable SavedModel and target-metadata resources plus a fixed selection
32
+ of Enformer's 5,313 human output tracks. A job is partitioned upstream into deterministic scoring batches;
33
+ the binding emits one independently retryable container task per batch. The runtime loads the model once per
34
+ task, microbatches reference/alternate sequence pairs, and writes:
35
+
36
+ - one primary row per variant, containing the track with the largest absolute SAD; and
37
+ - a `track_effects` detail row for every selected `(variant, track)` pair, preserving reference and alternate
38
+ activity, SAD, and SADR.
39
+
40
+ SAD and SADR follow the definitions in the [Basenji variant scorer linked by DeepMind's Enformer
41
+ release](https://github.com/calico/basenji/blob/738321c85f8925ae6ac318a6cd4901a42ea6bc3f/bin/basenji_sad.py):
42
+ `SAD = sum(alt) - sum(ref)` and `SADR = log2(sum(alt) + 1) - log2(sum(ref) + 1)` across the model's 896 output
43
+ bins. DeepMind's simpler [`EnformerScoreVariantsRaw`
44
+ notebook](https://github.com/google-deepmind/deepmind-research/blob/master/enformer/enformer-usage.ipynb) divides SAD
45
+ by the constant 896 by taking means; that does not change per-variant track ranking. `SAR` is a different Basenji
46
+ statistic—the sum of per-bin log ratios—and is intentionally not emitted under the wrong name. Track selection,
47
+ weight bytes, metadata bytes, runtime image, and hg38 reference identity are all durable inputs.
48
+
49
+ Scoring semantics 2.0 selects the primary track by absolute SAD, the main published Enformer variant-effect
50
+ score, and renames the former `sar`/`top_track_sar` fields to `sadr`/`top_track_sadr`. Existing primary scores
51
+ must be regenerated. Existing complete `track_effects` rows are sufficient for a later storage-layer migration
52
+ because their numeric `sar` values already contained SADR; Altar itself does not mutate deployed result stores.
53
+
54
+ The corresponding TensorFlow 2.13 / Python 3.10 executor lives under `runtimes/enformer` with its own lockfile
55
+ and immutable image lifecycle.
@@ -0,0 +1,30 @@
1
+ # Altar Enformer binding
2
+
3
+ `altar-enformer` is the lightweight control-plane binding for high-throughput Enformer variant scoring. It
4
+ registers `ENFORMER` in Altar's canonical model-plugin group and has no TensorFlow dependency.
5
+
6
+ One Altar model instance contains immutable SavedModel and target-metadata resources plus a fixed selection
7
+ of Enformer's 5,313 human output tracks. A job is partitioned upstream into deterministic scoring batches;
8
+ the binding emits one independently retryable container task per batch. The runtime loads the model once per
9
+ task, microbatches reference/alternate sequence pairs, and writes:
10
+
11
+ - one primary row per variant, containing the track with the largest absolute SAD; and
12
+ - a `track_effects` detail row for every selected `(variant, track)` pair, preserving reference and alternate
13
+ activity, SAD, and SADR.
14
+
15
+ SAD and SADR follow the definitions in the [Basenji variant scorer linked by DeepMind's Enformer
16
+ release](https://github.com/calico/basenji/blob/738321c85f8925ae6ac318a6cd4901a42ea6bc3f/bin/basenji_sad.py):
17
+ `SAD = sum(alt) - sum(ref)` and `SADR = log2(sum(alt) + 1) - log2(sum(ref) + 1)` across the model's 896 output
18
+ bins. DeepMind's simpler [`EnformerScoreVariantsRaw`
19
+ notebook](https://github.com/google-deepmind/deepmind-research/blob/master/enformer/enformer-usage.ipynb) divides SAD
20
+ by the constant 896 by taking means; that does not change per-variant track ranking. `SAR` is a different Basenji
21
+ statistic—the sum of per-bin log ratios—and is intentionally not emitted under the wrong name. Track selection,
22
+ weight bytes, metadata bytes, runtime image, and hg38 reference identity are all durable inputs.
23
+
24
+ Scoring semantics 2.0 selects the primary track by absolute SAD, the main published Enformer variant-effect
25
+ score, and renames the former `sar`/`top_track_sar` fields to `sadr`/`top_track_sadr`. Existing primary scores
26
+ must be regenerated. Existing complete `track_effects` rows are sufficient for a later storage-layer migration
27
+ because their numeric `sar` values already contained SADR; Altar itself does not mutate deployed result stores.
28
+
29
+ The corresponding TensorFlow 2.13 / Python 3.10 executor lives under `runtimes/enformer` with its own lockfile
30
+ and immutable image lifecycle.
@@ -0,0 +1,29 @@
1
+ # Copyright 2026 Riya Sinha
2
+ """Enformer model binding for Altar."""
3
+
4
+ from altar_enformer.plugin import (
5
+ DEFAULT_IMAGE,
6
+ HUMAN_TRACK_COUNT,
7
+ KIND_SCORING,
8
+ OUTPUT_BIN_SIZE,
9
+ OUTPUT_BINS,
10
+ SEQUENCE_LENGTH,
11
+ EnformerConfiguration,
12
+ EnformerPlugin,
13
+ EnformerRunInputs,
14
+ EnformerShardKind,
15
+ )
16
+
17
+
18
+ __all__ = [
19
+ "DEFAULT_IMAGE",
20
+ "HUMAN_TRACK_COUNT",
21
+ "KIND_SCORING",
22
+ "OUTPUT_BINS",
23
+ "OUTPUT_BIN_SIZE",
24
+ "SEQUENCE_LENGTH",
25
+ "EnformerConfiguration",
26
+ "EnformerPlugin",
27
+ "EnformerRunInputs",
28
+ "EnformerShardKind",
29
+ ]
@@ -0,0 +1,263 @@
1
+ # Copyright 2026 Riya Sinha
2
+ """Backend-neutral Enformer binding for large-scale variant scoring.
3
+
4
+ The binding emits one independent container task for the request's deterministic scoring batch. The runtime
5
+ loads a content-addressed Enformer SavedModel once, microbatches reference/alternate sequence pairs, writes a
6
+ compact primary row per variant, and preserves every selected track's official SAD/SADR effect as named detail.
7
+ """
8
+
9
+ from __future__ import annotations
10
+ import enum
11
+ from typing import TYPE_CHECKING, Any, cast
12
+
13
+ from altar.execution import GpuPool, ResourceRequest
14
+ from altar.models import (
15
+ ApiVersionRange,
16
+ ContainerImageDigest,
17
+ ContainerScoringPlan,
18
+ DetailSchema,
19
+ ModelConfiguration,
20
+ ModelPlugin,
21
+ OutputSelection,
22
+ PluginCapability,
23
+ PluginKind,
24
+ PluginManifest,
25
+ PrioritizationPolicy,
26
+ ReferenceGenomeRunInputs,
27
+ ResourceReference,
28
+ ResultField,
29
+ ResultSchema,
30
+ RuntimeIdentityPolicy,
31
+ RuntimeKind,
32
+ RuntimeRequirement,
33
+ ScoringRequest,
34
+ VariantClass,
35
+ VariantEligibility,
36
+ require_resource_digests,
37
+ require_strictly_increasing,
38
+ resource_transfer,
39
+ single_container_plan,
40
+ )
41
+ from altar.predicates import Abs, Col, Ge, Lit, Predicate
42
+ from pydantic import Field, model_validator
43
+
44
+ from altar_enformer.resources import manifest_resources
45
+
46
+
47
+ if TYPE_CHECKING:
48
+ import os
49
+
50
+
51
+ SEQUENCE_LENGTH = 393_216
52
+ OUTPUT_BINS = 896
53
+ OUTPUT_BIN_SIZE = 128
54
+ HUMAN_TRACK_COUNT = 5_313
55
+ DEFAULT_IMAGE = "riyavsinha/altar-enformer@sha256:52aafa8c8d781763559814c4ab956ea4094fadb556e6c4903097dc3fab500fe5"
56
+
57
+
58
+ class EnformerShardKind(enum.StrEnum):
59
+ """Stable execution kind emitted by :class:`EnformerPlugin`."""
60
+
61
+ SCORING = "enformer_scoring"
62
+
63
+
64
+ KIND_SCORING = EnformerShardKind.SCORING
65
+ _SCORE_RESOURCES = ResourceRequest(gpu=GpuPool.NORMAL_GPU, count=1, memory_gb=16)
66
+ _SAD_PRIORITIZE_THRESHOLD = 0.5
67
+
68
+
69
+ class EnformerConfiguration(ModelConfiguration):
70
+ """Immutable Enformer resources and selected human output tracks.
71
+
72
+ ``weights`` points to a tar archive containing one TensorFlow SavedModel. ``track_metadata`` is the
73
+ official ``targets_human.txt`` table whose ``index`` column maps output channels to stable identifiers.
74
+ Track indices are canonicalized by requiring strictly increasing values; equivalent selections therefore
75
+ cannot acquire different cache identities solely from ordering.
76
+ """
77
+
78
+ weights: ResourceReference
79
+ track_metadata: ResourceReference
80
+ track_indices: tuple[int, ...] = Field(min_length=1)
81
+ image: ContainerImageDigest = DEFAULT_IMAGE
82
+
83
+ @model_validator(mode="after")
84
+ def validate_resources_and_tracks(self) -> EnformerConfiguration:
85
+ require_resource_digests(self, owner="Enformer")
86
+ require_strictly_increasing(
87
+ self.track_indices, owner="Enformer", field="track_indices", minimum=0, maximum=HUMAN_TRACK_COUNT - 1
88
+ )
89
+ return self
90
+
91
+ @classmethod
92
+ def from_resource_manifest(
93
+ cls, path: str | os.PathLike[str], *, base_uri: str | None = None, **settings: Any
94
+ ) -> EnformerConfiguration:
95
+ """Build a configuration from the ``enformer.resources.json`` that ``altar-enformer-resources`` wrote.
96
+
97
+ ``settings`` are the remaining configuration fields: ``track_indices`` and ``image``. See
98
+ :func:`altar_enformer.resources.manifest_resources` for how ``base_uri`` locates the files.
99
+ """
100
+ resources = manifest_resources(path, base_uri=base_uri)
101
+ return cls(weights=resources.weights, track_metadata=resources.track_metadata, **settings)
102
+
103
+
104
+ class EnformerRunInputs(ReferenceGenomeRunInputs):
105
+ """Per-run Enformer inputs, currently restricted to its training assembly."""
106
+
107
+ supported_genome_builds = frozenset({"hg38"})
108
+
109
+
110
+ class EnformerPlugin(ModelPlugin):
111
+ """Predicts how a variant changes the chromatin, CAGE and TF tracks you select from Enformer's 5,313 (SAD)."""
112
+
113
+ model_type = "ENFORMER"
114
+ manifest = PluginManifest(
115
+ plugin_id="org.kundajelab.altar.model.enformer",
116
+ kind=PluginKind.MODEL,
117
+ capabilities=frozenset({PluginCapability.SCORE, PluginCapability.MATERIALIZE_RESULTS}),
118
+ architecture="Enformer",
119
+ plugin_version="0.0.1",
120
+ altar_api=ApiVersionRange("0.1.0", "0.2.0"),
121
+ configuration_schema_version="0.0.1",
122
+ result_schema=ResultSchema(
123
+ version="0.0.1",
124
+ fields=(
125
+ ResultField("top_track_index", "int", "Top Track Index", "Output index with largest absolute SAD"),
126
+ ResultField("top_track_id", "str", "Top Track", "Identifier of the output with largest absolute SAD"),
127
+ ResultField(
128
+ "top_track_sad",
129
+ "float",
130
+ "Top Track SAD",
131
+ "Alternate minus reference summed activity for the track with largest absolute SAD",
132
+ ),
133
+ ResultField(
134
+ "top_track_sadr",
135
+ "float",
136
+ "Top Track SADR",
137
+ "Log2 alternate/reference summed-activity ratio for the track with largest absolute SAD",
138
+ ),
139
+ ResultField("n_tracks_scored", "int", "Tracks Scored", "Number of selected human output tracks"),
140
+ ),
141
+ ),
142
+ detail_schemas=(
143
+ DetailSchema(
144
+ name="track_effects",
145
+ version="0.0.1",
146
+ description="Official Enformer SAD and SADR effects for every selected human output track.",
147
+ fields=(
148
+ ResultField("track_index", "int", "Track Index", "Zero-based Enformer human output index", False),
149
+ ResultField("track_id", "str", "Track ID", "Identifier from targets_human.txt", False),
150
+ ResultField(
151
+ "ref_signal", "float", "Reference Signal", "Reference activity summed over 896 bins", False
152
+ ),
153
+ ResultField(
154
+ "alt_signal", "float", "Alternate Signal", "Alternate activity summed over 896 bins", False
155
+ ),
156
+ ResultField("sad", "float", "SAD", "Alternate minus reference summed activity", False),
157
+ ResultField(
158
+ "sadr",
159
+ "float",
160
+ "SADR",
161
+ "Log2 alternate/reference summed-activity ratio with pseudocount 1",
162
+ False,
163
+ ),
164
+ ),
165
+ key_fields=("track_index",),
166
+ ),
167
+ ),
168
+ # The Enformer runtime substitutes one reference base; batch preparation filters out indels and MNVs.
169
+ variant_eligibility=VariantEligibility(frozenset({VariantClass.SNV})),
170
+ prioritization=PrioritizationPolicy(name="enformer-sad-effect-v2", version="0.0.1"),
171
+ runtimes=(
172
+ RuntimeRequirement(
173
+ name="model_runtime",
174
+ kind=RuntimeKind.CONTAINER_IMAGE,
175
+ policy=RuntimeIdentityPolicy.DIGEST_REQUIRED,
176
+ ),
177
+ ),
178
+ )
179
+ configuration_schema_id = "org.kundajelab.altar.model.enformer.configuration"
180
+ configuration_model = EnformerConfiguration
181
+ run_input_model = EnformerRunInputs
182
+ default_output = OutputSelection(outputs=("scores", "track_effects"), reducer="official_sad_sadr_v2")
183
+
184
+ def scoring_runtime_identities(self, configuration: ModelConfiguration) -> dict[str, str]:
185
+ """Record the configured Enformer image as the scoring plan's one runtime."""
186
+ return {"model_runtime": self.parse_configuration_as(configuration, EnformerConfiguration).image}
187
+
188
+ def build_scoring_plan(self, request: ScoringRequest) -> ContainerScoringPlan:
189
+ """Build one independently retryable task for one deterministic variant batch."""
190
+ configuration = self.parse_configuration_as(request.configuration, EnformerConfiguration)
191
+ run_inputs = self.parse_run_inputs_as(request.run_inputs, EnformerRunInputs)
192
+
193
+ layout, resolver = request.layout, request.resolver
194
+ weights_logical = layout.model_resource_file(request.model_id, "enformer.savedmodel.tar.gz")
195
+ metadata_logical = layout.model_resource_file(request.model_id, "targets_human.txt")
196
+ genome_logical = layout.genome_fasta(request.genome_label)
197
+ variants_logical = layout.model_variants_file(request.job_id, request.model_id, request.scoring_batch)
198
+ primary_logical = layout.scoring_result_file(request.job_id, request.model_id, request.scoring_batch)
199
+ details_logical = layout.detail_result_file(
200
+ request.job_id,
201
+ request.model_id,
202
+ "track_effects",
203
+ request.scoring_batch,
204
+ )
205
+
206
+ command = [
207
+ "enformer-score",
208
+ "score",
209
+ "--weights",
210
+ resolver.resolve(weights_logical),
211
+ "--weights-digest",
212
+ cast("str", configuration.weights.digest),
213
+ "--track-metadata",
214
+ resolver.resolve(metadata_logical),
215
+ "--track-metadata-digest",
216
+ cast("str", configuration.track_metadata.digest),
217
+ ]
218
+ for track_index in configuration.track_indices:
219
+ command += ["--track-index", str(track_index)]
220
+ command += [
221
+ "--genome",
222
+ resolver.resolve(genome_logical),
223
+ "--genome-digest",
224
+ cast("str", run_inputs.genome.digest),
225
+ "--variants",
226
+ resolver.resolve(variants_logical),
227
+ "--primary-output",
228
+ resolver.resolve(primary_logical),
229
+ "--detail-output",
230
+ resolver.resolve(details_logical),
231
+ ]
232
+
233
+ return single_container_plan(
234
+ self,
235
+ request,
236
+ kind=KIND_SCORING,
237
+ image=configuration.image,
238
+ command=command,
239
+ resources=_SCORE_RESOURCES,
240
+ inputs=[
241
+ resource_transfer(configuration.weights, weights_logical, locality_key=request.model_id),
242
+ resource_transfer(configuration.track_metadata, metadata_logical, locality_key=request.model_id),
243
+ resource_transfer(run_inputs.genome, genome_logical),
244
+ ],
245
+ )
246
+
247
+ def prioritize_predicate(self) -> Predicate:
248
+ # Placeholder effect threshold, not a calibrated biological cutoff.
249
+ return Ge(Abs(Col("top_track_sad")), Lit(_SAD_PRIORITIZE_THRESHOLD))
250
+
251
+
252
+ __all__ = [
253
+ "DEFAULT_IMAGE",
254
+ "HUMAN_TRACK_COUNT",
255
+ "KIND_SCORING",
256
+ "OUTPUT_BINS",
257
+ "OUTPUT_BIN_SIZE",
258
+ "SEQUENCE_LENGTH",
259
+ "EnformerConfiguration",
260
+ "EnformerPlugin",
261
+ "EnformerRunInputs",
262
+ "EnformerShardKind",
263
+ ]