diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index b9a6c6065..94cfc8ae5 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -63,20 +63,41 @@ The documentation is built using [Sphinx](https://www.sphinx-doc.org/en/master/)
The source code (restructured text) is in `./docs/source` and images are in `./docs/source/_static`.
The homepage source code is in `./docs/source/index.rst`.
-To build the documentation locally (not required but good check), go to `./docs/versions.yaml` and change the value of `latest` to be your local branch. Then run:
+To build the documentation locally for the current branch (not required, but a good check), run (after installing development dependencies with `pip install -e .[dev]`):
```bash
python3 docs/build_docs.py
```
-The built documentation will be in `./docs/pages` and the homepage is `./docs/pages/index.html`.
-To delete, do `python3 docs/clean_docs.py`.
+This runs the debug build, which builds the current branch only and publishes the output to `./docs/pages`. The homepage is `./docs/pages/index.html`.
+
+To build the configured multiversion documentation output (for example `latest` and `dev` from `docs/versions.yaml`), run:
+
+```bash
+python3 docs/build_docs.py --build production
+```
+
+To delete generated documentation artifacts, run:
+
+```bash
+python3 docs/clean_docs.py
+```
The documentation uses the Jupyter notebook tutorials in the `examples` directory.
When building the documentation locally you will need to have installed [pandoc](https://pandoc.org/installing.html) and [gifsicle](https://github.com/kohler/gifsicle).
We recommend installing pandoc using its Anaconda distribution: `conda install -c conda-forge pandoc`.
-**NOTE:** it may be expedient at times to avoid running the tutorial notebooks. To do so, add [`nbsphinx_execute = 'never'`](https://nbsphinx.readthedocs.io/en/0.9.3/configuration.html#nbsphinx_execute) to `docs/source/conf.py`. Make sure not to commit these changes!
+**NOTE:** Building the full documentation including tutorial notebooks is slow. To include the example pages without executing notebooks, use the `--skip-notebook-execution` flag.
+
+```bash
+python3 docs/build_docs.py --skip-notebook-execution
+```
+
+This option also works with production builds:
+
+```bash
+python3 docs/build_docs.py --build production --skip-notebook-execution
+```
If you add or change any hyperlinks in the documentation, we recommend checking the "Build documentation" warnings in the GitHub Actions CI workflow to make sure the links will not cause an issue. The CI will not fail due to broken links, only issue a warning (see [issue #286](https://github.com/sandialabs/WecOptTool/issues/286)).
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 5d850a84d..5b57220f0 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -80,6 +80,8 @@ jobs:
steps:
- uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
- uses: actions/setup-python@v4
with:
python-version: '3.12' # CHANGE: Python version
@@ -97,5 +99,6 @@ jobs:
- name: Build documentation
shell: bash -l {0}
run: |
- git fetch --tags
+ git fetch --force --prune --tags
+ git fetch --force --prune origin '+refs/heads/*:refs/remotes/origin/*'
python3 docs/build_docs.py --build production
diff --git a/.gitignore b/.gitignore
index ca1f033b9..77209c475 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,5 @@ results*
# Jupyter notebooks
*/.ipynb_checkpoints/*
+docs/source/_static/theory_animation_ps.gif
+docs/source/_static/theory_animation_td.gif
diff --git a/docs/build_docs.py b/docs/build_docs.py
index 2a6fca91a..6eba84359 100644
--- a/docs/build_docs.py
+++ b/docs/build_docs.py
@@ -1,111 +1,245 @@
+import argparse
+import logging
import os
-import subprocess
-import shutil
import re
-import yaml
-import argparse
+import shutil
+import subprocess
+import sys
+import yaml
from sphinx.application import Sphinx
-import source.make_theory_animations
-
docs_dir = os.path.dirname(os.path.abspath(__file__))
-source_dir = os.path.join(docs_dir, 'source')
+source_dir = os.path.join(docs_dir, "source")
conf_dir = source_dir
-build_dir = os.path.join(docs_dir, '_build')
-linkcheck_dir = os.path.join(build_dir, 'linkcheck')
-html_dir = os.path.join(build_dir, 'html')
-doctree_dir = os.path.join(build_dir, 'doctrees')
-
-parser = argparse.ArgumentParser()
-parser.add_argument('-b', '--build', nargs=1, type=str,
- choices=['debug', 'production'],
- default='debug')
-
-def linkcheck():
- app = Sphinx(source_dir,
- conf_dir,
- linkcheck_dir,
- doctree_dir,
- 'linkcheck',
- warningiserror=False)
+build_dir = os.path.join(docs_dir, "_build")
+linkcheck_dir = os.path.join(build_dir, "linkcheck")
+html_dir = os.path.join(build_dir, "html")
+doctree_dir = os.path.join(build_dir, "doctrees")
+versions_file = os.path.join(docs_dir, "versions.yaml")
+
+
+logger = logging.getLogger(__name__)
+
+
+def _parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ description="Build WecOptTool Sphinx docs in debug or production mode."
+ )
+ parser.add_argument(
+ "-b",
+ "--build",
+ choices=["debug", "production"],
+ default="debug",
+ help="Build mode: debug builds current branch, production builds all configured versions via sphinx-multiversion.",
+ )
+ parser.add_argument(
+ "--skip-notebook-execution",
+ action="store_true",
+ help="Include tutorial/example pages but do not execute notebooks during the build.",
+ )
+ return parser.parse_args()
+
+
+def _run_command(args: list[str], cwd: str | None = None) -> None:
+ result = subprocess.run(
+ args,
+ cwd=cwd,
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+ if result.returncode != 0:
+ stderr = result.stderr.strip()
+ stdout = result.stdout.strip()
+ details = stderr or stdout or "command failed"
+ raise RuntimeError(f"{' '.join(args)} failed: {details}")
+
+
+def _remove_if_exists(path: str) -> None:
+ if os.path.exists(path):
+ shutil.rmtree(path)
+
+
+def _load_versions() -> dict[str, str]:
+ with open(versions_file, "r", encoding="utf-8") as v_file:
+ versions = yaml.safe_load(v_file)
+
+ if not isinstance(versions, dict) or not versions:
+ raise ValueError("versions.yaml must contain a non-empty mapping of version name to git ref")
+ if "latest" not in versions:
+ raise ValueError("versions.yaml must include a 'latest' entry")
+
+ for name, ref in versions.items():
+ if not isinstance(name, str) or not isinstance(ref, str):
+ raise ValueError("All entries in versions.yaml must map string names to string git refs")
+ if not name.strip() or not ref.strip():
+ raise ValueError("Version names and git refs in versions.yaml cannot be empty")
+
+ return versions
+
+
+def _git_ref_exists(git_ref: str) -> bool:
+ result = subprocess.run(
+ ["git", "show-ref", "--verify", "--quiet", git_ref],
+ cwd=docs_dir,
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+ return result.returncode == 0
+
+
+def _pattern_from_refs(refs: list[str]) -> str:
+ if not refs:
+ return "^$"
+ return "^({})$".format("|".join(re.escape(ref) for ref in sorted(set(refs))))
+
+
+def _resolve_smv_ref_patterns(refs: list[str]) -> tuple[str, str]:
+ branch_refs: list[str] = []
+ tag_refs: list[str] = []
+
+ for ref in refs:
+ head_ref = f"refs/heads/{ref}"
+ remote_ref = f"refs/remotes/origin/{ref}"
+ tag_ref = f"refs/tags/{ref}"
+
+ if _git_ref_exists(head_ref):
+ branch_refs.append(ref)
+ continue
+
+ if _git_ref_exists(remote_ref):
+ logger.info("Creating local branch '%s' from origin/%s", ref, ref)
+ _run_command(["git", "branch", "--force", ref, remote_ref], cwd=docs_dir)
+ branch_refs.append(ref)
+ continue
+
+ if _git_ref_exists(tag_ref):
+ tag_refs.append(ref)
+ continue
+
+ raise RuntimeError(
+ f"Configured docs ref '{ref}' was not found as a local branch, origin branch, or tag. "
+ "Run git fetch --force --prune --tags and verify docs/versions.yaml."
+ )
+
+ return _pattern_from_refs(branch_refs), _pattern_from_refs(tag_refs)
+
+
+def _build_with_sphinx_multiversion(versions: dict[str, str]) -> None:
+ refs = sorted({ref for ref in versions.values()})
+ branch_whitelist, tag_whitelist = _resolve_smv_ref_patterns(refs)
+
+ logger.info("Running sphinx-multiversion for refs: %s", ", ".join(refs))
+ _run_command(
+ [
+ sys.executable,
+ "-m",
+ "sphinx_multiversion",
+ source_dir,
+ html_dir,
+ "-D",
+ f"smv_branch_whitelist={branch_whitelist}",
+ "-D",
+ f"smv_tag_whitelist={tag_whitelist}",
+ ],
+ cwd=docs_dir,
+ )
+
+
+def linkcheck() -> None:
+ app = Sphinx(
+ source_dir,
+ conf_dir,
+ linkcheck_dir,
+ doctree_dir,
+ "linkcheck",
+ warningiserror=False,
+ )
app.build()
-def html():
- app = Sphinx(source_dir,
- conf_dir,
- html_dir,
- doctree_dir,
- 'html',
- warningiserror=True)
-
+def html() -> None:
+ app = Sphinx(
+ source_dir,
+ conf_dir,
+ html_dir,
+ doctree_dir,
+ "html",
+ warningiserror=True,
+ )
app.build()
-def cleanup():
- index_file = os.path.join(html_dir, 'index.html')
- with open(index_file, 'r', encoding='utf-8') as f:
- data = f.read()
-
- with open(index_file, 'w', encoding='utf-8') as f:
- data2 = re.sub(
- '\',
- '', data, flags=re.DOTALL)
- f.write(data2)
-
-
-def build_doc(version, tag, home_branch, build):
- if build != 'debug':
- os.environ['current_version'] = version
- subprocess.run(f'git checkout {tag}', shell=True)
- subprocess.run(
- f"git checkout {home_branch} -- {os.path.join(source_dir, 'conf.py')}",
- shell=True)
- subprocess.run(
- f"git checkout {home_branch} -- {os.path.join(docs_dir, 'versions.yaml')}",
- shell=True)
- subprocess.run(
- f"git checkout {home_branch} -- {os.path.join(source_dir, '_templates/versions.html')}",
- shell=True)
- source.make_theory_animations
+def build_doc(version: str, build: str) -> None:
+ os.environ["current_version"] = version
+ if build != "debug":
+ raise ValueError("build_doc should only be used for debug mode")
+
+ logger.info("Running Sphinx linkcheck")
linkcheck()
+ logger.info("Building Sphinx HTML")
html()
- cleanup()
- if build != 'debug':
- subprocess.run(
- f"git checkout {home_branch}", shell=True)
-def move_pages(dest_dir=None):
- if dest_dir is None:
- print(f"Moving HTML pages to {os.path.join(docs_dir, 'pages')}...")
- shutil.copytree(
- html_dir, os.path.join(docs_dir, 'pages'))
- else:
- print(f"Moving HTML pages to {os.path.join(docs_dir, 'pages', dest_dir)}...")
- shutil.copytree(
- html_dir, os.path.join(docs_dir, 'pages', dest_dir))
- shutil.rmtree(build_dir, ignore_errors=True)
- print('Done.')
-
-
-if __name__ == '__main__':
- args = parser.parse_args()
- build = args.build[0]
- if build == 'debug':
- print(f'Building docs in current branch...')
- build_doc('latest', '', '', build)
- move_pages()
+def move_pages_debug() -> None:
+ pages_dir = os.path.join(docs_dir, "pages")
+ logger.info("Publishing HTML pages to %s", pages_dir)
+
+ _remove_if_exists(pages_dir)
+ os.makedirs(os.path.dirname(pages_dir), exist_ok=True)
+ shutil.copytree(html_dir, pages_dir)
+
+ shutil.rmtree(build_dir, ignore_errors=False)
+ logger.info("Publish complete")
+
+
+def move_pages_multiversion(versions: dict[str, str]) -> None:
+ pages_root = os.path.join(docs_dir, "pages")
+ logger.info("Publishing sphinx-multiversion output to %s", pages_root)
+
+ _remove_if_exists(pages_root)
+ os.makedirs(pages_root, exist_ok=True)
+
+ for name, ref in versions.items():
+ source = os.path.join(html_dir, ref)
+ if not os.path.exists(source):
+ raise RuntimeError(f"Expected sphinx-multiversion output not found for ref '{ref}' at {source}")
+
+ target = pages_root if name == "latest" else os.path.join(pages_root, name)
+ if name != "latest":
+ os.makedirs(os.path.dirname(target), exist_ok=True)
+ _remove_if_exists(target)
+ shutil.copytree(source, target)
+
+ shutil.rmtree(build_dir, ignore_errors=False)
+ logger.info("Publish complete")
+
+
+def main() -> None:
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
+ args = _parse_args()
+ build = args.build
+
+ if args.skip_notebook_execution:
+ os.environ["WOT_DOCS_SKIP_NOTEBOOK_EXECUTION"] = "1"
+ logger.info("Skipping notebook execution for this build")
else:
- home_name = 'latest'
- with open(os.path.join(docs_dir, 'versions.yaml'), 'r') as v_file:
- versions = yaml.safe_load(v_file)
- home_branch = versions[home_name]
- build_doc(home_name, home_branch, home_branch, build)
- move_pages()
- for name, tag in versions.items():
- if name != home_name:
- build_doc(name, tag, home_branch, build)
- move_pages(dest_dir=name)
+ os.environ.pop("WOT_DOCS_SKIP_NOTEBOOK_EXECUTION", None)
+
+ if build == "debug":
+ logger.info("Building docs for current branch in debug mode")
+ build_doc("latest", build)
+ move_pages_debug()
+ return
+
+ versions = _load_versions()
+ logger.info("Starting production build via sphinx-multiversion")
+ _build_with_sphinx_multiversion(versions)
+ move_pages_multiversion(versions)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs/clean_docs.py b/docs/clean_docs.py
index 3912bcf65..0d152aa05 100644
--- a/docs/clean_docs.py
+++ b/docs/clean_docs.py
@@ -8,11 +8,23 @@
api_dir = os.path.join(source_dir, 'api_docs')
pages_dir = os.path.join(docs_dir, 'pages')
-def clean():
- rmtree(build_dir, ignore_errors=True)
- rmtree(example_dir, ignore_errors=True)
- rmtree(api_dir, ignore_errors=True)
- rmtree(pages_dir, ignore_errors=True)
+
+def _assert_within_docs(path: str) -> None:
+ docs_root = os.path.realpath(docs_dir)
+ candidate = os.path.realpath(path)
+ if os.path.commonpath([docs_root, candidate]) != docs_root:
+ raise RuntimeError(f"Refusing to delete path outside docs directory: {candidate}")
+
+
+def clean() -> None:
+ targets = [build_dir, example_dir, api_dir, pages_dir]
+ for target in targets:
+ _assert_within_docs(target)
+ if os.path.exists(target):
+ print(f"Removing {target}")
+ rmtree(target, ignore_errors=False)
+ else:
+ print(f"Skipping {target} (not found)")
if __name__ == '__main__':
clean()
\ No newline at end of file
diff --git a/docs/source/conf.py b/docs/source/conf.py
index fa6802960..c7cd959da 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -1,6 +1,8 @@
import os
import sys
import shutil
+import importlib
+import re
import yaml
from wecopttool import __version__, __version_info__
@@ -10,6 +12,8 @@
project_root = os.path.abspath(
os.path.join(os.path.dirname(__file__), "../.."))
sys.path.insert(0, project_root)
+source_root = os.path.abspath(os.path.dirname(__file__))
+sys.path.insert(0, source_root)
code_root = os.path.abspath(
os.path.join(os.path.dirname(__file__), "../../wecopttool"))
@@ -27,11 +31,69 @@
version = '.'.join(__version_info__[:2])
release = __version__
-current_branch = os.environ.get('current_version')
-if current_branch == 'latest':
- url_prefix = '.'
-else:
- url_prefix = '..'
+with open(os.path.join(project_root, 'docs/versions.yaml'), 'r') as v_file:
+ versions = yaml.safe_load(v_file)
+
+
+def _normalize_ref(ref: str | None) -> str | None:
+ if ref is None:
+ return None
+
+ prefixes = ('refs/heads/', 'refs/tags/', 'origin/', 'heads/', 'tags/')
+ for prefix in prefixes:
+ if ref.startswith(prefix):
+ return ref[len(prefix):]
+ return ref
+
+
+def _resolve_version_context(config=None, current_ref_override=None) -> tuple[str, str, list[list[str]]]:
+ version_by_ref = {}
+ for name, ref in versions.items():
+ version_by_ref[ref] = name
+ normalized = _normalize_ref(ref)
+ if normalized is not None:
+ version_by_ref[normalized] = name
+
+ current_ref = (
+ current_ref_override
+ or getattr(config, 'smv_current_version', None)
+ or os.environ.get('SPHINX_MULTIVERSION_NAME')
+ or os.environ.get('current_version')
+ or 'latest'
+ )
+ normalized_current_ref = _normalize_ref(current_ref)
+ current_branch = (
+ version_by_ref.get(current_ref)
+ or version_by_ref.get(normalized_current_ref)
+ or normalized_current_ref
+ or current_ref
+ )
+
+ latest_ref = versions.get('latest', 'latest')
+ normalized_latest_ref = _normalize_ref(latest_ref)
+
+ if (
+ current_branch == 'latest'
+ or current_ref == latest_ref
+ or current_ref == normalized_latest_ref
+ or normalized_current_ref == latest_ref
+ or normalized_current_ref == normalized_latest_ref
+ ):
+ url_prefix = '.'
+ else:
+ url_prefix = '..'
+
+ other_versions = []
+ for name in versions.keys():
+ if name == 'latest':
+ other_versions.append([name, os.path.join(url_prefix)])
+ else:
+ other_versions.append([name, os.path.join(url_prefix, name)])
+
+ return current_branch, url_prefix, other_versions
+
+
+skip_notebook_execution = os.environ.get('WOT_DOCS_SKIP_NOTEBOOK_EXECUTION') == '1'
# -- General configuration ---------------------------------------------------
extensions = [
@@ -47,32 +109,116 @@
]
templates_path = ['_templates']
+exclude_patterns = []
+
+if skip_notebook_execution:
+ nbsphinx_execute = 'never'
html_theme = 'sphinx_rtd_theme'
html_theme_options = {
'navigation_depth': 5,
}
html_static_path = ['_static']
+current_branch, _url_prefix, other_versions = _resolve_version_context()
html_context = {
- 'current_version' : current_branch,
- 'other_versions' : [],
+ 'current_version' : current_branch,
+ 'other_versions' : other_versions,
}
-with open(os.path.join(project_root, 'docs/versions.yaml'), 'r') as v_file:
- versions = yaml.safe_load(v_file)
-for name in versions.keys():
- if name == 'latest':
- html_context['other_versions'].append([name, os.path.join(url_prefix)])
+
+def _all_but_ipynb(_dir, contents):
+ return [entry for entry in contents if not entry.endswith('.ipynb')]
+
+
+def _all_but_nc(_dir, contents):
+ return [entry for entry in contents if not (entry.endswith('.nc') or entry.endswith('.npz'))]
+
+
+def _copy_examples() -> None:
+ print('Copy example notebooks into docs/_examples')
+ examples_dst = os.path.join(project_root, 'docs/source/_examples')
+ shutil.rmtree(examples_dst, ignore_errors=True)
+ shutil.copytree(
+ os.path.join(project_root, 'examples'),
+ examples_dst,
+ ignore=_all_but_ipynb,
+ dirs_exist_ok=True
+ )
+ shutil.copytree(
+ os.path.join(project_root, 'examples/data'),
+ os.path.join(project_root, 'docs/source/_examples/data'),
+ ignore=_all_but_nc,
+ )
+
+
+def _generate_theory_animations() -> None:
+ global _theory_animations_generated
+ if _theory_animations_generated:
+ return
+
+ importlib.invalidate_caches()
+ module_name = 'make_theory_animations'
+ if module_name in sys.modules:
+ importlib.reload(sys.modules[module_name])
else:
- html_context['other_versions'].append([name, os.path.join(url_prefix, name)])
+ importlib.import_module(module_name)
+ _theory_animations_generated = True
+
+
+def _cleanup_index_html(outdir: str) -> None:
+ index_file = os.path.join(outdir, 'index.html')
+ if not os.path.exists(index_file):
+ return
+
+ with open(index_file, 'r', encoding='utf-8') as file_handle:
+ data = file_handle.read()
+
+ updated = re.sub(r'', '', data, flags=re.DOTALL)
+
+ with open(index_file, 'w', encoding='utf-8') as file_handle:
+ file_handle.write(updated)
+
+
+def _on_config_inited(_app, _config):
+ outdir_name = os.path.basename(os.path.normpath(getattr(_app, 'outdir', '') or ''))
+ current_ref_override = None if outdir_name in {'', 'html'} else outdir_name
+
+ current_branch, _url_prefix, other_versions = _resolve_version_context(
+ _config,
+ current_ref_override=current_ref_override,
+ )
+ _config.html_context = dict(_config.html_context or {})
+ _config.html_context['current_version'] = current_branch
+ _config.html_context['other_versions'] = other_versions
+
+ _copy_examples()
+
+ if skip_notebook_execution:
+ print('Skipping notebook execution')
+
+ _generate_theory_animations()
+
+
+def _on_build_finished(app, exception):
+ if exception is not None:
+ return
+ _cleanup_index_html(app.outdir)
+
def setup(app):
app.add_css_file('css/custom.css')
+ app.connect('config-inited', _on_config_inited)
+ app.connect('build-finished', _on_build_finished)
+
+
+_theory_animations_generated = False
suppress_warnings = ['autosectionlabel.*', # nbsphinx and austosectionlabel do not play well together
'app.add_node', # using multiple builders in custom Sphinx objects throws a bunch of these
'app.add_directive',
- 'app.add_role',]
+ 'app.add_role',
+ 'ref.python', # duplicate cross-reference targets from autosummary (e.g. wecopttool.WEC vs wecopttool.core.WEC)
+ ]
# -- References (BibTex) -----------------------------------------------------
bibtex_bibfiles = ['wecopttool_refs.bib']
@@ -81,32 +227,6 @@ def setup(app):
bibtex_reference_style = 'label'
bibtex_foot_reference_style = 'foot'
-# -- Tutorials (Jupyter) -----------------------------------------------------
-print("Copy example notebooks into docs/_examples")
-
-def all_but_ipynb(dir, contents):
- result = []
- for c in contents:
- if not c.endswith(".ipynb"):
- result += [c]
- return result
-
-def all_but_nc(dir, contents):
- result = []
- for c in contents:
- if not (c.endswith(".nc") or c.endswith(".npz")):
- result += [c]
- return result
-
-shutil.rmtree(os.path.join(
- project_root, "docs/source/_examples"), ignore_errors=True)
-shutil.copytree(os.path.join(project_root, "examples"),
- os.path.join(project_root, "docs/source/_examples"),
- ignore=all_but_ipynb)
-shutil.copytree(os.path.join(project_root, "examples/data"),
- os.path.join(project_root, "docs/source/_examples/data"),
- ignore=all_but_nc)
-
# -- API documentation -------------------------------------------------------
napoleon_numpy_docstring = True
napoleon_include_init_with_doc = True
diff --git a/docs/source/make_theory_animations.py b/docs/source/make_theory_animations.py
index 7038a9678..16c0c5135 100644
--- a/docs/source/make_theory_animations.py
+++ b/docs/source/make_theory_animations.py
@@ -12,7 +12,7 @@
# get relative paths
path_to_current_file = os.path.realpath(__file__)
current_directory = os.path.dirname(path_to_current_file)
-odir = os.path.join(current_directory, "..", "_build", "html", "_static")
+odir = os.path.join(current_directory, "_static")
if not os.path.exists(odir):
os.makedirs(odir)
diff --git a/docs/source/references.rst b/docs/source/references.rst
index f9644b9e8..28aa13725 100644
--- a/docs/source/references.rst
+++ b/docs/source/references.rst
@@ -5,9 +5,9 @@ General resources
-----------------
* Companion notebooks to published papers:
- * `Control Co-Design of Power Take-off Systems for Wave Energy Converters using WecOptTool `_ (:cite:t:`Michelen2023`:)
- * `Incorporating empirical nonlinear efficiency into control co-optimization of a real world heaving point absorber using WECOPTTOOL `_ (:cite:t:`Gaebele:2023wf`:)
- * `Control co-design and uncertainty analysis of the LUPA's PTO using WecOptTool `_ (:cite:t:`Strofer:2023vw`:)
+ * `Control Co-Design of Power Take-off Systems for Wave Energy Converters using WecOptTool `_ (:cite:t:`Michelen2023`)
+ * `Incorporating empirical nonlinear efficiency into control co-optimization of a real world heaving point absorber using WECOPTTOOL `_ (:cite:t:`Gaebele:2023wf`)
+ * `Control co-design and uncertainty analysis of the LUPA's PTO using WecOptTool `_ (:cite:t:`Strofer:2023vw`)
* Webinar recordings:
* `June, 2025 `_
* `October, 2022 `_
@@ -17,10 +17,4 @@ Bibliography
------------
.. bibliography:: wecopttool_refs.bib
:style: unsrt
-
- Falnes2002
- Grasberger:2023aa
- Strofer:2023vw
- Coe2020Initial
- DEVIN2024119124
- Coe:2024aa
\ No newline at end of file
+ :all:
\ No newline at end of file
diff --git a/docs/source/theory.rst b/docs/source/theory.rst
index d7f60be11..91fb17e29 100644
--- a/docs/source/theory.rst
+++ b/docs/source/theory.rst
@@ -54,7 +54,7 @@ How's this different from what I'm used to?
Most users will be more familiar with a time-stepping approach for differential equations--this is the method applied in Simulink and therefore `WEC-Sim`_.
Starting from an initial time (e.g., :math:`t=0`), the solution is solved by iteratively stepping forward in time.
-.. image:: ../pages/_static/theory_animation_td.gif
+.. image:: _static/theory_animation_td.gif
:width: 600
:alt: Time-domain solution animation
:align: center
@@ -62,7 +62,7 @@ Starting from an initial time (e.g., :math:`t=0`), the solution is solved by ite
Pseudo-spectral methods can be applied to solve the same differential equations, but solve the entire time period of interest at once.
At first the solution will not be correct, but as the optimization algorithm iterates, it will progressively improve the solution.
-.. image:: ../pages/_static/theory_animation_ps.gif
+.. image:: _static/theory_animation_ps.gif
:width: 600
:alt: Pseudo-spectral solution animation
:align: center
diff --git a/pyproject.toml b/pyproject.toml
index b8bc5635b..2ef35b937 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -32,7 +32,8 @@ dependencies = [
[project.optional-dependencies]
dev = [
"pytest",
- "sphinx",
+ "sphinx>=7,<8",
+ "sphinx-multiversion",
"sphinxcontrib-bibtex",
"sphinx_rtd_theme",
"jupyter",
diff --git a/wecopttool/utilities.py b/wecopttool/utilities.py
index 5e4896cc8..769684013 100644
--- a/wecopttool/utilities.py
+++ b/wecopttool/utilities.py
@@ -106,9 +106,9 @@ def plot_hydrodynamic_coefficients(bem_data,
bem_data.radiation_damping.sel(
radiating_dof=rdof, influenced_dof=idof).plot(ax=ax_rd[i, j])
if i == len(radiating_dofs)-1:
- ax_am[i, j].set_xlabel(f'$\omega$', fontsize=10)
- ax_rd[i, j].set_xlabel(f'$\omega$', fontsize=10)
- ax_ex[j, 0].set_xlabel(f'$\omega$', fontsize=10)
+ ax_am[i, j].set_xlabel(r'$\omega$', fontsize=10)
+ ax_rd[i, j].set_xlabel(r'$\omega$', fontsize=10)
+ ax_ex[j, 0].set_xlabel(r'$\omega$', fontsize=10)
else:
ax_am[i, j].set_xlabel('')
ax_rd[i, j].set_xlabel('')
@@ -175,12 +175,12 @@ def plot_bode_impedance(impedance: DataArray,
axes[2*i, j].grid(True, which = 'both')
axes[2*i+1, j].grid(True, which = 'both')
if i == len(radiating_dofs)-1:
- axes[2*i+1, j].set_xlabel(f'Frequency [Hz]', fontsize=10)
+ axes[2*i+1, j].set_xlabel(r'Frequency [Hz]', fontsize=10)
else:
axes[i, j].set_xlabel('')
if j == 0:
axes[2*i, j].set_ylabel(f'{rdof} \n Mag. [dB]', fontsize=10)
- axes[2*i+1, j].set_ylabel(f'Phase. [deg]', fontsize=10)
+ axes[2*i+1, j].set_ylabel(r'Phase. [deg]', fontsize=10)
else:
axes[i, j].set_ylabel('')
if i == 0: