{
 "nbformat": 4,
 "nbformat_minor": 0,
 "metadata": {
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.6"
  },
  "colab": {
   "name": "tutorial02.ipynb",
   "provenance": [
    {
     "file_id": "https://github.com/openforcefield/openff-evaluator/blob/master/docs/tutorials/tutorial02.ipynb",
     "timestamp": 1583275445271
    }
   ],
   "collapsed_sections": []
  },
  "accelerator": "GPU"
 },
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true,
    "pycharm": {
     "name": "#%% md\n"
    },
    "id": "1adKOSfooSU5",
    "colab_type": "text"
   },
   "source": [
    "# Tutorial 02 - Estimating Data Sets\n",
    "\n",
    "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/openforcefield/openff-evaluator/blob/master/docs/tutorials/tutorial02.ipynb)\n",
    "\n",
    "In this tutorial we will be estimating the data set we created in the [first tutorial](tutorial01.ipynb) using molecular \n",
    "simulation. The tutorial will cover:\n",
    "\n",
    "- loading in the data set to estimate, and the force field parameters to use in the calculations.\n",
    "- defining custom calculation schemas for the properties in our data set.\n",
    "- estimating the data set of properties using an [Evaluator server](../gettingstarted/server.rst) instance.\n",
    "- retrieving the results from the server and storing them on disk."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false,
    "id": "x9Qd3qlAoSU8",
    "colab_type": "text"
   },
   "source": [
    "*Note: If you are running this tutorial in google colab you will need to run a setup script instead of following the \n",
    "installation instructions:*"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "id": "lp7HdaO9oSU9",
    "colab_type": "code",
    "colab": {}
   },
   "source": [
    "# !wget https://raw.githubusercontent.com/openforcefield/openff-evaluator/master/docs/tutorials/colab_setup.ipynb\n",
    "# %run colab_setup.ipynb"
   ],
   "execution_count": 1,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false,
    "id": "TfarnlY3oSVC",
    "colab_type": "text"
   },
   "source": [
    "*For this tutorial make sure that you are using a GPU accelerated runtime.*\n",
    "\n",
    "For the sake of clarity all warnings will be disabled in this tutorial:"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "id": "cTYERblroSVE",
    "colab_type": "code",
    "colab": {}
   },
   "source": [
    "import warnings\n",
    "warnings.filterwarnings('ignore')\n",
    "import logging\n",
    "logging.getLogger(\"openforcefield\").setLevel(logging.ERROR)"
   ],
   "execution_count": 2,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false,
    "id": "wsn5fI_hoSVJ",
    "colab_type": "text"
   },
   "source": [
    "We will also enable time-stamped logging to help track the progress of our calculations:"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "id": "VjmBPJGfoSVL",
    "colab_type": "code",
    "colab": {}
   },
   "source": [
    "from openff.evaluator.utils import setup_timestamp_logging\n",
    "setup_timestamp_logging()"
   ],
   "execution_count": 3,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%% md\n"
    },
    "id": "OTDUNKu3oSVP",
    "colab_type": "text"
   },
   "source": [
    "## Loading the Data Set and Force Field Parameters\n",
    "\n",
    "We will begin by loading in the data set which we created in the previous tutorial:"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "id": "E3_ZeNPmoSVR",
    "colab_type": "code",
    "colab": {}
   },
   "source": [
    "from openff.evaluator.datasets import PhysicalPropertyDataSet\n",
    "\n",
    "data_set_path = \"filtered_data_set.json\"\n",
    "\n",
    "# If you have not yet completed that tutorial or do not have the data set file \n",
    "# available, a copy is provided by the framework:\n",
    "\n",
    "# from openff.evaluator.utils import get_data_filename\n",
    "# data_set_path = get_data_filename(\"tutorials/tutorial01/filtered_data_set.json\")\n",
    "\n",
    "data_set = PhysicalPropertyDataSet.from_json(data_set_path)"
   ],
   "execution_count": 4,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false,
    "id": "Ebkp-6LwoSVW",
    "colab_type": "text"
   },
   "source": [
    "As a reminder, this data contains the experimentally measured density and $H_{vap}$ measurements for ethanol and \n",
    "isopropanol at ambient conditions:"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "id": "ekwbH95noSVY",
    "colab_type": "code",
    "outputId": "cc0ecb85-f2c7-4ed4-d607-2aa68f240e41",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1583290074270,
     "user_tz": 420,
     "elapsed": 313118,
     "user": {
      "displayName": "Simon Boothroyd",
      "photoUrl": "",
      "userId": "14278355403410282083"
     }
    },
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 241
    }
   },
   "source": [
    "data_set.to_pandas().head()"
   ],
   "execution_count": 5,
   "outputs": [
    {
     "output_type": "execute_result",
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Temperature (K)</th>\n",
       "      <th>Pressure (kPa)</th>\n",
       "      <th>Phase</th>\n",
       "      <th>N Components</th>\n",
       "      <th>Component 1</th>\n",
       "      <th>Role 1</th>\n",
       "      <th>Mole Fraction 1</th>\n",
       "      <th>Exact Amount 1</th>\n",
       "      <th>Density Value (g / ml)</th>\n",
       "      <th>Density Uncertainty (g / ml)</th>\n",
       "      <th>EnthalpyOfVaporization Value (kJ / mol)</th>\n",
       "      <th>EnthalpyOfVaporization Uncertainty (kJ / mol)</th>\n",
       "      <th>Source</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>298.15</td>\n",
       "      <td>101.325</td>\n",
       "      <td>Liquid</td>\n",
       "      <td>1</td>\n",
       "      <td>CC(C)O</td>\n",
       "      <td>Solvent</td>\n",
       "      <td>1.0</td>\n",
       "      <td>None</td>\n",
       "      <td>0.78270</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>10.1016/j.fluid.2013.10.034</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>298.15</td>\n",
       "      <td>101.325</td>\n",
       "      <td>Liquid</td>\n",
       "      <td>1</td>\n",
       "      <td>CCO</td>\n",
       "      <td>Solvent</td>\n",
       "      <td>1.0</td>\n",
       "      <td>None</td>\n",
       "      <td>0.78507</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>10.1021/je1013476</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>298.15</td>\n",
       "      <td>101.325</td>\n",
       "      <td>Liquid + Gas</td>\n",
       "      <td>1</td>\n",
       "      <td>CCO</td>\n",
       "      <td>Solvent</td>\n",
       "      <td>1.0</td>\n",
       "      <td>None</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>42.26</td>\n",
       "      <td>0.02</td>\n",
       "      <td>10.1016/S0021-9614(71)80108-8</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>298.15</td>\n",
       "      <td>101.325</td>\n",
       "      <td>Liquid + Gas</td>\n",
       "      <td>1</td>\n",
       "      <td>CC(C)O</td>\n",
       "      <td>Solvent</td>\n",
       "      <td>1.0</td>\n",
       "      <td>None</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>45.34</td>\n",
       "      <td>0.02</td>\n",
       "      <td>10.1016/S0021-9614(71)80108-8</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   Temperature (K)  ...                         Source\n",
       "0           298.15  ...    10.1016/j.fluid.2013.10.034\n",
       "1           298.15  ...              10.1021/je1013476\n",
       "2           298.15  ...  10.1016/S0021-9614(71)80108-8\n",
       "3           298.15  ...  10.1016/S0021-9614(71)80108-8\n",
       "\n",
       "[4 rows x 13 columns]"
      ]
     },
     "metadata": {
      "tags": []
     },
     "execution_count": 5
    }
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false,
    "id": "lVxc5Li3oSVd",
    "colab_type": "text"
   },
   "source": [
    "We will also define the set of force field parameters which we wish to use to estimate this data set of properties. The\n",
    "framework has support for estimating force field parameters from a range of sources, including those in the OpenFF \n",
    "[SMIRNOFF format](https://open-forcefield-toolkit.readthedocs.io/en/latest/smirnoff.html), those which can be applied by \n",
    "[AmberTools](https://ambermd.org/AmberTools.php), [and more](../gettingstarted/client.rst). \n",
    "\n",
    "Each source of a force field has a corresponding source object in the framework. In this tutorial we will be using the\n",
    "OpenFF Parsley force field which is based off of the SMIRNOFF format:"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "id": "IG3NNKHdoSVf",
    "colab_type": "code",
    "colab": {}
   },
   "source": [
    "from openff.evaluator.forcefield import SmirnoffForceFieldSource\n",
    "\n",
    "force_field_path = \"openff-1.0.0.offxml\"\n",
    "force_field_source = SmirnoffForceFieldSource.from_path(force_field_path)"
   ],
   "execution_count": 6,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false,
    "id": "attbV8Y-oSVj",
    "colab_type": "text"
   },
   "source": [
    "## Defining the Calculation Schemas\n",
    "\n",
    "The next step we will take will be to define a custom calculation schema for each type of property in our data set.\n",
    " \n",
    "A calculation schema is the blueprint for how a type of property should be calculated using a particular \n",
    "[calculation approach](../layers/calculationlayers.rst), such as directly by simulation, by reprocessing cached \n",
    "simulation data or, in future, a range of other options.\n",
    "\n",
    "The framework has built-in schemas defining how densities and $H_{vap}$ should be estimated from molecular simulation, \n",
    "covering all aspects from coordinate generation, force field assignment, energy minimisation, equilibration and finally \n",
    "the production simulation and data analysis. All of this functionality is implemented via the frameworks built-in, \n",
    "lightweight [workflow engine](../workflows/workflows.rst), however we won't dive into the details of this until a later \n",
    "tutorial.\n",
    "\n",
    "For the purpose of this tutorial, we will simply modify the default calculation schemas to reduce the number of \n",
    "molecules to include in our simulations to speed up the calculations. This step can be skipped entirely if the default\n",
    "options (which we recommend using for 'real-world' calculations) are to be used:"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "id": "QfMb452WoSVk",
    "colab_type": "code",
    "colab": {}
   },
   "source": [
    "from openff.evaluator.properties import Density, EnthalpyOfVaporization\n",
    "\n",
    "density_schema = Density.default_simulation_schema(n_molecules=256)\n",
    "h_vap_schema = EnthalpyOfVaporization.default_simulation_schema(n_molecules=256)"
   ],
   "execution_count": 7,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false,
    "id": "_O3S6NMcoSVp",
    "colab_type": "text"
   },
   "source": [
    "We could further use this method to set either the absolute or the relative uncertainty that the property should be \n",
    "estimated to within. If either of these are set, the simulations will automatically be extended until the target \n",
    "uncertainty in the property has been met. \n",
    "\n",
    "For our purposes however we won't set any targets, leaving the simulations to run for the default 1 ns.\n",
    "\n",
    "To use these custom schemas, we need to add them to the a request options object which defines all of the options\n",
    "for estimating our data set:"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "id": "f-1zbAbboSVr",
    "colab_type": "code",
    "colab": {}
   },
   "source": [
    "from openff.evaluator.client import RequestOptions\n",
    "\n",
    "# Create an options object which defines how the data set should be estimated.\n",
    "estimation_options = RequestOptions()\n",
    "# Specify that we only wish to use molecular simulation to estimate the data set.\n",
    "estimation_options.calculation_layers = [\"SimulationLayer\"]\n",
    "\n",
    "# Add our custom schemas, specifying that the should be used by the 'SimulationLayer'\n",
    "estimation_options.add_schema(\"SimulationLayer\", \"Density\", density_schema)\n",
    "estimation_options.add_schema(\"SimulationLayer\", \"EnthalpyOfVaporization\", h_vap_schema)    "
   ],
   "execution_count": 8,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false,
    "id": "6oLOzaE5oSVv",
    "colab_type": "text"
   },
   "source": [
    "## Launching the Server\n",
    "\n",
    "The framework is split into two main applications - an `EvaluatorServer` and an `EvaluatorClient`.\n",
    "\n",
    "The `EvaluatorServer` is the main object which will perform any and all calculations needed to estimate sets of \n",
    "properties. It is design to run on whichever compute resources you may have available (whether that be a single machine \n",
    "or a high performance cluster), wait until a user requests a set of properties be estimated, and then handle that \n",
    "request.\n",
    "\n",
    "The `EvaluatorClient` is the object used by the user to send requests to estimate data sets to running server instances\n",
    "over a TCP connection. It is also used to query the server to see when that request has been fulfilled, and to pull back \n",
    "any results. \n",
    "\n",
    "Let us begin by spawning a new server instance. \n",
    "\n",
    "To launch a server, we need to define how this object is going to interact with the compute resource it is running on. \n",
    "\n",
    "This is accomplished using a [calculation backend](../backends/calculationbackend.rst). While there are several to \n",
    "choose from depending on your needs, well will go with a simple `dask` based one designed to run on a single machine:"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "id": "HwYhG5O1oSVw",
    "colab_type": "code",
    "colab": {}
   },
   "source": [
    "from openff.evaluator.backends import ComputeResources\n",
    "from openff.evaluator.backends.dask import DaskLocalCluster\n",
    "\n",
    "calculation_backend = DaskLocalCluster(\n",
    "    number_of_workers=1,\n",
    "    resources_per_worker=ComputeResources(\n",
    "        number_of_threads=1, \n",
    "        number_of_gpus=1, \n",
    "        preferred_gpu_toolkit=ComputeResources.GPUToolkit.CUDA\n",
    "    ),\n",
    ")\n",
    "calculation_backend.start()"
   ],
   "execution_count": 9,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false,
    "id": "n0KuNxcgoSV1",
    "colab_type": "text"
   },
   "source": [
    "Here we have specified that we want to run our calculations on a single worker which has access to a single GPU. \n",
    "\n",
    "With that defined, we can go ahead and spin up the server:"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "id": "wo8JdqKooSV2",
    "colab_type": "code",
    "outputId": "9b156566-995b-4721-db76-a9938acae4df",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1583290075434,
     "user_tz": 420,
     "elapsed": 314266,
     "user": {
      "displayName": "Simon Boothroyd",
      "photoUrl": "",
      "userId": "14278355403410282083"
     }
    },
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 35
    }
   },
   "source": [
    "from openff.evaluator.server import EvaluatorServer\n",
    "\n",
    "evaluator_server = EvaluatorServer(calculation_backend=calculation_backend)\n",
    "evaluator_server.start(asynchronous=True)"
   ],
   "execution_count": 10,
   "outputs": [
    {
     "output_type": "stream",
     "text": [
      "02:47:53.961 INFO     Server listening at port 8000\n"
     ],
     "name": "stdout"
    }
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false,
    "id": "I2_Q7QmPoSV7",
    "colab_type": "text"
   },
   "source": [
    "The server will run asynchronously in the background waiting until a client connects and requests that a data set be \n",
    "estimated.\n",
    "\n",
    "## Estimating the Data Set\n",
    "\n",
    "With the server spun up we can go ahead and connect to it using an `EvaluatorClient` and request that it estimate our \n",
    "data set using the custom options we defined earlier:"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "id": "N2BgAbafoSV8",
    "colab_type": "code",
    "outputId": "2b7897f5-d251-4fc7-e6ba-2623ef1cfc7e",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1583290075723,
     "user_tz": 420,
     "elapsed": 314551,
     "user": {
      "displayName": "Simon Boothroyd",
      "photoUrl": "",
      "userId": "14278355403410282083"
     }
    },
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 35
    }
   },
   "source": [
    "from openff.evaluator.client import EvaluatorClient\n",
    "evaluator_client = EvaluatorClient()\n",
    "\n",
    "request, exception = evaluator_client.request_estimate(\n",
    "    property_set=data_set,\n",
    "    force_field_source=force_field_source,\n",
    "    options=estimation_options,\n",
    ")\n",
    "\n",
    "assert exception is None"
   ],
   "execution_count": 11,
   "outputs": [
    {
     "output_type": "stream",
     "text": [
      "02:47:54.012 INFO     Received estimation request from ('127.0.0.1', 50618)\n"
     ],
     "name": "stdout"
    }
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false,
    "id": "ydTfQgsWoSWB",
    "colab_type": "text"
   },
   "source": [
    "The server will now receive the requests and begin whirring away fulfilling it. It should be noted that the \n",
    "`request_estimate()` function returns two values - a `request` object, and an `exception` object. If all went well (as \n",
    " it should do here) the `exception` object will be `None`.\n",
    " \n",
    " The `request` object represents the request which we just sent to the server. It stores the unique id which the server\n",
    " assigned to the request, as well as the address of the server that the request was sent to.\n",
    " \n",
    " The `request` object is primarily used to query the current state of our request, and to pull down the results when\n",
    " it the request finishes. Here we will use it it synchronously query the server every 30 seconds until our request has\n",
    " completed."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "id": "JL6Dh0vMoSWD",
    "colab_type": "code",
    "colab": {}
   },
   "source": [
    "# Wait for the results.\n",
    "results, exception = request.results(synchronous=True, polling_interval=30)\n",
    "assert exception is None"
   ],
   "execution_count": 12,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false,
    "id": "PkQxhup2oSWI",
    "colab_type": "text"
   },
   "source": [
    "*Note: we could also asynchronously query for the results of the request. The resultant results object would then \n",
    "contain the partial results of any completed estimates, as well as any exceptions raised during the estimation.*\n",
    "\n",
    "## Inspecting the Results\n",
    "\n",
    "Now that the server has finished estimating our data set and returned the results to us, we can begin to inspect the\n",
    "results of the calculations:"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "id": "ZHdILm6xoSWJ",
    "colab_type": "code",
    "outputId": "95ca17d0-d4b6-4ec6-d173-fb02b70f44e6",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1583292123168,
     "user_tz": 420,
     "elapsed": 44,
     "user": {
      "displayName": "Simon Boothroyd",
      "photoUrl": "",
      "userId": "14278355403410282083"
     }
    },
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 89
    }
   },
   "source": [
    "print(len(results.queued_properties))\n",
    "\n",
    "print(len(results.estimated_properties))\n",
    "\n",
    "print(len(results.unsuccessful_properties))\n",
    "print(len(results.exceptions))"
   ],
   "execution_count": 13,
   "outputs": [
    {
     "output_type": "stream",
     "text": [
      "0\n",
      "4\n",
      "0\n",
      "0\n"
     ],
     "name": "stdout"
    }
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false,
    "id": "UwJcIvzFoSWO",
    "colab_type": "text"
   },
   "source": [
    "We can (hopefully) see here that there were no exceptions raised during the calculation, and that all of our properties\n",
    "were successfully estimated.\n",
    "\n",
    "We will extract the estimated data set and save this to disk:"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "id": "2X5wCljioSWP",
    "colab_type": "code",
    "colab": {}
   },
   "source": [
    "results.estimated_properties.json(\"estimated_data_set.json\", format=True);"
   ],
   "execution_count": 14,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false,
    "id": "H7mpER-ToSWT",
    "colab_type": "text"
   },
   "source": [
    "## Conclusion\n",
    "\n",
    "And that concludes the second tutorial. In the next tutorial we will be performing some basic analysis on our estimated\n",
    "results.\n",
    "\n",
    "If you have any questions and / or feedback, please open an issue on the\n",
    "[GitHub issue tracker](https://github.com/openforcefield/openff-evaluator/issues).\n"
   ]
  }
 ]
}