altar-legnet 0.2.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.
- altar_legnet-0.2.0/.gitignore +33 -0
- altar_legnet-0.2.0/LICENSE +21 -0
- altar_legnet-0.2.0/PKG-INFO +37 -0
- altar_legnet-0.2.0/README.md +14 -0
- altar_legnet-0.2.0/altar_legnet/__init__.py +25 -0
- altar_legnet-0.2.0/altar_legnet/plugin.py +202 -0
- altar_legnet-0.2.0/altar_legnet/py.typed +1 -0
- altar_legnet-0.2.0/altar_legnet/resources.py +84 -0
- altar_legnet-0.2.0/pyproject.toml +49 -0
- altar_legnet-0.2.0/tests/test_plugin.py +275 -0
- altar_legnet-0.2.0/tests/test_resources.py +95 -0
|
@@ -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,37 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: altar-legnet
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: LegNet model binding for large-scale Altar MPRA 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.3,>=0.2.0
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'test'
|
|
21
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# Altar LegNet binding
|
|
25
|
+
|
|
26
|
+
`altar-legnet` is the lightweight control-plane binding for large-scale human LegNet MPRA activity
|
|
27
|
+
variant scoring. It produces backend-neutral container plans and does not import PyTorch.
|
|
28
|
+
|
|
29
|
+
The binding scores hg38 SNVs in independently retryable Altar variant batches. Each variant contributes a
|
|
30
|
+
200 bp genomic context with the SNV at index 100; the runtime adds the published 5′ and 3′ 15 bp LentiMPRA
|
|
31
|
+
adapters before the 230 bp model inference. The stable scalar effect is signed `alt_activity - ref_activity`.
|
|
32
|
+
|
|
33
|
+
Checkpoint bytes and the matching training configuration are independent, content-addressed resources.
|
|
34
|
+
This supports the upstream K562, HepG2, and WTC11 models as well as compatible checkpoints without tying
|
|
35
|
+
Altar to Zenodo, Hugging Face, Chorus, or another provider. The isolated executor is in `runtimes/legnet`.
|
|
36
|
+
`LegNetConfiguration` uses the digest-pinned runtime tested with the binding by default; callers can override
|
|
37
|
+
it with another immutable image reference.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Altar LegNet binding
|
|
2
|
+
|
|
3
|
+
`altar-legnet` is the lightweight control-plane binding for large-scale human LegNet MPRA activity
|
|
4
|
+
variant scoring. It produces backend-neutral container plans and does not import PyTorch.
|
|
5
|
+
|
|
6
|
+
The binding scores hg38 SNVs in independently retryable Altar variant batches. Each variant contributes a
|
|
7
|
+
200 bp genomic context with the SNV at index 100; the runtime adds the published 5′ and 3′ 15 bp LentiMPRA
|
|
8
|
+
adapters before the 230 bp model inference. The stable scalar effect is signed `alt_activity - ref_activity`.
|
|
9
|
+
|
|
10
|
+
Checkpoint bytes and the matching training configuration are independent, content-addressed resources.
|
|
11
|
+
This supports the upstream K562, HepG2, and WTC11 models as well as compatible checkpoints without tying
|
|
12
|
+
Altar to Zenodo, Hugging Face, Chorus, or another provider. The isolated executor is in `runtimes/legnet`.
|
|
13
|
+
`LegNetConfiguration` uses the digest-pinned runtime tested with the binding by default; callers can override
|
|
14
|
+
it with another immutable image reference.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Copyright 2026 Riya Sinha
|
|
2
|
+
"""LegNet model binding for Altar."""
|
|
3
|
+
|
|
4
|
+
from altar_legnet.plugin import (
|
|
5
|
+
CONTEXT_LENGTH,
|
|
6
|
+
DEFAULT_IMAGE,
|
|
7
|
+
KIND_SCORING,
|
|
8
|
+
MODEL_INPUT_LENGTH,
|
|
9
|
+
LegNetConfiguration,
|
|
10
|
+
LegNetPlugin,
|
|
11
|
+
LegNetRunInputs,
|
|
12
|
+
LegNetShardKind,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"CONTEXT_LENGTH",
|
|
18
|
+
"DEFAULT_IMAGE",
|
|
19
|
+
"KIND_SCORING",
|
|
20
|
+
"MODEL_INPUT_LENGTH",
|
|
21
|
+
"LegNetConfiguration",
|
|
22
|
+
"LegNetPlugin",
|
|
23
|
+
"LegNetRunInputs",
|
|
24
|
+
"LegNetShardKind",
|
|
25
|
+
]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Copyright 2026 Riya Sinha
|
|
2
|
+
"""Backend-neutral LegNet binding for MPRA activity variant effects."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
import enum
|
|
6
|
+
from typing import TYPE_CHECKING, Annotated, Any, cast
|
|
7
|
+
|
|
8
|
+
from altar.execution import ResourceRequest
|
|
9
|
+
from altar.models import (
|
|
10
|
+
ApiVersionRange,
|
|
11
|
+
ContainerImageDigest,
|
|
12
|
+
ContainerScoringPlan,
|
|
13
|
+
ModelConfiguration,
|
|
14
|
+
ModelPlugin,
|
|
15
|
+
OutputSelection,
|
|
16
|
+
PluginCapability,
|
|
17
|
+
PluginKind,
|
|
18
|
+
PluginManifest,
|
|
19
|
+
PrioritizationPolicy,
|
|
20
|
+
ReferenceGenomeRunInputs,
|
|
21
|
+
ResourceReference,
|
|
22
|
+
ResultField,
|
|
23
|
+
ResultSchema,
|
|
24
|
+
RuntimeIdentityPolicy,
|
|
25
|
+
RuntimeKind,
|
|
26
|
+
RuntimeRequirement,
|
|
27
|
+
ScoringRequest,
|
|
28
|
+
VariantClass,
|
|
29
|
+
VariantEligibility,
|
|
30
|
+
require_resource_digests,
|
|
31
|
+
resource_transfer,
|
|
32
|
+
single_container_plan,
|
|
33
|
+
)
|
|
34
|
+
from altar.predicates import Abs, Col, Ge, Lit, Predicate
|
|
35
|
+
from pydantic import Field, model_validator
|
|
36
|
+
|
|
37
|
+
from altar_legnet.resources import manifest_resources
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
import os
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
CONTEXT_LENGTH = 200
|
|
45
|
+
MODEL_INPUT_LENGTH = 230
|
|
46
|
+
LEFT_MPRA_FLANK = "AGGACCGGATCAACT"
|
|
47
|
+
RIGHT_MPRA_FLANK = "CATTGCGTGAACCGA"
|
|
48
|
+
DEFAULT_IMAGE = "riyavsinha/altar-legnet@sha256:43a4000583f5ebda33554a21a6b627f071a43a65da76c77c625eb1c4f3d50d0d"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class LegNetShardKind(enum.StrEnum):
|
|
52
|
+
"""Stable execution kind emitted by :class:`LegNetPlugin`."""
|
|
53
|
+
|
|
54
|
+
SCORING = "legnet_scoring"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
KIND_SCORING = LegNetShardKind.SCORING
|
|
58
|
+
_SCORE_RESOURCES = ResourceRequest(memory_gb=4)
|
|
59
|
+
_ACTIVITY_DELTA_THRESHOLD = 0.5
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class LegNetConfiguration(ModelConfiguration):
|
|
63
|
+
"""Immutable human LegNet checkpoint and MPRA scoring settings."""
|
|
64
|
+
|
|
65
|
+
weights: ResourceReference
|
|
66
|
+
training_config: ResourceReference
|
|
67
|
+
cell_type: Annotated[str, Field(min_length=1)]
|
|
68
|
+
assay: Annotated[str, Field(min_length=1)] = "LentiMPRA"
|
|
69
|
+
average_reverse: bool = True
|
|
70
|
+
image: ContainerImageDigest = DEFAULT_IMAGE
|
|
71
|
+
|
|
72
|
+
@model_validator(mode="after")
|
|
73
|
+
def validate_resources(self) -> LegNetConfiguration:
|
|
74
|
+
require_resource_digests(self, owner="LegNet")
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_resource_manifest(
|
|
79
|
+
cls, path: str | os.PathLike[str], *, base_uri: str | None = None, **settings: Any
|
|
80
|
+
) -> LegNetConfiguration:
|
|
81
|
+
"""Build a configuration from the ``legnet.resources.json`` that ``legnet-resources`` wrote.
|
|
82
|
+
|
|
83
|
+
``settings`` are the remaining configuration fields: ``assay``, ``average_reverse`` and ``image``, and
|
|
84
|
+
``cell_type`` to override the one the manifest records for the checkpoint. See
|
|
85
|
+
:func:`altar_legnet.resources.manifest_resources` for how ``base_uri`` locates the files.
|
|
86
|
+
"""
|
|
87
|
+
resources = manifest_resources(path, base_uri=base_uri)
|
|
88
|
+
settings.setdefault("cell_type", resources.cell_type)
|
|
89
|
+
return cls(weights=resources.weights, training_config=resources.training_config, **settings)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class LegNetRunInputs(ReferenceGenomeRunInputs):
|
|
93
|
+
"""Per-run inputs for the published human MPRA checkpoints."""
|
|
94
|
+
|
|
95
|
+
supported_genome_builds = frozenset({"hg38"})
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class LegNetPlugin(ModelPlugin):
|
|
99
|
+
"""Human LegNet sequence-to-MPRA activity semantics and scalable batch plan."""
|
|
100
|
+
|
|
101
|
+
model_type = "LEGNET"
|
|
102
|
+
manifest = PluginManifest(
|
|
103
|
+
plugin_id="org.kundajelab.altar.model.legnet",
|
|
104
|
+
kind=PluginKind.MODEL,
|
|
105
|
+
capabilities=frozenset({PluginCapability.SCORE, PluginCapability.MATERIALIZE_RESULTS}),
|
|
106
|
+
architecture="LegNet",
|
|
107
|
+
plugin_version="0.0.1",
|
|
108
|
+
altar_api=ApiVersionRange("0.1.0", "0.2.0"),
|
|
109
|
+
configuration_schema_version="0.0.1",
|
|
110
|
+
result_schema=ResultSchema(
|
|
111
|
+
version="0.0.1",
|
|
112
|
+
fields=(
|
|
113
|
+
ResultField("ref_activity", "float", "Reference Activity", "Predicted reference MPRA activity"),
|
|
114
|
+
ResultField("alt_activity", "float", "Alternate Activity", "Predicted alternate MPRA activity"),
|
|
115
|
+
ResultField(
|
|
116
|
+
"activity_delta",
|
|
117
|
+
"float",
|
|
118
|
+
"Activity Delta",
|
|
119
|
+
"Signed alternate-minus-reference predicted MPRA activity",
|
|
120
|
+
),
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
# The LegNet runtime substitutes one reference base; batch preparation filters out indels and MNVs.
|
|
124
|
+
variant_eligibility=VariantEligibility(frozenset({VariantClass.SNV})),
|
|
125
|
+
prioritization=PrioritizationPolicy(name="legnet-activity-delta-v1", version="0.0.1"),
|
|
126
|
+
runtimes=(
|
|
127
|
+
RuntimeRequirement(
|
|
128
|
+
name="model_runtime",
|
|
129
|
+
kind=RuntimeKind.CONTAINER_IMAGE,
|
|
130
|
+
policy=RuntimeIdentityPolicy.DIGEST_REQUIRED,
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
configuration_schema_id = "org.kundajelab.altar.model.legnet.configuration"
|
|
135
|
+
configuration_model = LegNetConfiguration
|
|
136
|
+
run_input_model = LegNetRunInputs
|
|
137
|
+
default_output = OutputSelection(outputs=("scores",), reducer="alt_minus_ref_activity_v1")
|
|
138
|
+
|
|
139
|
+
def build_scoring_plan(self, request: ScoringRequest) -> ContainerScoringPlan:
|
|
140
|
+
configuration = self.parse_configuration_as(request.configuration, LegNetConfiguration)
|
|
141
|
+
run_inputs = self.parse_run_inputs_as(request.run_inputs, LegNetRunInputs)
|
|
142
|
+
layout, resolver = request.layout, request.resolver
|
|
143
|
+
weights_logical = layout.model_resource_file(request.model_id, "legnet.weights.ckpt")
|
|
144
|
+
config_logical = layout.model_resource_file(request.model_id, "legnet.config.json")
|
|
145
|
+
genome_logical = layout.genome_fasta(request.genome_label)
|
|
146
|
+
variants_logical = layout.model_variants_file(request.job_id, request.model_id, request.scoring_batch)
|
|
147
|
+
output_logical = layout.scoring_result_file(request.job_id, request.model_id, request.scoring_batch)
|
|
148
|
+
command = [
|
|
149
|
+
"legnet-score",
|
|
150
|
+
"score",
|
|
151
|
+
"--weights",
|
|
152
|
+
resolver.resolve(weights_logical),
|
|
153
|
+
"--weights-digest",
|
|
154
|
+
cast("str", configuration.weights.digest),
|
|
155
|
+
"--model-config",
|
|
156
|
+
resolver.resolve(config_logical),
|
|
157
|
+
"--model-config-digest",
|
|
158
|
+
cast("str", configuration.training_config.digest),
|
|
159
|
+
"--cell-type",
|
|
160
|
+
configuration.cell_type,
|
|
161
|
+
"--assay",
|
|
162
|
+
configuration.assay,
|
|
163
|
+
"--genome",
|
|
164
|
+
resolver.resolve(genome_logical),
|
|
165
|
+
"--genome-digest",
|
|
166
|
+
cast("str", run_inputs.genome.digest),
|
|
167
|
+
"--variants",
|
|
168
|
+
resolver.resolve(variants_logical),
|
|
169
|
+
"--output",
|
|
170
|
+
resolver.resolve(output_logical),
|
|
171
|
+
]
|
|
172
|
+
if configuration.average_reverse:
|
|
173
|
+
command.append("--average-reverse")
|
|
174
|
+
return single_container_plan(
|
|
175
|
+
self,
|
|
176
|
+
request,
|
|
177
|
+
kind=KIND_SCORING,
|
|
178
|
+
image=configuration.image,
|
|
179
|
+
command=command,
|
|
180
|
+
resources=_SCORE_RESOURCES,
|
|
181
|
+
inputs=[
|
|
182
|
+
resource_transfer(configuration.weights, weights_logical, locality_key=request.model_id),
|
|
183
|
+
resource_transfer(configuration.training_config, config_logical, locality_key=request.model_id),
|
|
184
|
+
resource_transfer(run_inputs.genome, genome_logical),
|
|
185
|
+
],
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def prioritize_predicate(self) -> Predicate:
|
|
189
|
+
return Ge(Abs(Col("activity_delta")), Lit(_ACTIVITY_DELTA_THRESHOLD))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
__all__ = [
|
|
193
|
+
"CONTEXT_LENGTH",
|
|
194
|
+
"KIND_SCORING",
|
|
195
|
+
"LEFT_MPRA_FLANK",
|
|
196
|
+
"MODEL_INPUT_LENGTH",
|
|
197
|
+
"RIGHT_MPRA_FLANK",
|
|
198
|
+
"LegNetConfiguration",
|
|
199
|
+
"LegNetPlugin",
|
|
200
|
+
"LegNetRunInputs",
|
|
201
|
+
"LegNetShardKind",
|
|
202
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Copyright 2026 Riya Sinha
|
|
2
|
+
"""The manifest ``legnet-resources`` writes beside a prepared LegNet checkpoint, read back into references.
|
|
3
|
+
|
|
4
|
+
The LegNet runtime's ``legnet-resources`` command (``altar_legnet_runtime.resources``) validates a checkpoint by
|
|
5
|
+
loading it with PyTorch, copies it and its training configuration under stable names, and writes
|
|
6
|
+
``legnet.resources.json`` beside them with each file's SHA-256 digest and the checkpoint's cell type. The
|
|
7
|
+
preparer stays in the runtime because loading the checkpoint needs torch; this module knows only the manifest
|
|
8
|
+
it writes, so ``LegNetConfiguration.from_resource_manifest`` turns it into content-addressed
|
|
9
|
+
``ResourceReference`` values without anyone copying digests by hand.
|
|
10
|
+
|
|
11
|
+
Provider locations never enter the manifest. A reference's ``uri`` names the file beside the manifest, or
|
|
12
|
+
``base_uri`` joined with the recorded filename when the prepared directory was copied elsewhere; the storage
|
|
13
|
+
adapter that stages a reference decides where the bytes come from and verifies them against the digest.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
import json
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import TYPE_CHECKING, Any
|
|
21
|
+
|
|
22
|
+
from altar.models import ResourceReference
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
import os
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
ARCHITECTURE = "human LegNet"
|
|
30
|
+
MANIFEST_SCHEMA_VERSION = "0.0.1"
|
|
31
|
+
MANIFEST_FILENAME = "legnet.resources.json"
|
|
32
|
+
# The manifest's resource keys; the runtime records the training configuration as ``model_config``.
|
|
33
|
+
_WEIGHTS_RESOURCE = "weights"
|
|
34
|
+
_CONFIG_RESOURCE = "model_config"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class ManifestResources:
|
|
39
|
+
"""Location-neutral references and the checkpoint's cell type, read from one ``legnet.resources.json``."""
|
|
40
|
+
|
|
41
|
+
weights: ResourceReference
|
|
42
|
+
training_config: ResourceReference
|
|
43
|
+
cell_type: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def manifest_resources(path: str | os.PathLike[str], *, base_uri: str | None = None) -> ManifestResources:
|
|
47
|
+
"""Read the manifest the runtime's preparer writes into the weights and training-configuration references.
|
|
48
|
+
|
|
49
|
+
Each reference's ``uri`` names the file beside the manifest, or ``base_uri`` joined with the recorded
|
|
50
|
+
filename when the prepared directory was copied elsewhere, such as an object-storage prefix. The digests
|
|
51
|
+
are the manifest's; the storage adapter that stages a reference verifies the bytes against them. Raises
|
|
52
|
+
``ValueError`` for a manifest of another architecture or schema, or one that lacks a resource or the
|
|
53
|
+
checkpoint's cell type.
|
|
54
|
+
"""
|
|
55
|
+
manifest_path = Path(path)
|
|
56
|
+
manifest: dict[str, Any] = json.loads(manifest_path.read_text())
|
|
57
|
+
if manifest.get("architecture") != ARCHITECTURE or manifest.get("schema_version") != MANIFEST_SCHEMA_VERSION:
|
|
58
|
+
msg = f"{str(manifest_path)!r} is not a {ARCHITECTURE} resource manifest with schema {MANIFEST_SCHEMA_VERSION}"
|
|
59
|
+
raise ValueError(msg)
|
|
60
|
+
records = manifest.get("resources", {})
|
|
61
|
+
missing = [name for name in (_CONFIG_RESOURCE, _WEIGHTS_RESOURCE) if name not in records]
|
|
62
|
+
if missing:
|
|
63
|
+
msg = f"{str(manifest_path)!r} is missing resources {missing}"
|
|
64
|
+
raise ValueError(msg)
|
|
65
|
+
cell_type = manifest.get("cell_type")
|
|
66
|
+
if not isinstance(cell_type, str) or not cell_type:
|
|
67
|
+
msg = f"{str(manifest_path)!r} does not record the checkpoint's cell_type"
|
|
68
|
+
raise ValueError(msg)
|
|
69
|
+
|
|
70
|
+
def reference(name: str) -> ResourceReference:
|
|
71
|
+
record = records[name]
|
|
72
|
+
return ResourceReference(
|
|
73
|
+
uri=_resource_uri(manifest_path.parent, record["filename"], base_uri), digest=record["digest"]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return ManifestResources(
|
|
77
|
+
weights=reference(_WEIGHTS_RESOURCE), training_config=reference(_CONFIG_RESOURCE), cell_type=cell_type
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _resource_uri(directory: Path, filename: str, base_uri: str | None) -> str:
|
|
82
|
+
if base_uri is None:
|
|
83
|
+
return str(directory / filename)
|
|
84
|
+
return f"{base_uri.rstrip('/')}/{filename}"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "altar-legnet"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "LegNet model binding for large-scale Altar MPRA variant scoring"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
authors = [{ name = "Riya Sinha" }]
|
|
8
|
+
license = "MIT"
|
|
9
|
+
license-files = ["LICENSE"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Science/Research",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
18
|
+
"Typing :: Typed",
|
|
19
|
+
]
|
|
20
|
+
dependencies = ["altar>=0.2.0,<0.3"]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Documentation = "https://kundajelab.github.io/altar/"
|
|
24
|
+
|
|
25
|
+
[project.entry-points."altar.model_plugins"]
|
|
26
|
+
LEGNET = "altar_legnet:LegNetPlugin"
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
test = [
|
|
30
|
+
"pytest>=8",
|
|
31
|
+
"pytest-asyncio>=0.24",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["hatchling"]
|
|
36
|
+
build-backend = "hatchling.build"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["altar_legnet"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
asyncio_mode = "auto"
|
|
43
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
44
|
+
testpaths = ["tests"]
|
|
45
|
+
|
|
46
|
+
[tool.mypy]
|
|
47
|
+
python_version = "3.12"
|
|
48
|
+
strict = true
|
|
49
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Copyright 2026 Riya Sinha
|
|
2
|
+
"""Contract tests for the backend-neutral LegNet binding."""
|
|
3
|
+
|
|
4
|
+
# Assertions use fixed golden values intentionally.
|
|
5
|
+
# ruff: noqa: PLR2004
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from altar.execution import LocalStorage, PathResolver
|
|
15
|
+
from altar.models import (
|
|
16
|
+
ContainerImageDigest,
|
|
17
|
+
ContainerScoringPlan,
|
|
18
|
+
ResourceReference,
|
|
19
|
+
ScoringLayout,
|
|
20
|
+
ScoringRequest,
|
|
21
|
+
VariantClass,
|
|
22
|
+
VariantKey,
|
|
23
|
+
get_model_plugin_registry,
|
|
24
|
+
scoring_plan_from_json,
|
|
25
|
+
scoring_plan_to_json,
|
|
26
|
+
)
|
|
27
|
+
from altar.testing import ContainerScorerContract, ModelResultContract
|
|
28
|
+
from altar_legnet import (
|
|
29
|
+
CONTEXT_LENGTH,
|
|
30
|
+
DEFAULT_IMAGE,
|
|
31
|
+
KIND_SCORING,
|
|
32
|
+
MODEL_INPUT_LENGTH,
|
|
33
|
+
LegNetConfiguration,
|
|
34
|
+
LegNetPlugin,
|
|
35
|
+
LegNetRunInputs,
|
|
36
|
+
)
|
|
37
|
+
from pydantic import TypeAdapter, ValidationError
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
PLUGIN = LegNetPlugin()
|
|
41
|
+
IMAGE = f"ghcr.io/kundajelab/altar-legnet@sha256:{'1' * 64}"
|
|
42
|
+
WEIGHTS_DIGEST = f"sha256:{'2' * 64}"
|
|
43
|
+
CONFIG_DIGEST = f"sha256:{'3' * 64}"
|
|
44
|
+
GENOME_DIGEST = f"sha256:{'4' * 64}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _configuration(**overrides: object) -> LegNetConfiguration:
|
|
48
|
+
values: dict[str, object] = {
|
|
49
|
+
"weights": ResourceReference(uri="s3://models/WTC11.ckpt", digest=WEIGHTS_DIGEST),
|
|
50
|
+
"training_config": ResourceReference(uri="s3://models/WTC11.json", digest=CONFIG_DIGEST),
|
|
51
|
+
"cell_type": "WTC11",
|
|
52
|
+
"average_reverse": True,
|
|
53
|
+
"image": IMAGE,
|
|
54
|
+
}
|
|
55
|
+
values.update(overrides)
|
|
56
|
+
return LegNetConfiguration.model_validate(values)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _request(*, scoring_batch: int | None = 3) -> ScoringRequest:
|
|
60
|
+
return ScoringRequest(
|
|
61
|
+
job_id="J",
|
|
62
|
+
model_id="M",
|
|
63
|
+
configuration=_configuration(),
|
|
64
|
+
run_inputs=LegNetRunInputs(
|
|
65
|
+
genome_build="hg38",
|
|
66
|
+
genome=ResourceReference(uri="s3://genomes/hg38.fa", digest=GENOME_DIGEST),
|
|
67
|
+
),
|
|
68
|
+
output=PLUGIN.default_output,
|
|
69
|
+
resolver=PathResolver("/mnt/volume"),
|
|
70
|
+
layout=ScoringLayout(),
|
|
71
|
+
scoring_batch=scoring_batch,
|
|
72
|
+
scoring_batch_count=16,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestLegNetResultsConformance(ModelResultContract):
|
|
77
|
+
@pytest.fixture
|
|
78
|
+
def plugin(self) -> LegNetPlugin:
|
|
79
|
+
return LegNetPlugin()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TestLegNetContainerConformance(ContainerScorerContract):
|
|
83
|
+
@pytest.fixture
|
|
84
|
+
def plugin(self) -> LegNetPlugin:
|
|
85
|
+
return LegNetPlugin()
|
|
86
|
+
|
|
87
|
+
@pytest.fixture
|
|
88
|
+
def scoring_request(self) -> ScoringRequest:
|
|
89
|
+
return _request()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_registration_manifest_and_plan_shape() -> None:
|
|
93
|
+
assert get_model_plugin_registry().get("LEGNET") is LegNetPlugin
|
|
94
|
+
assert {
|
|
95
|
+
PLUGIN.manifest.plugin_version,
|
|
96
|
+
PLUGIN.manifest.configuration_schema_version,
|
|
97
|
+
PLUGIN.manifest.result_schema.version,
|
|
98
|
+
PLUGIN.manifest.prioritization.version,
|
|
99
|
+
} == {"0.0.1"}
|
|
100
|
+
assert CONTEXT_LENGTH == 200
|
|
101
|
+
assert MODEL_INPUT_LENGTH == 230
|
|
102
|
+
plan = PLUGIN.build_scoring_plan(_request())
|
|
103
|
+
assert isinstance(plan, ContainerScoringPlan)
|
|
104
|
+
assert len(plan.shards) == plan.ready_when == 1
|
|
105
|
+
assert plan.shards[0].kind == KIND_SCORING
|
|
106
|
+
assert plan.shards[0].resources.gpu is None
|
|
107
|
+
assert plan.shards[0].resources.memory_gb == 4
|
|
108
|
+
assert scoring_plan_from_json(scoring_plan_to_json(plan)) == plan
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_binding_supplies_digest_pinned_default_image() -> None:
|
|
112
|
+
configuration = _configuration()
|
|
113
|
+
configuration = LegNetConfiguration.model_validate(
|
|
114
|
+
{key: value for key, value in configuration.to_dict().items() if key != "image"}
|
|
115
|
+
)
|
|
116
|
+
assert configuration.image == DEFAULT_IMAGE
|
|
117
|
+
assert TypeAdapter(ContainerImageDigest).validate_python(DEFAULT_IMAGE) == DEFAULT_IMAGE
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_plan_stages_content_addressed_resources_and_primary_result() -> None:
|
|
121
|
+
plan = PLUGIN.build_scoring_plan(_request())
|
|
122
|
+
task = plan.shards[0]
|
|
123
|
+
transfers = {item.logical_path: item for item in task.inputs}
|
|
124
|
+
assert transfers["models/M/legnet.weights.ckpt"].digest == WEIGHTS_DIGEST
|
|
125
|
+
assert transfers["models/M/legnet.config.json"].digest == CONFIG_DIGEST
|
|
126
|
+
assert transfers["genomes/hg38/genome.fasta"].digest == GENOME_DIGEST
|
|
127
|
+
assert task.command[task.command.index("--genome-digest") + 1] == GENOME_DIGEST
|
|
128
|
+
assert "--average-reverse" in task.command
|
|
129
|
+
assert task.command[0:2] == ["legnet-score", "score"]
|
|
130
|
+
assert [result.name for result in plan.result_files] == ["primary"]
|
|
131
|
+
assert [item.logical_path for item in task.outputs] == ["jobs/J/results/M/batch_000003/mean.variant_scores.tsv"]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_forward_only_mode_changes_scientific_identity_and_command() -> None:
|
|
135
|
+
averaged = PLUGIN.build_scoring_plan(_request())
|
|
136
|
+
request = _request()
|
|
137
|
+
request = ScoringRequest(
|
|
138
|
+
job_id=request.job_id,
|
|
139
|
+
model_id=request.model_id,
|
|
140
|
+
configuration=_configuration(average_reverse=False),
|
|
141
|
+
run_inputs=request.run_inputs,
|
|
142
|
+
output=request.output,
|
|
143
|
+
resolver=request.resolver,
|
|
144
|
+
layout=request.layout,
|
|
145
|
+
)
|
|
146
|
+
forward = PLUGIN.build_scoring_plan(request)
|
|
147
|
+
assert "--average-reverse" not in forward.shards[0].command
|
|
148
|
+
assert (
|
|
149
|
+
averaged.plugin_identity.scientific_configuration_hash != forward.plugin_identity.scientific_configuration_hash
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_batch_paths_change_without_changing_scientific_identity() -> None:
|
|
154
|
+
first = PLUGIN.build_scoring_plan(_request(scoring_batch=0))
|
|
155
|
+
second = PLUGIN.build_scoring_plan(_request(scoring_batch=1))
|
|
156
|
+
assert first.plugin_identity == second.plugin_identity
|
|
157
|
+
assert first.shards[0].outputs != second.shards[0].outputs
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_resource_relocation_does_not_change_scientific_identity() -> None:
|
|
161
|
+
first_request = _request()
|
|
162
|
+
relocated = _configuration(
|
|
163
|
+
weights=ResourceReference(uri="https://mirror.example/WTC11.ckpt", digest=WEIGHTS_DIGEST),
|
|
164
|
+
training_config=ResourceReference(uri="file:///models/WTC11.json", digest=CONFIG_DIGEST),
|
|
165
|
+
)
|
|
166
|
+
second_request = ScoringRequest(
|
|
167
|
+
job_id="other-job",
|
|
168
|
+
model_id="other-model",
|
|
169
|
+
configuration=relocated,
|
|
170
|
+
run_inputs=first_request.run_inputs,
|
|
171
|
+
output=first_request.output,
|
|
172
|
+
resolver=first_request.resolver,
|
|
173
|
+
layout=first_request.layout,
|
|
174
|
+
)
|
|
175
|
+
first = PLUGIN.build_scoring_plan(first_request)
|
|
176
|
+
second = PLUGIN.build_scoring_plan(second_request)
|
|
177
|
+
assert first.plugin_identity.scientific_configuration_hash == second.plugin_identity.scientific_configuration_hash
|
|
178
|
+
assert first.plugin_identity.configuration_hash != second.plugin_identity.configuration_hash
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@pytest.mark.parametrize("field", ["weights", "training_config"])
|
|
182
|
+
def test_resources_require_digests(field: str) -> None:
|
|
183
|
+
with pytest.raises(ValidationError, match="require sha256 digests"):
|
|
184
|
+
_configuration(**{field: ResourceReference(uri=f"s3://models/{field}")})
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_only_hg38_is_accepted() -> None:
|
|
188
|
+
with pytest.raises(ValidationError, match="requires genome_build='hg38'"):
|
|
189
|
+
LegNetRunInputs(
|
|
190
|
+
genome_build="hg19",
|
|
191
|
+
genome=ResourceReference(uri="s3://genomes/hg19.fa", digest=GENOME_DIGEST),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_prioritization_and_materialization_preserve_signed_delta() -> None:
|
|
196
|
+
assert PLUGIN.prioritize({"activity_delta": -0.75}, {})
|
|
197
|
+
assert not PLUGIN.prioritize({"activity_delta": 0.2}, {})
|
|
198
|
+
score = PLUGIN.to_model_score(
|
|
199
|
+
model_id="m1",
|
|
200
|
+
model_name="LegNet WTC11",
|
|
201
|
+
score={"ref_activity": 2.0, "alt_activity": 1.25, "activity_delta": -0.75, "prioritized": True},
|
|
202
|
+
)
|
|
203
|
+
assert score["activity_delta"] == -0.75
|
|
204
|
+
assert score["ref_activity"] == 2.0
|
|
205
|
+
missing = PLUGIN.to_model_score(
|
|
206
|
+
model_id="m1", model_name="LegNet", score={"activity_delta": math.nan, "prioritized": None}
|
|
207
|
+
)
|
|
208
|
+
assert missing["activity_delta"] is None
|
|
209
|
+
assert missing["prioritized"] is False
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
async def test_codec_consumes_activity_rows(tmp_path: Path) -> None:
|
|
213
|
+
primary = tmp_path / "primary.tsv"
|
|
214
|
+
primary.write_text("variant_id\tref_activity\talt_activity\tactivity_delta\nchr1:7:A:T\t2.0\t1.25\t-0.75\n")
|
|
215
|
+
batches = [batch async for batch in PLUGIN.scoring_result_codec().decode(LocalStorage(), [str(primary)])]
|
|
216
|
+
assert batches[0].rows[0]["variant_id"] == "chr1:7:A:T"
|
|
217
|
+
assert batches[0].rows[0]["activity_delta"] == -0.75
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_manifest_declares_the_runtime_snv_only_input() -> None:
|
|
221
|
+
eligibility = PLUGIN.manifest.variant_eligibility
|
|
222
|
+
assert eligibility.variant_classes == {VariantClass.SNV}
|
|
223
|
+
assert eligibility.admits(VariantKey.parse("chr1:100:A:G"))
|
|
224
|
+
for variant_id in ("chr1:100:A:AT", "chr1:100:AT:A", "chr1:100:AC:GT"):
|
|
225
|
+
assert not eligibility.admits(VariantKey.parse(variant_id)), f"the LegNet runtime rejects {variant_id}"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# The paired runtime's tests parse every argv in this file with the runtime's own CLI parser.
|
|
229
|
+
RUNTIME_COMMANDS = Path(__file__).parents[3] / "runtimes" / "legnet" / "tests" / "data" / "binding_commands.json"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _golden_request() -> ScoringRequest:
|
|
233
|
+
"""Pin the command coverage independently of the other tests' helpers.
|
|
234
|
+
|
|
235
|
+
Covers --cell-type, --assay, and the optional --average-reverse.
|
|
236
|
+
"""
|
|
237
|
+
configuration = LegNetConfiguration(
|
|
238
|
+
weights=ResourceReference(uri="s3://models/HepG2.ckpt", digest=WEIGHTS_DIGEST),
|
|
239
|
+
training_config=ResourceReference(uri="s3://models/HepG2.json", digest=CONFIG_DIGEST),
|
|
240
|
+
cell_type="HepG2",
|
|
241
|
+
assay="LentiMPRA",
|
|
242
|
+
average_reverse=True,
|
|
243
|
+
image=IMAGE,
|
|
244
|
+
)
|
|
245
|
+
run_inputs = LegNetRunInputs(
|
|
246
|
+
genome_build="hg38", genome=ResourceReference(uri="s3://genomes/hg38.fa", digest=GENOME_DIGEST)
|
|
247
|
+
)
|
|
248
|
+
return ScoringRequest(
|
|
249
|
+
job_id="J",
|
|
250
|
+
model_id="M",
|
|
251
|
+
configuration=configuration,
|
|
252
|
+
run_inputs=run_inputs,
|
|
253
|
+
output=PLUGIN.default_output,
|
|
254
|
+
resolver=PathResolver("/mnt/volume"),
|
|
255
|
+
layout=ScoringLayout(),
|
|
256
|
+
scoring_batch=4,
|
|
257
|
+
scoring_batch_count=5,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_runtime_command_golden_matches_plan() -> None:
|
|
262
|
+
plan = PLUGIN.build_scoring_plan(_golden_request())
|
|
263
|
+
tasks = [*plan.shards, *([plan.summarize] if plan.summarize is not None else [])]
|
|
264
|
+
golden = {
|
|
265
|
+
"generated_by": "bindings/legnet/tests/test_plugin.py (ALTAR_UPDATE_GOLDENS=1 rewrites this file)",
|
|
266
|
+
"commands": [{"kind": task.kind, "argv": list(task.command)} for task in tasks],
|
|
267
|
+
}
|
|
268
|
+
text = json.dumps(golden, indent=2) + "\n"
|
|
269
|
+
if os.environ.get("ALTAR_UPDATE_GOLDENS") == "1":
|
|
270
|
+
if os.environ.get("CI"):
|
|
271
|
+
pytest.fail("ALTAR_UPDATE_GOLDENS must not rewrite goldens in CI")
|
|
272
|
+
RUNTIME_COMMANDS.write_text(text)
|
|
273
|
+
assert json.loads(RUNTIME_COMMANDS.read_text()) == json.loads(text), (
|
|
274
|
+
"the runtime command changed; rerun with ALTAR_UPDATE_GOLDENS=1, then run the runtime's tests"
|
|
275
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Copyright 2026 Riya Sinha
|
|
2
|
+
"""The LegNet resource manifest: written by the runtime's preparer, read back by the binding's configuration."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from altar.models import sha256_file
|
|
11
|
+
from altar_legnet import LegNetConfiguration, LegNetPlugin
|
|
12
|
+
from altar_legnet.resources import ARCHITECTURE, MANIFEST_FILENAME, MANIFEST_SCHEMA_VERSION, manifest_resources
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
IMAGE = f"ghcr.io/kundajelab/altar-legnet@sha256:{'1' * 64}"
|
|
16
|
+
# The one manifest golden: a directory as `legnet-resources package` leaves it, which the runtime's
|
|
17
|
+
# tests/test_resource_manifest.py regenerates from its own preparer (ALTAR_UPDATE_GOLDENS=1).
|
|
18
|
+
PREPARED = Path(__file__).parents[3] / "runtimes" / "legnet" / "tests" / "data" / "prepared_resources"
|
|
19
|
+
# The names `altar_legnet_runtime.resources` gives the prepared checkpoint and training configuration.
|
|
20
|
+
WEIGHTS_FILENAME = "legnet.weights.ckpt"
|
|
21
|
+
CONFIG_FILENAME = "legnet.config.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_manifest_resources_are_content_addressed_and_located_beside_the_manifest() -> None:
|
|
25
|
+
manifest_path = PREPARED / MANIFEST_FILENAME
|
|
26
|
+
|
|
27
|
+
resources = manifest_resources(manifest_path)
|
|
28
|
+
|
|
29
|
+
assert resources.weights.uri == str(manifest_path.parent / WEIGHTS_FILENAME)
|
|
30
|
+
assert resources.training_config.uri == str(manifest_path.parent / CONFIG_FILENAME)
|
|
31
|
+
assert resources.weights.digest == sha256_file(resources.weights.uri)
|
|
32
|
+
assert resources.training_config.digest == sha256_file(resources.training_config.uri)
|
|
33
|
+
assert resources.cell_type == "WTC11"
|
|
34
|
+
relocated = manifest_resources(manifest_path, base_uri="gs://bucket/legnet-wtc11/")
|
|
35
|
+
assert relocated.weights.uri == f"gs://bucket/legnet-wtc11/{WEIGHTS_FILENAME}"
|
|
36
|
+
assert relocated.training_config.uri == f"gs://bucket/legnet-wtc11/{CONFIG_FILENAME}"
|
|
37
|
+
assert relocated.weights.digest == resources.weights.digest
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_RECORD = {"filename": "f", "digest": f"sha256:{'2' * 64}", "size_bytes": 1, "source": "s"}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.parametrize(
|
|
44
|
+
("manifest", "match"),
|
|
45
|
+
[
|
|
46
|
+
(
|
|
47
|
+
{"architecture": "Enformer", "schema_version": MANIFEST_SCHEMA_VERSION, "resources": {}},
|
|
48
|
+
"is not a human LegNet resource manifest with schema 0.0.1",
|
|
49
|
+
),
|
|
50
|
+
(
|
|
51
|
+
{"architecture": ARCHITECTURE, "schema_version": "0.0.2", "resources": {}},
|
|
52
|
+
"is not a human LegNet resource manifest with schema 0.0.1",
|
|
53
|
+
),
|
|
54
|
+
(
|
|
55
|
+
{
|
|
56
|
+
"architecture": ARCHITECTURE,
|
|
57
|
+
"schema_version": MANIFEST_SCHEMA_VERSION,
|
|
58
|
+
"cell_type": "WTC11",
|
|
59
|
+
"resources": {"weights": _RECORD},
|
|
60
|
+
},
|
|
61
|
+
r"is missing resources \['model_config'\]",
|
|
62
|
+
),
|
|
63
|
+
(
|
|
64
|
+
{
|
|
65
|
+
"architecture": ARCHITECTURE,
|
|
66
|
+
"schema_version": MANIFEST_SCHEMA_VERSION,
|
|
67
|
+
"resources": {"weights": _RECORD, "model_config": _RECORD},
|
|
68
|
+
},
|
|
69
|
+
"does not record the checkpoint's cell_type",
|
|
70
|
+
),
|
|
71
|
+
],
|
|
72
|
+
)
|
|
73
|
+
def test_manifest_resources_rejects_other_or_incomplete_manifests(
|
|
74
|
+
tmp_path: Path, manifest: dict[str, Any], match: str
|
|
75
|
+
) -> None:
|
|
76
|
+
path = tmp_path / MANIFEST_FILENAME
|
|
77
|
+
path.write_text(json.dumps(manifest))
|
|
78
|
+
with pytest.raises(ValueError, match=match):
|
|
79
|
+
manifest_resources(path)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_configuration_from_resource_manifest() -> None:
|
|
83
|
+
manifest_path = PREPARED / MANIFEST_FILENAME
|
|
84
|
+
|
|
85
|
+
configuration = LegNetConfiguration.from_resource_manifest(manifest_path, image=IMAGE, average_reverse=False)
|
|
86
|
+
|
|
87
|
+
resources = manifest_resources(manifest_path)
|
|
88
|
+
assert configuration.weights == resources.weights
|
|
89
|
+
assert configuration.training_config == resources.training_config
|
|
90
|
+
assert (configuration.cell_type, configuration.assay) == ("WTC11", "LentiMPRA")
|
|
91
|
+
assert (configuration.average_reverse, configuration.image) == (False, IMAGE)
|
|
92
|
+
# The manifest's cell type is a default; a setting overrides it like any other field.
|
|
93
|
+
assert LegNetConfiguration.from_resource_manifest(manifest_path, cell_type="HepG2").cell_type == "HepG2"
|
|
94
|
+
# The run driver hands the built configuration to the plugin, which accepts it as its own.
|
|
95
|
+
assert LegNetPlugin().parse_configuration(configuration) is configuration
|