File size: 11,112 Bytes
4516781
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# Copyright 2026 Google LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Runs AlphaGenome track prediction evaluation.

By default this script refuses to download model weights. Pass --model_dir to
load a local Orbax checkpoint, or explicitly pass --allow_download=true to use
the original Kaggle Hub path.
"""

import functools
import pathlib
import pprint
import sys

_PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[1]
if str(_PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(_PROJECT_ROOT))

from absl import app
from absl import flags
from absl import logging
from flax_model.alphagenome._sdk.data import fold_intervals
from flax_model.alphagenome._sdk.models import dna_output
from flax_model.alphagenome._sdk.models import dna_model as dna_model_types
from flax_model.alphagenome.evals.track_prediction import (
    load_model as load_model_from_kaggle,
)
from flax_model.alphagenome.evals import regression_metrics
from flax_model.alphagenome.io.bundles import BundleName
from flax_model.alphagenome.io.dataset import get_numpy_dataset_iterator
from flax_model.alphagenome.model import dna_model as research_dna_model
from flax_model.alphagenome.model.metadata import metadata as metadata_lib
import jax
import jax.numpy as jnp
from jax import sharding
from jax.experimental import mesh_utils
import orbax.checkpoint as ocp
import pandas as pd
import tensorflow as tf


FLAGS = flags.FLAGS
PS = sharding.PartitionSpec

flags.DEFINE_string(
    "model_version",
    "FOLD_0",
    "Evaluation fold/model version. Supported values are "
    + ",".join(version.name for version in dna_model_types.ModelVersion)
    + ".",
)
flags.DEFINE_enum(
    "organism",
    "HOMO_SAPIENS",
    ["HOMO_SAPIENS", "MUS_MUSCULUS"],
    "Organism to evaluate.",
)
flags.DEFINE_string(
    "output_path",
    "./track_prediction_results.csv",
    "Output CSV path.",
)
flags.DEFINE_string(
    "model_dir",
    None,
    "Local AlphaGenome Orbax checkpoint directory. When set, model weights are "
    "loaded from this directory and no online model download is attempted.",
)
flags.DEFINE_bool(
    "allow_download",
    False,
    "Allow Kaggle Hub model download only when --model_dir is not provided.",
)
flags.DEFINE_string(
    "data_dir",
    None,
    "Optional AlphaGenome TFRecord root. If unset, the dataset loader uses its "
    "built-in default path.",
)
flags.DEFINE_list(
    "bundles",
    None,
    "Comma-separated bundle names. Defaults to all supported evaluation "
    "bundles: ATAC,CAGE,CHIP_HISTONE,CHIP_TF,DNASE,PROCAP,RNA_SEQ.",
)

_DEFAULT_EVAL_BUNDLES = [
    BundleName.ATAC,
    BundleName.CAGE,
    BundleName.CHIP_HISTONE,
    BundleName.CHIP_TF,
    BundleName.DNASE,
    BundleName.PROCAP,
    BundleName.RNA_SEQ,
]


def _parse_model_version(value: str) -> dna_model_types.ModelVersion:
    normalized = value.replace("-", "_").upper()
    if normalized.startswith("ALPHAGENOME_"):
        normalized = normalized.removeprefix("ALPHAGENOME_")
    try:
        return dna_model_types.ModelVersion[normalized]
    except KeyError as exc:
        valid = ", ".join(version.name for version in dna_model_types.ModelVersion)
        raise ValueError(
            f"Unsupported --model_version={value!r}. Valid values: {valid}."
        ) from exc


def _resolve_local_model_dir(path: str) -> pathlib.Path:
    model_dir = pathlib.Path(path).expanduser()
    if not model_dir.is_dir():
        raise FileNotFoundError(
            f"Local AlphaGenome model directory does not exist: {model_dir}"
        )
    if not (model_dir / "_CHECKPOINT_METADATA").exists():
        raise FileNotFoundError(
            "The model directory does not look like an Orbax checkpoint. "
            f"Missing: {model_dir / '_CHECKPOINT_METADATA'}"
        )
    return model_dir


def load_model_from_local_checkpoint(
    checkpoint_path: str,
    organism: dna_model_types.Organism,
):
    """Loads eval params/state/predict_fn like run_inference.py does."""
    checkpoint_path = _resolve_local_model_dir(checkpoint_path)
    logging.info("Loading AlphaGenome model from local checkpoint: %s", checkpoint_path)
    metadata = {organism: metadata_lib.load(organism)}
    init_fn, apply_fn, _ = research_dna_model.create_model(metadata)
    dna_sequence_shape = jax.ShapeDtypeStruct((1, 2048, 4), dtype=jnp.float32)
    organism_index_shape = jax.ShapeDtypeStruct((1,), dtype=jnp.int32)
    target_shapes = jax.eval_shape(
        init_fn,
        jax.random.PRNGKey(0),
        dna_sequence_shape,
        organism_index_shape,
    )
    params, state = ocp.StandardCheckpointer().restore(
        str(checkpoint_path),
        target=target_shapes,
        strict=True,
    )

    @jax.jit
    def predict(params, state, dna_sequence, organism_index):
        predictions = apply_fn(params, state, dna_sequence, organism_index)
        return research_dna_model.extract_predictions(predictions)

    return params, state, predict


def _load_model(
    model_version: dna_model_types.ModelVersion,
    organism: dna_model_types.Organism,
):
    if FLAGS.model_dir:
        return load_model_from_local_checkpoint(FLAGS.model_dir, organism)
    if FLAGS.allow_download:
        logging.warning(
            "No --model_dir was provided; downloading model weights from "
            "Kaggle Hub because --allow_download=true."
        )
        return load_model_from_kaggle(model_version)
    raise ValueError(
        "No --model_dir was provided. Refusing to download model weights. "
        "Pass --model_dir /path/to/alphagenome-all-folds, or explicitly pass "
        "--allow_download=true to use Kaggle Hub."
    )


def _resolve_data_dir(path: str | None) -> str | None:
    if not path:
        return None
    if path.startswith("gs://"):
        return path
    data_dir = pathlib.Path(path).expanduser()
    if not data_dir.is_dir():
        raise FileNotFoundError(
            f"Local AlphaGenome dataset directory does not exist: {data_dir}"
        )
    return str(data_dir)


def _mesh_context(mesh):
    if hasattr(jax, "set_mesh"):
        return jax.set_mesh(mesh)
    return mesh


def create_eval_step(predict_fn, bundles):
    """Returns a JAX-version-compatible eval step."""

    @jax.jit
    def eval_step(params, state, batch):
        predictions = predict_fn(
            params,
            state,
            batch.dna_sequence,
            batch.organism_index,
        )
        metrics_step = {}
        for bundle in bundles:
            targets_true, mask = batch.get_genome_tracks(bundle)
            targets_pred = predictions[dna_output.OutputType[bundle.name]]
            targets_pred = regression_metrics.crop_sequence_length(
                targets_pred,
                target_length=targets_true.shape[-2],
            )
            metrics_step[bundle.name] = regression_metrics.update_regression_metrics(
                targets_true,
                targets_pred,
                mask,
            )
        return metrics_step

    return eval_step


def evaluate(params, state, predict_fn, bundles, dataset_iterator):
    """Evaluates the model without relying on the library's jit decorator style."""
    devices = mesh_utils.create_device_mesh((jax.local_device_count(),))
    mesh = jax.sharding.Mesh(devices, axis_names=("data",))
    sharding_rep = sharding.NamedSharding(mesh, PS())
    sharding_data = sharding.NamedSharding(mesh, PS("data"))

    params = jax.device_put(params, sharding_rep)
    state = jax.device_put(state, sharding_rep)

    eval_step = create_eval_step(predict_fn, bundles)
    metrics = {
        bundle.name: regression_metrics.initialize_regression_metrics()
        for bundle in bundles
    }
    num_elements = 0

    for i, (batch, _) in enumerate(dataset_iterator):
        num_elements += batch.dna_sequence.shape[0]
        if i % 5 == 1:
            finalized = pprint.pformat(
                regression_metrics.finalize_regression_metrics(metrics)
            )
            logging.info("step %d: %s", i, finalized)

        with _mesh_context(mesh):
            batch = jax.device_put(batch, sharding_data)
            step_metrics = eval_step(params, state, batch)

        step_metrics = jax.device_get(step_metrics)
        metrics = regression_metrics.reduce_regression_metrics(
            metrics,
            step_metrics,
        )

    logging.info("num_elements: %d", num_elements)
    return regression_metrics.finalize_regression_metrics(metrics)


