Skip to content

Commit 37bff5f

Browse files
DeepMindcopybara-github
authored andcommitted
Enable Google Cloud Storage (gs://) path support in AlphaFold3 using etils.epath.
This change migrates path operations in AlphaFold3 from Python's standard `pathlib.Path` and `os.path` to the unified filesystem wrapper `etils.epath.Path`. This enables several core flags (`--json_path`, `--input_dir`, `--output_dir`, `--model_dir`, and `--pdb_database_path`) to natively support GCS cloud-hosted paths (`gs://...`). Support for Google Cloud Storage paths is treated as an optional dependency. Open source users can opt into GCS path support by installing the `gcsfs` add-in, e.g., via: `uv sync --extra` , or adding `--build-arg UV_EXTRAS="--extra gcsfs"` to their docker build command. PiperOrigin-RevId: 938058741 Change-Id: I3463fb159a082deca4e6752fceec7dae3d75c1d3
1 parent b2f3d45 commit 37bff5f

14 files changed

Lines changed: 825 additions & 148 deletions

File tree

docker/Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ WORKDIR /app/alphafold
7777
# --no-editable: install as a static package.
7878
# If using this as a recipe for local installation, we recommend removing the
7979
# --frozen and --no-editable flags.
80-
RUN --mount=type=cache,target=/root/.cache/uv \
81-
UV_LINK_MODE=copy uv sync --frozen --all-groups --no-editable
80+
ARG UV_EXTRAS=""
81+
RUN --mount=type=cache,target=/root/.cache/uv \
82+
UV_LINK_MODE=copy uv sync --frozen --all-groups --no-editable $UV_EXTRAS
8283

8384
# Build chemical components database (this binary was installed by uv sync).
8485
RUN uv run build_data

docs/input.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,20 @@
55
You can provide inputs to `run_alphafold.py` in one of two ways:
66

77
- Single input file: Use the `--json_path` flag followed by the path to a
8-
single JSON file.
8+
single JSON file. This path can be either a local path or a Google Cloud
9+
Storage path (starting with `gs://`), if enabled.
910
- Multiple input files: Use the `--input_dir` flag followed by the path to a
10-
directory of JSON files.
11+
directory of JSON files. This path can be either a local directory path or a
12+
GCS path, if enabled.
13+
- Within an input file, fields with names ending in `Path` (e.g.
14+
`pairedMsaPath`) can be provided as Google Cloud Storage paths, if enabled.
15+
16+
### Note: Google Cloud Storage paths
17+
18+
Google Cloud Storage (`gs://`) paths are supported for certain flags and fields
19+
if the optional `gcsfs` dependency is installed. See the
20+
[installation instructions](installation.md#optional-enable-google-cloud-storage-path-support)
21+
for details and a complete list of supported paths.
1122

1223
## Input Format
1324

docs/installation.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ should aid others with different setups. If you are installing locally outside
1717
of a Docker container, please ensure CUDA, cuDNN, and JAX are correctly
1818
installed; the
1919
[JAX installation documentation](https://jax.readthedocs.io/en/latest/installation.html#nvidia-gpu)
20-
is a useful reference for this case. Please note that the Docker container
21-
requires that the host machine has CUDA 12.6 installed.
20+
is a useful reference for this case. Note that the Docker container requires
21+
that the host machine has CUDA 12.6 installed.
2222

2323
The instructions provided below describe how to:
2424

@@ -368,6 +368,33 @@ docker run alphafold3 python run_alphafold.py --help
368368

369369
for more information.
370370

371+
:ledger: **Optional: Enable Google cloud storage path support**
372+
373+
AlphaFold 3 supports reading and writing specific files directly from Google
374+
Cloud Storage (`gs://` paths). Since GCS is an optional feature, the required
375+
`gcsfs` dependency is not installed by default. To enable GCS support when
376+
building the Docker container, pass the `UV_EXTRAS` build argument when running
377+
`docker build`:
378+
379+
```sh
380+
docker build --build-arg UV_EXTRAS="--extra gcsfs" -t alphafold3 -f docker/Dockerfile .
381+
```
382+
383+
The following command-line flags support `gs://` paths:
384+
385+
- `--input_dir`
386+
- `--json_path`
387+
- `--output_dir`
388+
- `--model_dir`
389+
- `--pdb_database_path`
390+
- In the JSON input file, fields with names ending in `Path` (e.g.
391+
`pairedMsaPath`) can be provided as Google Cloud Storage paths, if enabled.
392+
393+
:warning: All other database paths (e.g., `--db_dir`,
394+
`--small_bfd_database_path`, etc.) and binary paths (e.g.,
395+
`--jackhmmer_binary_path`) do **not** support `gs://` paths and must be local
396+
file paths.
397+
371398
## Running Using Singularity Instead of Docker
372399

373400
You may prefer to run AlphaFold 3 within Singularity. You'll still need to

docs/output.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ AlphaFold 3 will write its outputs in a directory called `My_first_fold_TEST`
99
append a timestamp to the directory name to avoid overwriting existing data
1010
unless `--force_output_dir` is passed.
1111

12+
The output directory can also be a Google Cloud Storage path (`gs://`) if the
13+
`gcsfs` dependency is installed (see the
14+
[installation instructions](installation.md#optional-enable-google-cloud-storage-path-support)).
15+
1216
The following structure is used within the output directory:
1317

1418
* Sub-directories with results for each sample and seed. There will be

pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ license = {file = "LICENSE"}
1717
dependencies = [
1818
"absl-py>=2.3.1",
1919
"dm-haiku==0.0.16",
20+
"etils[epath]",
2021
"jax==0.9.1",
2122
"jax[cuda12]==0.9.1",
2223
"numpy",
@@ -26,6 +27,12 @@ dependencies = [
2627
"zstandard",
2728
]
2829

30+
[project.optional-dependencies]
31+
gcsfs = [
32+
# Use `uv sync --extra gcsfs` to enable GCS paths for inputs and outputs
33+
"gcsfs",
34+
]
35+
2936
[dependency-groups]
3037
dev = [
3138
"pytest>=6.0",

run_alphafold.py

Lines changed: 47 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import datetime
3535
import functools
3636
import os
37-
import pathlib
3837
import shutil
3938
import string
4039
import textwrap
@@ -56,34 +55,34 @@
5655
from alphafold3.model import params
5756
from alphafold3.model import post_processing
5857
from alphafold3.model.components import utils
58+
from etils import epath
5959
import haiku as hk
6060
import jax
6161
from jax import numpy as jnp
6262
import numpy as np
6363
import tokamax
6464

65-
_HOME_DIR = pathlib.Path.home()
65+
_HOME_DIR = epath.Path('~').expanduser()
6666
_DEFAULT_MODEL_DIR = _HOME_DIR / 'models'
6767
_DEFAULT_DB_DIR = _HOME_DIR / 'public_databases'
6868

69-
7069
# Input and output paths.
71-
_JSON_PATH = flags.DEFINE_string(
70+
_JSON_PATH = epath.DEFINE_path(
7271
'json_path',
7372
None,
7473
'Path to the input JSON file.',
7574
)
76-
_INPUT_DIR = flags.DEFINE_string(
75+
_INPUT_DIR = epath.DEFINE_path(
7776
'input_dir',
7877
None,
7978
'Path to the directory containing input JSON files.',
8079
)
81-
_OUTPUT_DIR = flags.DEFINE_string(
80+
_OUTPUT_DIR = epath.DEFINE_path(
8281
'output_dir',
8382
None,
8483
'Path to a directory where the results will be saved.',
8584
)
86-
MODEL_DIR = flags.DEFINE_string(
85+
MODEL_DIR = epath.DEFINE_path(
8786
'model_dir',
8887
_DEFAULT_MODEL_DIR.as_posix(),
8988
'Path to the model to use for inference.',
@@ -220,7 +219,7 @@
220219
' calculation. Must be set for sharded databases.',
221220
lower_bound=0.0,
222221
)
223-
_PDB_DATABASE_PATH = flags.DEFINE_string(
222+
_PDB_DATABASE_PATH = epath.DEFINE_path(
224223
'pdb_database_path',
225224
'${DB_DIR}/mmcif_files',
226225
'PDB database directory with mmCIF files path, used for template search.',
@@ -420,11 +419,11 @@ def __init__(
420419
self,
421420
config: model.Model.Config,
422421
device: jax.Device,
423-
model_dir: pathlib.Path,
422+
model_dir: epath.PathLike,
424423
):
425424
self._model_config = config
426425
self._device = device
427-
self._model_dir = model_dir
426+
self._model_dir = epath.Path(model_dir)
428427

429428
@functools.cached_property
430429
def model_params(self) -> hk.Params:
@@ -605,19 +604,19 @@ def predict_structure(
605604

606605
def write_fold_input_json(
607606
fold_input: folding_input.Input,
608-
output_dir: os.PathLike[str] | str,
607+
output_dir: epath.PathLike,
609608
) -> None:
610609
"""Writes the input JSON to the output directory."""
611-
os.makedirs(output_dir, exist_ok=True)
612-
path = os.path.join(output_dir, f'{fold_input.sanitised_name()}_data.json')
610+
output_dir = epath.Path(output_dir)
611+
output_dir.mkdir(parents=True, exist_ok=True)
612+
path = output_dir / f'{fold_input.sanitised_name()}_data.json'
613613
print(f'Writing model input JSON to {path}')
614-
with open(path, 'wt') as f:
615-
f.write(fold_input.to_json())
614+
path.write_text(fold_input.to_json())
616615

617616

618617
def write_outputs(
619618
all_inference_results: Sequence[ResultsForSeed],
620-
output_dir: os.PathLike[str] | str,
619+
output_dir: epath.PathLike,
621620
job_name: str,
622621
compress_large_output_files: bool = False,
623622
) -> None:
@@ -627,15 +626,16 @@ def write_outputs(
627626
max_ranking_result = None
628627

629628
output_terms = (
630-
pathlib.Path(alphafold3.cpp.__file__).parent / 'OUTPUT_TERMS_OF_USE.md'
629+
epath.Path(alphafold3.cpp.__file__).parent / 'OUTPUT_TERMS_OF_USE.md'
631630
).read_text()
632631

633-
os.makedirs(output_dir, exist_ok=True)
632+
output_dir = epath.Path(output_dir)
633+
output_dir.mkdir(parents=True, exist_ok=True)
634634
for results_for_seed in all_inference_results:
635635
seed = results_for_seed.seed
636636
for sample_idx, result in enumerate(results_for_seed.inference_results):
637-
sample_dir = os.path.join(output_dir, f'seed-{seed}_sample-{sample_idx}')
638-
os.makedirs(sample_dir, exist_ok=True)
637+
sample_dir = output_dir / f'seed-{seed}_sample-{sample_idx}'
638+
sample_dir.mkdir(parents=True, exist_ok=True)
639639
post_processing.write_output(
640640
inference_result=result,
641641
output_dir=sample_dir,
@@ -649,21 +649,19 @@ def write_outputs(
649649
max_ranking_result = result
650650

651651
if embeddings := results_for_seed.embeddings:
652-
embeddings_dir = os.path.join(output_dir, f'seed-{seed}_embeddings')
653-
os.makedirs(embeddings_dir, exist_ok=True)
652+
embeddings_dir = output_dir / f'seed-{seed}_embeddings'
653+
embeddings_dir.mkdir(parents=True, exist_ok=True)
654654
post_processing.write_embeddings(
655655
embeddings=embeddings,
656656
output_dir=embeddings_dir,
657657
name=f'{job_name}_seed-{seed}',
658658
)
659659

660660
if (distogram := results_for_seed.distogram) is not None:
661-
distogram_dir = os.path.join(output_dir, f'seed-{seed}_distogram')
662-
os.makedirs(distogram_dir, exist_ok=True)
663-
distogram_path = os.path.join(
664-
distogram_dir, f'{job_name}_seed-{seed}_distogram.npz'
665-
)
666-
with open(distogram_path, 'wb') as f:
661+
distogram_dir = output_dir / f'seed-{seed}_distogram'
662+
distogram_dir.mkdir(parents=True, exist_ok=True)
663+
distogram_path = distogram_dir / f'{job_name}_seed-{seed}_distogram.npz'
664+
with distogram_path.open('wb') as f:
667665
np.savez_compressed(f, distogram=distogram.astype(np.float16))
668666

669667
if max_ranking_result is not None: # True iff ranking_scores non-empty.
@@ -677,29 +675,31 @@ def write_outputs(
677675
)
678676
# Save csv of ranking scores with seeds and sample indices, to allow easier
679677
# comparison of ranking scores across different runs.
680-
with open(
681-
os.path.join(output_dir, f'{job_name}_ranking_scores.csv'), 'wt'
682-
) as f:
678+
ranking_scores_csv_path = output_dir / f'{job_name}_ranking_scores.csv'
679+
with ranking_scores_csv_path.open('w') as f:
683680
writer = csv.writer(f)
684681
writer.writerow(['seed', 'sample', 'ranking_score'])
685682
writer.writerows(ranking_scores)
686683

687684

688-
def replace_db_dir(path_with_db_dir: str, db_dirs: Sequence[str]) -> str:
685+
def replace_db_dir(
686+
path_with_db_dir: epath.PathLike, db_dirs: Sequence[str]
687+
) -> str:
689688
"""Replaces the DB_DIR placeholder in a path with the given DB_DIR."""
689+
path_with_db_dir = os.fspath(path_with_db_dir)
690690
template = string.Template(path_with_db_dir)
691691
if 'DB_DIR' in template.get_identifiers():
692692
for db_dir in db_dirs:
693693
path = template.substitute(DB_DIR=db_dir)
694-
if os.path.exists(path):
694+
if epath.Path(path).exists():
695695
return path
696696
raise FileNotFoundError(
697697
f'{path_with_db_dir} with ${{DB_DIR}} not found in any of {db_dirs}.'
698698
)
699699
if (sharded_paths := shards.get_sharded_paths(path_with_db_dir)) is not None:
700-
db_exists = all(os.path.exists(p) for p in sharded_paths)
700+
db_exists = all(epath.Path(p).exists() for p in sharded_paths)
701701
else:
702-
db_exists = os.path.exists(path_with_db_dir)
702+
db_exists = epath.Path(path_with_db_dir).exists()
703703
if not db_exists:
704704
raise FileNotFoundError(f'{path_with_db_dir} does not exist.')
705705
return path_with_db_dir
@@ -711,7 +711,7 @@ def process_fold_input(
711711
data_pipeline_config: pipeline.DataPipelineConfig | None,
712712
*,
713713
model_runner: None,
714-
output_dir: os.PathLike[str] | str,
714+
output_dir: epath.PathLike,
715715
buckets: Sequence[int] | None = None,
716716
ref_max_modified_date: datetime.date | None = None,
717717
conformer_max_iterations: int | None = None,
@@ -729,7 +729,7 @@ def process_fold_input(
729729
data_pipeline_config: pipeline.DataPipelineConfig | None,
730730
*,
731731
model_runner: ModelRunner,
732-
output_dir: os.PathLike[str] | str,
732+
output_dir: epath.PathLike,
733733
buckets: Sequence[int] | None = None,
734734
ref_max_modified_date: datetime.date | None = None,
735735
conformer_max_iterations: int | None = None,
@@ -746,7 +746,7 @@ def process_fold_input(
746746
data_pipeline_config: pipeline.DataPipelineConfig | None,
747747
*,
748748
model_runner: ModelRunner | None,
749-
output_dir: os.PathLike[str] | str,
749+
output_dir: epath.PathLike,
750750
buckets: Sequence[int] | None = None,
751751
ref_max_modified_date: datetime.date | None = None,
752752
conformer_max_iterations: int | None = None,
@@ -803,13 +803,11 @@ def process_fold_input(
803803
if not fold_input.chains:
804804
raise ValueError('Fold input has no chains.')
805805

806-
if (
807-
not force_output_dir
808-
and os.path.exists(output_dir)
809-
and os.listdir(output_dir)
810-
):
806+
output_dir = epath.Path(output_dir)
807+
if not force_output_dir and output_dir.exists() and any(output_dir.iterdir()):
811808
new_output_dir = (
812-
f'{output_dir}_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}'
809+
output_dir.parent
810+
/ f'{output_dir.name}_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}'
813811
)
814812
print(
815813
f'Output will be written in {new_output_dir} since {output_dir} is'
@@ -874,13 +872,9 @@ def main(_):
874872
)
875873

876874
if _INPUT_DIR.value is not None:
877-
fold_inputs = folding_input.load_fold_inputs_from_dir(
878-
pathlib.Path(_INPUT_DIR.value)
879-
)
875+
fold_inputs = folding_input.load_fold_inputs_from_dir(_INPUT_DIR.value)
880876
elif _JSON_PATH.value is not None:
881-
fold_inputs = folding_input.load_fold_inputs_from_path(
882-
pathlib.Path(_JSON_PATH.value)
883-
)
877+
fold_inputs = folding_input.load_fold_inputs_from_path(_JSON_PATH.value)
884878
else:
885879
raise AssertionError(
886880
'Exactly one of --json_path or --input_dir must be specified.'
@@ -891,7 +885,7 @@ def main(_):
891885

892886
# Make sure we can create the output directory before running anything.
893887
try:
894-
os.makedirs(_OUTPUT_DIR.value, exist_ok=True)
888+
_OUTPUT_DIR.value.mkdir(parents=True, exist_ok=True)
895889
except OSError as e:
896890
print(f'Failed to create output directory {_OUTPUT_DIR.value}: {e}')
897891
raise
@@ -993,7 +987,7 @@ def main(_):
993987
return_distogram=_SAVE_DISTOGRAM.value,
994988
),
995989
device=devices[_GPU_DEVICE.value],
996-
model_dir=pathlib.Path(MODEL_DIR.value),
990+
model_dir=MODEL_DIR.value,
997991
)
998992
# Check we can load the model parameters before launching anything.
999993
print('Checking that model parameters can be loaded...')
@@ -1010,7 +1004,7 @@ def main(_):
10101004
fold_input=fold_input,
10111005
data_pipeline_config=data_pipeline_config,
10121006
model_runner=model_runner,
1013-
output_dir=os.path.join(_OUTPUT_DIR.value, fold_input.sanitised_name()),
1007+
output_dir=_OUTPUT_DIR.value / fold_input.sanitised_name(),
10141008
buckets=tuple(int(bucket) for bucket in _BUCKETS.value),
10151009
ref_max_modified_date=max_template_date,
10161010
conformer_max_iterations=_CONFORMER_MAX_ITERATIONS.value,

0 commit comments

Comments
 (0)