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..7c8b701 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,7 @@
[](https://openfree.energy/)
[](https://github.com/OpenFreeEnergy/openfe_analysis/actions/workflows/ci.yaml)
[](https://codecov.io/gh/OpenFreeEnergy/openfe_analysis)
+[](https://docs.openfree.energy/en/stable/?badge=stable)
[](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/_sass/deflist-flowchart.scss b/docs/_sass/deflist-flowchart.scss
new file mode 100644
index 0000000..b65f54b
--- /dev/null
+++ b/docs/_sass/deflist-flowchart.scss
@@ -0,0 +1,397 @@
+:root {
+ --arrow-thickness: 4px;
+ --arrow-head-size: 7px;
+ --arrow-length: 2em;
+ --arrow-multiple-gap: 20px;
+ --arrow-color: var(--pst-color-text-muted);
+ --arrow-fade-dist: 0px;
+ --flowchart-def-bg-color: var(--pst-color-surface);
+ --flowchart-bg-color: var(--pst-color-background);
+ --flowchart-def-border-color: var(--pst-color-border);
+ --flowchart-unit-width: 45px;
+ --flowchart-spacing: 0.5rem;
+ --flowchart-column-gap: calc(1.5 * var(--flowchart-spacing));
+ --flowchart-top-label-space: 26px;
+}
+.arrow.thick {
+ --arrow-thickness: 6px;
+ --arrow-head-size: 10px;
+}
+
+.deflist-flowchart ul,
+ul.deflist-flowchart {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ height: 100%;
+ grid-column-gap: var(--flowchart-column-gap);
+ margin: 0;
+ padding: 0;
+}
+
+.deflist-flowchart {
+ margin: 1em 0;
+
+ p:first-child {
+ margin-top: 0;
+ }
+
+ p:last-child {
+ margin-bottom: 0;
+ }
+
+ li,
+ li ul
+ {
+ margin: 0;
+ padding: 0;
+ }
+
+ li:empty:not([class])
+ {
+ display: None;
+ }
+
+ li {
+ list-style: none;
+ }
+
+ .arrow-down::after,
+ .arrow-up::after,
+ .arrow-multiple.arrow-down::before,
+ .arrow-multiple.arrow-up::before,
+ .arrow-cycle::after,
+ .arrow-cycle::before {
+ content: "";
+ }
+
+ .arrow-down,
+ .arrow-up,
+ .arrow-cycle
+ {
+ --arrow-head-size-clamped: calc(min(var(--arrow-head-size), var(--arrow-length) / 2));
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-grow: 1;
+ min-height: var(--arrow-length);
+ width: 100%;
+ margin: calc(2 * var(--flowchart-spacing)) auto;
+ position: relative;
+ z-index: 1;
+ padding: calc(var(--arrow-length) / 4) 0;
+
+ &::before, &::after {
+ --actual-arrow-length: max(var(--arrow-length), 100%);
+ --arrow-tail-gradient:
+ linear-gradient(
+ 45deg,
+ transparent calc(50% - var(--arrow-thickness)/2),
+ var(--arrow-color) calc(50% - var(--arrow-thickness)/2),
+ var(--arrow-color) calc(50% + var(--arrow-thickness)/2),
+ transparent calc(50% + var(--arrow-thickness)/2)
+ );
+ --arrow-head-gradient:
+ linear-gradient(
+ -45deg,
+ var(--arrow-color) var(--arrow-head-size-clamped),
+ transparent var(--arrow-head-size-clamped)
+ );
+ height: calc(var(--actual-arrow-length)/1.4142);
+ width: auto;
+ aspect-ratio: 1;
+ padding: 0;
+ display: inline-block;
+ transform: rotate(45deg);
+ background-image:
+ var(--arrow-tail-gradient),
+ var(--arrow-head-gradient);
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform-origin: 0 0;
+ z-index: -1;
+ }
+
+ &.arrow-tail {
+ &::before, &::after {
+ background-image:
+ var(--arrow-tail-gradient);
+ }
+ }
+
+ > p {
+ background: linear-gradient(
+ transparent,
+ var(--flowchart-bg-color) var(--arrow-fade-dist),
+ var(--flowchart-bg-color) calc(100% - var(--arrow-fade-dist)),
+ transparent,
+ );
+ line-height: 1.5;
+ z-index: 10;
+ }
+ }
+
+ .arrow-down:not(.arrow-tail),
+ .arrow-cycle {
+ padding-bottom: calc(var(--arrow-head-size-clamped) + var(--arrow-length) / 4);
+ }
+
+ .arrow-up:not(.arrow-tail),
+ .arrow-cycle {
+ padding-top: calc(var(--arrow-head-size-clamped) + var(--arrow-length) / 4);
+ }
+
+ .arrow-cycle, .arrow-multiple {
+ &::after {
+ translate: calc(0.5 * var(--arrow-multiple-gap)) 0;
+ }
+ &::before {
+ translate: calc(-0.5 * var(--arrow-multiple-gap)) 0;
+ }
+ }
+
+ .arrow-up::after,
+ .arrow-multiple.arrow-up::before,
+ .arrow-cycle::before
+ {
+ transform: rotate(-135deg);
+ translate: 0 calc(var(--actual-arrow-length) + 2 * var(--flowchart-spacing) + var(--arrow-head-size-clamped) / 2);
+ }
+
+ .arrow-cycle::before {
+ translate:
+ calc(-0.5 * var(--arrow-multiple-gap))
+ 140%;
+ }
+
+ .arrow-aside {
+ margin-left: calc(8 * var(--arrow-head-size-clamped));
+ &::after {
+ left: calc(-4 * var(--arrow-head-size-clamped));
+ }
+ }
+
+ .arrow-multiple-combine {
+ &::before {
+ content: "";
+ width: var(--arrow-multiple-gap);
+ border: var(--arrow-thickness) solid var(--arrow-color);
+ height: calc(var(--arrow-length) / 2);
+ background: var(--flowchart-bg-color);
+ transform: none;
+ left: auto;
+ z-index: 2;
+ }
+
+ &.arrow-down {
+ padding-top: calc(0.75 * var(--arrow-length) - var(--arrow-head-size-clamped) / 2);
+ padding-bottom: calc(0.5 * var(--arrow-head-size-clamped) + 0.25 * var(--arrow-length));
+ &::before {
+ border-top: 1px solid var(--flowchart-bg-color);
+ }
+ }
+
+ &.arrow-up {
+ &::before {
+ border-bottom: 1px solid var(--flowchart-bg-color);
+ top: auto;
+ bottom: -1px;
+ }
+ }
+ }
+
+ .arrow-tail {
+ &.arrow-down {
+ margin-bottom: 0;
+ }
+ &.arrow-up {
+ margin-top: 0;
+ }
+ }
+
+ .arrow-head {
+ &.arrow-up {
+ margin-bottom: 0;
+ }
+ &.arrow-down {
+ margin-top: 0;
+ }
+ }
+
+ .arrow-combine, .arrow-combine-left, .arrow-combine-right {
+ &.arrow-down.arrow-tail, &.arrow-up.arrow-head {
+ --arrow-combine-gradient-angle: 0deg;
+ padding-bottom: calc(0.5 * var(--arrow-thickness));
+ margin-bottom: calc(-0.5 * var(--arrow-thickness));
+ }
+ &.arrow-up.arrow-tail, &.arrow-down.arrow-head {
+ --arrow-combine-gradient-angle: 180deg;
+ padding-top: calc(0.5 * var(--arrow-thickness));
+ margin-top: calc(-0.5 * var(--arrow-thickness));
+ }
+ background-image:
+ linear-gradient(
+ var(--arrow-combine-gradient-angle),
+ var(--arrow-color) var(--arrow-thickness),
+ transparent var(--arrow-thickness)
+ );
+ background-repeat: no-repeat;
+
+ width: calc(max(100% + 2 * var(--flowchart-column-gap), var(--flowchart-unit-width)));
+ margin-left: calc(-1 * var(--flowchart-column-gap));
+
+ &.arrow-combine-left, &.arrow-combine-right {
+ background-size: 50%;
+
+ &.arrow-multiple {
+ background-size: calc(50% + 0.5 * var(--arrow-multiple-gap));
+ }
+ }
+
+ &.arrow-combine-right {
+ background-position-x: 100%;
+ }
+ }
+
+ > ul > li {
+ &.arrow-down,
+ &.arrow-up,
+ &.arrow-cycle {
+ width: calc(100% - var(--flowchart-top-label-space));
+ margin-left: 0;
+ }
+ }
+
+ dl {
+ display: flex;
+ flex-direction: row-reverse;
+ margin: 0;
+ padding: 0 var(--flowchart-spacing);
+ }
+ dt {
+ display: inline-block;
+ writing-mode: vertical-rl;
+ margin-top: .25rem;
+ flex-grow: 0;
+ width: var(--flowchart-top-label-space);
+ font-size: 1.1em;
+ }
+ dd {
+ text-align: center;
+ position: relative;
+ border: 1px solid var(--flowchart-def-border-color);
+ border-radius: .25rem;
+ margin: 0;
+ display: inline-block;
+ flex-grow: 1;
+ container-type: inline-size;
+ container-name: flowchart;
+ overflow-x: auto;
+ }
+
+ dd dl {
+ background-color: var(--flowchart-def-bg-color);
+ border-radius: 4px;
+ box-shadow: 0 6px 10px 0 rgba(0,0,0,0.14),
+ 0 1px 18px 0 rgba(0,0,0,0.12),
+ 0 3px 5px -1px rgba(0,0,0,0.4);
+ display: block;
+ margin: 0 auto;
+ padding: calc(var(--flowchart-spacing) / 2);
+ max-width: calc(100cqw - 2 * var(--flowchart-spacing));
+ min-width: calc(2 * var(--flowchart-unit-width) + var(--flowchart-column-gap));
+ }
+ dd dt {
+ writing-mode: horizontal-tb;
+ display: block;
+ margin-top: 0;
+ width: unset;
+ font-size: unset;
+ }
+ dd dd {
+ border: none;
+ display: block;
+ container-type: unset;
+ overflow-x: unset;
+ padding: calc(var(--flowchart-spacing) / 2);
+ }
+
+ dd > ul {
+ width: fit-content;
+ padding: var(--flowchart-spacing);
+ margin: 0 auto;
+ overflow: hidden;
+ }
+
+ dd dd > ul {
+ min-width: unset;
+ padding: 0;
+ margin: 0;
+ }
+
+ dl a, a {
+ font-weight: bold;
+ }
+
+ div.flowchart-sidebyside > ul:only-child {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ }
+
+ .flowchart-spacer {
+ height: 100%;
+ flex-shrink: 9999;
+ min-height: calc(2 * var(--flowchart-spacing))
+ }
+
+ .width-1 {
+ width: calc(var(--flowchart-unit-width));
+ }
+ .width-2 {
+ width: calc(2 * var(--flowchart-unit-width) + var(--flowchart-column-gap));
+ }
+ .width-3 {
+ width: calc(3 * var(--flowchart-unit-width) + 2 * var(--flowchart-column-gap));
+ }
+ .width-4 {
+ width: calc(4 * var(--flowchart-unit-width) + 3 * var(--flowchart-column-gap));
+ }
+ .width-5 {
+ width: calc(5 * var(--flowchart-unit-width) + 4 * var(--flowchart-column-gap));
+ }
+ .width-6 {
+ width: calc(6 * var(--flowchart-unit-width) + 5 * var(--flowchart-column-gap));
+ }
+ .width-7 {
+ width: calc(7 * var(--flowchart-unit-width) + 6 * var(--flowchart-column-gap));
+ }
+ .width-8 {
+ width: calc(8 * var(--flowchart-unit-width) + 7 * var(--flowchart-column-gap));
+ }
+ .width-9 {
+ width: calc(9 * var(--flowchart-unit-width) + 8 * var(--flowchart-column-gap));
+ }
+ .width-10 {
+ width: calc(10 * var(--flowchart-unit-width) + 9 * var(--flowchart-column-gap));
+ }
+ li {
+ &.width-2,
+ &.width-3,
+ &.width-4,
+ &.width-5,
+ &.width-6,
+ &.width-7,
+ &.width-8,
+ &.width-9,
+ &.width-10,
+ &.width-full {
+ > dl {
+ max-width: unset;
+ }
+ }
+ }
+}
diff --git a/docs/_static/API.svg b/docs/_static/API.svg
new file mode 100644
index 0000000..9c2cb31
--- /dev/null
+++ b/docs/_static/API.svg
@@ -0,0 +1 @@
+
diff --git a/docs/_static/CLI.svg b/docs/_static/CLI.svg
new file mode 100644
index 0000000..3d170a9
--- /dev/null
+++ b/docs/_static/CLI.svg
@@ -0,0 +1 @@
+
diff --git a/docs/_static/Cookbook.svg b/docs/_static/Cookbook.svg
new file mode 100644
index 0000000..d2f72b4
--- /dev/null
+++ b/docs/_static/Cookbook.svg
@@ -0,0 +1 @@
+
diff --git a/docs/_static/Download.svg b/docs/_static/Download.svg
new file mode 100644
index 0000000..3e425ca
--- /dev/null
+++ b/docs/_static/Download.svg
@@ -0,0 +1 @@
+
diff --git a/docs/_static/OFE-color-icon.svg b/docs/_static/OFE-color-icon.svg
new file mode 100644
index 0000000..0a58ca5
--- /dev/null
+++ b/docs/_static/OFE-color-icon.svg
@@ -0,0 +1,12 @@
+
diff --git a/docs/_static/Rocket.svg b/docs/_static/Rocket.svg
new file mode 100644
index 0000000..ae1ed81
--- /dev/null
+++ b/docs/_static/Rocket.svg
@@ -0,0 +1 @@
+
diff --git a/docs/_static/Showcase.svg b/docs/_static/Showcase.svg
new file mode 100644
index 0000000..953eb0d
--- /dev/null
+++ b/docs/_static/Showcase.svg
@@ -0,0 +1,75 @@
+
+
+
+
diff --git a/docs/_static/Tutorial.svg b/docs/_static/Tutorial.svg
new file mode 100644
index 0000000..a42592d
--- /dev/null
+++ b/docs/_static/Tutorial.svg
@@ -0,0 +1 @@
+
diff --git a/docs/_static/UserGuide.svg b/docs/_static/UserGuide.svg
new file mode 100644
index 0000000..e8cf53a
--- /dev/null
+++ b/docs/_static/UserGuide.svg
@@ -0,0 +1 @@
+
diff --git a/docs/_templates/autosummary/base.rst b/docs/_templates/autosummary/base.rst
new file mode 100644
index 0000000..cdb86a6
--- /dev/null
+++ b/docs/_templates/autosummary/base.rst
@@ -0,0 +1,5 @@
+.. title:: {{ objname }}
+
+.. currentmodule:: {{ module }}
+
+.. auto{{ objtype }}:: {{ objname }}
diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst
new file mode 100644
index 0000000..cdb86a6
--- /dev/null
+++ b/docs/_templates/autosummary/class.rst
@@ -0,0 +1,5 @@
+.. title:: {{ objname }}
+
+.. currentmodule:: {{ module }}
+
+.. auto{{ objtype }}:: {{ objname }}
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..d3f9db6
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,254 @@
+# 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 = "2022, 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 = [
+ "cinnabar",
+ "dill",
+ "MDAnalysis",
+ "matplotlib",
+ "mdtraj",
+ "openmmforcefields",
+ "openmmtools",
+ "pymbar",
+ "openff.interchange",
+ "openmmforcefields",
+ "psutil",
+ "py3Dmol",
+ "zstandard",
+]
+
+# 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 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"
+
+# Clone or update ExampleNotebooks
+example_notebooks_path = Path("ExampleNotebooks")
+try:
+ if example_notebooks_path.exists():
+ repo = git.Repo(example_notebooks_path)
+ try:
+ repo.remote("origin").pull()
+ except git.exc.GitCommandError:
+ # cannot pull if on a tag
+ pass
+ else:
+ repo = git.Repo.clone_from(
+ "https://github.com/OpenFreeEnergy/ExampleNotebooks.git",
+ branch="2025.12.04",
+ to_path=example_notebooks_path,
+ )
+except Exception as e:
+ from sphinx.util.logging import getLogger
+
+ filename = e.__traceback__.tb_frame.f_code.co_filename
+ lineno = e.__traceback__.tb_lineno
+ getLogger("sphinx.ext.openfe_git").warning(
+ f"Getting ExampleNotebooks failed in {filename} line {lineno}: {e}"
+ )
+
+
+# First, create links at top of notebook pages
+# All notebooks are in ExampleNotebooks repo, so link to that
+# Finally, add sphinx reference anchor in prolog so that we can make refs
+nbsphinx_prolog = cleandoc(r"""
+ {%- set gh_repo = "OpenFreeEnergy/ExampleNotebooks" -%}
+ {%- set gh_branch = "main" -%}
+ {%- set path = env.doc2path(env.docname, base=None) -%}
+ {%- if path.endswith(".nblink") -%}
+ {%- set path = env.metadata[env.docname]["nbsphinx-link-target"] -%}
+ {%- endif -%}
+ {%- if path.startswith("ExampleNotebooks/") -%}
+ {%- set path = path.replace("ExampleNotebooks/", "", 1) -%}
+ {%- endif -%}
+ {%- set gh_url =
+ "https://www.github.com/"
+ ~ gh_repo
+ ~ "/blob/"
+ ~ gh_branch
+ ~ "/"
+ ~ path
+ -%}
+ {%- set dl_url =
+ "https://raw.githubusercontent.com/"
+ ~ gh_repo
+ ~ "/"
+ ~ gh_branch
+ ~ "/"
+ ~ path
+ -%}
+
+ .. container:: ofe-top-of-notebook
+
+ .. button-link:: {{gh_url}}
+ :color: primary
+ :shadow:
+ :outline:
+
+ :octicon:`mark-github` View on GitHub
+
+ .. button-link:: {{dl_url}}
+ :color: primary
+ :shadow:
+ :outline:
+
+ :octicon:`download` Download Notebook
+
+ .. _{{ env.doc2path(env.docname, base=None) }}:
+""")
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..2595f4b
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,26 @@
+.. 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.
+
+.. grid:: 1 2 2 4
+ :gutter: 3
+
+ .. grid-item-card:: :fas:`code` Python API
+ :text-align: center
+ :link: reference/api/index
+ :link-type: doc
+
+ Comprehensive details of the **openfe_analysis** Python API.
+
+
+.. toctree::
+ :maxdepth: 2
+ :hidden:
+
+ reference/index
diff --git a/docs/reference/api/generated/openfe_analysis.rst b/docs/reference/api/generated/openfe_analysis.rst
new file mode 100644
index 0000000..743b7b6
--- /dev/null
+++ b/docs/reference/api/generated/openfe_analysis.rst
@@ -0,0 +1,37 @@
+openfe\_analysis package
+========================
+
+Subpackages
+-----------
+
+.. toctree::
+ :maxdepth: 4
+
+ openfe_analysis.utils
+
+Submodules
+----------
+
+openfe\_analysis.reader module
+------------------------------
+
+.. automodule:: openfe_analysis.reader
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+openfe\_analysis.rmsd module
+----------------------------
+
+.. automodule:: openfe_analysis.rmsd
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+openfe\_analysis.transformations module
+---------------------------------------
+
+.. automodule:: openfe_analysis.transformations
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/reference/api/generated/openfe_analysis.utils.rst b/docs/reference/api/generated/openfe_analysis.utils.rst
new file mode 100644
index 0000000..df59c30
--- /dev/null
+++ b/docs/reference/api/generated/openfe_analysis.utils.rst
@@ -0,0 +1,29 @@
+openfe\_analysis.utils package
+==============================
+
+Submodules
+----------
+
+openfe\_analysis.utils.multistate module
+----------------------------------------
+
+.. automodule:: openfe_analysis.utils.multistate
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+openfe\_analysis.utils.serialization module
+-------------------------------------------
+
+.. automodule:: openfe_analysis.utils.serialization
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+Module contents
+---------------
+
+.. automodule:: openfe_analysis.utils
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/reference/api/index.rst b/docs/reference/api/index.rst
new file mode 100644
index 0000000..1d15575
--- /dev/null
+++ b/docs/reference/api/index.rst
@@ -0,0 +1,10 @@
+.. _api:
+
+OpenFE Analysis API Reference
+=============================
+
+.. toctree::
+ :maxdepth: 2
+
+ generated/openfe_analysis.rst
+ generated/openfe_analysis.utils.rst
diff --git a/docs/reference/index.rst b/docs/reference/index.rst
new file mode 100644
index 0000000..c2a34f1
--- /dev/null
+++ b/docs/reference/index.rst
@@ -0,0 +1,9 @@
+Reference
+=========
+
+This contains details of the Python API.
+
+.. toctree::
+ :maxdepth: 2
+
+ api/index
diff --git a/src/openfe_analysis/rmsd.py b/src/openfe_analysis/rmsd.py
index 1519d64..5de9057 100644
--- a/src/openfe_analysis/rmsd.py
+++ b/src/openfe_analysis/rmsd.py
@@ -14,22 +14,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
+ - 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:
+ 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.
+
+ Returns
+ -------
+ MDAnalysis.Universe
+ A Universe with trajectory transformations applied.
"""
u = mda.Universe(
top,