diff --git a/.gitignore b/.gitignore index ee110784..276a5804 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ ENV/ .envrc .env .venv +uv.lock # Build artifacts, distribution / packaging src/stacie/_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ff21d3c..c8439f32 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,11 +29,11 @@ repos: - id: remove-crlf exclude_types: [binary] - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.37.2 + rev: 0.37.4 hooks: - id: check-github-workflows - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.12 + rev: v0.15.20 hooks: - id: ruff-format - id: ruff-check @@ -44,7 +44,7 @@ repos: - id: nb-clean args: ["--remove-empty-cells", "--preserve-cell-metadata", "tags", "--"] - repo: https://github.com/DavidAnson/markdownlint-cli2 - rev: v0.22.1 + rev: v0.23.0 hooks: - id: markdownlint-cli2 exclude: '^test' diff --git a/docs/data/README.md b/docs/data/README.md index bd4919df..28c13973 100644 --- a/docs/data/README.md +++ b/docs/data/README.md @@ -8,7 +8,7 @@ To rerun the MD simulations, you need files that are not included in the Zenodo such as the LAMMPS and OpenMM input files, and the force fields. These are available in the STACIE Git repository: -Each directory under `docs/data` contains a `plan.py` script that can be executed with `stepup boot` +Each directory under `docs/data` contains a `plan.py` script that can be executed with `sb` to run all simulations in parallel. See the `README.md` file in each directory for more details. diff --git a/docs/data/lammps_lj3d/README.md b/docs/data/lammps_lj3d/README.md index 39fab42a..be4fca60 100644 --- a/docs/data/lammps_lj3d/README.md +++ b/docs/data/lammps_lj3d/README.md @@ -37,7 +37,7 @@ but only to read the outputs. All LAMMPS simulations can be executed efficiently as follows: ```bash -stepup boot -n 1.0 +sb -j 1.0 ``` This will create input files from the templates `template-init.lammps` and `template-ext.lammps` diff --git a/docs/data/lammps_lj3d/job.sh b/docs/data/lammps_lj3d/job.sh index 80aa7c45..4b887343 100755 --- a/docs/data/lammps_lj3d/job.sh +++ b/docs/data/lammps_lj3d/job.sh @@ -8,4 +8,4 @@ cd ../../../../../ . activate cd ${SLURM_SUBMIT_DIR} -time stepup boot -n ${SLURM_CPUS_PER_TASK} +time sb -j ${SLURM_CPUS_PER_TASK} diff --git a/docs/data/lammps_lj3d/plan.py b/docs/data/lammps_lj3d/plan.py index 75e13e7d..56e379e5 100755 --- a/docs/data/lammps_lj3d/plan.py +++ b/docs/data/lammps_lj3d/plan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 from runlammps import runlammps -from stepup.core.api import mkdir, render_jinja, static +from stepup.core.api import render_jinja, static def plan_extension(ireplica: int, part: int, additional_steps: int): @@ -17,7 +17,6 @@ def plan_extension(ireplica: int, part: int, additional_steps: int): Number of additional steps to run in this extension. """ name = f"sims/replica_{ireplica:04d}_part_{part:02d}" - mkdir(f"{name}/") render_jinja( "template-ext.lammps", { @@ -33,12 +32,10 @@ def plan_extension(ireplica: int, part: int, additional_steps: int): static("runlammps.py", "template-init.lammps", "template-ext.lammps") -mkdir("sims/") nreplica = 100 for ireplica in range(nreplica): # Initial production run name_i = f"sims/replica_{ireplica:04d}_part_00" - mkdir(f"{name_i}/") render_jinja("template-init.lammps", {"seed": ireplica + 1}, f"{name_i}/in.lammps") runlammps(f"{name_i}/") diff --git a/docs/data/lammps_lj3d/runlammps.py b/docs/data/lammps_lj3d/runlammps.py index a7e9ca7a..8ded47c3 100755 --- a/docs/data/lammps_lj3d/runlammps.py +++ b/docs/data/lammps_lj3d/runlammps.py @@ -6,19 +6,19 @@ """ import argparse -import subprocess from path import Path -from stepup.core.api import amend, runpy +from stepup.core.api import amend, run +from stepup.core.extapi import run_subprocess __all__ = ("runlammps",) def runlammps(workdir: str, inp: list[str] = ()): workdir = Path(workdir) - runpy( + run( f"./runlammps.py {workdir}", - inp=["runlammps.py", workdir / "in.lammps", *inp], + inp=[workdir / "in.lammps", *inp], out=[workdir / "log.txt"], ) @@ -29,11 +29,15 @@ def main(argv: list[str] | None = None): for file in args.rundir.files(): if file.name != "in.lammps": file.remove() - subprocess.run( - ["lmp", "-i", "in.lammps", "-l", "log.txt", "-sc", "none"], - cwd=args.rundir, - check=True, + run_subprocess( + "lmp -i in.lammps -l log.txt -sc none", + workdir=args.rundir, ) + # This goes against good practices: ideally amend should come before run_subprocess. + # However, LAMMPS output files are not known in advance in general. + # This depends on the details of the input file. + # Here, we take the lazy approach of amending all files + # that are not the input file or the log file. extra_out = [path for path in args.rundir.files() if path.name not in ["in.lammps", "log.txt"]] amend(out=extra_out) diff --git a/docs/data/openmm_salt/README.md b/docs/data/openmm_salt/README.md index 81bc707b..833b8199 100644 --- a/docs/data/openmm_salt/README.md +++ b/docs/data/openmm_salt/README.md @@ -51,7 +51,7 @@ to avoid unit confusion in the worked example. All OpenMM simulations can be executed efficiently on a single compute node with StepUp: ```bash -OPENMM_CPU_THREADS=1 stepup boot -n 1.0 +OPENMM_CPU_THREADS=1 sb -j 1.0 ``` This will run in parallel the Jupyter notebooks `initial.ipynb` and `extension.ipynb` diff --git a/docs/data/openmm_salt/job.sh b/docs/data/openmm_salt/job.sh index 2bf44507..a6cc51da 100755 --- a/docs/data/openmm_salt/job.sh +++ b/docs/data/openmm_salt/job.sh @@ -10,4 +10,4 @@ cd ../../../../../ cd ${SLURM_SUBMIT_DIR} export OPENMM_CPU_THREADS=1 # export OPENMM_DEFAULT_PLATFORM=CPU -time stepup boot -n ${SLURM_CPUS_PER_TASK} +time sb -j ${SLURM_CPUS_PER_TASK} diff --git a/docs/data/openmm_salt/plan.py b/docs/data/openmm_salt/plan.py index 1bb42187..5f7413d4 100755 --- a/docs/data/openmm_salt/plan.py +++ b/docs/data/openmm_salt/plan.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from stepup.core.api import mkdir, runpy, static +from stepup.core.api import run, shq, static from stepup.reprep.api import execute_papermill @@ -29,19 +29,19 @@ def plan_extension(seed: int, part: int, nstep: int): def plan_extract(seed: int, part: int, ensemble: str): """Extract essentials from OpenMM output (DCD and CSV) and store as NPZ.""" - runpy( - "./${inp} ${out}", - inp=[ - "extract.py", - f"output/sim{seed:04d}_part{part:02d}_{ensemble}_traj.csv", - f"output/sim{seed:04d}_part{part:02d}_{ensemble}_traj.dcd", - f"output/sim{seed:04d}_part{part:02d}_{ensemble}_last.pdb", - ], - out=f"output/sim{seed:04d}_part{part:02d}_{ensemble}_traj.npz", + inp = [ + f"output/sim{seed:04d}_part{part:02d}_{ensemble}_traj.csv", + f"output/sim{seed:04d}_part{part:02d}_{ensemble}_traj.dcd", + f"output/sim{seed:04d}_part{part:02d}_{ensemble}_last.pdb", + ] + out = f"output/sim{seed:04d}_part{part:02d}_{ensemble}_traj.npz" + run( + f"./extract.py {shq(inp)} {shq(out)}", + inp=inp, + out=out, ) -mkdir("output") static("bhmtf.py", "initial.ipynb", "extension.ipynb", "utils.py", "extract.py") for seed in range(100): # Initial production runs diff --git a/docs/data/plan.py b/docs/data/plan.py index 73abf13b..e1503e30 100755 --- a/docs/data/plan.py +++ b/docs/data/plan.py @@ -2,12 +2,11 @@ """StepUp Plan to create ZIP files for Zenodo after all simulations are done.""" from path import Path -from stepup.core.api import copy, glob, mkdir, runpy, runsh, static +from stepup.core.api import copy, glob, run, static from stepup.reprep.api import compile_typst, make_inventory, sync_zenodo, zip_inventory # Convert JupyText files to Jupyter notebooks -static("../source/", "../source/examples/", "../source/examples/matplotlibrc", "preprocess.py") -mkdir("preprocessed/") +static("preprocess.py", "../source/examples/matplotlibrc") copy("../source/examples/matplotlibrc", "./") paths_ipynb = ["./matplotlibrc"] for path_py in glob("../source/examples/*.py"): @@ -16,25 +15,22 @@ paths_ipynb.append(path_py.name) else: path_pre = Path("preprocessed") / path_py.name - runpy("./${inp} ${out}", inp=["preprocess.py", path_py], out=path_pre) + run(f"./preprocess.py {path_py} {path_pre}", inp=path_py, out=path_pre) path_ipynb = path_py.name.replace(".py", ".ipynb") paths_ipynb.append(path_ipynb) - runsh("jupytext --to notebook ${inp} --output ${out}", inp=path_pre, out=path_ipynb) + run( + f"jupytext --to notebook {path_pre} --output {path_ipynb}", inp=path_pre, out=path_ipynb + ) # LAMMPS LJ3D example -static("zenodo.yaml", "zenodo.md") -static("lammps_lj3d/") -static("lammps_lj3d/sims/") -glob("lammps_lj3d/sims/replica_????_part_??/") lammps_paths_txt = glob("lammps_lj3d/sims/replica_????_part_??/nv?_*.txt") lammps_paths_yaml = glob("lammps_lj3d/sims/replica_????_part_??/info.yaml") # OpenMM Molten Salt example -static("openmm_salt/", "openmm_salt/output/") openmm_paths_npz = glob("openmm_salt/output/*.npz") # Cloud cover example data -static("cloud-cover/", "cloud-cover/download.sh") +static("cloud-cover/download.sh") cloudcover_paths = glob("cloud-cover/*.csv") # Compile the README @@ -53,5 +49,8 @@ *cloudcover_paths, ] make_inventory(*paths, "inventory.txt") + +# Sync with Zenodo +static("zenodo.yaml", "zenodo.md") zip_inventory("inventory.txt", "examples.zip") sync_zenodo("zenodo.yaml") diff --git a/docs/source/development/setup.md b/docs/source/development/setup.md index ed72e499..0121a1b3 100644 --- a/docs/source/development/setup.md +++ b/docs/source/development/setup.md @@ -2,19 +2,34 @@ ## Repository, Tests and Documentation Build -It is assumed that you have previously installed Python, Git, pre-commit and direnv. +It is assumed that you have previously installed +[Python](https://www.python.org/), +[uv](https://docs.astral.sh/uv/), +[Git](https://git-scm.com/), +[pre-commit](https://pre-commit.com/) and +[direnv](https://direnv.net/). A local installation for testing and development can be installed as follows: ```bash git clone git@github.com:molmod/stacie.git cd stacie +uv sync --extra docs,tests,dev pre-commit install -python -m venv venv -echo 'source venv/bin/activate' > .envrc +echo 'source .venv/bin/activate' > .envrc direnv allow -pip install -U pip -pip install -e .[docs,tests] +``` + +Tests are implemented with [pytest](https://docs.pytest.org/). +Run them as follows: + +```bash pytest -vv +``` + +Documentation is built with [Sphinx](https://www.sphinx-doc.org/). +Rebuild the documentation as follows: + +```bash cd docs ./compile_html.sh ./compile_pdf.sh @@ -22,8 +37,6 @@ cd docs ## Documentation Live Preview -The documentation is created using [Sphinx](https://www.sphinx-doc.org/). - Edit the documentation Markdown files with a live preview by running the following command *in the root* of the repository: diff --git a/pyproject.toml b/pyproject.toml index 7e979c74..43a4d9da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,8 +63,8 @@ docs = [ "sphinx-tippy", "sphinxcontrib-bibtex", "sphinxcontrib-svg2pdfconverter", - "stepup>=3.1.2", - "stepup-reprep>=3.1.5", + "stepup>=4.0.0rc7; python_version>='3.11'", + "stepup-reprep>=4.0.0rc3; python_version>='3.11'", ] tests = [ "pytest", @@ -72,6 +72,7 @@ tests = [ "pytest-xdist", "numdifftools", ] +dev = ["pre-commit"] [project.urls] Documentation = "https://molmod.github.io/stacie/" diff --git a/src/stacie/utils.py b/src/stacie/utils.py index bdec1ae1..7d683e0f 100644 --- a/src/stacie/utils.py +++ b/src/stacie/utils.py @@ -159,7 +159,7 @@ def split(sequences: NDArray[float], nsplit: int) -> NDArray: """ sequences = np.asarray(sequences) if sequences.ndim == 1: - sequences.shape = (1, -1) + sequences = sequences.reshape(1, -1) if not isinstance(nsplit, int) or nsplit <= 0 or nsplit > sequences.shape[-1] / 2: raise ValueError("nsplit must be a positive integer smaller than half the sequence length.") length = sequences.shape[1] // nsplit