altar-cherimoya 0.1.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,32 @@
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/raw/
32
+
@@ -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,62 @@
1
+ Metadata-Version: 2.5
2
+ Name: altar-cherimoya
3
+ Version: 0.1.0
4
+ Summary: Cherimoya model binding for Altar
5
+ Project-URL: Documentation, https://kundajelab.github.io/altar/
6
+ Project-URL: Issues, https://github.com/kundajelab/altar/issues
7
+ Project-URL: Repository, https://github.com/kundajelab/altar
8
+ Author: Riya Sinha
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: altar<0.2,>=0.1
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 Cherimoya binding
27
+
28
+ `altar-cherimoya` is the lightweight control-plane binding between Altar and the Cherimoya variant-effect
29
+ runtime. Installing it registers `CHERIMOYA` in the canonical `altar.model_plugins` discovery group.
30
+
31
+ The binding owns Cherimoya's artifact schema, score schema, prioritization predicate, resource requests, and
32
+ backend-neutral container plan. It does not import PyTorch, Cherimoya, a model registry, object-storage SDK,
33
+ or compute-provider SDK. The heavyweight `cherimoya-score` CLI and independently locked image live under
34
+ `runtimes/cherimoya` in this repository.
35
+
36
+ ```bash
37
+ pip install altar-cherimoya
38
+ ```
39
+
40
+ ```python
41
+ from altar.models import get_model_plugin
42
+
43
+ plugin = get_model_plugin("CHERIMOYA")
44
+ ```
45
+
46
+ `CherimoyaConfiguration.image` defaults to the immutable runtime digest tested with the installed binding.
47
+ Callers may override it with another `name@sha256:<digest>` image reference; mutable tags remain invalid. Its
48
+ `weights` tuple contains one generic `ResourceReference` per fold, each with a required SHA-256 digest. The
49
+ URI may be local, HTTPS, GCS, S3, Hugging Face, or any other location understood by the selected storage
50
+ adapter. The digest, not the location, defines scientific identity. Mutable images and unidentified weight
51
+ bytes fail validation before a plan can be built.
52
+
53
+ The binding emits one GPU scoring task per weight resource followed by one CPU summarize task. Every weight
54
+ is staged to a stable logical path, and the runtime receives only that mounted path and expected digest. It
55
+ does not download weights or know which provider stored them.
56
+
57
+ The runtime streams large variant files in fixed outer batches and validates CATv1's single-track output
58
+ contract. `profile_l1` is defined over softmax-normalized profile distributions.
59
+
60
+ The tests compose `altar.testing.ModelResultContract` and `ContainerScorerContract` and fake only the outer
61
+ Modal and Kubernetes clients, so they verify both result semantics and the plan/fan-in contract without an
62
+ ML framework, cloud account, cluster, GPU, or image pull.
@@ -0,0 +1,37 @@
1
+ # Altar Cherimoya binding
2
+
3
+ `altar-cherimoya` is the lightweight control-plane binding between Altar and the Cherimoya variant-effect
4
+ runtime. Installing it registers `CHERIMOYA` in the canonical `altar.model_plugins` discovery group.
5
+
6
+ The binding owns Cherimoya's artifact schema, score schema, prioritization predicate, resource requests, and
7
+ backend-neutral container plan. It does not import PyTorch, Cherimoya, a model registry, object-storage SDK,
8
+ or compute-provider SDK. The heavyweight `cherimoya-score` CLI and independently locked image live under
9
+ `runtimes/cherimoya` in this repository.
10
+
11
+ ```bash
12
+ pip install altar-cherimoya
13
+ ```
14
+
15
+ ```python
16
+ from altar.models import get_model_plugin
17
+
18
+ plugin = get_model_plugin("CHERIMOYA")
19
+ ```
20
+
21
+ `CherimoyaConfiguration.image` defaults to the immutable runtime digest tested with the installed binding.
22
+ Callers may override it with another `name@sha256:<digest>` image reference; mutable tags remain invalid. Its
23
+ `weights` tuple contains one generic `ResourceReference` per fold, each with a required SHA-256 digest. The
24
+ URI may be local, HTTPS, GCS, S3, Hugging Face, or any other location understood by the selected storage
25
+ adapter. The digest, not the location, defines scientific identity. Mutable images and unidentified weight
26
+ bytes fail validation before a plan can be built.
27
+
28
+ The binding emits one GPU scoring task per weight resource followed by one CPU summarize task. Every weight
29
+ is staged to a stable logical path, and the runtime receives only that mounted path and expected digest. It
30
+ does not download weights or know which provider stored them.
31
+
32
+ The runtime streams large variant files in fixed outer batches and validates CATv1's single-track output
33
+ contract. `profile_l1` is defined over softmax-normalized profile distributions.
34
+
35
+ The tests compose `altar.testing.ModelResultContract` and `ContainerScorerContract` and fake only the outer
36
+ Modal and Kubernetes clients, so they verify both result semantics and the plan/fan-in contract without an
37
+ ML framework, cloud account, cluster, GPU, or image pull.
@@ -0,0 +1,26 @@
1
+ """Cherimoya model binding for Altar."""
2
+
3
+ from altar_cherimoya.plugin import (
4
+ DEFAULT_IMAGE,
5
+ DEFAULT_IN_WINDOW,
6
+ KIND_SCORING_FOLDS,
7
+ KIND_SCORING_SUMMARIZE,
8
+ CherimoyaArtifact,
9
+ CherimoyaConfiguration,
10
+ CherimoyaPlugin,
11
+ CherimoyaRunInputs,
12
+ CherimoyaShardKind,
13
+ )
14
+
15
+
16
+ __all__ = [
17
+ "DEFAULT_IMAGE",
18
+ "DEFAULT_IN_WINDOW",
19
+ "KIND_SCORING_FOLDS",
20
+ "KIND_SCORING_SUMMARIZE",
21
+ "CherimoyaArtifact",
22
+ "CherimoyaConfiguration",
23
+ "CherimoyaPlugin",
24
+ "CherimoyaRunInputs",
25
+ "CherimoyaShardKind",
26
+ ]
@@ -0,0 +1,317 @@
1
+ """Backend-neutral Altar binding for Cherimoya variant scoring.
2
+
3
+ The plugin is deliberately a control-plane adapter. It validates typed model configuration and emits a
4
+ ``ContainerScoringPlan`` containing one GPU task per fold plus a CPU reduction task. The same plan runs
5
+ through the local, Modal, or Kubernetes execution backend; the plugin never imports an ML framework or a
6
+ provider SDK.
7
+
8
+ The corresponding ``cherimoya-score`` runtime and image definition live under ``runtimes/cherimoya`` in
9
+ the same repository. Keeping that data-plane implementation out of both ``altar`` and this binding lets the
10
+ kernel and adapter remain lightweight while model images use independent Python versions and dependency
11
+ locks.
12
+ """
13
+
14
+ from __future__ import annotations
15
+ import enum
16
+ from typing import TYPE_CHECKING, Literal, cast
17
+
18
+ from altar.execution import ContainerTaskSpec, GpuPool, ResourceRequest, Transfer
19
+ from altar.models import (
20
+ ApiVersionRange,
21
+ ConfigurationModel,
22
+ ConfigurationVersionError,
23
+ ContainerImageDigest,
24
+ ContainerScoringPlan,
25
+ LegacyArtifactError,
26
+ LegacyScoringConversion,
27
+ ModelConfiguration,
28
+ ModelPlugin,
29
+ OutputSelection,
30
+ PluginCapability,
31
+ PluginKind,
32
+ PluginManifest,
33
+ PrioritizationPolicy,
34
+ ReferenceGenomeRunInputs,
35
+ ResourceReference,
36
+ ResultField,
37
+ ResultSchema,
38
+ RuntimeIdentityPolicy,
39
+ RuntimeKind,
40
+ RuntimeRequirement,
41
+ ScoringRequest,
42
+ require_resource_digests,
43
+ resource_transfer,
44
+ )
45
+ from altar.predicates import Abs, Col, Ge, Lit, Predicate
46
+ from pydantic import Field, ValidationError, model_validator
47
+
48
+
49
+ if TYPE_CHECKING:
50
+ from collections.abc import Mapping
51
+
52
+
53
+ DEFAULT_IN_WINDOW = 2114
54
+ DEFAULT_IMAGE = "riyavsinha/altar-cherimoya@sha256:565117e3e34af57aa0f5ec851045450f7015b4b58c8b8a4a019a092c775790cc"
55
+
56
+ _LOG2FC_PRIORITIZE_THRESHOLD = 0.25
57
+ _VARIANT_BATCH_SIZE = 4096
58
+ _MODEL_BATCH_SIZE = 64
59
+
60
+
61
+ class CherimoyaShardKind(enum.StrEnum):
62
+ """Stable task kinds emitted by :class:`CherimoyaPlugin`."""
63
+
64
+ SCORING_FOLDS = "cherimoya_scoring_folds"
65
+ SCORING_SUMMARIZE = "cherimoya_scoring_summarize"
66
+
67
+
68
+ KIND_SCORING_FOLDS = CherimoyaShardKind.SCORING_FOLDS
69
+ KIND_SCORING_SUMMARIZE = CherimoyaShardKind.SCORING_SUMMARIZE
70
+
71
+ _SCORE_RESOURCES = ResourceRequest(gpu=GpuPool.NORMAL_GPU, count=1, memory_gb=32)
72
+ _SUMMARIZE_RESOURCES = ResourceRequest(memory_gb=8)
73
+
74
+
75
+ class CherimoyaConfiguration(ModelConfiguration):
76
+ """Durable model resources and scientific settings for Cherimoya.
77
+
78
+ ``image`` defaults to the immutable runtime tested with this binding release. Callers may override it
79
+ with another digest-pinned image built from ``runtimes/cherimoya``. ``weights`` contains one
80
+ provider-neutral, content-addressed resource per fold; its length is the fold count. Storage adapters
81
+ may retrieve those bytes from any location they support.
82
+ """
83
+
84
+ weights: tuple[ResourceReference, ...] = Field(min_length=1)
85
+ in_window: int = Field(default=DEFAULT_IN_WINDOW, gt=0)
86
+ image: ContainerImageDigest = DEFAULT_IMAGE
87
+
88
+ @model_validator(mode="after")
89
+ def require_content_addressed_weights(self) -> CherimoyaConfiguration:
90
+ require_resource_digests(self, owner="Cherimoya")
91
+ return self
92
+
93
+
94
+ class CherimoyaRunInputs(ReferenceGenomeRunInputs):
95
+ """Typed biological inputs restricted to CATv1's training assembly."""
96
+
97
+ @model_validator(mode="after")
98
+ def require_hg38(self) -> CherimoyaRunInputs:
99
+ if self.genome_build != "hg38":
100
+ msg = f"Cherimoya scoring currently requires genome_build='hg38', got {self.genome_build!r}"
101
+ raise ValueError(msg)
102
+ return self
103
+
104
+
105
+ class CherimoyaArtifact(ConfigurationModel):
106
+ """Strict generic artifact mapping accepted by the legacy conversion entry point."""
107
+
108
+ mode: Literal["container"] = "container"
109
+ weights: tuple[ResourceReference, ...] = Field(min_length=1)
110
+ genome: ResourceReference
111
+ in_window: int = Field(default=DEFAULT_IN_WINDOW, gt=0)
112
+ image: ContainerImageDigest = DEFAULT_IMAGE
113
+
114
+ @model_validator(mode="after")
115
+ def require_content_addressed_resources(self) -> CherimoyaArtifact:
116
+ require_resource_digests(self, owner="Cherimoya")
117
+ return self
118
+
119
+
120
+ class CherimoyaPlugin(ModelPlugin):
121
+ """Cherimoya results contract and backend-neutral container plan builder."""
122
+
123
+ model_type = "CHERIMOYA"
124
+ manifest = PluginManifest(
125
+ plugin_id="org.kundajelab.altar.model.cherimoya",
126
+ kind=PluginKind.MODEL,
127
+ capabilities=frozenset({PluginCapability.SCORE, PluginCapability.MATERIALIZE_RESULTS}),
128
+ architecture="Cherimoya",
129
+ plugin_version="0.0.1",
130
+ altar_api=ApiVersionRange("0.1.0", "0.2.0"),
131
+ configuration_schema_version="0.0.1",
132
+ result_schema=ResultSchema(
133
+ version="0.0.1",
134
+ fields=(
135
+ ResultField(
136
+ "counts_log2fc",
137
+ "float",
138
+ "Counts Log2FC",
139
+ "Alternate-minus-reference log2 effect on the single-track log(count + 1) head",
140
+ ),
141
+ ResultField(
142
+ "profile_l1",
143
+ "float",
144
+ "Profile L1",
145
+ "L1 distance between alternate and reference softmax-normalized profile distributions",
146
+ ),
147
+ ),
148
+ ),
149
+ prioritization=PrioritizationPolicy(name="cherimoya-counts-effect-v1", version="0.0.1"),
150
+ runtimes=(
151
+ RuntimeRequirement(
152
+ name="model_runtime",
153
+ kind=RuntimeKind.CONTAINER_IMAGE,
154
+ policy=RuntimeIdentityPolicy.DIGEST_REQUIRED,
155
+ ),
156
+ ),
157
+ )
158
+ configuration_schema_id = "org.kundajelab.altar.model.cherimoya.configuration"
159
+ configuration_model = CherimoyaConfiguration
160
+ run_input_model = CherimoyaRunInputs
161
+ default_output = OutputSelection(reducer="mean")
162
+
163
+ def _configuration(self, value: ModelConfiguration) -> CherimoyaConfiguration:
164
+ return cast("CherimoyaConfiguration", self.parse_configuration(value))
165
+
166
+ def migrate_configuration(
167
+ self,
168
+ from_version: str,
169
+ configuration: Mapping[str, object],
170
+ ) -> Mapping[str, object]:
171
+ """Accept an already generic resource set or reject provider-shaped configurations explicitly."""
172
+ if from_version not in {"1.0.0", "1.1.0"}:
173
+ return super().migrate_configuration(from_version, configuration)
174
+ try:
175
+ return CherimoyaConfiguration.model_validate(configuration).to_dict()
176
+ except ValidationError as error:
177
+ msg = (
178
+ f"CHERIMOYA configuration {from_version} cannot migrate automatically: replace the "
179
+ "provider-specific weights object and num_folds with a non-empty weights list containing "
180
+ "one ResourceReference(uri=..., digest='sha256:...') per fold, then resave with the current schema"
181
+ )
182
+ raise ConfigurationVersionError(msg) from error
183
+
184
+ def convert_legacy_artifact(
185
+ self,
186
+ artifact: Mapping[str, object],
187
+ *,
188
+ genome_build: str,
189
+ ) -> LegacyScoringConversion:
190
+ """Convert a strict provider-neutral artifact mapping, rejecting every unknown key."""
191
+ allowed = {"mode", "weights", "genome", "in_window", "image"}
192
+ unknown = set(artifact) - allowed
193
+ if unknown:
194
+ msg = f"unknown Cherimoya legacy artifact fields: {sorted(unknown)}"
195
+ raise LegacyArtifactError(msg)
196
+ try:
197
+ legacy = CherimoyaArtifact.model_validate(dict(artifact))
198
+ except ValidationError as exc:
199
+ msg = f"invalid Cherimoya legacy artifact: {exc}"
200
+ raise LegacyArtifactError(msg) from exc
201
+ return LegacyScoringConversion(
202
+ configuration=CherimoyaConfiguration(
203
+ weights=legacy.weights,
204
+ in_window=legacy.in_window,
205
+ image=legacy.image,
206
+ ),
207
+ run_inputs=CherimoyaRunInputs(genome_build=genome_build, genome=legacy.genome),
208
+ output=self.default_output,
209
+ )
210
+
211
+ def build_scoring_plan(self, request: ScoringRequest) -> ContainerScoringPlan:
212
+ """Build one score task per fold and a CPU task that averages their outputs."""
213
+ configuration = self._configuration(request.configuration)
214
+ run_inputs = cast("CherimoyaRunInputs", self.parse_run_inputs(request.run_inputs))
215
+ output = self.validate_output_selection(request.output)
216
+ identity = self.scoring_run_identity(
217
+ {"model_runtime": configuration.image},
218
+ configuration=configuration,
219
+ output=output,
220
+ run_inputs=run_inputs,
221
+ )
222
+ num_folds = len(configuration.weights)
223
+
224
+ resolver, layout = request.resolver, request.layout
225
+ job_id, model_id, genome = request.job_id, request.model_id, request.genome_label
226
+ scoring_batch = request.scoring_batch
227
+
228
+ genome_logical = layout.genome_fasta(genome)
229
+ variants_logical = layout.model_variants_file(job_id, model_id, scoring_batch)
230
+ batch_labels = {"scoring_batch": str(scoring_batch)} if scoring_batch is not None else {}
231
+
232
+ shards: list[ContainerTaskSpec] = []
233
+ for fold, weights in enumerate(configuration.weights):
234
+ weights_logical = layout.fold_model_file(model_id, fold, ".torch")
235
+ output_logical = layout.fold_score_file(job_id, model_id, fold, scoring_batch)
236
+ command = [
237
+ "cherimoya-score",
238
+ "score",
239
+ "--weights",
240
+ resolver.resolve(weights_logical),
241
+ "--weights-digest",
242
+ cast("str", weights.digest),
243
+ "--in-window",
244
+ str(configuration.in_window),
245
+ "--variant-batch-size",
246
+ str(_VARIANT_BATCH_SIZE),
247
+ "--model-batch-size",
248
+ str(_MODEL_BATCH_SIZE),
249
+ "-g",
250
+ resolver.resolve(genome_logical),
251
+ "--genome-digest",
252
+ cast("str", run_inputs.genome.digest),
253
+ "-v",
254
+ resolver.resolve(variants_logical),
255
+ "-o",
256
+ resolver.resolve(output_logical),
257
+ ]
258
+ shards.append(
259
+ ContainerTaskSpec(
260
+ kind=KIND_SCORING_FOLDS,
261
+ image=configuration.image,
262
+ command=command,
263
+ labels={
264
+ "job_id": job_id,
265
+ "model_id": model_id,
266
+ "fold": str(fold),
267
+ "num_folds": str(num_folds),
268
+ "kind": KIND_SCORING_FOLDS,
269
+ **batch_labels,
270
+ },
271
+ resources=_SCORE_RESOURCES,
272
+ inputs=[
273
+ resource_transfer(weights, weights_logical, locality_key=model_id),
274
+ resource_transfer(run_inputs.genome, genome_logical),
275
+ Transfer(uri=variants_logical, logical_path=variants_logical, locality_key=model_id),
276
+ ],
277
+ outputs=[Transfer(uri=output_logical, logical_path=output_logical, locality_key=model_id)],
278
+ plugin_identity=identity,
279
+ )
280
+ )
281
+
282
+ fold_score_logicals = [
283
+ layout.fold_score_file(job_id, model_id, fold, scoring_batch) for fold in range(num_folds)
284
+ ]
285
+ result_logical = layout.scoring_result_file(job_id, model_id, scoring_batch)
286
+ summarize_command = ["cherimoya-score", "summarize"]
287
+ for fold, logical in enumerate(fold_score_logicals):
288
+ summarize_command += [f"-f{fold}", resolver.resolve(logical)]
289
+ summarize_command += ["-o", resolver.resolve(result_logical)]
290
+
291
+ summarize = ContainerTaskSpec(
292
+ kind=KIND_SCORING_SUMMARIZE,
293
+ image=configuration.image,
294
+ command=summarize_command,
295
+ labels={
296
+ "job_id": job_id,
297
+ "model_id": model_id,
298
+ "kind": KIND_SCORING_SUMMARIZE,
299
+ **batch_labels,
300
+ },
301
+ resources=_SUMMARIZE_RESOURCES,
302
+ inputs=[
303
+ Transfer(uri=logical, logical_path=logical, locality_key=model_id) for logical in fold_score_logicals
304
+ ],
305
+ outputs=[Transfer(uri=result_logical, logical_path=result_logical, locality_key=model_id)],
306
+ plugin_identity=identity,
307
+ )
308
+ return ContainerScoringPlan(
309
+ shards=shards,
310
+ plugin_identity=identity,
311
+ summarize=summarize,
312
+ ready_when=num_folds,
313
+ )
314
+
315
+ def prioritize_predicate(self) -> Predicate:
316
+ # Conservative placeholder, not a calibrated biological threshold.
317
+ return Ge(Abs(Col("counts_log2fc")), Lit(_LOG2FC_PRIORITIZE_THRESHOLD))
File without changes