diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 76717c6..501592f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,24 +1,19 @@ -# .readthedocs.yaml -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -# Required version: 2 build: - os: ubuntu-20.04 + os: "ubuntu-24.04" tools: - python: "mambaforge-4.10" + python: "miniconda3-3.12-24.9" sphinx: configuration: docs/conf.py fail_on_warning: true conda: - environment: environment.yml + environment: docs/environment.yaml python: - # Install our python package before building the docs so setuptools-scm generates the version for RTD to find. + # Install our python package before building the docs install: - method: pip path: . diff --git a/README.md b/README.md index f182bf6..cdb54a6 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Logo](https://img.shields.io/badge/OSMF-OpenFreeEnergy-%23002f4a)](https://openfree.energy/) [![CI](https://github.com/OpenFreeEnergy/openfe_analysis/actions/workflows/ci.yaml/badge.svg)](https://github.com/OpenFreeEnergy/openfe_analysis/actions/workflows/ci.yaml) [![Coverage](https://codecov.io/gh/OpenFreeEnergy/openfe_analysis/graph/badge.svg?token=krb231ftki)](https://codecov.io/gh/OpenFreeEnergy/openfe_analysis) +[![documentation](https://readthedocs.org/projects/openfe_analysis/badge/?version=stable)](https://openfe_analysis.openfree.energy/en/stable/?badge=stable) [![Powered by MDAnalysis](https://img.shields.io/badge/powered%20by-MDAnalysis-orange.svg?logoWidth=16&logo=data:image/x-icon;base64,AAABAAEAEBAAAAEAIAAoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJD+XwCY/fEAkf3uAJf97wGT/a+HfHaoiIWE7n9/f+6Hh4fvgICAjwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACT/yYAlP//AJ///wCg//8JjvOchXly1oaGhv+Ghob/j4+P/39/f3IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJH8aQCY/8wAkv2kfY+elJ6al/yVlZX7iIiI8H9/f7h/f38UAAAAAAAAAAAAAAAAAAAAAAAAAAB/f38egYF/noqAebF8gYaagnx3oFpUUtZpaWr/WFhY8zo6OmT///8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgICAn46Ojv+Hh4b/jouJ/4iGhfcAAADnAAAA/wAAAP8AAADIAAAAAwCj/zIAnf2VAJD/PAAAAAAAAAAAAAAAAICAgNGHh4f/gICA/4SEhP+Xl5f/AwMD/wAAAP8AAAD/AAAA/wAAAB8Aov9/ALr//wCS/Z0AAAAAAAAAAAAAAACBgYGOjo6O/4mJif+Pj4//iYmJ/wAAAOAAAAD+AAAA/wAAAP8AAABhAP7+FgCi/38Axf4fAAAAAAAAAAAAAAAAiIiID4GBgYKCgoKogoB+fYSEgZhgYGDZXl5e/m9vb/9ISEjpEBAQxw8AAFQAAAAAAAAANQAAADcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjo6Mb5iYmP+cnJz/jY2N95CQkO4pKSn/AAAA7gAAAP0AAAD7AAAAhgAAAAEAAAAAAAAAAACL/gsAkv2uAJX/QQAAAAB9fX3egoKC/4CAgP+NjY3/c3Nz+wAAAP8AAAD/AAAA/wAAAPUAAAAcAAAAAAAAAAAAnP4NAJL9rgCR/0YAAAAAfX19w4ODg/98fHz/i4uL/4qKivwAAAD/AAAA/wAAAP8AAAD1AAAAGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALGxsVyqqqr/mpqa/6mpqf9KSUn/AAAA5QAAAPkAAAD5AAAAhQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADkUFBSuZ2dn/3V1df8uLi7bAAAATgBGfyQAAAA2AAAAMwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0AAADoAAAA/wAAAP8AAAD/AAAAWgC3/2AAnv3eAJ/+dgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9AAAA/wAAAP8AAAD/AAAA/wAKDzEAnP3WAKn//wCS/OgAf/8MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQAAANwAAADtAAAA7QAAAMAAABUMAJn9gwCe/e0Aj/2LAP//AQAAAAAAAAAA)](https://www.mdanalysis.org) ## Quickstart diff --git a/docs/ExampleNotebooks b/docs/ExampleNotebooks new file mode 160000 index 0000000..cef0451 --- /dev/null +++ b/docs/ExampleNotebooks @@ -0,0 +1 @@ +Subproject commit cef0451003a732a3014d00a4f2f25869ac40ce85 diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..eae0d86 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= -v -W --keep-going +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_ext/sass.py b/docs/_ext/sass.py new file mode 100644 index 0000000..4ec4b35 --- /dev/null +++ b/docs/_ext/sass.py @@ -0,0 +1,86 @@ +""" +sphinxcontrib-sass +https://github.com/attakei-lab/sphinxcontrib-sass +Kayuza Takei +Apache 2.0 + +Modified to: +- Write directly to Sphinx output directory +- Infer targets if not given +- Ensure ``target: Path`` in ``configure_path()`` +- Return version number and thread safety from ``setup()`` +- Use compressed style by default +- More complete type checking +""" + +from os import PathLike +from pathlib import Path +from typing import Optional, Union + +import sass +from sphinx.application import Sphinx +from sphinx.environment import BuildEnvironment +from sphinx.util import logging + +logger = logging.getLogger(__name__) + + +def configure_path(conf_dir: str, src: Optional[Union[PathLike, Path]]) -> Path: + if src is None: + target = Path(conf_dir) + else: + target = Path(src) + if not target.is_absolute(): + target = Path(conf_dir) / target + return target + + +def get_targets(app: Sphinx) -> dict[Path, Path]: + src_dir = configure_path(app.confdir, app.config.sass_src_dir) + dst_dir = configure_path(app.outdir, app.config.sass_out_dir) + + if isinstance(app.config.sass_targets, dict): + targets = app.config.sass_targets + else: + targets = { + path: path.relative_to(src_dir).with_suffix(".css") + for path in src_dir.glob("**/[!_]*.s[ca]ss") + } + + return {src_dir / src: dst_dir / dst for src, dst in targets.items()} + + +def build_sass_sources(app: Sphinx, env: BuildEnvironment): + logger.debug("Building stylesheet files") + include_paths = [str(p) for p in app.config.sass_include_paths] + targets = get_targets(app) + output_style = app.config.sass_output_style + # Build css files + for src, dst in targets.items(): + content = src.read_text() + css = sass.compile( + string=content, + output_style=output_style, + include_paths=[str(src.parent)] + include_paths, + ) + dst.parent.mkdir(exist_ok=True, parents=True) + dst.write_text(css) + + +def setup(app: Sphinx): + """ + Setup function for this extension. + """ + logger.debug(f"Using {__name__}") + app.add_config_value("sass_include_paths", [], "html") + app.add_config_value("sass_src_dir", None, "html") + app.add_config_value("sass_out_dir", None, "html") + app.add_config_value("sass_targets", None, "html") + app.add_config_value("sass_output_style", "compressed", "html") + app.connect("env-updated", build_sass_sources) + + return { + "version": "0.3.4ofe", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..f863321 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,167 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +from importlib.metadata import version +from inspect import cleandoc +from pathlib import Path + +import git +import nbformat +import nbsphinx +from packaging.version import parse + +sys.path.insert(0, os.path.abspath("../")) + + +os.environ["SPHINX"] = "True" + +# -- Project information ----------------------------------------------------- + +project = "OpenFE Analysis" +copyright = "2026, The OpenFE Development Team" +author = "The OpenFE Development Team" +version = f"{parse(version('openfe_analysis')).major}.{parse(version('openfe_analysis')).minor}" + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx_click.ext", + "sphinxcontrib.autodoc_pydantic", + "sphinx_toolbox.collapse", + "sphinx.ext.autosectionlabel", + "sphinx_design", + "sphinx.ext.intersphinx", + "sphinx.ext.autosummary", + "docs._ext.sass", + "myst_parser", + "nbsphinx", + "nbsphinx_link", + "sphinx.ext.mathjax", +] +suppress_warnings = ["config.cache"] # https://github.com/sphinx-doc/sphinx/issues/12300 + +intersphinx_mapping = { + "python": ("https://docs.python.org/3.9", None), + "numpy": ("https://numpy.org/doc/stable", None), + "scikit.learn": ("https://scikit-learn.org/stable", None), + "openmm": ("https://docs.openmm.org/latest/api-python/", None), + "rdkit": ("https://www.rdkit.org/docs", None), + "openeye": ("https://docs.eyesopen.com/toolkits/python/", None), + "mdtraj": ("https://www.mdtraj.org/1.9.5/", None), + "openff.units": ("https://docs.openforcefield.org/projects/units/en/stable", None), + "gufe": ("https://gufe.openfree.energy/en/latest/", None), +} + +autoclass_content = "both" +# Make sure labels are unique +# https://www.sphinx-doc.org/en/master/usage/extensions/autosectionlabel.html#confval-autosectionlabel_prefix_document +autosectionlabel_prefix_document = True + +autodoc_pydantic_model_show_json = False + +autodoc_default_options = { + "members": True, + "member-order": "bysource", + "inherited-members": "GufeTokenizable,BaseModel", + "undoc-members": True, + "special-members": "__call__", +} +toc_object_entries_show_parents = "hide" + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [ + "_build", + "**/Thumbs.db", + "**/.DS_Store", + "_ext", + "_sass", + "**/README.md", + "ExampleNotebooks", +] + +autodoc_mock_imports = [ + "MDAnalysis", +] + +# Extensions for the myst parser +myst_enable_extensions = [ + "dollarmath", + "colon_fence", + "smartquotes", + "replacements", + "deflist", + "attrs_inline", +] +myst_heading_anchors = 3 + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "ofe_sphinx_theme" +html_theme_options = { + "logo": {"text": "openfe-analysis docs"}, + "icon_links": [ + { + "name": "GitHub", + "url": "https://github.com/OpenFreeEnergy/openfe_analysis", + "icon": "fa-brands fa-square-github", + "type": "fontawesome", + } + ], + "accent_color": "cantina-purple", + "navigation_with_keys": False, +} +html_logo = "_static/OFE-color-icon.svg" +html_favicon = "_static/OFE-color-icon.svg" +# temporary fix, see https://github.com/pydata/pydata-sphinx-theme/issues/1662 +html_sidebars = { + "installation": [], + "CHANGELOG": [], +} +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ['_static'] + + +# replace macros +rst_prolog = """ +.. |rdkit.mol| replace:: :class:`rdkit.Chem.rdchem.Mol` +""" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] +html_css_files = [ + "css/custom.css", + "css/custom-api.css", + "css/deflist-flowchart.css", +] + +# custom-api.css is compiled from custom-api.scss +sass_src_dir = "_sass" +sass_out_dir = "_static/css" diff --git a/docs/environment.yaml b/docs/environment.yaml new file mode 100644 index 0000000..e85ad3c --- /dev/null +++ b/docs/environment.yaml @@ -0,0 +1,36 @@ +name: openfe_analysis-docs +channels: +- https://conda.anaconda.org/conda-forge + +# explicit pins to speed up build: +dependencies: +- autodoc-pydantic >= 2.1 +- docutils == 0.20 +- gitpython +- libsass +- myst-parser +- nbsphinx +- nbsphinx-link +- openff-toolkit-base == 0.17.0 +- openff-units == 0.3.1 +- packaging +- pip +- plugcli >= 0.2.1 +- python +- pydantic >=2.0.0, <2.12.0 # https://github.com/openforcefield/openff-interchange/issues/1346 +- sphinx ==7.2.6 # TODO: debug "duplicate object" warning with later versions +- sphinx-click +- sphinx-design +- sphinx-toolbox +- threadpoolctl +- tqdm +- pip: + - git+https://github.com/OpenFreeEnergy/ofe-sphinx-theme@v0.3.1 + # pip install these so that we can make sure docs build on main while these packages' docs are under development + +# These are added automatically by RTD, so we include them here +# for a consistent environment. +- mock +- pillow +# - sphinx +# - sphinx_rtd_theme diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..78d0405 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,16 @@ +.. template taken from SciPy who took it from Pandas (keep the chain going) + +.. module:: openfe_analysis + +========================================== +Welcome to OpenFE Analysis' documentation! +========================================== + +The **OpenFE Analysis** toolkit provides a free and open-source framework for analyzing alchemical free energy calculations. + + +.. toctree:: + :maxdepth: 2 + :hidden: + + api diff --git a/src/openfe_analysis/rmsd.py b/src/openfe_analysis/rmsd.py index 2f5774a..54a94f4 100644 --- a/src/openfe_analysis/rmsd.py +++ b/src/openfe_analysis/rmsd.py @@ -16,22 +16,44 @@ def make_Universe(top: pathlib.Path, trj: nc.Dataset, state: int) -> mda.Universe: - """Makes a Universe and applies some transformations + """ + Construct an MDAnalysis Universe from a MultiState NetCDF trajectory + and apply standard analysis transformations. + + The Universe is created using the custom ``FEReader`` to extract a + single state from a multistate simulation. Identifies two AtomGroups: - - protein, defined as having standard amino acid names, then filtered - down to CA - - ligand, defined as resname UNK + - Protein, defined as having standard amino acid names, then filtered down to CA + - Ligand, defined as resname UNK - Then applies some transformations. + Depending on whether a protein is present, a sequence of trajectory + transformations is applied: If a protein is present: - - prevents the protein from jumping between periodic images - - moves the ligand to the image closest to the protein - - aligns the entire system to minimise the protein RMSD + - Prevents the protein from jumping between periodic images (class:`NoJump`) + - Moves the ligand to the image closest to the protein (:class:`Minimiser`) + - Aligns the entire system to minimise the protein RMSD (:class:`Aligner`) + + If only a ligand is present: + - Prevents the ligand from jumping between periodic images + - Aligns the ligand to minimize its RMSD + + Parameters + ---------- + top : pathlib.Path or Topology + Path to a topology file (e.g. PDB) or an already-loaded MDAnalysis + topology object. + trj : netCDF4.Dataset + Open NetCDF dataset produced by + ``openmmtools.multistate.MultiStateReporter``. + state : int + Thermodynamic state index to extract from the multistate trajectory. - If only a ligand: - - prevents the ligand from jumping between periodic images + Returns + ------- + MDAnalysis.Universe + A Universe with trajectory transformations applied. """ u = mda.Universe( top,