Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions actions/pyrosettacluster/test_env_reproducibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def recreate_environment_test(
)
elif environment_manager == "uv":
cmd = (
f"uv run --project {original_env_dir} python -u -m {module} "
f"uv run --locked --project {original_env_dir} python -u -m {module} "
f"--env_manager '{environment_manager}' "
f"--output_path '{original_output_path}' "
f"--scorefile_name '{original_scorefile_name}'"
Expand Down Expand Up @@ -174,7 +174,7 @@ def recreate_environment_test(
)
elif environment_manager == "uv":
cmd = (
f"uv run --project {original_env_dir} python -u {dump_env_file_script} "
f"uv run --locked --project {original_env_dir} python -u {dump_env_file_script} "
f"--scorefile '{original_scorefile_path}' "
f"--decoy_name '{original_decoy_name}' "
f"--env_dir '{reproduce_env_dir}' "
Expand Down Expand Up @@ -230,7 +230,7 @@ def recreate_environment_test(
)
elif environment_manager == "uv":
cmd = (
f"uv run --project {reproduce_env_dir} python -u -m {module} "
f"uv run --locked --project {reproduce_env_dir} python -u -m {module} "
f"--env_manager '{environment_manager}' "
f"--output_path '{reproduce_output_path}' "
f"--scorefile_name '{reproduce_scorefile_name}' "
Expand Down Expand Up @@ -310,7 +310,7 @@ def recreate_environment_test(
)
elif environment_manager == "uv":
cmd = (
f"uv run --project {reproduce_env_dir} python -u -m {module} "
f"uv run --locked --project {reproduce_env_dir} python -u -m {module} "
f"--original_output_file '{original_output_file}' "
f"--reproduce_output_file '{reproduce_output_file}' "
f"--pyrosetta_init_flags '{pyrosetta_init_flags}'"
Expand Down
3 changes: 2 additions & 1 deletion pyrosettacluster/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Please see the [official PyRosettaCluster documentation](https://graylab.jhu.edu
> If using the `uv` environment manager, please remember to set the `UV_PROJECT` environment variable to the `uv` project root directory, or run the `PyRosettaCluster` simulation from the `uv` project root directory, in order for `PyRosettaCluster` to automatically detect and cache the `uv` project's `pyproject.toml` file for environment reproducibility.

> [!TIP]
> If using the `uv` environment manager with custom/experimental libraries built from [source distributions (sdists)](https://packaging.python.org/en/latest/specifications/source-distribution-format/#source-distribution-file-format) instead of wheels, it may be helpful to cache the environment's `uv.lock` file contents for accounting or debugging purposes. The `uv.lock` file may be either committed to the GitHub repository before running the `PyRosettaCluster` simulation, or stored in the `system_info` keyword argument for persistent storage in the output decoys (and scorefile(s) if `simulation_records_in_scorefile=True`). Although very uncommon, if a `uv` project contains packages built from sdists, it may be necessary to reproduce the environment using `uv sync` from the `uv.lock` file rather than `uv pip sync` from the automatically exported and cached `requirements.txt` file. For example:
> If using the `uv` environment manager with PyRosetta version `2026.3+releasequarterly.5e498f1409` and custom/experimental libraries built from [source distributions (sdists)](https://packaging.python.org/en/latest/specifications/source-distribution-format/#source-distribution-file-format) instead of wheels, it may be helpful to cache the environment's `uv.lock` file contents for accounting or debugging purposes. Note that subsequent quarterly releases of `pyrosetta` (and weekly releases `>2026.05` if using the PyPI `pyrosetta-installer` package for PyRosetta installation) automatically cache the `uv.lock` file. The `uv.lock` file may be either committed to the GitHub repository before running the `PyRosettaCluster` simulation, or stored in the `system_info` keyword argument for persistent storage in the output decoys (and scorefile(s) if `simulation_records_in_scorefile=True`). Although very uncommon, if a `uv` project contains packages built from sdists, it may be necessary to reproduce the environment using `uv sync` from the `uv.lock` file rather than `uv pip sync` from the automatically exported and cached `requirements.txt` file. For example:
> ```
> import sys
> from pathlib import Path
Expand Down Expand Up @@ -239,3 +239,4 @@ if __name__ == "__main__":




19 changes: 18 additions & 1 deletion pyrosettacluster/dump_env_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@
from typing import Optional


def detect_uv_environment_format(environment: str) -> str:
"""
Detect whether the PyRosettaCluster environment file string is in 'uv.lock' or 'requirements.txt' format.

Args:
environment: A `str` object representing the PyRosettaCluster environment file string.

Returns:
A `str` object of either 'uv.lock' or 'requirements.txt'.
"""
for line in environment.splitlines():
if line.strip().startswith("[[package]]"):
return "uv.lock"
return "requirements.txt"


def main(
input_file: Optional[str],
scorefile: Optional[str],
Expand Down Expand Up @@ -91,7 +107,8 @@ def main(
)

elif env_manager == "uv":
env_file = os.path.join(env_dir, "requirements.txt")
uv_environment_format = detect_uv_environment_format(environment)
env_file = os.path.join(env_dir, uv_environment_format)
write_file(env_file, environment)
if toml:
toml_file = os.path.join(env_dir, toml_format)
Expand Down
35 changes: 25 additions & 10 deletions pyrosettacluster/recreate_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ def run_subprocess(
raise RuntimeError(cmd) from ex


def uv_lock_package_present(lock_file: str, name: str) -> bool:
"""Test if a package name is specified in an input 'uv.lock' file."""
with open(lock_file, "r") as f:
contents = f.read()
return bool(re.search(rf'^\[\[package\]\]\s*\n\s*name\s*=\s*"{name}"\s*$', contents, flags=re.MULTILINE))


def requirement_present(req_file: str, name: str) -> bool:
"""Test if a package name is specified in an input 'requirements.txt' file."""
with open(req_file, "r") as f:
Expand Down Expand Up @@ -94,16 +101,24 @@ def recreate_environment(env_dir: str, env_manager: str, timeout: float, mirror_
env_create_cmd = "pixi install --frozen"

elif env_manager == "uv":
req_file = os.path.join(env_dir, "requirements.txt")
if not os.path.isfile(req_file):
raise FileNotFoundError(
"Please ensure that the uv project 'requirements.txt' file "
"is in the uv project directory, then try again."
)
# Install packages strictly from requirements.txt
env_create_cmd = f"uv venv --project '{env_dir}' && uv pip sync --project '{env_dir}' '{req_file}'"
# Test if the 'pyrosetta-installer' package is specified
use_pyrosetta_installer = requirement_present(req_file, "pyrosetta-installer")
lock_file = os.path.join(env_dir, "uv.lock")
if os.path.isfile(lock_file):
# Install packages strictly from uv.lock
env_create_cmd = f"uv sync --frozen --project '{env_dir}'"
# Test if the 'pyrosetta-installer' package is specified
use_pyrosetta_installer = uv_lock_package_present(lock_file, "pyrosetta-installer")
else:
req_file = os.path.join(env_dir, "requirements.txt")
if os.path.isfile(req_file):
# Install packages strictly from requirements.txt
env_create_cmd = f"uv venv --project '{env_dir}' && uv pip sync --project '{env_dir}' '{req_file}' && uv lock"
# Test if the 'pyrosetta-installer' package is specified
use_pyrosetta_installer = requirement_present(req_file, "pyrosetta-installer")
else:
raise FileNotFoundError(
"Please ensure that the uv project 'uv.lock' (or 'requirements.txt' file) "
"is in the uv project directory, then try again."
)

elif env_manager == "conda":
yml_file = os.path.join(env_dir, "environment.yml")
Expand Down
Loading