def main(_):
    # TensorFlow is only used for data loading; keep GPUs available to JAX.
    tf.config.set_visible_devices([], "GPU")

    model_version = _parse_model_version(FLAGS.model_version)
    organism = dna_model_types.Organism[FLAGS.organism]
    data_dir = _resolve_data_dir(FLAGS.data_dir)

    logging.info("JAX devices: %s", jax.devices())
    logging.info(
        "Evaluation config: model_version=%s, organism=%s, model_dir=%s, "
        "data_dir=%s",
        model_version.name,
        FLAGS.organism,
        FLAGS.model_dir or "<download disabled>",
        data_dir or "<loader default>",
    )

    if FLAGS.bundles:
        eval_bundles = [BundleName[bundle.strip()] for bundle in FLAGS.bundles]
    else:
        eval_bundles = _DEFAULT_EVAL_BUNDLES
    logging.info("Evaluation bundles: %s", [bundle.name for bundle in eval_bundles])

    params, state, predict_fn = _load_model(model_version, organism)

    dataset_iterator = get_numpy_dataset_iterator(
        batch_size=jax.local_device_count(),
        organism=organism,
        model_version=model_version,
        subset=fold_intervals.Subset.VALID,
        bundles=eval_bundles,
        path=data_dir,
    )

    logging.info("Starting evaluation.")
    results = evaluate(
        params=params,
        state=state,
        predict_fn=predict_fn,
        bundles=eval_bundles,
        dataset_iterator=dataset_iterator,
    )

    rows = []
    for bundle_name, metrics in results.items():
        for metric_name, value in metrics.items():
            logging.info("%s / %s: %.4f", bundle_name, metric_name, value)
            rows.append({
                "bundle": bundle_name,
                "metric": metric_name,
                "value": float(value),
                "model_version": model_version.name,
                "organism": FLAGS.organism,
            })

    output_path = pathlib.Path(FLAGS.output_path)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    pd.DataFrame(rows).to_csv(output_path, index=False)
    logging.info("Saved results to: %s", output_path)


if __name__ == "__main__":
    app.run(main)