Repository: openforcefield/openff-toolkit

Issue:
RDKit stereo enumeration does not flip existing stereochemistry
**Describe the bug**

Attempting to enumerate all stereoisomers of a molecule with already defined stereochemistry yields no stereoisomers when using the RDKit backend.

**To Reproduce**

Using the toolkit:

```
>>> off_molecule = Molecule.from_smiles('[H][C@@](F)(Cl)Br')
>>>
>>> print(len(off_molecule.enumerate_stereoisomers(
>>>     undefined_only=False, toolkit_registry=RDKitToolkitWrapper())))
>>> 
>>> print(len(off_molecule.enumerate_stereoisomers(
>>>     undefined_only=False, toolkit_registry=OpenEyeToolkitWrapper())))

0
1
```

comparison with the RDKit toolkit directly:

```
>>> rdkit_molecule = Chem.MolFromSmiles('[H][C@@](F)(Cl)Br')
>>> options = StereoEnumerationOptions(onlyUnassigned=False)
>>>
>>> for isomer in EnumerateStereoisomers(rdkit_molecule, options=options):
>>>     print(Chem.MolToSmiles(isomer))

F[C@H](Cl)Br
F[C@@H](Cl)Br
```

**Output**
The full error message (may be large, that's fine. Better to paste too much than too little.)

**Computing environment (please complete the following information):**
 - Operating system: OSX
 - Output of running `conda list` :

```
rdkit                     2020.09.3        py38h4648dd3_0    conda-forge
openeye-toolkits          2020.2.0                 py38_0    openeye
openff-toolkit            0.9.1           dev_0    <develop>
```

**Additional context**
Add any other context about the problem here.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/openforcefield/openff-toolkit/pull/897?src=pr&el=h1) Report
> Merging [#897](https://codecov.io/gh/openforcefield/openff-toolkit/pull/897?src=pr&el=desc) (6830451) into [master](https://codecov.io/gh/openforcefield/openff-toolkit/commit/e54052afcf0ee401fb65d84e995d53f1f77e3240?el=desc) (e54052a) will **decrease** coverage by `10.00%`.
> The diff coverage is `100.00%`.



Patch:
diff --git a/docs/concepts.md b/docs/concepts.md
new file mode 100644
index 00000000..ee631e88
--- /dev/null
+++ b/docs/concepts.md
@@ -0,0 +1,34 @@
+# Core concepts
+
+OpenFF [`Molecule`](openff.toolkit.topology.Molecule)
+: A graph representation of a molecule containing enough information to unambiguously parametrize it.
+  Required data fields for a `Molecule` are:
+
+  - `atoms`: element (integer), formal_charge (integer), is_aromatic (boolean), stereochemistry (`'R'`/`'S'`/`None`)
+  - `bonds`: order (integer), is_aromatic (boolean), stereochemistry (`'E'`/`'Z'`/`None`)
+
+  There are several other optional attributes such as `conformers` and `partial_charges` that may be populated in the `Molecule` data structure.
+  These are considered "optional" because they are not required for system creation, however if those fields are populated, the user MAY use them to override values that would otherwise be generated during system creation.
+
+  A dictionary, `Molecule.properties` is exposed, which is a Python dict that can be populated with arbitrary data.
+  This data should be considered cosmetic and should not affect system creation.
+  Whenever possible, molecule serialization or format conversion should preserve this data.
+
+OpenFF `System`
+: An object that contains everything needed to calculate a molecular system's energy, except the atomic coordinates.
+  Note that this does not exist yet, and that OpenMM `System` objects are being used for this purpose right now. Development is underway on [GitHub](https://github.com/openforcefield/openff-system).
+
+OpenFF [`Topology`](openff.toolkit.topology.Topology)
+: An object that efficiently holds many OpenFF `Molecule` objects.
+  The atom indexing in a `Topology` may differ from those of the underlying `Molecule`s
+
+OpenFF [`TopologyMolecule`](openff.toolkit.topology.TopologyMolecule)
+: The efficient data structures that make up an OpenFF `Topology`.
+  There is one `TopologyMolecule` for each instance of a chemical species in a `Topology`.
+  However, each unique chemical species has a single OpenFF `Molecule` representing it, which may be shared by multiple `TopologyMolecules`.
+  `TopologyMolecule`s contain an atom index map, as several copies of the same chemical species in a `Topology` may be present with different atom orderings.
+  This data structure allows the OpenFF toolkit to only parametrize each unique `Molecule` once, and then write a copy of the assigned parameters out for each of the `Molecule` in the `Topology` (accounting for atom indexing differences in the process).
+
+OpenFF [`ForceField`](openff.toolkit.typing.engines.smirnoff.forcefield.ForceField)
+: An object generated from an OFFXML file (or other source of SMIRNOFF data).
+  Most information from the SMIRNOFF data source is stored in this object's several `ParameterHandler`s, however some top-level SMIRNOFF data is stored in the `ForceField` object itself.
diff --git a/docs/conf.py b/docs/conf.py
index 6aa9ce12..2e5e6fcb 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -80,7 +80,8 @@ myst_enable_extensions = [
     "dollarmath",
     "colon_fence",
     "smartquotes",
-    "replacements"
+    "replacements",
+    "deflist"
 ]
 # Stop myst from disabling MathJax's $...$ and $$...$$ environments
 myst_update_mathjax = False
diff --git a/docs/developing.md b/docs/developing.md
new file mode 100644
index 00000000..4aeade65
--- /dev/null
+++ b/docs/developing.md
@@ -0,0 +1,347 @@
+(developing)=
+
+# Developing for the toolkit
+
+:::{contents}
+  :local:
+:::
+
+This guide is written with the understanding that our contributors are NOT professional software developers, but are instead computational chemistry trainees and professionals.
+With this in mind, we aim to use a minimum of bleeding-edge technology and alphabet soup, and we will define any potentially unfamiliar processes or technologies the first time they are mentioned.
+We enforce use of certain practices (tests, formatting, coverage analysis, documentation) primarily because they are worthwhile upfront investments in the long-term sustainability of this project.
+The resources allocated to this project will come and go, but we hope that following these practices will ensure that minimal developer time will maintain this software far into the future.
+
+The process of contributing to the OpenFF Toolkit is more than just writing code.
+Before contributing, it is a very good idea to start a discussion on the [Issue tracker](https://github.com/openforcefield/openff-toolkit/issues) about the functionality you'd like to add.
+This Issue discussion will help us decide with you where in the codebase it should go, any overlapping efforts with other developers, and what the user experience should be.
+Please note that the OpenFF Toolkit is intended to be used primarily as one piece of larger workflows, and that simplicity and reliability are two of our primary goals.
+Often, the cost/benefit of new features must be discussed, as a complex codebase is harder to maintain.
+When new functionality is added to the OpenFF Toolkit, it becomes our responsibility to maintain it, so it's important that we understand contributed code and are in a position to keep it up to date.
+
+## Overview
+
+### Philosophy
+
+- The *core functionality* of the OpenFF Toolkit is to combine an Open Force Field [`ForceField`](openff.toolkit.typing.engines.smirnoff.forcefield.ForceField) and [`Topology`](openff.toolkit.topology.Topology) to create an OpenMM [`System`](http://docs.openmm.org/latest/api-python/generated/simtk.openmm.openmm.System.html).
+- An OpenMM [`System`](http://docs.openmm.org/latest/api-python/generated/simtk.openmm.openmm.System.html) contains *everything* needed to compute the potential energy of a system, except the coordinates and (optionally) box vectors.
+- The OpenFF toolkit employs a modular "plugin" architecture wherever possible, providing a standard interface for contributed features.
+
+
+### Terminology
+
+For high-level toolkit concepts and terminology important for both development and use of the Toolkit, see the [core concepts page](concepts).
+
+#### SMIRNOFF and the OpenFF Toolkit
+
+SMIRNOFF data
+: A hierarchical data structure that complies with the [SMIRNOFF specification](smirnoff).
+  This can be serialized in many formats, including XML (OFFXML).
+  The subsections in a SMIRNOFF data source generally correspond to one energy term in the functional form of a force field.
+
+Cosmetic attribute
+: Data in a SMIRNOFF data source that does not correspond to a known attribute.
+  These have no functional effect, but several programs use the extensibility of the OFFXML format to define additional attributes for their own use, and their workflows require the OFF toolkit to process the files while retaining these keywords.
+
+#### Development Infrastructure
+
+Continuous Integration (CI)
+: Tests that run frequently while the code is undergoing changes, ensuring that the codebase still installs and has the intended behavior.
+  Currently, we use a service called [GitHub Actions](https://github.com/features/actions) for this.
+  CI jobs run every time a commit is made to the `master` branch of the `openff-toolkit` GitHub repository or in a PR opened against it.
+  These runs start by booting virtual machines that mimic brand new Linux and macOS computers.
+  They then follow build instructions (see the `.github/workflows/CI.yml` file) to install the toolkit.
+  After installing the OpenFF Toolkit and its dependencies, these virtual machines run our test suite.
+  If the tests all pass, the build "passes" (returns a green check mark on GitHub).
+
+  If all the tests for a specific change to the `master` branch return green, then we know that the change has not broken the toolkit's existing functionality.
+  When proposing code changes, we ask that contributors open a Pull Request (PR) on GitHub to merge their changes into the `master` branch.
+  When a pull request is open, CI will run on the latest set of proposed changes and indicate whether they are safe to merge through status checks, summarized as a green check mark or red cross.
+
+CodeCov
+: An extension to our testing framework that reports the fraction of our source code lines that were run during the tests (our "code coverage").
+  This functionality is actually the combination of several components -- GitHub Actions runners run the tests using `pytest` with the `pytest-cov` plugin, and then coverage reports are uploaded to [CodeCov's website](https://codecov.io).
+  This analysis is re-run alongside the rest of our CI, and a badge showing our coverage percentage is in the project README.
+
+"Looks Good To Me" (LGTM)
+: A service that analyzes the code in our repository for simple style and formatting issues.
+  This service assigns a letter grade to the codebase, and a badge showing our LGTM report is in the project README.
+
+ReadTheDocs (RTD)
+: A service that compiles and renders the package's documentation (from the `docs/` folder).
+  The documentation itself can be accessed from the ReadTheDocs badge in the README.
+  It is compiled by RTD alongside the other CI checks, and the compiled documentation for a pull request can be viewed by clicking the "details" link after the status.
+
+### User Experience
+
+One important aspect of how we make design decisions is by asking "who do we envision using this software, and what would they want it to do here?".
+There is a wide range of possible users, from non-chemists, to students/trainees, to expert computational medicinal chemists.
+We have decided to build functionality intended for use by *expert medicinal chemists*, and whenever possible, add fatal errors if the toolkit risks doing the wrong thing.
+So, for example, if a molecule is loaded with an odd ionization state, we assume that the user has input it this way intentionally.
+
+This design philosophy inevitably has trade-offs --- For example, the OpenFF Toolkit will give the user a hard time if they try to load a "dirty" molecule dataset, where some molecules have errors or are not described in enough detail for the toolkit to unambiguously parametrize them.
+If there is risk of misinterpreting the molecule (for example, bond orders being undefined or chiral centers without defined stereochemistry), the toolkit should raise an error that the user can override.
+In this regard we differ from RDKit, which is more permissive in the level of detail it requires when creating molecules.
+This makes sense for RDKit's use cases, as several of its analyses can operate with a lower level of detail about the molecules.
+Often, the same design decision is the best for all types of users, but when we do need to make trade-offs, we assume the user is an expert.
+
+At the same time, we aim for "automagic" behavior whenever a decision will clearly go one way over another.
+System parameterization is an inherently complex topic, and the OFF toolkit would be nearly unusable if we required the user to explicitly approve every aspect of the process.
+For example, if a `Topology` has its `box_vectors` attribute defined, we assume that the resulting OpenMM `System` should be periodic.
+
+
+
+## Modular design features
+
+There are a few areas where we've designed the toolkit with extensibility in mind.
+Adding functionality at these interfaces should be considerably easier than in other parts of the toolkit, and we encourage experimentation and contribution on these fronts.
+
+These features have occasionally confusing names.
+"Parameter" here refers to a single value in a force field, as it is generally used in biophysics; it does not refer to an argument to a function.
+"Attribute" is used to refer to an XML attribute, which allows data to be defined for a particular tag; it does not refer to a member of a Python class or object.
+For example, in the following XML excerpt the `<SMIRNOFF>` tag has the attributes `version` and `aromaticity_model`:
+
+```xml
+<SMIRNOFF version="0.3" aromaticity_model="OEAroModel_MDL">
+  ...
+</SMIRNOFF>
+```
+
+"Member" is used here to describe Python attributes.
+This terminology is borrowed for the sake of clarity in this section from languages like C++ and Java.
+
+
+### `ParameterAttribute`
+A [`ParameterAttribute`](openff.toolkit.typing.engines.smirnoff.parameters.ParameterAttribute) is a single value that can be validated at runtime.
+
+A `ParameterAttribute` can be instantiated as Python class or instance members to define the kinds of value that a particular parameter can take.
+They are used in the definitions of both `ParameterHandler` and `ParameterType`.
+The sorts of values a `ParameterAttribute` can take on are restricted by runtime validation.
+This validation is highly customizable, and may do things like allowing only certain values for a string or enforcing the correct units or array dimensions on the value; in fact, the validation can be defined using arbitrary code.
+The name of a `ParameterAttribute` should correspond exactly to the corresponding attribute in an OFFXML file.
+
+#### `IndexedParameterAttribute`
+An [`IndexedParameterAttribute`](openff.toolkit.typing.engines.smirnoff.parameters.IndexedParameterAttribute) is a  `ParameterAttribute` with a sequence of values, rather than just one. Each value in the sequence is indexed by an integer.
+
+The exact name of an `IndexedParameterAttribute` is NOT expected to appear verbatim in a OFFXML file, but instead should appear with a numerical integer suffix.
+For example the `IndexedParameterAttribute` `k` should only appear as `k1`, `k2`, `k3`, and so on in an OFFXML.
+The current implementation requires this indexing to start at 1 and subsequent values be contiguous (no skipping numbers), but does not enforce an upper limit on the integer.
+
+For example, dihedral torsions are often parameterized as the sum of several sine wave terms.
+Each of the parameters of the sine wave `k`, `periodicity`, and `phase` is implemented as an `IndexedParameterAttribute`.
+
+#### `MappedParameterAttribute`
+A [`MappedParameterAttribute`](openff.toolkit.typing.engines.smirnoff.parameters.MappedParameterAttribute) is a  `ParameterAttribute` with several values, with some arbitrary mapping to access values.
+
+#### `IndexedMappedParameterAttribute`
+An [`IndexedMappedParameterAttribute`](openff.toolkit.typing.engines.smirnoff.parameters.IndexedMappedParameterAttribute) is a `ParameterAttribute` with a sequence of maps of values.
+
+### `ParameterHandler`
+[`ParameterHandler`](openff.toolkit.typing.engines.smirnoff.parameters.ParameterHandler) is a generic base class for objects that perform parameterization for one section in a SMIRNOFF data source.
+A `ParameterHandler` has the ability to produce one component of an OpenMM `System`.
+Extend this class to add a support for a new force or energy term to the toolkit.
+
+Each `ParameterHandler`-derived class MUST implement the following methods and define the following attributes:
+
+- `create_force(self, system, topology, **kwargs)`: takes an OpenMM `System` and a OpenFF `Topology` as input, as well as optional keyword arguments, and modifies the `System` to contain the appropriate parameters.
+- Class members `ParameterAttributes`: These correspond to the header-level attributes in a SMIRNOFF data source.
+For example, the `Bonds` tag in the SMIRNOFF spec has an optional `fractional_bondorder_method` field, which corresponds to the line  `fractional_bondorder_method = ParameterAttribute(default=None)` in the `BondHandler` class definition.
+The `ParameterAttribute` and `IndexedParameterAttribute` classes offer considerable flexibility for validating inputs.
+Defining these attributes at the class level implements the corresponding behavior in the default `__init__` function.
+- Class members `_MIN_SUPPORTED_SECTION_VERSION` and `_MAX_SUPPORTED_SECTION_VERSION`.
+`ParameterHandler` versions allow us to evolve `ParameterHandler` behavior in a controlled, recorded way.
+Force field development is experimental by nature, and it is unlikely that the initial choice of header attributes is suitable for all use cases.
+Recording the "versions" of a SMIRNOFF spec tag allows us to encode the default behavior and API of a specific generation of a `ParameterHandler`, while allowing the safe addition of new attributes and behaviors.
+If these attributes are not defined, defaults in the base class will apply and updates introducing new versions may break the existing code.
+
+Each `ParameterHandler`-derived class MAY implement:
+  - `_KWARGS`: Keyword arguments passed to `ForceField.create_openmm_system` are validated against the `_KWARGS` lists of each ParameterHandler that the ForceField owns.
+    If present, these keyword arguments and their values will be passed on to the `ParameterHandler`.
+  - `_TAGNAME`: The name of the SMIRNOFF OFFXML tag used to parameterize the class.
+    This tag should appear in the top level within the [`<SMIRNOFF>`](smirnoff.html#the-enclosing-smirnoff-tag) tag; see the [Parameter generators](smirnoff.html#parameter-generators) section of the SMIRNOFF specification.
+  - `_INFOTYPE`: The `ParameterType` subclass used to parse the elements in the `ParameterHandler`'s parameter list.
+  - `_DEPENDENCIES`: A list of `ParameterHandler` subclasses that, when present, must run before this one.
+    Note that this is *not* a list of `ParameterHandler` subclasses that are required by this one.
+    Ideally, child classes of `ParameterHandler` are entirely independent, and energy components of a force field form distinct terms; when this is impossible, `_DEPENDENCIES` may be used to guarantee execution order.
+  - `to_dict`: converts the `ParameterHandler` to a hierarchical dict compliant with the SMIRNOFF specification.
+    The default implementation of this function should suffice for most developers.
+  - `check_handler_compatibility`: Checks whether this `ParameterHandler` is "compatible" with another.
+    This function is used when a `ForceField` is attempted to be constructed from *multiple* SMIRNOFF data sources, and it is necessary to check that two sections with the same tag name can be combined in a sane way.
+    For example, if the user instructed two `vdW` sections to be read, but the sections defined different vdW potentials, then this function should raise an Exception indicating that there is no safe way to combine the parameters.
+    The default implementation of this function should suffice for most developers.
+  - `postprocess_system`: operates identically to `create_force`, but is run after each ParameterHandlers' `create_force` has already been called.
+    The default implementation of this method simply does nothing, and should suffice for most developers.
+
+### `ParameterType`
+[`ParameterType`](openff.toolkit.typing.engines.smirnoff.parameters.ParameterType) is a base class for the SMIRKS-based parameters of a `ParameterHandler`.
+Extend this alongside `ParameterHandler` to define and validate the force field parameters of a new force.
+This is analogous to ParmEd's XType classes, like [BondType](https://parmed.github.io/ParmEd/html/api/parmed/parmed.html?highlight=bondtype#parmed.BondType). A `ParameterType` should correspond to a single SMARTS-based parameter.
+
+For example, the Lennard-Jones potential can be parameterized through either the size `ParameterAttribute` `sigma` or `r_min`, alongside the energy `ParameterAttribute` `epsilon`. Both options are handled through the [`vdWType`](openff.toolkit.typing.engines.smirnoff.parameters.vdWHandler.vdWType) class, a subclass of `ParameterType`.
+
+
+% TODO : fill in the modular components below
+%    Molecule.to_X
+%    Molecule.from_X
+%    Force field directories
+
+
+%  TODO : fill in the sections below
+%  Molecule definition
+%  '''''''''''''''''''
+%
+%  Required stereochemistry
+%  ''''''''''''''''''''''''
+%
+%  Conformation dependence
+%  '''''''''''''''''''''''
+%
+%
+%
+%  Reliance on external dependencies
+%  '''''''''''''''''''''''''''''''''
+%
+%
+%
+%  ForceField file paths
+%  '''''''''''''''''''''
+
+%  TODO : expand this section
+%  Documentation
+%  '''''''''''''
+%  If you define a new class, add new files to autodoc
+
+
+## Contributing
+
+We always welcome [GitHub pull requests](https://github.com/openforcefield/openff-toolkit/pulls).
+For bug fixes, major feature additions, or refactoring, please raise an issue on the [GitHub issue tracker](http://github.com/openforcefield/openff-toolkit/issues) first to ensure the design will be amenable to current developer plans.
+Development of new toolkit features generally proceeds in the following stages:
+
+* Begin a discussion on the [GitHub issue tracker](http://github.com/openforcefield/openff-toolkit/issues) to determine big-picture "what should this feature do?" and "does it fit in the scope of the OpenFF Toolkit?"
+    * ["... typically, for existing water models, we want to assign library charges"](https://github.com/openforcefield/openff-toolkit/issues/25)
+* Start identifying details of the implementation that will be clear from the outset
+    * ["Create a new "special section" in the SMIRNOFF format (kind of analogous to the BondChargeCorrections section) which allows SMIRKS patterns to specify use of library charges for specific groups](https://github.com/openforcefield/openff-toolkit/issues/25#issue-225173968)
+    * ["Following #86, here's how library charges might work: ..."](https://github.com/openforcefield/openff-toolkit/issues/25#issuecomment-354636391)
+* Create a branch or fork for development
+    * The OpenFF Toolkit has one unusual aspect of its CI build process, which is that certain functionality requires the OpenEye toolkits, so the builds must contain a valid OpenEye license file.
+    An OpenEye license is stored as an encrypted token within the `openforcefield` organization on GitHub.
+    For security reasons, builds run from forks cannot access this key.
+    Therefore, tests that depend on the OpenEye Toolkits will be skipped on forks.
+    Contributions run on forks are still welcome, especially as features that do not interact directly with the OpenEye Toolktis are not likely affected by this limitation.
+
+
+
+### Setting up a development environment
+
+1. Install the `conda` package manager as part of the Anaconda Distribution from [here](https://www.anaconda.com/distribution/)
+
+2. Set up conda environment:
+
+    ```shell
+    # Create a conda environment with the Open Force Field toolkit and its dependencies
+    conda create --name openff-dev -c conda-forge -c openeye openff-toolkit openeye-toolkits
+    conda activate openff-dev
+    # Remove (only) the toolkit and replace it with a local install
+    conda remove --force openff-toolkit
+    git clone https://github.com/openforcefield/openff-toolkit
+    cd openff-toolkit
+    pip install -e .
+    ```
+
+3. Obtain and store Open Eye license somewhere like `~/.oe_license.txt`.
+   Optionally store the path in environmental variable `OE_LICENSE`, i.e. using a command like `echo
+   "export OE_LICENSE=/Users/yournamehere/.oe_license.txt" >> ~/.bashrc`
+
+#### Building the Docs
+
+The documentation is composed of two parts, a hand-written user guide and an auto-generated API reference.
+Both are compiled by Sphinx, and can be automatically served and regenerated on changes with `sphinx-autobuild`.
+Documentation for released versions is available at [ReadTheDocs](https://open-forcefield-toolkit.readthedocs.io/en/latest/).
+ReadTheDocs also builds the documentation for each Pull Request opened on GitHub and keeps the output for 90 days.
+
+To add the documentation dependencies to your existing `openff-dev` Conda environment:
+
+```shell
+# Add the documentation requirements to your Conda environment
+conda env update --name openff-dev --file docs/environment.yml
+conda install --name openff-dev -c conda-forge sphinx-autobuild
+```
+
+To build the documentation from scratch:
+
+```shell
+# Build the documentation
+# From the openff-toolkit root directory
+conda activate openff-dev
+cd docs
+make html
+# Documentation can be found in docs/_build/html/index.html
+```
+
+To watch the source directory for changes and automatically rebuild the documentation and refresh your browser:
+
+```shell
+# Host the docs on a local HTTP server and rebuild when a source file is changed
+# Works best when the docs have already been built
+# From the openff-toolkit root directory
+conda activate openff-dev
+sphinx-autobuild docs docs/_build/html --watch openff
+# Then navigate your web browser to http://localhost:8000
+```
+
+### Style guide
+
+Development for the `openff-toolkit` conforms to the recommendations given by the [Software Development Best Practices for Computational Chemistry](https://github.com/choderalab/software-development) guide.
+
+The naming conventions of classes, functions, and variables follows [PEP8](https://www.python.org/dev/peps/pep-0008/), consistently with the best practices guide. 
+The naming conventions used in this library not covered by PEP8 are:
+- Use `file_path`, `file_name`, and `file_stem` to indicate `path/to/stem.extension`, `stem.extension`, and `stem` respectively, consistent with the variables in the [`pathlib` module](https://docs.python.org/3/library/pathlib.html) of the standard library.
+- Use `n_x` to abbreviate "number of $x$" (e.g. `n_atoms`, `n_molecules`).
+
+We place a high priority on code cleanliness and readability, even if code could be written more compactly. 
+For example, 15-character variable names are fine. Triply nested list comprehensions are not.
+
+The `openff-toolkit` has adopted code formatting tools ("linters") to maintain consistent style and remove the burden of adhering to these standards by hand.
+Currently, two are employed:
+1. [Black](https://black.readthedocs.io/), the uncompromising code formatter, automatically formats code with a consistent style.
+1. [isort](https://timothycrosley.github.io/isort/), sorts imports
+
+There is a step in CI that uses these tools to check for a consistent style (see the file [`.github/workflows/lint.yml`](https://github.com/openforcefield/openff-toolkit/blob/master/.github/workflows/lint.yml)).
+These checks will use the most recent versions of each linter.
+To ensure that changes follow these standards, you can install and run these tools locally:
+
+```shell
+conda install black isort -c conda-forge
+black openff
+isort openff
+```
+
+Anything not covered above is currently up to personal preference, but this may change as new linters are added.
+
+## Supported Python versions
+
+The OpenFF Toolkit roughly follows [NEP 29](https://numpy.org/neps/nep-0029-deprecation_policy.html). 
+As of version 0.9.1 (March 2021) this means Python 3.7-3.9 is officially supported. 
+We develop, test, and distribute on macOS and Linux-based operating systems. 
+We do not currently support Windows. 
+Some CI builds run using only RDKit as a backend, some run using only OpenEye Toolkits, and some run using both installed at once.
+
+The CI matrix is currently as follows:
+
+:::{eval-rst}
++-----------------------+------------+-----------+-------------+------------+-----------+-------------+
+|                       | Linux                                | macOS                                |
++                       +------------+-----------+-------------+------------+-----------+-------------+
+|                       | RDKit      | OpenEye   | RDKit + OE  | RDKit      | OpenEye   | RDKit + OE  |
++=======================+============+===========+=============+============+===========+=============+
+| Python 3.6 and older  | No support after 0.9.1 (March 2021)                                         |
++-----------------------+------------+-----------+-------------+------------+-----------+-------------+
+| Python 3.7            | **Test**   | **Test**  | **Test**    | **Test**   | **Test**  | **Test**    |
++-----------------------+------------+-----------+-------------+------------+-----------+-------------+
+| Python 3.8            | **Test**   | Skip      | Skip        | **Test**   | Skip      | Skip        |
++-----------------------+------------+-----------+-------------+------------+-----------+-------------+
+| Python 3.9            | **Test**   | Skip      | Skip        | **Test**   | Skip      | Skip        |
++-----------------------+------------+-----------+-------------+------------+-----------+-------------+
+| Python 3.10 and newer | Waiting on official releases and upstream support                           |
++-----------------------+------------+-----------+-------------+------------+-----------+-------------+
+:::
diff --git a/docs/developing.rst b/docs/developing.rst
deleted file mode 100644
index 3a4d44a9..00000000
--- a/docs/developing.rst
+++ /dev/null
@@ -1,361 +0,0 @@
-.. _developing:
-
-Developing for the toolkit
-**************************
-
-Overview
-""""""""
-
-Introduction
-''''''''''''
-
-This guide is written with the understanding that our contributors are NOT professional software developers, but are instead computational chemistry trainees and professionals.
-With this in mind, we aim to use a minimum of bleeding-edge technology and alphabet soup, and we will define any potentially unfamiliar processes or technologies the first time they are mentioned.
-We enforce use of certain practices (tests, formatting, coverage analysis, documentation) primarily because they are worthwhile upfront investments in the long-term sustainability of this project.
-The resources allocated to this project will come and go, but we hope that following these practices will ensure that minimal developer time will maintain this software far into the future.
-
-The process of contributing to the OpenFF Toolkit is more than just writing code.
-Before contributing, it is a very good idea to start a discussion on the Issue tracker about the functionality you'd like to add.
-This Issue will help us identify where in the codebase it should go, any overlapping efforts with other developers, and what the user experience should be.
-Please note that the OpenFF Toolkit is intended to be used primarily as one piece of larger workflows, and that simplicity and reliability are two of our primary goals.
-Often, the cost/benefit of new features must be discussed, as a complex codebase is harder to maintain.
-When new functionality is added to the OpenFF Toolkit, it becomes our responsibility to maintain it, so it's important that we understand contributed code and are in a position to keep it up to date.
-
-Philosophy
-''''''''''
-
-- The *core functionality* of the OpenFF Toolkit is to combine an Open Force Field ``ForceField`` and ``Topology`` to create an OpenMM ``System``.
-- An OpenMM ``System`` contains *everything* needed to compute the potential energy of a system, except the coordinates and (optionally) box vectors.
-- The OpenFF toolkit employs a modular "plugin" architecture wherever possible, providing a standard interface for contributed features.
-
-
-Terminology
-'''''''''''
-
-Open Force Field Toolkit Concepts
-
-
-``OpenFF Molecule``
-  A graph representation of a molecule containing enough information to unambiguously parametrize it.
-  Required data fields for an ``OpenFF Molecule`` are:
-
-  - ``atoms``: element (integer), formal_charge (integer), is_aromatic (boolean), stereochemistry (R/S/None)
-  - ``bonds``: order (integer), is_aromatic (boolean), stereochemistry (E/Z/None)
-
-  There are several other optional attributes such as ``conformers`` and ``partial_charges`` that may be populated in the ``Molecule`` data structure.
-  These are considered "optional" because they are not required for system creation, however if those fields are populated, the user MAY use them to override values that would otherwise be generated during system creation.
-
-  A dictionary, ``Molecule.properties`` is exposed, which is a Python dict that can be populated with arbitrary data.
-  This data should be considered cosmetic and should not affect system creation.
-  Whenever possible, molecule serialization or format conversion should preserve this data.
-
-``OpenFF System``
-  An object that contains everything needed to calculate a molecular system's energy, except the atomic coordinates.
-  Note that this does not exist yet, and that OpenMM ``System`` objects are being used for this purpose right now.
-
-``OpenFF Topology``
-  An object that efficiently holds many OpenFF ``Molecule`` objects.
-  The atom indexing in a ``Topology`` may differ from those of the underlying ``Molecule``\ s
-
-``OpenFF TopologyMolecule``
-  The efficient data structures that make up an OpenFF Topology.
-  There is one TopologyMolecule for each instance of a chemical species in a Topology.
-  However, each unique chemical species has a single OpenFF Molecule representing it, which may be shared by multiple TopologyMolecules.
-  TopologyMolecules contain an atom index map, as several copies of the same chemical species in a Topology may be present with different atom orderings.
-  This data structure allows the OpenFF toolkit to only parametrize each unique Molecule once, and then write a copy of the assigned parameters out for each of the Molecule in the Topology (accounting for atom indexing differences in the process).
-
-
-``OpenFF ForceField``
-  An object generated from an OFFXML file (or other source of SMIRNOFF data).
-  Most information from the SMIRNOFF data source is stored in this object's several ``ParameterHandler``s, however some top-level SMIRNOFF data is stored in the ``ForceField`` object itself.
-
-``SMIRNOFF data``
-  A hierarchical data structure that complies with the SMIRNOFF specification.
-  This can be serialized in many formats, including XML (OFFXML).
-  The subsections in a SMIRNOFF data source generally correspond to one energy term in the functional form of a force field.
-
-``ParameterHandler``
-  An object that has the ability to produce one component of an OpenMM ``System``, corresponding to one subsection in a SMIRNOFF data source.
-  Most ``ParameterHandler`` objects contain a list of ``ParameterType`` objects.
-
-``ParameterType``
-  An object corresponding to a single SMARTS-based parameter.
-
-``Cosmetic attribute``
-  Data in a SMIRNOFF data source that does not correspond to a known attribute.
-  These have no functional effect, but several programs use the extensibility of the OFFXML format to define additional attributes for their own use, and their workflows require the OFF toolkit to process the files while retaining these keywords.
-
-Development Infrastructure
-
-``CI``
-    "Continuous integration" testing.
-
-    Services that run frequently while the code is undergoing changes, ensuring that the codebase still installs and has the intended behavior.
-    Currently, we use a service called `GitHub Actions <https://github.com/features/actions>`_ for this.
-    CI jobs run every time a commit is made to the ``master`` branch of the ``openff-toolkit`` Github repository or in a PR opened against it.
-    These runs start by booting virtual machines that mimic brand new Linux and macOS computers.
-    They then follow build instructions (see the ``.github/workflows/CI.yml`` file) to install the toolkit.
-    After installing the OpenFF Toolkit and its dependencies, these virtual machines run our test suite.
-    If the tests all pass, the build "passes" (returns a green check mark on GitHub).
-    If all the tests for a specific change to the ``master`` branch return green, then we know that the change has not broken the toolkit's existing functionality.
-    When proposing code changes, we ask that contributors open a Pull Request (PR) on GitHub to merge their changes into the ``master`` branch.
-    When a pull request is open, CI will run on the latest set of proposed changes and indicate whether they are safe to merge through status checks, summarized as a green check mark or red X.
-
-``CodeCov``
-  Code coverage.
-
-  An extension to our testing framework that reports the fraction of our source code lines that were run during the tests.
-  This functionality is actually the combination of several components -- GitHub Actions runners run the tests using ``pytest`` with the ``pytest-cov`` plugin, and then coverage reports are uploaded to `CodeCov's website <https://codecov.io>`_.
-  This analysis is re-run with each change to the ``master`` branch, and a badge showing our coverage percentage is in the project README.
-
-``LGTM``
-  "Looks Good To Me".
-
-  A service that analyzes the code in our repository for simple style and formatting issues.
-  This service assigns a letter grade to codebases, and a badge showing our LGTM report is in the project README.
-
-``RTD``
-  ReadTheDocs.
-
-  A service that compiles and renders the packages documentation (from the ``docs/`` folder).
-  The documentation itself can be accessed from the ReadTheDocs badge in the README.
-
-Modular design features
-'''''''''''''''''''''''
-
-There are a few areas where we've designed the toolkit with extensibility in mind.
-Adding functionality at these interfaces should be considerably easier than in other parts of the toolkit, and we encourage experimentation and contribution on these fronts.
-
-ParameterHandler
-    A generic base class for objects that perform parametrization for one section in a SMIRNOFF data source.
-
-    Each ParameterHandler-derived class MUST implement:
-        - ``create_force(self, system, topology, **kwargs)``: takes an OpenMM ``System`` and a OpenFF ``Topology`` as input, as well as optional keyword arguments, and modifies the ``System`` to contain the appropriate parameters.
-        - Class-level ``ParameterAttributes`` and ``IndexedParameterAttributes``: These correspond to the header-level attributes in a SMIRNOFF data source.
-          For example,, the ``Bonds`` tag in the SMIRNOFF spec has an optional ``fractional_bondorder_method`` field, which corresponds to the line  ``fractional_bondorder_method = ParameterAttribute(default=None)`` in the ``BondHandler`` class definition.
-          The ``ParameterAttribute`` and ``IndexedParameterAttribute`` classes offer considerable flexibility for validating inputs.
-          Defining these attributes at the class level implements the corresponding behavior in the default ``__init__`` function.
-        - Class-level definitions ``_MAX_SUPPORTED_SECTION_VERSION`` and ``_MAX_SUPPORTED_SECTION_VERSION``.
-          ParameterHandler versions allow us to evolve ParameterHandler behavior in a controlled, recorded way.
-          Force field development is experimental by nature, and it is unlikely that the initial choice of header attributes is suitable for all use cases.
-          Recording the "versions" of a SMIRNOFF spec tag allows us to encode the default behavior and API of a specific generation of ParameterHandlers, while allowing the safe addition of new attributes and behaviors.
-    Each ParameterHandler-derived class MAY implement:
-        - ``known_kwargs``: Keyword arguments passed to ``ForceField.create_openmm_system`` are validated against the ``known_kwargs`` lists of each ParameterHandler that the ForceField owns.
-          If present, these kwargs and their values will be passed on to the ParameterHandler.
-        - ``to_dict``: converts the ParameterHandler to a hierarchical dict compliant with the SMIRNOFF specification.
-          The default implementation of this function should suffice for most developers.
-        - ``check_handler_compatibility``: Checks whether this ParameterHandler is "compatible" with another.
-          This function is used when a ForceField is attempted to be constructed from *multiple* SMIRNOFF data sources, and it is necessary to check that two sections with the same tagname can be combined in a sane way.
-          For example, if the user instructed two ``vdW`` sections to be read, but the sections defined different vdW potentials, then this function should raise an Exception indicating that there is no safe way to combine the parameters.
-          The default implementation of this function should suffice for most developers.
-        - ``postprocess_system``: operates identically to ``create_force``, but is run after each ParameterHandlers' ``create_force`` has already been called.
-          The default implementation of this method simply does nothing, and should suffice for most developers.
-
-ToolkitRegistry
-    ``ToolkitRegistry.from_object``  / ``ToolkitRegistry.from_smiles`` / ``OpenEyeToolkitWrapper.from_openeye`` / ``RDKitToolkitWrapper.from_rdkit``
-          These methods are a bit strange because they are effectively classmethods for ``FrozenMolecule`` and ``Molecule`` subclasses.
-          In `PR #583 <https://github.com/openforcefield/openff-toolkit/pull/583>`_, jaimergp raised a concern that effectively boils down to "if I subclass ``Molecule`` into a new class, ``MyMol``, then I expect ``MyMol.from_rdkit`` to return an instance of ``MyMol``, not ``Molecule``.
-          However, before this PR, methods like ``ToolkitRegistry.from_smiles`` didn't have any way to know what type of object they should return, and instead always returned ``Molecule`` objects.
-          So as of  `PR #583 <https://github.com/openforcefield/openff-toolkit/pull/583>`_, ToolkitRegistry methods that produce a Molecule must take a private parameter, ``_cls``, indicating the type of object to return.
-          This parameter should be of type ``type`` and should subclass ``FrozenMolecule``, or otherwise expose ``Molecule._add_atom``, ``._add_bond``, ``.add_conformer``, and ``.partial_charges``.
-
-
-
-.. TODO : fill in the modular components below, and clarify ToolkitRegistry above
-
-    ParameterType
-
-    Molecule.to_X
-
-    Molecule.from_X
-
-    Force field directories
-
-
-.. TODO : fill in the sections below
-.. Molecule definition
-   '''''''''''''''''''
-   
-   Required stereochemistry
-   ''''''''''''''''''''''''
-   
-   Conformation dependence
-   '''''''''''''''''''''''
-   
-   
-   
-   Reliance on external dependencies
-   '''''''''''''''''''''''''''''''''
-   
-   
-   
-   ForceField file paths
-   '''''''''''''''''''''
-
-.. TODO : expand this section
-.. Documentation
-   '''''''''''''
-   If you define a new class, add new files to autodoc
-
-User Experience
-'''''''''''''''
-
-One important aspect of how we make design decisions is by asking "who do we envision using this software, and what would they want it to do here?".
-There is a wide range of possible users, from non-chemists, to students/trainees, to expert computational medicinal chemists.
-We have decided to build functionality intended for use by `expert medicinal chemists`, and whenever possible, add fatal errors if the toolkit risks doing the wrong thing.
-So, for example, if a molecule is loaded with an odd ionization state, we assume that the user has input it this way intentionally.
-This design philosophy invariably has tradeoffs -- For example, the OpenFF Toolkit will give the user a hard time if they try to load a "dirty" molecule dataset, where some molecules have errors or are not described in enough detail for the toolkit to unambiguously parametrize them.
-If there is risk of misinterpreting the molecule (for example, bond orders being undefined or chiral centers without defined stereochemistry), the toolkit should raise an error that the user can override.
-In this regard we differ from RDKit, which is more permissive in the level of detail it requires when creating molecules.
-This makes sense for RDKit's use cases, as several of its analyses can operate with a lower level of detail about the molecules.
-Often, the same design decision is the best for all types of users, and there is no need for discussion.
-But when we do need to make tradeoffs, "assume the user is an expert" is our guiding principle.
-
-At the same time, we aim for "automagic" behavior whenever a decision will clearly go one way over another.
-System parametrization is an inherently complex topic, and the OFF toolkit would be nearly unusable if we required the user to explicitly approve every aspect of the process.
-For example, if a ``Topology`` has its ``box_vectors`` attribute defined, we assume that the resulting OpenMM ``System`` should be periodic.
-
-
-
-Setting up a development environment
-""""""""""""""""""""""""""""""""""""
-
-1. Install the ``conda`` package manager as part of the Anaconda Distribution from `here <https://www.anaconda.com/distribution/>`_
-
-2. Set up conda environment
-
-.. code-block:: shell
-
-    # Create a conda environment with the Open Force Field toolkit and its dependencies
-    conda create --name openff-dev -c conda-forge -c openeye openff-toolkit openeye-toolkits
-    conda activate openff-dev
-    # Remove (only) the toolkit and replace it with a local install
-    conda remove --force openff-toolkit
-    git clone https://github.com/openforcefield/openff-toolkit
-    cd openff-toolkit
-    pip install -e .
-
-3. Obtain and store Open Eye license somewhere like ``~/.oe_license.txt``.
-   Optionally store the path in environmental variable ``OE_LICENSE``, i.e. using a command like ``echo
-   "export OE_LICENSE=/Users/yournamehere/.oe_license.txt" >> ~/.bashrc``
-
-Building the Docs
-'''''''''''''''''
-
-The documentation is composed of two parts, a hand-written user guide and an auto-generated API
-documentation.
-Both are compiled by Sphinx, and can be automatically served and regenerated on changes with sphinx-autobuild.
-Documentation for released versions is available at `ReadTheDocs <https://open-forcefield-toolkit.readthedocs.io/en/latest/>`_.
-ReadTheDocs also builds the documentation for each Pull Request opened on GitHub and keeps the output for 90 days.
-
-To add the documentation dependencies to your existing ``openff-dev`` Conda environment:
-
-.. code-block:: shell
-
-    # Add the documentation requirements to your Conda environment
-    conda env update --name openff-dev --file docs/environment.yml
-    conda install --name openff-dev -c conda-forge sphinx-autobuild
-
-To build the documentation from scratch:
-
-.. code-block:: shell
-
-    # Build the documentation
-    # From the openff-toolkit root directory
-    conda activate openff-dev
-    cd docs
-    make html
-    # Documentation can be found in docs/_build/html/index.html
-
-To watch the source directory for changes and automatically rebuild the documentation and refresh your browser:
-
-.. code-block:: shell
-
-    # Host the docs on a local HTTP server and rebuild when a source file is changed
-    # Works best when the docs have already been built
-    # From the openff-toolkit root directory
-    conda activate openff-dev
-    sphinx-autobuild docs docs/_build/html --watch openff
-    # Then navigate your web browser to http://localhost:8000
-
-
-
-
-Development Process
-"""""""""""""""""""
-
-Development of new toolkit features generally proceeds in the following stages:
-
-* Begin a discussion on the `GitHub issue tracker <http://github.com/openforcefield/openff-toolkit/issues>`_ to determine big-picture "what should this feature do?" and "does it fit in the scope of the OpenFF Toolkit?"
-    * `"... typically, for existing water models, we want to assign library charges" <https://github.com/openforcefield/openff-toolkit/issues/25>`_
-* Start identifying details of the implementation that will be clear from the outset
-    * `"Create a new "special section" in the SMIRNOFF format (kind of analogous to the BondChargeCorrections section) which allows SMIRKS patterns to specify use of library charges for specific groups <https://github.com/openforcefield/openff-toolkit/issues/25#issue-225173968>`_
-    * `"Following #86, here's how library charges might work: ..." <https://github.com/openforcefield/openff-toolkit/issues/25#issuecomment-354636391>`_
-* Create a branch or fork for development
-    * The OpenFF Toolkit has one unusual aspect of its CI build process, which is that certain functionality requires the OpenEye toolkits, so the builds must contain a valid OpenEye license file.
-      An encrypted OpenEye license is stored as an encrypted token within the ``openforcefield`` organization on GitHub.
-      For security reasons, builds run from forks cannot access this key.
-      Therefore, tests that depend on the OpenEye Toolkits will be skipped on forks.
-      Contributions run on forks are still welcome, especially as features that do not interact directly with the OpenEye Toolktis are not likely affected by this limitation.
-
-
-Contributing
-""""""""""""
-
-We always welcome `GitHub pull requests <https://github.com/openforcefield/openff-toolkit/pulls>`_.
-For bug fixes, major feature additions, or refactoring, please raise an issue on the `GitHub issue tracker <http://github.com/openforcefield/openff-toolkit/issues>`_ first to ensure the design will be amenable to current developer plans.
-
-How can I become a developer?
-"""""""""""""""""""""""""""""
-
-If you would like to contribute, please post an issue on the `GitHub issue tracker <http://github.com/openforcefield/openff-toolkit/issues>`_ describing the contribution you would like to make to start a discussion.
-
-Style guide
-"""""""""""
-
-Development for the ``openff-toolkit`` conforms to the recommendations given by the `Software Development Best Practices for Computational Chemistry <https://github.com/choderalab/software-development>`_ guide.
-
-The naming conventions of classes, functions, and variables follows `PEP8 <https://www.python.org/dev/peps/pep-0008/>`_, consistently with the best practices guide. The naming conventions used in this library not covered by PEP8 are:
-- Use ``file_path``, ``file_name``, and ``file_stem`` to indicate ``path/to/stem.extension``, ``stem.extension``, and ``stem`` respectively, consistently with the variables in the standard ``pathlib`` library.
-- Use ``n_x`` to abbreviate "number of X` (e.g. `n_atoms`, `n_molecules`).
-
-We place a high priority on code cleanliness and readability, even if code could be written more compactly. For example, 15-character variable names are fine. Triply nested list comprehensions are not.
-
-The ``openff-toolkit`` has adopted code formatting tools ("linters") to maintain consistent style and remove the burden of adhering to these standards by hand. Currently, two are employed:
-1. `Black <https://black.readthedocs.io/>`_, the uncompromising code formatter, automatically formats code with a consistent style.
-1. `isort <https://timothycrosley.github.io/isort/>`_, sorts imports
-
-There is a step in CI that uses these tools to check for a consistent style (see the file ``.github/workflows/lint.yml``). These checks will use the most recent versions of each linter. To ensure that changes follow these standards, you can install and run these tools locally:
-
-.. code-block:: shell
-
-    $ conda install black isort -c conda-forge
-    $ black openff
-    $ isort openff
-
-Anything not covered above is currently up to personal preference, but may change as new linters are added.
-
-Supported Python versions
-"""""""""""""""""""""""""
-
-The OpenFF Toolkit roughly follows `NEP 29 <https://numpy.org/neps/nep-0029-deprecation_policy.html>`_. As of version 0.9.1 (March 2021) this means Python 3.7-3.9 is officially supported. We develop, test, and distribute on macOS and Linux-based operating systems. We do not currently support Windows. Some CI builds run using only The RDKit as a backend, some run using only OpenEye Toolkits, and some run using both installed at once.
-
-The CI matrix is currently as follows:
-
-+-----------------------+------------+-----------+-------------+------------+-----------+-------------+
-| Operating system      | Linux                                | macOS                                |
-+-----------------------+------------+-----------+-------------+------------+-----------+-------------+
-| Wrapped toolkit(s)    | RDKit      | OpenEye   | RDKit + OE  | RDKit      | OpenEye   | RDKit + OE  |
-+-----------------------+------------+-----------+-------------+------------+-----------+-------------+
-| Python version        |            |           |             |            |           |             |
-+=======================+========================+=============+========================+=============+
-| Python 3.6 and older  | No support after 0.9.1 (March 20201)                                        |
-+-----------------------+------------+-----------+-------------+------------+-----------+-------------+
-| Python 3.7            | Test       | Test      | Test        | Test       | Test      | Test        |
-+-----------------------+------------+-----------+-------------+------------+-----------+-------------+
-| Python 3.8            | Test       | Skip      |        Skip | Test       | Skip      | Skip        |
-+-----------------------+------------+-----------+-------------+------------+-----------+-------------+
-| Python 3.9            | Test       | Skip      |        Skip | Test       | Skip      | Skip        |
-+-----------------------+------------+-----------+-------------+------------+-----------+-------------+
-| Python 3.10 and newer |   Waiting on official releases and upstream support                         |
-+-----------------------+------------+-----------+-------------+------------+-----------+-------------+
diff --git a/docs/index.rst b/docs/index.rst
index e6bd1171..2505d2e7 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -25,6 +25,7 @@ Using the toolkit
 .. toctree::
    :maxdepth: 1
 
+   concepts
    smirnoff
    virtualsites
    developing
diff --git a/docs/releasehistory.md b/docs/releasehistory.md
index 553ea6ea..ba0a2882 100644
--- a/docs/releasehistory.md
+++ b/docs/releasehistory.md
@@ -8,15 +8,24 @@ Releases follow the `major.minor.micro` scheme recommended by [PEP440](https://w
 
 ## Changes since last release
 
+### Behaviors changed
+
+- [PR #894](https://github.com/openforcefield/openforcefield/pull/894): Calls to `Molecule.from_openeye`, 
+  `Molecule.from_rdkit`, `Molecule.from_smiles`, `OpenEyeToolkitWrapper.from_smiles`, and 
+  `RDKitToolkitWrapper.from_smiles` will now load atom maps into the the resulting 
+  `Molecule's` `offmol.properties['atom_map']` field, even if not all atoms have map indices assigned.
+
 ### Bugfixes
 
 - [PR #891](https://github.com/openforcefield/openforcefield/pull/891): Calls to `Molecule.from_openeye` no longer mutate the input OE molecule.
+- [PR #897](https://github.com/openforcefield/openforcefield/pull/897): Fixes enumeration of stereoisomers for molecules with already defined stereochemistry using RDKit.
 
 ### Improved documentation and warnings
 - [PR #862](https://github.com/openforcefield/openforcefield/pull/862): Clarify that `System` objects produced by the toolkit are OpenMM `System`s in anticipation of forthcoming OpenFF `System`s. Fixes [Issue #618](https://github.com/openforcefield/openforcefield/issues/618).
 - [PR #863](https://github.com/openforcefield/openff-toolkit/pull/863): Documented how to build the docs in the developers guide.
 - [PR #870](https://github.com/openforcefield/openff-toolkit/pull/870): Reorganised documentation to improve discoverability and allow future additions.
 - [PR #871](https://github.com/openforcefield/openff-toolkit/pull/871): Changed Markdown parser from m2r2 to MyST for improved documentation rendering.
+- [PR #880](https://github.com/openforcefield/openff-toolkit/pull/880): Cleanup and partial rewrite of the developer's guide.
 
 :::{TODO}
 Translate previous release history to MyST markdown
diff --git a/openff/toolkit/typing/engines/smirnoff/parameters.py b/openff/toolkit/typing/engines/smirnoff/parameters.py
index 98d7330b..55d4717d 100644
--- a/openff/toolkit/typing/engines/smirnoff/parameters.py
+++ b/openff/toolkit/typing/engines/smirnoff/parameters.py
@@ -4904,9 +4904,9 @@ class VirtualSiteHandler(_NonbondedHandler):
                 )
             )
         self._virtual_site_types[vsite_name] = vsite_cls
-        self._parameters = [
-            param for param in self._parameters if param.type != vsite_name
-        ]
+        self._parameters = ParameterList(
+            [param for param in self._parameters if param.type != vsite_name]
+        )
 
     @property
     def virtual_site_types(self):
diff --git a/openff/toolkit/utils/toolkits.py b/openff/toolkit/utils/toolkits.py
index 1644b445..2deb22bc 100644
--- a/openff/toolkit/utils/toolkits.py
+++ b/openff/toolkit/utils/toolkits.py
@@ -1409,9 +1409,13 @@ class OpenEyeToolkitWrapper(ToolkitWrapper):
             ] = atom_index  # store for mapping oeatom to molecule atom indices below
             atom_mapping[atom_index] = map_id
 
-        # if we have a full atom map add it to the molecule, 0 indicates a missing mapping or no mapping
-        if 0 not in atom_mapping.values():
-            molecule._properties["atom_map"] = atom_mapping
+        # If we have a full / partial atom map add it to the molecule. Zeroes 0
+        # indicates no mapping
+        if {*atom_mapping.values()} != {0}:
+
+            molecule._properties["atom_map"] = {
+                idx: map_idx for idx, map_idx in atom_mapping.items() if map_idx != 0
+            }
 
         for oebond in oemol.GetBonds():
             atom1_index = map_atoms[oebond.GetBgnIdx()]
@@ -3142,7 +3146,9 @@ class RDKitToolkitWrapper(ToolkitWrapper):
         rdmol = self.to_rdkit(molecule=molecule)
 
         # in case any bonds/centers are missing stereo chem flag it here
-        Chem.AssignStereochemistry(rdmol, force=True, flagPossibleStereoCenters=True)
+        Chem.AssignStereochemistry(
+            rdmol, cleanIt=True, force=True, flagPossibleStereoCenters=True
+        )
         Chem.FindPotentialStereoBonds(rdmol)
 
         # set up the options
@@ -4090,9 +4096,13 @@ class RDKitToolkitWrapper(ToolkitWrapper):
             map_atoms[rd_idx] = atom_index
             atom_mapping[atom_index] = map_id
 
-        # if we have a full atom map add it to the molecule, 0 indicates a missing mapping or no mapping
-        if 0 not in atom_mapping.values():
-            offmol._properties["atom_map"] = atom_mapping
+        # If we have a full / partial atom map add it to the molecule. Zeroes 0
+        # indicates no mapping
+        if {*atom_mapping.values()} != {0}:
+
+            offmol._properties["atom_map"] = {
+                idx: map_idx for idx, map_idx in atom_mapping.items() if map_idx != 0
+            }
 
         # Similar to chirality, stereochemistry of bonds in OE is set relative to their neighbors
         for rdb in rdmol.GetBonds():


Test patch:
diff --git a/openff/toolkit/tests/test_molecule.py b/openff/toolkit/tests/test_molecule.py
index f108968c..182b210d 100644
--- a/openff/toolkit/tests/test_molecule.py
+++ b/openff/toolkit/tests/test_molecule.py
@@ -52,6 +52,7 @@ from openff.toolkit.topology.molecule import (
     FrozenMolecule,
     InvalidConformerError,
     Molecule,
+    SmilesParsingError,
 )
 from openff.toolkit.utils import get_data_file_path
 from openff.toolkit.utils.toolkits import (
@@ -522,6 +523,18 @@ class TestMolecule:
         smiles2 = molecule2.to_smiles(toolkit_registry=toolkit_wrapper)
         assert smiles1 == smiles2
 
+    @pytest.mark.parametrize(
+        "smiles, expected", [("[Cl:1]Cl", {0: 1}), ("[Cl:1][Cl:2]", {0: 1, 1: 2})]
+    )
+    @pytest.mark.parametrize(
+        "toolkit_class", [OpenEyeToolkitWrapper, RDKitToolkitWrapper]
+    )
+    def test_from_smiles_with_map(self, smiles, expected, toolkit_class):
+        if not (toolkit_class.is_available()):
+            pytest.skip(f"Required toolkit {toolkit_class} is unavailable")
+        molecule = Molecule.from_smiles(smiles, toolkit_registry=toolkit_class())
+        assert molecule.properties["atom_map"] == expected
+
     smiles_types = [
         {"isomeric": True, "explicit_hydrogens": True, "mapped": True, "error": None},
         {"isomeric": False, "explicit_hydrogens": True, "mapped": True, "error": None},
@@ -1844,6 +1857,66 @@ class TestMolecule:
         else:
             pytest.skip("Required toolkit is unavailable")
 
+    @pytest.mark.parametrize(
+        "toolkit_class", [OpenEyeToolkitWrapper, RDKitToolkitWrapper]
+    )
+    @pytest.mark.parametrize(
+        "smiles, undefined_only, expected",
+        [
+            (
+                "FC(Br)(Cl)[C@@H](Br)(Cl)",
+                False,
+                [
+                    "F[C@](Br)(Cl)[C@@H](Br)(Cl)",
+                    "F[C@](Br)(Cl)[C@H](Br)(Cl)",
+                    "F[C@@](Br)(Cl)[C@@H](Br)(Cl)",
+                    "F[C@@](Br)(Cl)[C@H](Br)(Cl)",
+                ],
+            ),
+            (
+                "FC(Br)(Cl)[C@@H](Br)(Cl)",
+                True,
+                ["F[C@](Br)(Cl)[C@@H](Br)(Cl)", "F[C@@](Br)(Cl)[C@@H](Br)(Cl)"],
+            ),
+            ("F[C@H](Cl)Br", False, ["F[C@@H](Cl)Br"]),
+            ("F[C@H](Cl)Br", True, []),
+        ],
+    )
+    def test_enumerating_stereo_partially_defined(
+        self, toolkit_class, smiles, undefined_only, expected
+    ):
+        """Test the enumerating stereo of molecules with partially defined chirality"""
+
+        if not toolkit_class.is_available():
+            pytest.skip("Required toolkit is unavailable")
+
+        toolkit = toolkit_class()
+
+        # test undefined only
+        mol = Molecule.from_smiles(
+            smiles, toolkit_registry=toolkit, allow_undefined_stereo=True
+        )
+        stereoisomers = mol.enumerate_stereoisomers(
+            undefined_only=undefined_only, rationalise=False
+        )
+
+        # Ensure that the results of the enumeration are what the test expects.
+        # This roundtrips the expected output from SMILES --> OFFMol --> SMILES,
+        # since the SMILES for stereoisomers generated in this test may change depending
+        # on which cheminformatics toolkit is used.
+        expected = {
+            Molecule.from_smiles(stereoisomer, allow_undefined_stereo=True).to_smiles(
+                explicit_hydrogens=True, isomeric=True, mapped=False
+            )
+            for stereoisomer in expected
+        }
+        actual = {
+            stereoisomer.to_smiles(explicit_hydrogens=True, isomeric=True, mapped=False)
+            for stereoisomer in stereoisomers
+        }
+
+        assert expected == actual
+
     @requires_rdkit
     def test_from_pdb_and_smiles(self):
         """Test the ability to make a valid molecule using RDKit and SMILES together"""
@@ -2149,6 +2222,20 @@ class TestMolecule:
         with pytest.raises(KeyError):
             mapping = mol._properties["atom_map"]
 
+    @pytest.mark.parametrize(
+        "toolkit_class", [OpenEyeToolkitWrapper, RDKitToolkitWrapper]
+    )
+    def test_from_mapped_smiles_partial(self, toolkit_class):
+        """Test that creating a molecule from a partially mapped SMILES raises an
+        exception."""
+        if not (toolkit_class.is_available()):
+            pytest.skip(f"Required toolkit {toolkit_class} is unavailable")
+        with pytest.raises(
+            SmilesParsingError,
+            match="The mapped smiles does not contain enough indexes",
+        ):
+            Molecule.from_mapped_smiles("[Cl:1][Cl]", toolkit_registry=toolkit_class())
+
     @pytest.mark.parametrize("molecule", mini_drug_bank())
     def test_n_particles(self, molecule):
         """Test n_particles property"""
diff --git a/openff/toolkit/tests/test_parameters.py b/openff/toolkit/tests/test_parameters.py
index bd8f1a8a..72f0372f 100644
--- a/openff/toolkit/tests/test_parameters.py
+++ b/openff/toolkit/tests/test_parameters.py
@@ -1843,6 +1843,27 @@ class TestVirtualSiteHandler:
                 exclusion_policy="bad_attribute_value_that_will_never_be_used",
             )
 
+    def test_serialize_virtual_site_handler(self):
+        """Test serializing a populated VirtualSiteHandler"""
+        handler = VirtualSiteHandler(
+            skip_version_check=True, exclusion_policy="parents"
+        )
+
+        handler.add_parameter(
+            {
+                "smirks": "[#1:1]-[#8X2H2+0:2]-[#1:3]",
+                "type": "DivalentLonePair",
+                "distance": -0.0106 * unit.nanometers,
+                "outOfPlaneAngle": 0.0 * unit.degrees,
+                "match": "once",
+                "charge_increment1": 0.5 * unit.elementary_charge,
+                "charge_increment2": -1.0 * unit.elementary_charge,
+                "charge_increment3": 0.5 * unit.elementary_charge,
+            }
+        )
+
+        handler.to_dict()
+
     def test_virtual_site_bond_charge_type(self):
         """
         Ensure that an error is raised if incorrect parameters are given to
diff --git a/openff/toolkit/tests/test_toolkits.py b/openff/toolkit/tests/test_toolkits.py
index 604c82d9..c78d8a9d 100644
--- a/openff/toolkit/tests/test_toolkits.py
+++ b/openff/toolkit/tests/test_toolkits.py
@@ -646,6 +646,22 @@ class TestOpenEyeToolkitWrapper:
         )
         assert offmol.n_atoms == 4
 
+    @pytest.mark.parametrize(
+        "smiles, expected_map", [("[Cl:1][H]", {0: 1}), ("[Cl:1][H:2]", {0: 1, 1: 2})]
+    )
+    def test_from_openeye_atom_map(self, smiles, expected_map):
+        """
+        Test OpenEyeToolkitWrapper for loading a molecule with implicit
+        hydrogens (correct behavior is to add them explicitly)
+        """
+        from openeye import oechem
+
+        oemol = oechem.OEMol()
+        oechem.OESmilesToMol(oemol, smiles)
+
+        off_molecule = Molecule.from_openeye(oemol)
+        assert off_molecule.properties["atom_map"] == expected_map
+
     @pytest.mark.parametrize("molecule", get_mini_drug_bank(OpenEyeToolkitWrapper))
     def test_to_inchi(self, molecule):
         """Test conversion to standard and non-standard InChI"""
@@ -2119,6 +2135,19 @@ class TestRDKitToolkitWrapper:
             == expected_output_smiles
         )
 
+    @pytest.mark.parametrize(
+        "smiles, expected_map", [("[Cl:1][Cl]", {0: 1}), ("[Cl:1][Cl:2]", {0: 1, 1: 2})]
+    )
+    def test_from_rdkit_atom_map(self, smiles, expected_map):
+        """
+        Test OpenEyeToolkitWrapper for loading a molecule with implicit
+        hydrogens (correct behavior is to add them explicitly)
+        """
+        from rdkit import Chem
+
+        off_molecule = Molecule.from_rdkit(Chem.MolFromSmiles(smiles))
+        assert off_molecule.properties["atom_map"] == expected_map
+
     def test_file_extension_case(self):
         """
         Test round-trips of some file extensions when called directly from the toolkit wrappers,