diff --git a/examples/ase/ase_basic_example.ipynb b/examples/ase/ase_basic_example.ipynb index 9a5ba57a..25d7f94f 100644 --- a/examples/ase/ase_basic_example.ipynb +++ b/examples/ase/ase_basic_example.ipynb @@ -63,7 +63,7 @@ " size=(size, size, size),\n", " pbc=True)\n", "# Describe the interatomic interactions with Effective Medium Theory\n", - "atoms.set_calculator(EMT())\n", + "atoms.calc = EMT()\n", "# Set the atomic momenta to 300 Kelvin. \n", "MaxwellBoltzmannDistribution(atoms, temperature_K=300 * units.kB)" ] @@ -132,7 +132,7 @@ { "data": { "text/plain": [ - "-0.18180820374102957" + "-0.18180823716253158" ] }, "execution_count": 5, @@ -141,42 +141,26 @@ } ], "source": [ + "# Note that this is the energy in eV, not kJ mol-1 (the standard units of ASE are different to those of OpenMM and NanoVer)\n", "dyn.atoms.get_potential_energy()" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Set up the NanoVer iMD server" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We fetch the NanoVer object we need to serve the dynamics. We provide a handy wrapper that understands ASE simulations and will serve the dynamics for you." - ] - }, { "cell_type": "code", "execution_count": 6, - "metadata": { - "pycharm": { - "is_executing": false - } - }, + "metadata": {}, "outputs": [], "source": [ - "from nanover.app import NanoverImdApplication\n", - "from nanover.ase import NanoverASEDynamics" + "from nanover.omni.ase import ASESimulation\n", + "\n", + "nanover_imd = ASESimulation.from_ase_dynamics(dyn)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's set up an IMD server: we set `port=0` to let the operating system pick a free port for us." + "Let's run a couple of steps to make sure it's working" ] }, { @@ -187,66 +171,55 @@ "is_executing": false } }, - "outputs": [], - "source": [ - "app_server = NanoverImdApplication.basic_server(port=0) #Try using Shift+TAB+TAB to see what you can set up here." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Note**: Be careful if you run the above cell multiple times without running `app_server.close()` (see below), as you will start multiple servers, which may be discovered if you use autoconnect. You can guard against this by swapping the cell above with:\n", - "\n", - "```python\n", - "try:\n", - " app_server.close()\n", - "except NameError: # If the server hasn't been defined yet, there will be an error\n", - " pass\n", - "app_server = NanoverImdApplication.basic_server(port=0)\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "11" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "A server is now ready to be discovered, we can see where it's running with the following:" + "nanover_imd.dynamics.run(10)\n", + "nanover_imd.dynamics.nsteps" ] }, { "cell_type": "code", "execution_count": 8, - "metadata": { - "pycharm": { - "is_executing": false - } - }, + "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "jon: NanoVer iMD Server: Running at [::]:44973\n" - ] + "data": { + "text/plain": [ + "-0.18179726385287687" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "print(f'{app_server.name}: Running at {app_server.address}:{app_server.port}')" + "nanover_imd.dynamics.atoms.get_potential_energy()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The `[::]` means it is running on all available addresses, e.g. your WiFi and cabled access, and it's found an available port" + "## Set up the NanoVer iMD server" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now, let's attach it to the ASE simulation" + "We fetch the NanoVer object we need to serve the dynamics. We provide a handy wrapper that understands ASE simulations and will serve the dynamics for you." ] }, { @@ -259,14 +232,14 @@ }, "outputs": [], "source": [ - "nanover_imd = NanoverASEDynamics(app_server, dyn)" + "from nanover.omni import OmniRunner" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's run a couple of steps to make sure it's working" + "Let's set up an IMD server: we set `port=0` to let the operating system pick a free port for us." ] }, { @@ -279,7 +252,29 @@ }, "outputs": [], "source": [ - "nanover_imd.run(10, block=True)" + "runner = OmniRunner.with_basic_server(nanover_imd, port=0) #Try using Shift+TAB+TAB to see what you can set up here." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Be careful if you run the above cell multiple times without running `app_server.close()` (see below), as you will start multiple servers, which may be discovered if you use autoconnect. You can guard against this by swapping the cell above with:\n", + "\n", + "```python\n", + "try:\n", + " runner.close()\n", + "except NameError: # If the server hasn't been defined yet, there will be an error\n", + " pass\n", + "runner = OmniRunner.with_basic_server(port=0)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A server is now ready to be discovered, we can see where it's running with the following:" ] }, { @@ -292,38 +287,38 @@ }, "outputs": [ { - "data": { - "text/plain": [ - "11" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "harrystroud: NanoVer iMD Server: Running at [::]:63903\n" + ] } ], "source": [ - "nanover_imd.dynamics.nsteps" + "print(f'{runner.app_server.name}: Running at {runner.app_server.address}:{runner.app_server.port}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Ok, it's working, so let's leave it running dynamics in background thread. This is what happens by default when you call `run`" + "The `[::]` means it is running on all available addresses, e.g. your WiFi and cabled access, and it's found an available port" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's start the ASE simulation" ] }, { "cell_type": "code", "execution_count": 12, - "metadata": { - "pycharm": { - "is_executing": false - } - }, + "metadata": {}, "outputs": [], "source": [ - "nanover_imd.run()" + "runner.next()" ] }, { @@ -339,7 +334,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Simulation time: 2.160992853462094, (22 steps)\n" + "Simulation time: 1.7680850619235313, (18 steps)\n" ] } ], @@ -348,6 +343,13 @@ "print(f'Simulation time: {nanover_imd.dynamics.get_time()}, ({nanover_imd.dynamics.nsteps} steps)')" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ok, it's working, so let's leave it running dynamics in background thread. This is what happens by default when you call `run`" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -360,7 +362,9 @@ { "cell_type": "markdown", "metadata": {}, - "source": "You can also visualize it live in the notebook, (check out this [example with NGLView](../basics/nanover_nglview.ipynb))." + "source": [ + "You can also visualize it live in the notebook, (check out this [example with NGLView](../basics/nanover_nglview.ipynb))." + ] }, { "cell_type": "markdown", @@ -387,7 +391,7 @@ "outputs": [], "source": [ "from nanover.app import NanoverImdClient\n", - "client = NanoverImdClient.connect_to_single_server(port=app_server.port)" + "client = NanoverImdClient.connect_to_single_server(port=runner.app_server.port)" ] }, { @@ -424,12 +428,32 @@ "outputs": [], "source": [ "client.wait_until_first_frame()\n", - "frame = client.latest_frame" + "frame = client.current_frame" ] }, { "cell_type": "code", "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "32" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "frame.particle_count" + ] + }, + { + "cell_type": "code", + "execution_count": 18, "metadata": { "pycharm": { "is_executing": false @@ -439,16 +463,17 @@ { "data": { "text/plain": [ - "32" + "-17.533980241570806" ] }, - "execution_count": 17, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "frame.particle_count # use TAB to see what's available!" + "# Print the potential energy, here in kJ mol-1 as we are accessing it via NanoVer\n", + "frame.potential_energy # use TAB to see what's available!" ] }, { @@ -460,7 +485,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 19, "metadata": { "pycharm": { "is_executing": false @@ -471,10 +496,16 @@ { "data": { "text/plain": [ + "values {\n", + " key: \"system.simulation.counter\"\n", + " value {\n", + " number_value: 0\n", + " }\n", + "}\n", "values {\n", " key: \"server.timestamp\"\n", " value {\n", - " number_value: 61981.883366045\n", + " number_value: 71745.708391166\n", " }\n", "}\n", "values {\n", @@ -492,13 +523,13 @@ "values {\n", " key: \"energy.potential\"\n", " value {\n", - " number_value: -17.531668290927588\n", + " number_value: -17.532057455938844\n", " }\n", "}\n", "values {\n", " key: \"energy.kinetic\"\n", " value {\n", - " number_value: 0.011534119468729485\n", + " number_value: 0.010858480390873912\n", " }\n", "}\n", "values {\n", @@ -590,102 +621,102 @@ " key: \"particle.positions\"\n", " value {\n", " float_values {\n", - " values: 3.28507781e-06\n", - " values: 6.92371e-05\n", - " values: -0.00012874922\n", - " values: 0.180514857\n", - " values: 0.180455193\n", - " values: -3.49673828e-05\n", - " values: 0.180406049\n", - " values: -0.000121892743\n", - " values: 0.180585355\n", - " values: 2.79924843e-05\n", - " values: 0.180529684\n", - " values: 0.180502385\n", - " values: 0.360968471\n", - " values: 9.5160317e-07\n", - " values: 2.07705089e-05\n", - " values: 0.541499794\n", - " values: 0.180510744\n", - " values: 3.17802869e-06\n", - " values: 0.541474462\n", - " values: 8.61464214e-05\n", - " values: 0.180452779\n", - " values: 0.360933602\n", - " values: 0.180488572\n", - " values: 0.18048881\n", - " values: -2.77618365e-05\n", - " values: 0.36098966\n", - " values: -0.000100682053\n", - " values: 0.180616692\n", - " values: 0.54154253\n", - " values: -4.45125916e-05\n", - " values: 0.180510551\n", - " values: 0.361002862\n", - " values: 0.180459201\n", - " values: 1.96660858e-05\n", - " values: 0.541504443\n", - " values: 0.180473059\n", - " values: 0.360932827\n", - " values: 0.360937506\n", - " values: -1.70527153e-06\n", - " values: 0.541591585\n", - " values: 0.541560948\n", - " values: 5.38147942e-05\n", - " values: 0.541621745\n", - " values: 0.360923469\n", - " values: 0.180553615\n", - " values: 0.361063689\n", - " values: 0.54145807\n", - " values: 0.180471554\n", - " values: -0.000130070563\n", - " values: 6.13400189e-05\n", - " values: 0.361090571\n", - " values: 0.180384412\n", - " values: 0.180465981\n", - " values: 0.361024141\n", - " values: 0.180568308\n", - " values: 6.71299858e-05\n", - " values: 0.541510165\n", - " values: -9.05262568e-05\n", - " values: 0.180540755\n", - " values: 0.541512489\n", - " values: 0.361037076\n", - " values: -0.000111934241\n", - " values: 0.361083776\n", - " values: 0.541487396\n", - " values: 0.180450678\n", - " values: 0.360990524\n", - " values: 0.541398406\n", - " values: -3.65134474e-05\n", - " values: 0.541491389\n", - " values: 0.361062944\n", - " values: 0.180633172\n", - " values: 0.541459858\n", - " values: 3.68724932e-06\n", - " values: 0.361044049\n", - " values: 0.361014456\n", - " values: 0.180656612\n", - " values: 0.541588902\n", - " values: 0.36102587\n", - " values: 0.18043974\n", - " values: 0.360911578\n", - " values: 0.541519\n", - " values: 7.53806526e-05\n", - " values: 0.541534\n", - " values: 0.541450381\n", - " values: 0.360926658\n", - " values: 0.361004591\n", - " values: 0.361001849\n", - " values: 0.541527331\n", - " values: 0.541467249\n", - " values: 0.360999465\n", - " values: 0.541394055\n", - " values: 0.361007959\n", - " values: 0.541488886\n", - " values: 0.361046\n", - " values: 0.541450739\n", - " values: 0.541552186\n", + " values: -8.08861296e-05\n", + " values: -7.04341028e-06\n", + " values: 3.44130422e-05\n", + " values: 0.18042554\n", + " values: 0.180388376\n", + " values: -5.42464331e-05\n", + " values: 0.180535182\n", + " values: 4.58165741e-05\n", + " values: 0.180564299\n", + " values: 6.37728881e-05\n", + " values: 0.180476949\n", + " values: 0.180508599\n", + " values: 0.361018121\n", + " values: 2.66284333e-05\n", + " values: -6.3303989e-05\n", + " values: 0.541461766\n", + " values: 0.180484831\n", + " values: 4.9048318e-05\n", + " values: 0.541466117\n", + " values: -3.439801e-05\n", + " values: 0.180518121\n", + " values: 0.360976517\n", + " values: 0.180539131\n", + " values: 0.180530474\n", + " values: 4.32907509e-05\n", + " values: 0.360934794\n", + " values: -9.07003196e-05\n", + " values: 0.180561259\n", + " values: 0.541539\n", + " values: 1.6454569e-05\n", + " values: 0.180554852\n", + " values: 0.361037016\n", + " values: 0.180560142\n", + " values: -5.34986866e-05\n", + " values: 0.541486084\n", + " values: 0.180490911\n", + " values: 0.36085\n", + " values: 0.361027539\n", + " values: 2.54441748e-05\n", + " values: 0.541395843\n", + " values: 0.541514218\n", + " values: 8.32559e-05\n", + " values: 0.541443229\n", + " values: 0.360983938\n", + " values: 0.180545077\n", + " values: 0.361049861\n", + " values: 0.541501701\n", + " values: 0.180474699\n", + " values: 0.000100237128\n", + " values: 6.13787488e-05\n", + " values: 0.360888392\n", + " values: 0.180479616\n", + " values: 0.180498898\n", + " values: 0.360999823\n", + " values: 0.180450305\n", + " values: 6.63495885e-05\n", + " values: 0.541473031\n", + " values: -7.80999617e-05\n", + " values: 0.180430397\n", + " values: 0.54154557\n", + " values: 0.361086\n", + " values: 2.14801639e-05\n", + " values: 0.3610605\n", + " values: 0.541564286\n", + " values: 0.180553809\n", + " values: 0.361035645\n", + " values: 0.54155618\n", + " values: -4.97167057e-05\n", + " values: 0.541514456\n", + " values: 0.361072928\n", + " values: 0.180593193\n", + " values: 0.541416705\n", + " values: -9.44544536e-06\n", + " values: 0.360899121\n", + " values: 0.361074507\n", + " values: 0.180493355\n", + " values: 0.541564882\n", + " values: 0.360985\n", + " values: 0.180486605\n", + " values: 0.360950112\n", + " values: 0.541464865\n", + " values: -2.54885272e-05\n", + " values: 0.541466534\n", + " values: 0.541552067\n", + " values: 0.361087292\n", + " values: 0.36101082\n", + " values: 0.360883027\n", + " values: 0.541595399\n", + " values: 0.541372716\n", + " values: 0.36103496\n", + " values: 0.541488826\n", + " values: 0.361009538\n", + " values: 0.54156816\n", + " values: 0.361011147\n", + " values: 0.541493833\n", + " values: 0.541347742\n", " }\n", " }\n", "}\n", @@ -784,14 +815,14 @@ "}" ] }, - "execution_count": 18, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# view the latest frame\n", - "client.latest_frame.raw" + "client.current_frame.raw" ] }, { @@ -803,7 +834,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 20, "metadata": { "pycharm": { "is_executing": false @@ -824,7 +855,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 21, "metadata": { "pycharm": { "is_executing": false @@ -846,7 +877,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 22, "metadata": { "pycharm": { "is_executing": false @@ -856,10 +887,10 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 21, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -878,7 +909,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 23, "metadata": { "pycharm": { "is_executing": false @@ -914,7 +945,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -934,7 +965,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -954,7 +985,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -974,7 +1005,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -994,7 +1025,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1014,7 +1045,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1034,7 +1065,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1054,7 +1085,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1074,7 +1105,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1114,7 +1145,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1134,7 +1165,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1174,7 +1205,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1214,7 +1245,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1234,7 +1265,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1274,7 +1305,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1294,7 +1325,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1314,7 +1345,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1334,7 +1365,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1354,7 +1385,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1394,7 +1425,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1454,7 +1485,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1494,7 +1525,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1514,7 +1545,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1534,7 +1565,7 @@ "\n", " \n", "\n", - " \n", + " \n", "\n", " \n", "\n", @@ -1567,7 +1598,7 @@ "" ] }, - "execution_count": 22, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } @@ -1592,7 +1623,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 24, "metadata": { "pycharm": { "is_executing": false @@ -1600,8 +1631,7 @@ }, "outputs": [], "source": [ - "nanover_imd.close()\n", - "app_server.close()" + "runner.close()" ] }, { @@ -1617,7 +1647,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 25, "metadata": { "pycharm": { "is_executing": false @@ -1625,8 +1655,8 @@ }, "outputs": [], "source": [ - "with NanoverASEDynamics.basic_imd(dyn) as app:\n", - " app.run(20)" + "with OmniRunner.with_basic_server(ASESimulation.from_ase_dynamics(dyn), port=0) as runner:\n", + " runner.next()" ] }, { @@ -1644,20 +1674,13 @@ "* Run bigger molecular mechanics simulations with OpenMM. The [nanotube](./ase_openmm_nanotube.ipynb) and [neuraminidase](./ase_openmm_neuraminidase.ipynb) examples show how to set up simulations with OpenMM.\n", "* To understand what's going on under the hood, consider starting with the [NanoVer frame explained](../fundamentals/frame.ipynb) example. " ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python (nanover)", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "nanover" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -1669,7 +1692,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.12.0" }, "pycharm": { "stem_cell": { @@ -1682,5 +1705,5 @@ } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/examples/ase/ase_openmm_neuraminidase.ipynb b/examples/ase/ase_openmm_neuraminidase.ipynb index 5581d2ea..f26ad9b7 100644 --- a/examples/ase/ase_openmm_neuraminidase.ipynb +++ b/examples/ase/ase_openmm_neuraminidase.ipynb @@ -51,7 +51,12 @@ { "cell_type": "code", "execution_count": 1, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:02:37.212605Z", + "start_time": "2024-10-11T11:02:36.898363Z" + } + }, "outputs": [], "source": [ "import openmm as mm\n", @@ -62,7 +67,12 @@ { "cell_type": "code", "execution_count": 2, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:02:37.999054Z", + "start_time": "2024-10-11T11:02:37.749626Z" + } + }, "outputs": [], "source": [ "prmtop = app.AmberPrmtopFile(\"openmm_files/3TI6_ose_wt.top\")\n", @@ -100,7 +110,12 @@ { "cell_type": "code", "execution_count": 3, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:02:39.650723Z", + "start_time": "2024-10-11T11:02:39.461710Z" + } + }, "outputs": [ { "name": "stderr", @@ -122,6 +137,10 @@ "cell_type": "code", "execution_count": 4, "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:02:42.514705Z", + "start_time": "2024-10-11T11:02:42.510481Z" + }, "pycharm": { "is_executing": true } @@ -141,7 +160,12 @@ { "cell_type": "code", "execution_count": 5, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:02:44.327171Z", + "start_time": "2024-10-11T11:02:44.225632Z" + } + }, "outputs": [], "source": [ "simulation = app.Simulation(prmtop.topology, system, integrator)" @@ -150,7 +174,12 @@ { "cell_type": "code", "execution_count": 6, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:02:45.946111Z", + "start_time": "2024-10-11T11:02:45.929227Z" + } + }, "outputs": [], "source": [ "simulation.context.setPositions(amber_coords.positions)\n", @@ -168,7 +197,12 @@ { "cell_type": "code", "execution_count": 7, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:02:48.726771Z", + "start_time": "2024-10-11T11:02:47.571332Z" + } + }, "outputs": [], "source": [ "simulation.minimizeEnergy()" @@ -184,7 +218,12 @@ { "cell_type": "code", "execution_count": 8, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:02:49.802180Z", + "start_time": "2024-10-11T11:02:49.779330Z" + } + }, "outputs": [], "source": [ "simulation.context.setVelocitiesToTemperature(300 * unit.kelvin)" @@ -193,7 +232,12 @@ { "cell_type": "code", "execution_count": 9, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:02:51.579581Z", + "start_time": "2024-10-11T11:02:51.576491Z" + } + }, "outputs": [], "source": [ "import sys" @@ -202,30 +246,35 @@ { "cell_type": "code", "execution_count": 10, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:02:58.583405Z", + "start_time": "2024-10-11T11:02:52.972305Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "#\"Step\",\"Potential Energy (kJ/mole)\",\"Temperature (K)\"\n", - "500,-36040.71460745954,211.99360761693245\n", - "1000,-32952.37799857282,245.63783128257157\n", - "1500,-31100.121986609927,265.3944013729671\n", - "2000,-30437.55245230817,277.6407132964383\n", - "2500,-29802.374489051334,286.6963348786343\n", - "3000,-29126.247352820865,287.3089727023475\n", - "3500,-29039.515907508365,287.1444284209067\n", - "4000,-29376.106544715396,293.2596440578256\n", - "4500,-29068.517067176334,302.23661957760527\n", - "5000,-28971.829567176334,294.6464976216505\n" + "100,-39686.99454520368,171.15355646771883\n", + "200,-37969.39191458844,174.47405302152586\n", + "300,-36926.46134207868,185.81969663776286\n", + "400,-36312.37470267438,197.06870343518318\n", + "500,-35988.50852225446,211.3250616837355\n", + "600,-35148.14240286969,216.96937436728012\n", + "700,-34459.43003104352,223.55437355836068\n", + "800,-34169.05051444196,232.81622289859095\n", + "900,-33392.66649649762,234.1818586856779\n", + "1000,-33060.31974051618,244.3846325582557\n" ] } ], "source": [ - "simulation.reporters.append(app.StateDataReporter(sys.stdout, 500, step=True,\n", + "simulation.reporters.append(app.StateDataReporter(sys.stdout, 100, step=True,\n", " potentialEnergy=True, temperature=True))\n", - "simulation.step(5000)" + "simulation.step(1000)" ] }, { @@ -249,7 +298,12 @@ { "cell_type": "code", "execution_count": 11, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:04.176799Z", + "start_time": "2024-10-11T11:03:01.073873Z" + } + }, "outputs": [], "source": [ "from nanover.openmm.serializer import serialize_simulation\n", @@ -303,14 +357,26 @@ { "cell_type": "code", "execution_count": 12, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:08.198123Z", + "start_time": "2024-10-11T11:03:07.931047Z" + } + }, "outputs": [], - "source": "from nanover.omni.ase_omm import OpenMMCalculator" + "source": [ + "from nanover.ase.openmm import OpenMMCalculator" + ] }, { "cell_type": "code", "execution_count": 13, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:10.021005Z", + "start_time": "2024-10-11T11:03:10.017521Z" + } + }, "outputs": [], "source": [ "calculator = OpenMMCalculator(simulation)" @@ -319,7 +385,12 @@ { "cell_type": "code", "execution_count": 14, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:11.841719Z", + "start_time": "2024-10-11T11:03:11.574654Z" + } + }, "outputs": [ { "data": { @@ -342,59 +413,58 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now we've got an ASE Atoms object, and a calculator, we can set up NanoVer with ASE as [usual](./ase_basic_example.ipynb).\n", + "Now we've got an ASE Atoms object, and a calculator, we can set up NanoVer with ASE as [usual](./ase_basic_example.ipynb). \n", "The only difference here is that we swap out the default way of sending frames with a specially made one, `openmm_ase_frame_adaptor`, for OpenMM that knows about OpenMM topology" ] }, { "cell_type": "code", "execution_count": 15, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:13.895040Z", + "start_time": "2024-10-11T11:03:13.889625Z" + } + }, "outputs": [], "source": [ "from ase.md import Langevin\n", "import ase.units as ase_units\n", - "dynamics = Langevin(atoms, timestep=1.0 * ase_units.fs, temperature_K=300 * ase_units.kB, friction=1.0e-03)" + "dynamics = Langevin(atoms, timestep=1.0 * ase_units.fs, temperature_K=300, friction=1.0e-03)" ] }, { "cell_type": "code", "execution_count": 16, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:19.899207Z", + "start_time": "2024-10-11T11:03:19.894562Z" + } + }, "outputs": [], "source": [ + "from nanover.ase.openmm.frame_adaptor import openmm_ase_atoms_to_frame_data\n", "from nanover.omni import OmniRunner\n", - "from nanover.omni.ase_omm import ASEOpenMMSimulation, openmm_ase_frame_adaptor" + "from nanover.omni.ase import ASESimulation" ] }, { "cell_type": "code", "execution_count": 17, - "metadata": {}, - "outputs": [], - "source": [ - "neuraminidase_simulation = ASEOpenMMSimulation.from_simulation(dynamics)\n", - "imd_runner = OmniRunner.with_basic_server(neuraminidase_simulation, name=\"neuraminidase-ase-omm-sim\")" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:21.800989Z", + "start_time": "2024-10-11T11:03:21.762441Z" } - ], + }, + "outputs": [], "source": [ - "neuraminidase_simulation.simulation.atoms.get_calculator()" + "omni_sim = ASESimulation.from_ase_dynamics(\n", + " dynamics, \n", + " ase_atoms_to_frame_data=openmm_ase_atoms_to_frame_data,\n", + ")\n", + "omni = OmniRunner.with_basic_server(omni_sim)" ] }, { @@ -406,43 +476,28 @@ }, { "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "10" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" + "execution_count": 18, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:25.543985Z", + "start_time": "2024-10-11T11:03:24.067230Z" } - ], - "source": [ - "neuraminidase_simulation.simulation.run(10)\n", - "neuraminidase_simulation.simulation.get_number_of_steps()" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, + }, "outputs": [ { "data": { "text/plain": [ - "-391.6982377566493" + "-431.0731347117381" ] }, - "execution_count": 20, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "neuraminidase_simulation.simulation.atoms.get_potential_energy()" + "omni_sim.dynamics.run(100)\n", + "omni_sim.atoms.get_potential_energy()" ] }, { @@ -454,40 +509,16 @@ }, { "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "exception in simulation\n", - "Traceback (most recent call last):\n", - " File \"/Users/harrystroud/IRL/nanover-protocol/python-libraries/nanover-omni/src/nanover/omni/omni.py\", line 236, in run\n", - " self.simulation.load()\n", - " File \"/Users/harrystroud/IRL/nanover-protocol/python-libraries/nanover-omni/src/nanover/omni/ase_omm.py\", line 98, in load\n", - " self.openmm_calculator = OpenMMCalculator(self.simulation)\n", - " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " File \"/Users/harrystroud/IRL/nanover-protocol/python-libraries/nanover-ase/src/nanover/ase/openmm/calculator.py\", line 37, in __init__\n", - " self.context = self.simulation.context\n", - " ^^^^^^^^^^^^^^^^^^^^^^^\n", - "AttributeError: 'Langevin' object has no attribute 'context'\n", - "exception in simulation\n", - "Traceback (most recent call last):\n", - " File \"/Users/harrystroud/IRL/nanover-protocol/python-libraries/nanover-omni/src/nanover/omni/omni.py\", line 236, in run\n", - " self.simulation.load()\n", - " File \"/Users/harrystroud/IRL/nanover-protocol/python-libraries/nanover-omni/src/nanover/omni/ase_omm.py\", line 98, in load\n", - " self.openmm_calculator = OpenMMCalculator(self.simulation)\n", - " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " File \"/Users/harrystroud/IRL/nanover-protocol/python-libraries/nanover-ase/src/nanover/ase/openmm/calculator.py\", line 37, in __init__\n", - " self.context = self.simulation.context\n", - " ^^^^^^^^^^^^^^^^^^^^^^^\n", - "AttributeError: 'Langevin' object has no attribute 'context'\n" - ] + "execution_count": 19, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:27.561636Z", + "start_time": "2024-10-11T11:03:27.550961Z" } - ], + }, + "outputs": [], "source": [ - "imd_runner.next()" + "omni.next()" ] }, { @@ -522,12 +553,17 @@ }, { "cell_type": "code", - "execution_count": 22, - "metadata": {}, + "execution_count": 20, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:44.647191Z", + "start_time": "2024-10-11T11:03:44.611045Z" + } + }, "outputs": [], "source": [ "from nanover.app import NanoverImdClient\n", - "client = NanoverImdClient.connect_to_single_server(port=imd_runner.app_server.port)\n", + "client = NanoverImdClient.connect_to_single_server(port=omni.app_server.port)\n", "client.subscribe_to_frames()\n", "client.wait_until_first_frame();" ] @@ -541,8 +577,13 @@ }, { "cell_type": "code", - "execution_count": 23, - "metadata": {}, + "execution_count": 21, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:46.675877Z", + "start_time": "2024-10-11T11:03:46.488107Z" + } + }, "outputs": [], "source": [ "import matplotlib.cm\n", @@ -554,8 +595,13 @@ }, { "cell_type": "code", - "execution_count": 24, - "metadata": {}, + "execution_count": 22, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:48.580877Z", + "start_time": "2024-10-11T11:03:48.086332Z" + } + }, "outputs": [], "source": [ "from nanover.mdanalysis import frame_data_to_mdanalysis\n", @@ -574,8 +620,13 @@ }, { "cell_type": "code", - "execution_count": 25, - "metadata": {}, + "execution_count": 23, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:50.261018Z", + "start_time": "2024-10-11T11:03:50.253941Z" + } + }, "outputs": [], "source": [ "root_selection = client.root_selection\n", @@ -593,8 +644,13 @@ }, { "cell_type": "code", - "execution_count": 26, - "metadata": {}, + "execution_count": 24, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:53.226476Z", + "start_time": "2024-10-11T11:03:53.219531Z" + } + }, "outputs": [], "source": [ "protein = client.create_selection(\"Protein\", [])" @@ -602,8 +658,13 @@ }, { "cell_type": "code", - "execution_count": 27, - "metadata": {}, + "execution_count": 25, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:54.671539Z", + "start_time": "2024-10-11T11:03:54.630624Z" + } + }, "outputs": [], "source": [ "with protein.modify():\n", @@ -623,8 +684,13 @@ }, { "cell_type": "code", - "execution_count": 28, - "metadata": {}, + "execution_count": 26, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:56.755224Z", + "start_time": "2024-10-11T11:03:56.733137Z" + } + }, "outputs": [], "source": [ "with protein.modify():\n", @@ -649,8 +715,13 @@ }, { "cell_type": "code", - "execution_count": 29, - "metadata": {}, + "execution_count": 27, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:58.254043Z", + "start_time": "2024-10-11T11:03:58.230434Z" + } + }, "outputs": [], "source": [ "# Select ligand\n", @@ -661,8 +732,13 @@ }, { "cell_type": "code", - "execution_count": 30, - "metadata": {}, + "execution_count": 28, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:03:59.744295Z", + "start_time": "2024-10-11T11:03:59.735321Z" + } + }, "outputs": [], "source": [ "with ligand.modify():\n", @@ -693,21 +769,17 @@ }, { "cell_type": "code", - "execution_count": 31, - "metadata": {}, - "outputs": [], - "source": [ - "client.close()" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "metadata": {}, + "execution_count": 29, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-11T11:04:14.060954Z", + "start_time": "2024-10-11T11:04:14.056779Z" + } + }, "outputs": [], "source": [ - "imd_runner.close()\n", - "#nanover_server.close()" + "client.close()\n", + "omni.close()" ] }, { @@ -721,7 +793,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "* Set up an OpenMM simulation of [graphene](./ase_openmm_graphene.ipynb) with restraints and add UI and custom commands in the notebook" + "* Set up an OpenMM simulation of [graphene](./ase_openmm_graphene.ipynb) with restraints and add UI and custom commands in the notebook " ] }, { diff --git a/python-libraries/nanover-ase/src/nanover/ase/converter.py b/python-libraries/nanover-ase/src/nanover/ase/converter.py index d06d0879..6548dd7b 100644 --- a/python-libraries/nanover-ase/src/nanover/ase/converter.py +++ b/python-libraries/nanover-ase/src/nanover/ase/converter.py @@ -50,6 +50,15 @@ } +def ase_atoms_to_frame_data( + ase_atoms: Atoms, + *, + topology: bool, + **kwargs, +) -> FrameData: + return ase_to_frame_data(ase_atoms, topology=topology, **kwargs) + + def ase_to_frame_data( ase_atoms: Atoms, positions=True, diff --git a/python-libraries/nanover-ase/src/nanover/ase/frame_adaptor.py b/python-libraries/nanover-ase/src/nanover/ase/frame_adaptor.py index babd1457..a1b6c681 100644 --- a/python-libraries/nanover-ase/src/nanover/ase/frame_adaptor.py +++ b/python-libraries/nanover-ase/src/nanover/ase/frame_adaptor.py @@ -4,8 +4,9 @@ from typing import Callable from ase import Atoms # type: ignore + +from nanover.ase.converter import ase_atoms_to_frame_data from nanover.trajectory import FramePublisher -from nanover.ase import ase_to_frame_data def send_ase_frame( @@ -39,8 +40,10 @@ def send_ase_frame( def send(): nonlocal frame_index - frame = ase_to_frame_data( + include_topology = frame_index == 0 + frame = ase_atoms_to_frame_data( ase_atoms, + topology=include_topology, include_velocities=include_velocities, include_forces=include_forces, ) diff --git a/python-libraries/nanover-ase/src/nanover/ase/openmm/frame_adaptor.py b/python-libraries/nanover-ase/src/nanover/ase/openmm/frame_adaptor.py new file mode 100644 index 00000000..bd211b83 --- /dev/null +++ b/python-libraries/nanover-ase/src/nanover/ase/openmm/frame_adaptor.py @@ -0,0 +1,48 @@ +from ase import Atoms + +from nanover.ase import ase_to_frame_data +from nanover.openmm.converter import add_openmm_topology_to_frame_data +from nanover.trajectory import FramePublisher, FrameData + + +def openmm_ase_frame_adaptor( + ase_atoms: Atoms, + frame_publisher: FramePublisher, + **kwargs, +): + """ + Generates and sends frames for a simulation using an :class: OpenMMCalculator. + """ + + frame_index = 0 + + def send(): + nonlocal frame_index + include_topology = frame_index == 0 + frame_data = openmm_ase_atoms_to_frame_data( + ase_atoms, topology=include_topology, **kwargs + ) + frame_publisher.send_frame(frame_index, frame_data) + frame_index += 1 + + return send + + +def openmm_ase_atoms_to_frame_data( + ase_atoms: Atoms, + *, + topology: bool, + **kwargs, +) -> FrameData: + frame_data = ase_to_frame_data( + ase_atoms, + topology=False, + **kwargs, + ) + + if topology: + imd_calculator = ase_atoms.calc + topology = imd_calculator.calculator.topology + add_openmm_topology_to_frame_data(frame_data, topology) + + return frame_data diff --git a/python-libraries/nanover-ase/src/nanover/ase/openmm/runner.py b/python-libraries/nanover-ase/src/nanover/ase/openmm/runner.py index 0476b3d2..871dd0cf 100644 --- a/python-libraries/nanover-ase/src/nanover/ase/openmm/runner.py +++ b/python-libraries/nanover-ase/src/nanover/ase/openmm/runner.py @@ -6,21 +6,19 @@ from pathlib import Path from typing import Optional, List -from ase import units, Atoms # type: ignore +from ase import units # type: ignore from ase.md import MDLogger, Langevin from ase.md.velocitydistribution import MaxwellBoltzmannDistribution from attr import dataclass from nanover.app import NanoverImdApplication, NanoverRunner from nanover.app.app_server import DEFAULT_NANOVER_PORT +from nanover.ase.openmm.frame_adaptor import openmm_ase_frame_adaptor from nanover.core import NanoverServer, DEFAULT_SERVE_ADDRESS from nanover.ase import TrajectoryLogger from nanover.essd import DiscoveryServer -from nanover.openmm import openmm_to_frame_data, serializer -from nanover.trajectory.frame_publisher import FramePublisher -from openmm.app import Simulation, Topology +from nanover.openmm import serializer +from openmm.app import Simulation -from nanover.ase import ase_to_frame_data -from nanover.ase.converter import add_ase_positions_to_frame_data from nanover.ase.imd import NanoverASEDynamics from nanover.ase.openmm.calculator import OpenMMCalculator from nanover.ase.wall_constraint import VelocityWallConstraint @@ -35,41 +33,6 @@ ) -def openmm_ase_frame_adaptor( - ase_atoms: Atoms, - frame_publisher: FramePublisher, - include_velocities=False, - include_forces=False, -): - """ - Generates and sends frames for a simulation using an :class: OpenMMCalculator. - """ - - frame_index = 0 - topology: Optional[Topology] = None - - def send(): - nonlocal frame_index, topology - # generate topology frame using OpenMM converter. - if frame_index == 0: - imd_calculator = ase_atoms.calc - topology = imd_calculator.calculator.topology - frame = openmm_to_frame_data( - state=None, - topology=topology, - include_velocities=include_velocities, - include_forces=include_forces, - ) - add_ase_positions_to_frame_data(frame, ase_atoms.get_positions()) - # from then on, just send positions and state. - else: - frame = ase_to_frame_data(ase_atoms, topology=False) - frame_publisher.send_frame(frame_index, frame) - frame_index += 1 - - return send - - @dataclass class ImdParams: """ diff --git a/python-libraries/nanover-omni/src/nanover/omni/ase.py b/python-libraries/nanover-omni/src/nanover/omni/ase.py new file mode 100644 index 00000000..030b033d --- /dev/null +++ b/python-libraries/nanover-omni/src/nanover/omni/ase.py @@ -0,0 +1,210 @@ +from dataclasses import dataclass +from typing import Optional, Any, Protocol + +import numpy as np +from ase import Atoms +from ase.calculators.calculator import Calculator +from ase.md import MDLogger +from ase.md.md import MolecularDynamics + +from nanover.app import NanoverImdApplication +from nanover.ase.converter import EV_TO_KJMOL, ase_atoms_to_frame_data +from nanover.ase.imd_calculator import ImdCalculator +from nanover.ase.wall_constraint import VelocityWallConstraint +from nanover.trajectory import FrameData +from nanover.utilities.event import Event + + +@dataclass +class InitialState: + positions: Any + velocities: Any + cell: Any + + +class ASEAtomsToFrameData(Protocol): + def __call__(self, ase_atoms: Atoms, *, topology: bool, **kwargs) -> FrameData: ... + + +class ASESimulation: + """ + A wrapper for ASE simulations so they can be run inside the OmniRunner. + """ + + @classmethod + def from_ase_dynamics( + cls, + dynamics: MolecularDynamics, + *, + name: Optional[str] = None, + ase_atoms_to_frame_data: ASEAtomsToFrameData = ase_atoms_to_frame_data + ): + """ + Construct this from an existing ASE dynamics. + :param dynamics: An existing ASE Dynamics + :param name: An optional name for the simulation instead of default + :param ase_atoms_to_frame_data: An optional callback to extra frames from the system + """ + sim = cls(name) + sim.dynamics = dynamics + sim.ase_atoms_to_frame_data = ase_atoms_to_frame_data + sim.initial_calc = dynamics.atoms.calc + + return sim + + @property + def atoms(self): + if self.dynamics is None: + return None + else: + return self.dynamics.atoms + + def __init__(self, name: Optional[str] = None): + self.name = name or "Unnamed ASE OpenMM Simulation" + + self.app_server: Optional[NanoverImdApplication] = None + + self.on_reset_energy_exceeded = Event() + + self.verbose = False + self.use_walls = False + self.reset_energy: Optional[float] = None + self.frame_interval = 5 + self.include_velocities = False + self.include_forces = False + + self.dynamics: Optional[MolecularDynamics] = None + self.checkpoint: Optional[InitialState] = None + self.initial_calc: Optional[Calculator] = None + + self.frame_index = 0 + self.ase_atoms_to_frame_data = ase_atoms_to_frame_data + + def load(self): + """ + Load and set up the simulation if it isn't done already. + """ + assert self.dynamics is not None + + if self.use_walls: + self.atoms.constraints.append(VelocityWallConstraint()) + + self.checkpoint = InitialState( + positions=self.atoms.get_positions(), + velocities=self.atoms.get_velocities(), + cell=self.atoms.get_cell(), + ) + + def reset(self, app_server: NanoverImdApplication): + """ + Reset the simulation to its initial conditions, reset IMD interactions, and reset frame stream to begin with + topology and continue. + :param app_server: The app server hosting the frame publisher and imd state + """ + assert ( + self.dynamics is not None + and self.atoms is not None + and self.checkpoint is not None + and self.initial_calc is not None + ) + + self.app_server = app_server + + # reset atoms to initial state + self.atoms.set_positions(self.checkpoint.positions) + self.atoms.set_velocities(self.checkpoint.velocities) + self.atoms.set_cell(self.checkpoint.cell) + + # setup calculator + self.atoms.calc = ImdCalculator( + self.app_server.imd, + self.initial_calc, + dynamics=self.dynamics, + ) + + # send the initial topology frame + frame_data = self.make_topology_frame() + self.app_server.frame_publisher.send_frame(0, frame_data) + self.frame_index = 1 + + # TODO: deal with this when its clear if dynamics should be reconstructed or not.. + if self.verbose: + self.dynamics.attach( + MDLogger( + self.dynamics, + self.atoms, + "-", + header=True, + stress=False, + peratom=False, + ), + interval=100, + ) + + def advance_by_one_step(self): + """ + Advance the simulation to the next point a frame should be reported, and send that frame. + """ + self.advance_to_next_report() + + def advance_by_seconds(self, dt: float): + """ + Advance playback time by some seconds, and advance the simulation to the next frame output. + :param dt: Time to advance playback by in seconds (ignored) + """ + self.advance_to_next_report() + + def advance_to_next_report(self): + """ + Step the simulation to the next point a frame should be reported, and send that frame. + """ + assert self.dynamics is not None and self.app_server is not None + + # determine step count for next frame + steps_to_next_frame = ( + self.frame_interval + - self.dynamics.get_number_of_steps() % self.frame_interval + ) + + # advance the simulation + self.dynamics.run(steps_to_next_frame) + + # generate the next frame + frame_data = self.make_regular_frame() + + # send the next frame + self.app_server.frame_publisher.send_frame(self.frame_index, frame_data) + self.frame_index += 1 + + # check if excessive energy requires sim reset + if self.reset_energy is not None and self.app_server is not None: + energy = self.atoms.get_total_energy() * EV_TO_KJMOL + if not np.isfinite(energy) or energy > self.reset_energy: + self.on_reset_energy_exceeded.invoke() + self.reset(self.app_server) + + def make_topology_frame(self): + """ + Make a NanoVer FrameData corresponding to the current particle positions and topology of the simulation. + """ + assert self.atoms is not None + + return self.ase_atoms_to_frame_data( + self.atoms, + topology=True, + include_velocities=self.include_velocities, + include_forces=self.include_forces, + ) + + def make_regular_frame(self): + """ + Make a NanoVer FrameData corresponding to the current state of the simulation. + """ + assert self.atoms is not None + + return self.ase_atoms_to_frame_data( + self.atoms, + topology=False, + include_velocities=self.include_velocities, + include_forces=self.include_forces, + ) diff --git a/python-libraries/nanover-omni/src/nanover/omni/ase_omm.py b/python-libraries/nanover-omni/src/nanover/omni/ase_omm.py index 2fe1f00e..8aabd784 100644 --- a/python-libraries/nanover-omni/src/nanover/omni/ase_omm.py +++ b/python-libraries/nanover-omni/src/nanover/omni/ase_omm.py @@ -1,24 +1,21 @@ import warnings -from dataclasses import dataclass from os import PathLike from pathlib import Path -from typing import Optional, Any +from typing import Optional -import numpy as np from ase import units, Atoms from ase.md import Langevin, MDLogger -from ase.md.md import MolecularDynamics from ase.md.velocitydistribution import MaxwellBoltzmannDistribution from openmm.app import Simulation from nanover.app import NanoverImdApplication -from nanover.ase.converter import EV_TO_KJMOL from nanover.ase.imd_calculator import ImdCalculator from nanover.ase.openmm import OpenMMCalculator -from nanover.ase.openmm.runner import openmm_ase_frame_adaptor -from nanover.ase.wall_constraint import VelocityWallConstraint +from nanover.ase.openmm.frame_adaptor import ( + openmm_ase_atoms_to_frame_data, +) +from nanover.omni.ase import ASESimulation from nanover.openmm import serializer -from nanover.utilities.event import Event CONSTRAINTS_UNSUPPORTED_MESSAGE = ( @@ -26,20 +23,18 @@ ) -@dataclass -class InitialState: - positions: Any - velocities: Any - cell: Any - - -class ASEOpenMMSimulation: +class ASEOpenMMSimulation(ASESimulation): """ A wrapper for ASE OpenMM simulations so they can be run inside the OmniRunner. """ @classmethod - def from_simulation(cls, simulation: Simulation, *, name: Optional[str] = None): + def from_simulation( + cls, + simulation: Simulation, + *, + name: Optional[str] = None, + ): """ Construct this from an existing ASE OpenMM simulation. :param simulation: An existing ASE OpenMM Simulation @@ -61,27 +56,17 @@ def from_xml_path(cls, path: PathLike[str], *, name: Optional[str] = None): return sim def __init__(self, name: Optional[str] = None): - self.name = name or "Unnamed ASE OpenMM Simulation" + name = name or "Unnamed ASE OpenMM Simulation" - self.xml_path: Optional[PathLike[str]] = None - self.app_server: Optional[NanoverImdApplication] = None + super().__init__(name or "Unnamed ASE OpenMM Simulation") - self.on_reset_energy_exceeded = Event() + self.ase_atoms_to_frame_data = openmm_ase_atoms_to_frame_data - self.verbose = False - self.use_walls = False - self.reset_energy: Optional[float] = None - self.time_step = 1 - self.frame_interval = 5 - self.include_velocities = False - self.include_forces = False - self.platform: Optional[str] = None + self.xml_path: Optional[PathLike[str]] = None - self.atoms: Optional[Atoms] = None - self.dynamics: Optional[MolecularDynamics] = None + self.platform: Optional[str] = None self.simulation: Optional[Simulation] = None self.openmm_calculator: Optional[OpenMMCalculator] = None - self.checkpoint: Optional[InitialState] = None def load(self): """ @@ -96,19 +81,17 @@ def load(self): assert self.simulation is not None self.openmm_calculator = OpenMMCalculator(self.simulation) - self.atoms = self.openmm_calculator.generate_atoms() - if self.use_walls: - self.atoms.constraints.append(VelocityWallConstraint()) + atoms = self.openmm_calculator.generate_atoms() + + # we don't read this from the openmm xml + self.dynamics = make_default_ase_omm_dynamics(atoms) + self.atoms.calc = self.openmm_calculator # Set the momenta corresponding to T=300K MaxwellBoltzmannDistribution(self.atoms, temperature_K=300) - self.checkpoint = InitialState( - positions=self.atoms.get_positions(), - velocities=self.atoms.get_velocities(), - cell=self.atoms.get_cell(), - ) + super().load() def reset(self, app_server: NanoverImdApplication): """ @@ -118,6 +101,7 @@ def reset(self, app_server: NanoverImdApplication): """ assert ( self.simulation is not None + and self.dynamics is not None and self.atoms is not None and self.checkpoint is not None and self.openmm_calculator is not None @@ -125,36 +109,25 @@ def reset(self, app_server: NanoverImdApplication): self.app_server = app_server + self.atoms.set_positions(self.checkpoint.positions) + self.atoms.set_velocities(self.checkpoint.velocities) + self.atoms.set_cell(self.checkpoint.cell) + if self.simulation.system.getNumConstraints() > 0: warnings.warn(CONSTRAINTS_UNSUPPORTED_MESSAGE) - # We do not remove the center of mass (fixcm=False). If the center of - # mass translations should be removed, then the removal should be added - # to the OpenMM system. - self.dynamics = Langevin( - atoms=self.atoms, - timestep=self.time_step * units.fs, - temperature_K=300, - friction=1e-2, - fixcm=False, - ) - self.atoms.calc = ImdCalculator( self.app_server.imd, self.openmm_calculator, dynamics=self.dynamics, ) - self.dynamics.attach( - openmm_ase_frame_adaptor( - self.atoms, - self.app_server.frame_publisher, - include_velocities=self.include_velocities, - include_forces=self.include_forces, - ), - interval=self.frame_interval, - ) + # send the initial topology frame + frame_data = self.make_topology_frame() + self.app_server.frame_publisher.send_frame(0, frame_data) + self.frame_index = 1 + # TODO: deal with this when its clear if dynamics should be reconstructed or not.. if self.verbose: self.dynamics.attach( MDLogger( @@ -168,32 +141,17 @@ def reset(self, app_server: NanoverImdApplication): interval=100, ) - self.atoms.set_positions(self.checkpoint.positions) - self.atoms.set_velocities(self.checkpoint.velocities) - self.atoms.set_cell(self.checkpoint.cell) - def advance_by_one_step(self): - """ - Advance the simulation to the next point a frame should be reported, and send that frame. - """ - self.advance_to_next_report() - - def advance_by_seconds(self, dt: float): - """ - Advance playback time by some seconds, and advance the simulation to the next frame output. - :param dt: Time to advance playback by in seconds (ignored) - """ - self.advance_to_next_report() +def make_default_ase_omm_dynamics(atoms: Atoms): + # We do not remove the center of mass (fixcm=False). If the center of + # mass translations should be removed, then the removal should be added + # to the OpenMM system. + dynamics = Langevin( + atoms=atoms, + timestep=1 * units.fs, + temperature_K=300, + friction=1e-2, + fixcm=False, + ) - def advance_to_next_report(self): - """ - Step the simulation to the next point a frame should be reported. - """ - assert self.dynamics is not None - self.dynamics.run(self.frame_interval) - - if self.reset_energy is not None and self.app_server is not None: - energy = self.dynamics.atoms.get_total_energy() * EV_TO_KJMOL - if not np.isfinite(energy) or energy > self.reset_energy: - self.on_reset_energy_exceeded.invoke() - self.reset(self.app_server) + return dynamics diff --git a/python-libraries/nanover-omni/tests/test_all.py b/python-libraries/nanover-omni/tests/test_all.py index 7caab9fe..90bb30b3 100644 --- a/python-libraries/nanover-omni/tests/test_all.py +++ b/python-libraries/nanover-omni/tests/test_all.py @@ -9,6 +9,7 @@ from nanover.testing import assert_equal_soon from test_openmm import example_openmm from test_ase_omm import example_ase_omm +from test_ase import example_ase, example_dynamics from test_playback import example_playback from openmm_simulation_utils import single_atom_simulation from common import app_server @@ -17,11 +18,13 @@ "example_openmm", "example_playback", "example_ase_omm", + "example_ase", ) SIMULATION_FIXTURES_WITHOUT_PLAYBACK = [ "example_openmm", "example_ase_omm", + "example_ase", ] diff --git a/python-libraries/nanover-omni/tests/test_ase.py b/python-libraries/nanover-omni/tests/test_ase.py new file mode 100644 index 00000000..d7a35f5b --- /dev/null +++ b/python-libraries/nanover-omni/tests/test_ase.py @@ -0,0 +1,58 @@ +import pytest +from ase import units, Atoms +from ase.calculators.lj import LennardJones +from ase.md import VelocityVerlet + +from nanover.imd import ParticleInteraction +from nanover.omni import OmniRunner +from nanover.omni.ase import ASESimulation + +from common import app_server + + +@pytest.fixture +def example_ase(app_server, example_dynamics): + sim = ASESimulation.from_ase_dynamics(example_dynamics) + sim.load() + sim.reset(app_server) + yield sim + + +@pytest.fixture +def example_dynamics(): + atoms = Atoms("C", positions=[(0, 0, 0)], cell=[2, 2, 2]) + atoms.calc = LennardJones() + dynamics = VelocityVerlet(atoms, timestep=0.5) + yield dynamics + + +def test_step_interval(example_ase): + """ + Test that advancing by one step increments the dynamics steps by frame_interval. + """ + for i in range(5): + assert ( + example_ase.dynamics.get_number_of_steps() == i * example_ase.frame_interval + ) + example_ase.advance_by_one_step() + + +def test_dynamics_interaction(example_ase): + """ + Test that example dynamics responds to interactions. + """ + example_ase.app_server.imd.insert_interaction( + "interaction.0", + ParticleInteraction( + position=(0.0, 0.0, 10.0), + particles=[0], + interaction_type="constant", + ), + ) + for _ in range(30): + example_ase.advance_by_one_step() + + positions = example_ase.atoms.get_positions() + (x, y, z) = positions[0] + + assert z > 2.5 diff --git a/python-libraries/nanover-omni/tests/test_ase_omm.py b/python-libraries/nanover-omni/tests/test_ase_omm.py index 0d35afb7..49492b25 100644 --- a/python-libraries/nanover-omni/tests/test_ase_omm.py +++ b/python-libraries/nanover-omni/tests/test_ase_omm.py @@ -29,16 +29,6 @@ def test_step_interval(example_ase_omm): example_ase_omm.advance_by_one_step() -@pytest.mark.parametrize("time_step", (0.5, 1.0, 1.5)) -def test_time_step(example_ase_omm, time_step, app_server): - example_ase_omm.time_step = time_step - example_ase_omm.frame_interval = 1 - example_ase_omm.reset(app_server) - for i in range(5): - assert example_ase_omm.dynamics.dt == pytest.approx(time_step * units.fs) - example_ase_omm.advance_by_one_step() - - # TODO: test it actually outputs def test_verbose(example_ase_omm, app_server): """