Unverified Commit f5166695 authored by Josh A. Mitchell's avatar Josh A. Mitchell Committed by GitHub
Browse files

Reorganise documentation (#3165)

* Break user guide into parts

* Break up file I missed

* Add basic .gitignore to suit out-of-tree builds in build, build1, build2 ... build9

* Small changes to autonumber.py for doc compilation

This is a small change designed not to fix the incorrect logic of
autonumber.py, but just to get the docs compiling. While assigning
numbers, the code now just ignores the autonumber_by_chapter
setting for a particular object if it can't find the appropriate
section in section_numbers. This is a temporary fix!

* Include part and chapter in autonumbered objects

* Fix autonumber.py to correctly reference autonumber roles in file-level sections

* Unify and simplify styling across documentation

* Break dev docs down into individual chapter-files

* Correct absolute links to relative

* Disable browser suggestions for lunrsearch box in API docs

* Remove part name from chapter 2.1

* Rename ambiguous 'Home' link to OpenMM.org

* Typo

* Minor fixes and reversions

Reverts some changes I had made and later thought better of,
and fixes some recurring typos across the documentation.

* Number developers guide chapters

* Fix responsiveness

* Remove header.rst and center captions

* Add a level of depth to main TOC and styling to accomodate

* Add third level to Part-level TOCs

* Use :numref: instead of :ref: to number links to sections

* Continuously number chapters in user guide

* navbar links to other docs relative, not absolute

* Improve navbar spacing with new template

* Fix sidebar while allowing it to scroll

* Hard -> Soft links for navigation.html template

* Add navigation.html template to cmakelists

* Add another level of .. to relative links

* Fix flex on C++ and remove layer of ..
parent 1344f2e0
../../api-python/_static/logo.png
\ No newline at end of file
../../api-python/_templates/navigation.html
\ No newline at end of file
......@@ -11,176 +11,219 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
import os
import sys
# 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.
sys.path.insert(0, os.path.abspath('.'))
sys.path.append(os.path.abspath('../sphinx'))
sys.path.insert(0, os.path.abspath("."))
sys.path.append(os.path.abspath("../sphinx"))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['autonumber', 'numsec']
extensions = ["autonumber"]
# :numref: role should just use the number
numfig_format = {
"figure": "%s",
"table": "%s",
"code-block": "%s",
"section": "%s",
}
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix of source filenames.
source_suffix = '.rst'
source_suffix = ".rst"
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = u'OpenMM Developer Guide'
copyright = u'2011-2017, Stanford University'
project = u"OpenMM Dev Guide"
copyright = u"2011-2017, Stanford University"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = os.getenv('OPENMM_VERSION')
version = os.getenv("OPENMM_VERSION")
# The full version, including alpha/beta/rc tags.
release = os.getenv('OPENMM_VERSION')
release = os.getenv("OPENMM_VERSION")
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
exclude_patterns = ["_build"]
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# modindex_common_prefix = []
# -- 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 = 'alabaster'
html_theme = "alabaster"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
html_theme_options = {
"github_button": False,
"github_user": "openmm",
"github_repo": "openmm",
"logo_name": True,
"logo": "logo.png",
"extra_nav_links": [
{
"title": "OpenMM.org",
"uri": "https://openmm.org",
"relative": False,
},
{
"title": "User's Manual",
"uri": "../userguide/",
"relative": True,
},
{
"title": "Python API reference",
"uri": "../api-python/",
"relative": True,
},
{
"title": "C++ API reference",
"uri": "../api-c++/",
"relative": True,
},
{
"title": "GitHub",
"uri": "https://github.com/openmm",
"relative": False,
},
],
"show_relbar_bottom": True,
}
# Custom sidebar templates, maps document names to template names.
html_sidebars = {"**": ["about.html", "searchbox.html", "navigation.html"]}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# html_favicon = None
# 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_static_path = ["_static"]
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# html_domain_indices = True
# If false, no index is generated.
html_use_index = False
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'OpenMMDeveloperGuidedoc'
htmlhelp_basename = "OpenMMDeveloperGuidedoc"
# -- Options for LaTeX output --------------------------------------------------
latex_engine = 'xelatex'
latex_engine = "xelatex"
latex_use_xindy = False
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
'papersize': 'letterpaper,openany',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
'preamble': """
# The paper size ('letterpaper' or 'a4paper').
"papersize": "letterpaper,openany",
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
"preamble": """
\\usepackage[none]{hyphenat}
\\usepackage{xstring}
\\usepackage{color}
......@@ -203,29 +246,34 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'OpenMMDeveloperGuide.tex', u'OpenMM Developer Guide',
u'Peter Eastman', 'manual'),
(
"index",
"OpenMMDeveloperGuide.tex",
u"OpenMM Developer Guide",
u"Peter Eastman",
"manual",
),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
......@@ -233,12 +281,11 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'openmmdeveloperguide', u'OpenMM Developer Guide',
[u'Peter Eastman'], 1)
("index", "openmmdeveloperguide", u"OpenMM Developer Guide", [u"Peter Eastman"], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
......@@ -247,16 +294,22 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'OpenMMDeveloperGuide', u'OpenMM Developer Guide',
u'Peter Eastman', 'OpenMMDeveloperGuide', 'One line description of project.',
'Miscellaneous'),
(
"index",
"OpenMMDeveloperGuide",
u"OpenMM Developer Guide",
u"Peter Eastman",
"OpenMMDeveloperGuide",
"One line description of project.",
"Miscellaneous",
),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# texinfo_show_urls = 'footnote'
######################
OpenMM Developer Guide
######################
########################
OpenMM Developer's Guide
########################
.. only:: latex
......@@ -10,7 +10,14 @@ OpenMM Developer Guide
:maxdepth: 3
:numbered:
developer
01_introduction.rst
02_core_library.rst
03_writing_plugins.rst
04_reference_platform.rst
05_cpu_platform.rst
06_opencl_platform.rst
07_cuda_platform.rst
08_common_compute.rst
.. only:: html
......
from pathlib import Path
from docutils.nodes import Text, label, reference, section
from docutils.parsers.rst import roles
from docutils.nodes import Text, reference, section, label
from sphinx.roles import XRefRole
class autonumber(label):
pass
class autonumber_ref(reference):
pass
def autonumber_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
return ([autonumber(text=text)], [])
def doctree_resolved(app, doctree, docname):
index = {};
refTable = {}
if app.config.autonumber_by_chapter:
# Record the number of each chapter
env = app.builder.env
sectionNumbers = {}
for doc in env.toc_secnumbers:
sections = env.toc_secnumbers[doc]
for sectionId in sections:
sectionNumbers[sectionId[1:]] = sections[sectionId]
lastChapter = -1
def warn(*args, **kwargs):
"""Issue a warning/notification to the user. Same signature as `print`"""
print("\nAutonumber: ", *args, **kwargs)
def chapter_numbers_by_section(env):
"""Collect a mapping from a section's reference key to its position in the TOC
The reference key is of the form `f'{path-to-doc}:{section-id}'`.
The position in the TOC is useful eg. to label sections with numbers.
It is returned as a tuple of ints"""
# Record the number of each chapter
section_numbers = {}
for doc in env.toc_secnumbers:
sections = env.toc_secnumbers[doc]
for section_id in sections:
# Include the src to disambiguate duplicates
src = env.srcdir + "/" + doc
key = f"{src}:{section_id[1:]}"
if key in section_numbers:
warn(f"{section_id} is duplicated in {src}")
section_numbers[key] = sections[section_id]
return section_numbers
def get_chapter(node, depth, section_numbers):
"""Get the numerical position of the chapter in which node resides
args:
node:
A docutils node whose chapter we want the number of
depth:
How many levels deep into the toctree is a "chapter"
section_numbers:
The output of chapter_numbers_by_section(env)
"""
parent = node.parent
chapter = None
while chapter is None:
if isinstance(parent, section):
chapter = parent
parent = parent.parent
src = str(Path(chapter.source).with_suffix(""))
chapter_id = chapter.attributes["ids"][0]
key = src + ":" + chapter_id
try:
chapter = section_numbers[key][:depth]
except KeyError:
# The above will fail if the section is at the top of a file;
# There doesn't seem to be a way to get the top section label
# in chapter_numbers_by_section, so we'll just assume that if
# the above fails, we're looking for a section with no label:
key = src + ":"
warn(f"Assuming {repr(chapter_id)} is a top level section")
chapter = section_numbers[key][:depth]
return ".".join(str(i) for i in chapter)
def proc_autonumber(app, doctree, docname):
index = {}
ref_table = {}
autonum_depth = app.config.autonumber_by_depth
if autonum_depth:
section_numbers = chapter_numbers_by_section(app.builder.env)
last_chapter = None
# Assign numbers to all the autonumbered objects.
for node in doctree.traverse(autonumber):
category = node.astext().split(',')[0]
category = node.astext().split(",")[0]
if category in index:
nextNumber = index[category]+1
next_number = index[category] + 1
else:
nextNumber = 1
if app.config.autonumber_by_chapter:
parent = node.parent
chapter = None
while chapter is None:
if isinstance(parent, section):
chapter = parent
parent = parent.parent
chapter = sectionNumbers[chapter.attributes['ids'][0]][0]
if chapter != lastChapter:
next_number = 1
if autonum_depth:
chapter = get_chapter(node, autonum_depth, section_numbers)
if chapter != last_chapter:
index = {}
nextNumber = 1
newNode = Text('%s %d-%d' % (category, chapter, nextNumber))
lastChapter = chapter
next_number = 1
new_node = Text(f"{category} {chapter}-{next_number}")
last_chapter = chapter
else:
newNode = Text('%s %d' % (category, nextNumber))
index[category] = nextNumber
refTable[node.astext()] = newNode
node.parent.replace(node, newNode)
new_node = Text(f"{category} {next_number}")
index[category] = next_number
ref_table[node.astext()] = new_node
node.parent.replace(node, new_node)
# Replace references with the name of the referenced object
for ref_info in doctree.traverse(autonumber_ref):
target = ref_info['reftarget']
if target not in refTable:
raise ValueError('Unknown target for autonumber reference: '+target)
ref_info.replace_self(Text(refTable[target].astext()))
target = ref_info["reftarget"]
if target not in ref_table:
raise ValueError(f"Unknown target for autonumber reference: {target}")
ref_info.replace_self(Text(ref_table[target].astext()))
def setup(app):
app.add_config_value('autonumber_by_chapter', True, False)
roles.register_local_role('autonumber', autonumber_role)
app.add_config_value("autonumber_by_depth", 1, "env")
roles.register_local_role("autonumber", autonumber_role)
app.add_node(autonumber)
app.add_node(autonumber_ref)
app.add_role('autonumref', XRefRole(nodeclass=autonumber_ref))
app.connect('doctree-resolved', doctree_resolved)
app.add_role("autonumref", XRefRole(nodeclass=autonumber_ref))
app.connect("doctree-resolved", proc_autonumber)
""" continuous.py
A Sphinx extension to number chapters continuously across toctrees
Derived from sphinx-multitoc-numbering with thanks
https://github.com/executablebooks/sphinx-multitoc-numbering"""
from typing import Dict, Iterable, List, Set, Tuple, cast
from docutils import nodes
from docutils.nodes import Element
from sphinx import addnodes
from sphinx.environment import BuildEnvironment
from sphinx.environment.collectors.toctree import TocTreeCollector
from sphinx.locale import __
from sphinx.util import logging, url_re
logger = logging.getLogger(__name__)
def find_numbered_toctree_nodes(
env, iterable: Iterable[addnodes.toctree]
) -> Tuple[Set[str], List[addnodes.toctree]]:
"""Recursively walk the toctree, recording docnames and numbered nodes"""
toctree_nodes = []
assigned = set([])
for node in iterable:
if node["numbered"]:
toctree_nodes.append(node)
else:
for _, ref in node["entries"]:
assigned.add(ref)
doctree = env.get_doctree(ref)
inner_toctree_nodes = doctree.traverse(addnodes.toctree)
# RECURSION
# Base case: All nodes in inner_toctree_nodes are numbered,
# or inner_toctree_nodes is empty
# This'll happen eventually as long as the toctree is acyclic,
# as we are guaranteed to get a level deeper with every call.
# Sphinx already requires an acyclic toctree, so we're fine.
inner_assigned, inner_toctree_nodes = find_numbered_toctree_nodes(
env, inner_toctree_nodes
)
assigned.update(inner_assigned)
toctree_nodes.extend(inner_toctree_nodes)
return assigned, toctree_nodes
def get_toctree_nodes(env: BuildEnvironment) -> Tuple[Set[str], List[addnodes.toctree]]:
"""Get all numbered toctrees, in the order they appear in the document
Walks the entire toctree, starting with the index, and records numbered
toctrees as it finds them."""
## Get the toctrees in the correct order
# Sphinx toctrees always start at index
index_doctree = env.get_doctree("index")
index_toctree_nodes = index_doctree.traverse(addnodes.toctree)
assigned, toctree_nodes = find_numbered_toctree_nodes(env, index_toctree_nodes)
assigned.add("index")
if assigned != env.numbered_toctrees:
logger.warning(
"Couldn't number some toctrees: {env.numbered_toctrees - assigned}",
type="toc",
)
return assigned, toctree_nodes
def assign_section_numbers(self, env: BuildEnvironment) -> List[str]:
"""Assign a section number to each heading under a numbered toctree."""
# a list of all docnames whose section numbers changed
rewrite_needed = []
old_secnumbers = env.toc_secnumbers
env.toc_secnumbers = {}
self.last_chapter_number = 0
assigned, toctree_nodes = get_toctree_nodes(env)
def _walk_toc(
node: Element, secnums: Dict, depth: int, titlenode: nodes.title = None
) -> None:
# titlenode is the title of the document, it will get assigned a
# secnumber too, so that it shows up in next/prev/parent rellinks
for subnode in node.children:
if isinstance(subnode, nodes.bullet_list):
numstack.append(0)
_walk_toc(subnode, secnums, depth - 1, titlenode)
numstack.pop()
titlenode = None
elif isinstance(subnode, nodes.list_item):
_walk_toc(subnode, secnums, depth, titlenode)
titlenode = None
elif isinstance(subnode, addnodes.only):
# at this stage we don't know yet which sections are going
# to be included; just include all of them, even if it leads
# to gaps in the numbering
_walk_toc(subnode, secnums, depth, titlenode)
titlenode = None
elif isinstance(subnode, addnodes.compact_paragraph):
numstack[-1] += 1
reference = cast(nodes.reference, subnode[0])
# if a new chapter is encountered increment the chapter number
if len(numstack) == 1:
self.last_chapter_number += 1
if depth > 0:
number = list(numstack)
secnums[reference["anchorname"]] = tuple(numstack)
else:
number = None
secnums[reference["anchorname"]] = None
reference["secnumber"] = number
if titlenode:
titlenode["secnumber"] = number
titlenode = None
elif isinstance(subnode, addnodes.toctree):
_walk_toctree(subnode, depth)
def _walk_toctree(toctreenode: addnodes.toctree, depth: int) -> None:
if depth == 0:
return
for _, ref in toctreenode["entries"]:
if url_re.match(ref) or ref == "self":
# don't mess with those
continue
elif ref in assigned:
logger.warning(
__(
"%s is already assigned section numbers (nested numbered toctree?)"
),
ref,
location=toctreenode,
type="toc",
subtype="secnum",
)
elif ref in env.tocs:
secnums = {} # type: Dict[str, Tuple[int, ...]]
env.toc_secnumbers[ref] = secnums
assigned.add(ref)
_walk_toc(env.tocs[ref], secnums, depth, env.titles.get(ref))
if secnums != old_secnumbers.get(ref):
rewrite_needed.append(ref)
for toctreenode in toctree_nodes:
depth = toctreenode.get("numbered", 0)
if depth:
# every numbered toctree continues the numbering
numstack = [self.last_chapter_number]
_walk_toctree(toctreenode, depth)
return rewrite_needed
def setup(app):
TocTreeCollector.assign_section_numbers = assign_section_numbers
"""
Changes section references to be the section number
instead of the title of the section.
"""
from docutils import nodes
import sphinx.domains.std
class CustomStandardDomain(sphinx.domains.std.StandardDomain):
def __init__(self, env):
env.settings['footnote_references'] = 'superscript'
sphinx.domains.std.StandardDomain.__init__(self, env)
def resolve_xref(self, env, fromdocname, builder,
typ, target, node, contnode):
res = super(CustomStandardDomain, self).resolve_xref(env, fromdocname, builder,
typ, target, node, contnode)
if res is None:
return res
if typ == 'ref' and not node['refexplicit']:
docname, labelid, sectname = self.data['labels'].get(target, ('','',''))
res['refdocname'] = docname
return res
def doctree_resolved(app, doctree, docname):
secnums = app.builder.env.toc_secnumbers
for node in doctree.traverse(nodes.reference):
if 'refdocname' in node:
refdocname = node['refdocname']
if refdocname in secnums:
secnum = secnums[refdocname]
emphnode = node.children[0]
textnode = emphnode.children[0]
toclist = app.builder.env.tocs[refdocname]
anchorname = None
for refnode in toclist.traverse(nodes.reference):
if refnode.astext() == textnode.astext():
anchorname = refnode['anchorname']
if anchorname is None:
continue
linktext = '.'.join(map(str, secnum[anchorname]))
node.replace(emphnode, nodes.Text(linktext))
def setup(app):
app.connect('doctree-resolved', doctree_resolved)
../../api-python/_static/custom.css
\ No newline at end of file
../../api-python/_static/logo.png
\ No newline at end of file
../../api-python/_templates/navigation.html
\ No newline at end of file
This diff is collapsed.
.. default-domain:: py
.. _the-openmm-application-layer-introduction:
Getting Started
###############
Introduction
************
The first thing to understand about the OpenMM “application layer” is that it is
not exactly an application in the traditional sense: there is no program called
“OpenMM” that you run. Rather, it is a collection of libraries written in the
Python programming language. Those libraries can easily be chained together to
create Python programs that run simulations. But don’t worry! You don’t need
to know anything about Python programming (or programming at all) to use it.
Nearly all molecular simulation applications ask you to write some sort of
“script” that specifies the details of the simulation to run. With OpenMM, that
script happens to be written in Python. But it is no harder to write than those
for most other applications, and this guide will teach you everything you need
to know. There is even a graphical interface that can write the script for you
based on a simple set of options (see Section :numref:`the-script-builder-application`),
so you never need to type a single line of code!
On the other hand, if you don’t mind doing a little programming, this approach
gives you enormous power and flexibility. Your script has complete access to
the entire OpenMM application programming interface (API), as well as the full
power of the Python language and libraries. You have complete control over
every detail of the simulation, from defining the molecular system to analyzing
the results.
.. _installing-openmm:
Installing OpenMM
*****************
OpenMM is installed using the Conda package manager (https://docs.conda.io).
Conda is included as part of the Anaconda Python distribution, which you can
download from https://docs.continuum.io/anaconda/install. This is a Python
distribution specifically designed for scientific applications, with many of the
most popular mathematical and scientific packages preinstalled. Alternatively
you can use Miniconda (available from https://docs.conda.io/en/latest/miniconda.html),
which includes only Python itself, plus the Conda package manager. That offers
a much smaller initial download, with the ability to then install only the
packages you want.
(A third option is to compile OpenMM from source. This provides more flexibility,
but it is much more work, and there is rarely a need for anyone but advanced users
to compile from source. Detailed instruction are in Chapter :numref:`compiling-openmm-from-source-code`.)
\1. Begin by installing the most recent 64 bit, Python 3.x version of either
Anaconda or Miniconda.
\2. (Optional) If you want to run OpenMM on a GPU, make sure you have installed
modern drivers from your vendor.
* If you have an Nvidia GPU, download the latest drivers from
https://www.nvidia.com/Download/index.aspx. CUDA itself will be provided by
the :code:`cudatoolkit` package when you install :code:`openmm` in the next steps.
* If you have an AMD GPU and are using Linux or Windows, download the latest
version of the drivers from https://support.amd.com. On OS X, OpenCL
is included with the operating system and is supported on OS X 10.10.3 or
later.
3. Open a command line terminal and type the following command
::
conda install -c conda-forge openmm
With recent :code:`conda` versions (v4.8.4+), this will install a version of
OpenMM compiled with the latest version of CUDA supported by your drivers.
Alternatively you can request a version that is compiled for a specific CUDA
version with the command
::
conda install -c conda-forge openmm cudatoolkit=10.0
where :code:`10.0` should be replaced with the particular CUDA version
you want to target. We build packages for CUDA 9.2 and above on Linux,
and CUDA 10.0 and above on Windows. Because different CUDA releases are
not binary compatible with each other, OpenMM can only work with the particular
CUDA version it was compiled with.
.. note::
Prior to v7.5, conda packages for OpenMM where distributed through the
:code:`omnia` channel (https://anaconda.org/omnia). Starting with v7.5,
OpenMM will use the :code:`conda-forge` channel. Check the documentation
for previous versions in case you want to install older packages.
4. Verify your installation by typing the following command:
::
python -m openmm.testInstallation
This command confirms that OpenMM is installed, checks whether GPU acceleration
is available (via the OpenCL and/or CUDA platforms), and verifies that all
platforms produce consistent results.
This diff is collapsed.
.. default-domain:: py
.. _model-building-and-editing:
Model Building and Editing
##########################
Sometimes you have a PDB file that needs some work before you can simulate it.
Maybe it doesn’t contain hydrogen atoms (which is common for structures
determined by X-ray crystallography), so you need to add them. Or perhaps you
want to simulate the system in explicit water, but the PDB file doesn’t contain
water molecules. Or maybe it does contain water molecules, but they contain the
wrong number of interaction sites for the water model you want to use. OpenMM’s
Modeller class can fix problems such as these.
To use it, create a :class:`Modeller` object, providing the initial :class:`Topology` and atom
positions. You then can invoke various modelling functions on it. Each one
modifies the system in some way, creating a new :class:`Topology` and list of positions.
When you are all done, you can retrieve them from the :class:`Modeller` and use them as
the starting point for your simulation:
.. samepage::
::
...
pdb = PDBFile('input.pdb')
modeller = Modeller(pdb.topology, pdb.positions)
# ... Call some modelling functions here ...
system = forcefield.createSystem(modeller.topology, nonbondedMethod=PME)
simulation = Simulation(modeller.topology, system, integrator)
simulation.context.setPositions(modeller.positions)
.. caption::
:autonumber:`Example,Modeller outline`
Now let’s consider the particular functions you can call.
Adding Hydrogens
****************
Call the :meth:`addHydrogens` function to add missing hydrogen atoms:
::
modeller.addHydrogens(forcefield)
The force field is needed to determine the positions for the hydrogen atoms. If
the system already contains some hydrogens but is missing others, that is fine.
The Modeller will recognize the existing ones and figure out which ones need to
be added.
Some residues can exist in different protonation states depending on the pH and
on details of the local environment. By default it assumes pH 7, but you can
specify a different value:
::
modeller.addHydrogens(forcefield, pH=5.0)
For each residue, it selects the protonation state that is most common at the
specified pH. In the case of Cysteine residues, it also checks whether the
residue participates in a disulfide bond when selecting the state to use.
Histidine has two different protonation states that are equally likely at
neutral pH. It therefore selects which one to use based on which will form a
better hydrogen bond.
If you want more control, it is possible to specify exactly which protonation
state to use for particular residues. For details, consult the API
documentation for the Modeller class.
Adding Solvent
**************
Call :meth:`addSolvent` to create a box of solvent (water and ions) around the model:
::
modeller.addSolvent(forcefield)
This constructs a box of water around the solute, ensuring that no water
molecule comes closer to any solute atom than the sum of their van der Waals
radii. It also determines the charge of the solute, and adds enough positive or
negative ions to make the system neutral.
When called as shown above, :meth:`addSolvent` expects that periodic box dimensions were
specified in the PDB file, and it uses them as the size for the water box. If
your PDB file does not specify a box size, or if you want to use a different
size, you can specify one:
::
modeller.addSolvent(forcefield, boxSize=Vec3(5.0, 3.5, 3.5)*nanometers)
This requests a 5 nm by 3.5 nm by 3.5 nm box. For a non-rectangular box, you
can specify the three box vectors defining the unit cell:
::
modeller.addSolvent(forcefield, boxVectors=(avec, bvec, cvec))
Another option is to specify a padding distance:
::
modeller.addSolvent(forcefield, padding=1.0*nanometers)
This determines the largest size of the solute along any axis (x, y, or z). It
then creates a cubic box of width (solute size)+2*(padding). The above line
guarantees that no part of the solute comes closer than 1 nm to any edge of the
box.
Finally, you can specify the exact number of solvent molecules (including both
water and ions) to add. This is useful when you want to solvate several different
conformations of the same molecule while guaranteeing they all have the same
amount of solvent:
::
modeller.addSolvent(forcefield, numAdded=5000)
By default, :meth:`addSolvent` creates TIP3P water molecules, but it also supports other
water models:
::
modeller.addSolvent(forcefield, model='tip5p')
Allowed values for the :code:`model` option are ``'tip3p'``, ``'tip3pfb'``, ``'spce'``,
``'tip4pew'``, ``'tip4pfb'``, and ``'tip5p'``. Be sure to include the single quotes
around the value.
Another option is to add extra ion pairs to give a desired total ionic strength.
For example:
::
modeller.addSolvent(forcefield, ionicStrength=0.1*molar)
This solvates the system with a salt solution whose ionic strength is 0.1 molar.
Note that when computing the ionic strength, it does *not* consider the ions
that were added to neutralize the solute. It assumes those are bound to the
solute and do not contribute to the bulk ionic strength.
By default, Na\ :sup:`+` and Cl\ :sup:`-` ions are used, but you can specify
different ones using the :code:`positiveIon` and :code:`negativeIon`
options. For example, this creates a potassium chloride solution:
::
modeller.addSolvent(forcefield, ionicStrength=0.1*molar, positiveIon='K+')
Allowed values for :code:`positiveIon` are ``'Cs+'``, ``'K+'``, ``'Li+'``, ``'Na+'``, and
``'Rb+'``. Allowed values for :code:`negativeIon` are ``'Cl-'``, ``'Br-'``, ``'F-'``, and
``'I-'``. Be sure to include the single quotes around the value. Also be aware
some force fields do not include parameters for all of these ion types, so you
need to use types that are supported by your chosen force field.
Adding a Membrane
*****************
If you want to simulate a membrane protein, you may need to create a membrane as
well. You can do this by calling :meth:`addMembrane`. Call it *instead* of
:meth:`addSolvent`, not in addition to it. This one method adds the membrane,
solvent, and ions all at once, making sure the lipid head groups are properly
solvated. For example, this creates a POPC membrane, ensuring at least 1 nm of
padding on all sides:
::
modeller.addMembrane(forcefield, lipidType='POPC', minimumPadding=1*nanometer)
The membrane is added in the XY plane, and the existing protein is assumed to already be oriented
and positioned correctly. When possible, it is recommended to start with a model
from the `Orientations of Proteins in Membranes`_ (OPM) database. Otherwise, it
is up to you to select the protein position yourself.
Because this method also adds solvent, it takes many of the same arguments as
:meth:`addSolvent`. See the API documentation for details.
.. _`Orientations of Proteins in Membranes`: http://opm.phar.umich.edu
.. _adding-or-removing-extra-particles:
Adding or Removing Extra Particles
**********************************
“Extra particles” are particles that do not represent ordinary atoms. This
includes the virtual interaction sites used in many water models, Drude
particles, etc. If you are using a force field that involves extra particles,
you must add them to the :class:`Topology`. To do this, call:
::
modeller.addExtraParticles(forcefield)
This looks at the force field to determine what extra particles are needed, then
modifies each residue to include them. This function can remove extra particles
as well as adding them.
Removing Water
**************
Call deleteWater to remove all water molecules from the system:
::
modeller.deleteWater()
This is useful, for example, if you want to simulate it with implicit solvent.
Be aware, though, that this only removes water molecules, not ions or other
small molecules that might be considered “solvent”.
.. _saving-the-results:
Saving The Results
******************
Once you have finished editing your model, you can immediately use the resulting
:class:`Topology` object and atom positions as the input to a :class:`Simulation`. If you plan to
simulate it many times, though, it is usually better to save the result to a new
PDB file, then use that as the input for the simulations. This avoids the cost
of repeating the modelling operations at the start of every simulation, and also
ensures that all your simulations are really starting from exactly the same
structure.
The following example loads a PDB file, adds missing hydrogens, builds a solvent
box around it, performs an energy minimization, and saves the result to a new
PDB file.
.. samepage::
::
from openmm.app import *
from openmm import *
from openmm.unit import *
print('Loading...')
pdb = PDBFile('input.pdb')
forcefield = ForceField('amber99sb.xml', 'tip3p.xml')
modeller = Modeller(pdb.topology, pdb.positions)
print('Adding hydrogens...')
modeller.addHydrogens(forcefield)
print('Adding solvent...')
modeller.addSolvent(forcefield, model='tip3p', padding=1*nanometer)
print('Minimizing...')
system = forcefield.createSystem(modeller.topology, nonbondedMethod=PME)
integrator = VerletIntegrator(0.001*picoseconds)
simulation = Simulation(modeller.topology, system, integrator)
simulation.context.setPositions(modeller.positions)
simulation.minimizeEnergy(maxIterations=100)
print('Saving...')
positions = simulation.context.getState(getPositions=True).getPositions()
PDBFile.writeFile(simulation.topology, positions, open('output.pdb', 'w'))
print('Done')
.. caption::
:autonumber:`Example,Modeller complete`
.. default-domain:: py
.. _advanced-simulation-examples:
Advanced Simulation Examples
############################
In the previous chapter, we looked at some basic scripts for running simulations
and saw lots of ways to customize them. If that is all you want to do—run
straightforward molecular simulations—you already know everything you need to
know. Just use the example scripts and customize them in the ways described in
Section :numref:`simulation-parameters`.
OpenMM can do far more than that. Your script has the full OpenMM API at its
disposal, along with all the power of the Python language and libraries. In
this chapter, we will consider some examples that illustrate more advanced
techniques. Remember that these are still only examples; it would be impossible
to give an exhaustive list of everything OpenMM can do. Hopefully they will
give you a sense of what is possible, and inspire you to experiment further on
your own.
Starting in this section, we will assume some knowledge of programming, as well
as familiarity with the OpenMM API. Consult this User's Guide and the OpenMM
API documentation if you are uncertain about how something works. You can also
usethe Python :code:`help` command. For example,
::
help(Simulation)
will print detailed documentation on the :class:`Simulation` class.
Simulated Annealing
*******************
Here is a very simple example of how to do simulated annealing. The following
lines linearly reduce the temperature from 300 K to 0 K in 100 increments,
executing 1000 time steps at each temperature:
.. samepage::
::
...
simulation.context.setPositions(pdb.positions)
simulation.minimizeEnergy()
for i in range(100):
integrator.setTemperature(3*(100-i)*kelvin)
simulation.step(1000)
.. caption::
:autonumber:`Example,simulated annealing`
This code needs very little explanation. The loop is executed 100 times. Each
time through, it adjusts the temperature of the :class:`LangevinMiddleIntegrator` and then
calls :code:`step(1000)` to take 1000 time steps.
Applying an External Force to Particles: a Spherical Container
**************************************************************
In this example, we will simulate a non-periodic system contained inside a
spherical container with radius 2 nm. We implement the container by applying a
harmonic potential to every particle:
.. math::
E(r) = \begin{cases}
0 & r\le2\\
100(r-2)^2 & r>2
\end{cases}
where *r* is the distance of the particle from the origin, measured in nm.
We can easily do this using OpenMM’s :class:`CustomExternalForce` class. This class
applies a force to some or all of the particles in the system, where the energy
is an arbitrary function of each particle’s (\ *x*\ , *y*\ , *z*\ )
coordinates. Here is the code to do it:
.. samepage::
::
...
system = forcefield.createSystem(pdb.topology, nonbondedMethod=CutoffNonPeriodic,
nonbondedCutoff=1*nanometer, constraints=None)
force = CustomExternalForce('100*max(0, r-2)^2; r=sqrt(x*x+y*y+z*z)')
system.addForce(force)
for i in range(system.getNumParticles()):
force.addParticle(i, [])
integrator = LangevinMiddleIntegrator(300*kelvin, 91/picosecond, 0.004*picoseconds)
...
.. caption::
:autonumber:`Example,spherical container`
The first thing it does is create a :class:`CustomExternalForce` object and add it to the
:class:`System`. The argument to :class:`CustomExternalForce` is a mathematical expression
specifying the potential energy of each particle. This can be any function of *x*\ ,
*y*\ , and *z* you want. It also can depend on global or per-particle
parameters. A wide variety of restraints, steering forces, shearing forces,
etc. can be implemented with this method.
Next it must specify which particles to apply the force to. In this case, we
want it to affect every particle in the system, so we loop over them and call
:meth:`addParticle` once for each one. The two arguments are the index of
the particle to affect, and the list of per-particle parameter values (an empty
list in this case). If we had per-particle parameters, such as to make the
force stronger for some particles than for others, this is where we would
specify them.
Notice that we do all of this immediately after creating the :class:`System`. That is
not an arbitrary choice.
.. warning::
If you add new forces to a :class:`System`, you must do so before creating the :class:`Simulation`.
Once you create a :class:`Simulation`, modifying the :class:`System` will have no effect on that :class:`Simulation`.
Extracting and Reporting Forces (and other data)
************************************************
OpenMM provides reporters for three output formats: `PDB <https://www.wwpdb.org/documentation/file-format-content/format33/v3.3.html>`_, `PDBx/mmCIF <https://mmcif.wwpdb.org/pdbx-mmcif-home-page.html>`_ and `DCD <https://www.ks.uiuc.edu/Research/namd/2.6/ug/node13.html>`_.
All of those formats store only positions, not velocities, forces, or other data. In this
section, we create a new reporter that outputs forces. This illustrates two
important things: how to write a reporter, and how to query the simulation for
forces or other data.
Here is the definition of the :class:`ForceReporter` class:
.. samepage::
::
class ForceReporter(object):
def __init__(self, file, reportInterval):
self._out = open(file, 'w')
self._reportInterval = reportInterval
def __del__(self):
self._out.close()
def describeNextReport(self, simulation):
steps = self._reportInterval - simulation.currentStep%self._reportInterval
return (steps, False, False, True, False, None)
def report(self, simulation, state):
forces = state.getForces().value_in_unit(kilojoules/mole/nanometer)
for f in forces:
self._out.write('%g %g %g\n' % (f[0], f[1], f[2]))
.. caption::
:autonumber:`Example,ForceReporter`
The constructor and destructor are straightforward. The arguments to the
constructor are the output filename and the interval (in time steps) at which it
should generate reports. It opens the output file for writing and records the
reporting interval. The destructor closes the file.
We then have two methods that every reporter must implement:
:meth:`describeNextReport()` and :meth:`report()`. A Simulation object
periodically calls :meth:`describeNextReport()` on each of its reporters to
find out when that reporter will next generate a report, and what information
will be needed to generate it. The return value should be a six element tuple,
whose elements are as follows:
* The number of time steps until the next report. We calculate this as
*(report interval)*\ -\ *(current step)*\ %\ *(report interval)*\ . For
example, if we want a report every 100 steps and the simulation is currently on
step 530, we will return 100-(530%100) = 70.
* Whether the next report will need particle positions.
* Whether the next report will need particle velocities.
* Whether the next report will need forces.
* Whether the next report will need energies.
* Whether the positions should be wrapped to the periodic box. If None, it will
automatically decide whether to wrap positions based on whether the System uses
periodic boundary conditions.
When the time comes for the next scheduled report, the :class:`Simulation` calls
:meth:`report()` to generate the report. The arguments are the :class:`Simulation`
object, and a :class:`State` that is guaranteed to contain all the information that was
requested by :meth:`describeNextReport()`\ . A State object contains a
snapshot of information about the simulation, such as forces or particle
positions. We call :meth:`getForces()` to retrieve the forces and convert
them to the units we want to output (kJ/mole/nm). Then we loop over each value
and write it to the file. To keep the example simple, we just print the values
in text format, one line per particle. In a real program, you might choose a
different output format.
Now that we have defined this class, we can use it exactly like any other
reporter. For example,
::
simulation.reporters.append(ForceReporter('forces.txt', 100))
will output forces to a file called “forces.txt” every 100 time steps.
Computing Energies
******************
This example illustrates a different sort of analysis. Instead of running a
simulation, assume we have already identified a set of structures we are
interested in. These structures are saved in a set of PDB files. We want to
loop over all the files in a directory, load them in one at a time, and compute
the potential energy of each one. Assume we have already created our :class:`System` and
:class:`Simulation`. The following lines perform the analysis:
.. samepage::
::
import os
for file in os.listdir('structures'):
pdb = PDBFile(os.path.join('structures', file))
simulation.context.setPositions(pdb.positions)
state = simulation.context.getState(getEnergy=True)
print(file, state.getPotentialEnergy())
.. caption::
:autonumber:`Example,computing energies`
We use Python’s :code:`listdir()` function to list all the files in the
directory. We create a :class:`PDBFile` object for each one and call
:meth:`setPositions()` on the Context to specify the particle positions loaded
from the PDB file. We then compute the energy by calling :meth:`getState()`
with the option :code:`getEnergy=True`\ , and print it to the console along
with the name of the file.
This diff is collapsed.
This diff is collapsed.
.. role:: code
.. raw:: html
<style> .code {font-family:monospace;} </style>
<style> .caption {text-align:center;} </style>
.. |--| replace:: :option:`--`
.. include:: header.rst
####################################
OpenMM Users Manual and Theory Guide
####################################
#####################################
OpenMM User's Manual and Theory Guide
#####################################
.. only:: latex
......@@ -10,12 +8,21 @@ OpenMM Users Manual and Theory Guide
.. toctree::
:numbered:
:maxdepth: 4
:maxdepth: 3
introduction
.. toctree::
:maxdepth: 3
application
library
theory
.. toctree::
:numbered:
:maxdepth: 3
zbibliography
.. only:: html
......
.. include:: header.rst
Introduction
############
......@@ -13,13 +11,13 @@ OpenMM consists of two parts:
This guide is divided into three sections:
* Part I (Chapters :ref:`the-openmm-application-layer-introduction`\ -\ :ref:`creating-force-fields`\ )
* :ref:`Part I <the-openmm-application-layer>`
describes the application layer. It is relevant to all users, but especially relevant to people
who want to use OpenMM as a stand-alone application for running simulations.
* Part II (Chapters :ref:`the-openmm-library-introduction`\ -\ :ref:`drude-plugin`\ )
* :ref:`Part II <the-openmm-library>`
describes how to use the OpenMM libraries within your own applications. It is primarily
relevant to programmers who want to write simulation applications.
* Part III (Chapters :ref:`the-theory-behind-openmm-introduction`\ -\ :ref:`other-features`\ )
* :ref:`Part III <the-theory-behind-openmm>`
describes the mathematical theory behind the features found in OpenMM. It is relevant to all users.
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment